Recipes
Each of these is a complete integration, not a fragment. Set DOOZY_API_KEY in the environment; the first recipe defines the API and headers constants that the rest reuse.
File a todo from another system
The most common integration: something happens elsewhere, a todo appears in Doozy.
const API = 'https://api.usedoozy.com/api/v1';
const headers = {
Authorization: `Bearer ${process.env.DOOZY_API_KEY}`,
'Content-Type': 'application/json',
};
async function fileTodo(ticket) {
const response = await fetch(`${API}/todos`, {
method: 'POST',
headers: { ...headers, 'Idempotency-Key': `ticket-${ticket.id}` },
body: JSON.stringify({
title: ticket.subject,
content: `${ticket.body}\n\nFrom: ${ticket.url}`,
priority: ticket.urgent ? 'urgent' : 'medium',
dueDate: ticket.dueDate,
}),
});
if (!response.ok) {
const { error } = await response.json();
throw new Error(`${error.code}: ${error.message} (${error.requestId})`);
}
return response.json();
}Keying idempotency on the ticket id means replaying a webhook cannot create a duplicate, however many times it fires.
Ask an agent a question and use the answer
async function ask(question, agentId) {
const start = await fetch(`${API}/chats`, {
method: 'POST',
headers,
body: JSON.stringify({
message: question,
agentId,
timezone: 'Europe/London',
waitSeconds: 60,
}),
}).then((response) => response.json());
const messages = await fetch(
`${API}/chats/${start.id}/messages`,
{ headers },
).then((response) => response.json());
const reply = messages.data
.filter((message) => message.role === 'assistant')
.at(-1);
return { answer: reply?.text ?? null, chatId: start.id };
}Keep the chatId. Sending another message to it continues the conversation with everything already in context.
Sync outstanding work into a dashboard
async function outstandingWork() {
const todos = [];
let cursor = null;
do {
const url = new URL(`${API}/todos`);
url.searchParams.set('limit', '100');
url.searchParams.set('status', 'ready');
url.searchParams.set('sort', 'dueDate');
url.searchParams.set('order', 'asc');
if (cursor) url.searchParams.set('cursor', cursor);
const page = await fetch(url, { headers }).then((r) => r.json());
todos.push(...page.data);
cursor = page.nextCursor;
} while (cursor);
return todos;
}todos:read is all this needs. Keep the key to that.
Write a meeting note into the workspace
async function recordNote(title, body, tags) {
return fetch(`${API}/captures`, {
method: 'POST',
headers,
body: JSON.stringify({
type: 'note',
content: `# ${title}\n\n${body}`,
tags,
}),
}).then((response) => response.json());
}The leading heading becomes the title. A moment after it lands, Doozy generates a summary and suggests todos from it, so fetch it again if you want those.
Read a meeting transcript
This one needs captures:read, which opens what was actually said in a meeting. Grant it deliberately.
async function recentTranscripts(sinceIso) {
const url = new URL(`${API}/captures`);
url.searchParams.set('createdAfter', sinceIso);
url.searchParams.set('limit', '20');
const page = await fetch(url, { headers }).then((r) => r.json());
const withContent = await Promise.all(
page.data
.filter((capture) => capture.type === 'recording' && capture.status === 'completed')
.map((capture) =>
fetch(`${API}/captures/${capture.id}`, { headers }).then((r) => r.json()),
),
);
return withContent.map((capture) => ({
title: capture.title,
summary: capture.summary,
transcript: capture.transcript,
notes: capture.notes,
}));
}Lists leave the body out because a page of twenty transcripts is not something a client can use. Fetch each capture for its content.
Filtering on status === 'completed' matters: a recording that is still being transcribed has a null transcript, which is not the same as a meeting where nobody spoke.
Give an agent a new standing instruction
async function tightenAgent(agentId) {
return fetch(`${API}/agents/${agentId}`, {
method: 'PATCH',
headers,
body: JSON.stringify({
instructions:
'Answer with sources. Say when you are unsure rather than guessing.',
requiresApproval: true,
}),
}).then((response) => response.json());
}Instructions apply to every conversation the agent has from then on, including ones already open.
Watch a chat as it happens
async function* follow(chatId) {
let seen = 0;
for (;;) {
const chat = await fetch(`${API}/chats/${chatId}`, { headers })
.then((response) => response.json());
const page = await fetch(
`${API}/chats/${chatId}/messages?afterSequence=${seen}&limit=100`,
{ headers },
).then((response) => response.json());
for (const message of page.data) {
seen = Math.max(seen, message.sequence);
yield message;
}
if (chat.status !== 'running' && chat.status !== 'queued') return;
// Two requests per loop. At one second this sits exactly on the default
// 120-per-minute budget with no headroom, so anything else using the same
// key would start getting 429s. Two seconds leaves room.
await new Promise((resolve) => setTimeout(resolve, 2000));
}
}Useful for streaming an agent’s progress into your own interface, tool calls included.