Skip to content
Skip to Content
APIRecipes

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.

Last updated on