Skip to content
Skip to Content
APIWorking with agents

Working with agents

An agent takes seconds to minutes. It reads, calls tools, sometimes stops to ask a person for approval. None of that fits inside one HTTP request, so the API does not pretend it does.

Every operation that starts an agent returns as soon as the work has started. The answer arrives afterwards.

There are three ways to handle that, and you will probably use all of them:

  • Poll the chat until it stops running, then read the new messages. This always works.
  • Wait inline with waitSeconds, for short questions where polling is more code than it is worth.
  • Hand it to a person when the status comes back awaiting_input, because the agent has stopped for an approval and will not continue on its own.

Starting a chat

curl -X POST https://api.usedoozy.com/api/v1/chats \ -H "Authorization: Bearer $DOOZY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "message": "What did we agree in yesterday'"'"'s design review?", "agentId": "3c8e...", "timezone": "Europe/London" }'
{ "object": "chat", "id": "8e91...", "title": null, "status": "running", "agentId": "3c8e...", "messageCount": 1, "createdAt": "2026-08-20T14:03:11.000Z" }

status is running. There is no reply yet. The title is null because it is generated from the conversation a moment later.

Chat status

StatusMeaning
queuedAccepted, not started.
runningThe agent is working.
awaiting_inputIt stopped and needs a person, usually to approve a tool. It will not continue on its own.
completedThe turn finished.
failedSomething went wrong. errorMessage says what.
cancelledSomeone stopped it.

awaiting_input is the one worth handling. Approvals happen in the Doozy app, not over the API, so a program that hits this state should surface it to a person rather than wait.

Polling for the reply

Messages carry a sequence that increases over time. The response to a send carries messageSequence, the number of the message you just sent; ask for everything after it and you get the reply and nothing you have already seen. Keep the highest sequence you have seen and pass it back each time:

curl "https://api.usedoozy.com/api/v1/chats/8e91.../messages?afterSequence=3" \ -H "Authorization: Bearer $DOOZY_API_KEY"
{ "object": "list", "data": [ { "object": "message", "id": "b7d2...", "chatId": "8e91...", "role": "assistant", "sequence": 4, "text": "You agreed to ship the settings redesign behind a flag...", "blocks": [ { "type": "tool_call", "id": "toolu_01", "name": "list_captures", "input": { "titleSearch": "design review" } }, { "type": "tool_result", "toolCallId": "toolu_01", "isError": false, "content": "..." }, { "type": "text", "text": "You agreed to ship the settings redesign behind a flag..." } ], "createdAt": "2026-08-20T14:03:29.000Z" } ], "hasMore": false, "nextCursor": null }

text is the readable answer. blocks is the full turn, including what the agent called and what came back, if you want to show its work.

async function waitForReply(apiKey, chatId, afterSequence) { const headers = { Authorization: `Bearer ${apiKey}` }; for (;;) { const chat = await fetch( `https://api.usedoozy.com/api/v1/chats/${chatId}`, { headers }, ).then((response) => response.json()); if (chat.status !== 'running' && chat.status !== 'queued') { const messages = await fetch( `https://api.usedoozy.com/api/v1/chats/${chatId}/messages?afterSequence=${afterSequence}`, { headers }, ).then((response) => response.json()); return { status: chat.status, messages: messages.data }; } await new Promise((resolve) => setTimeout(resolve, 1000)); } }

Poll about once a second. Faster spends your rate limit on nothing.

Waiting inline

For short questions, polling from a script is more code than it is worth. Pass waitSeconds and the request holds open until the agent finishes:

curl -X POST https://api.usedoozy.com/api/v1/chats/8e91.../messages \ -H "Authorization: Bearer $DOOZY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "message": "Summarise that in one line.", "waitSeconds": 30 }'
{ "object": "chat_run", "chatId": "8e91...", "status": "completed", "messageSequence": 5, "replies": [ { "object": "message", "role": "assistant", "text": "Ship the redesign behind a flag, revisit in two weeks." } ], "timedOut": false }

waitSeconds runs to 120. If the time runs out, timedOut is true, the agent carries on, and messageSequence is where to resume polling. Nothing is lost.

This is a convenience over the same poll, not a different mechanism. For work that might take minutes, poll.

Running a todo

Handing a todo to an agent works the same way:

curl -X POST https://api.usedoozy.com/api/v1/todos/1b4c.../runs \ -H "Authorization: Bearer $DOOZY_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "agentId": "3c8e..." }'

The response carries a chatId. That chat is where the work happens, and everything above applies to it.

Assigning an agent to a todo through createTodo or updateTodo starts a run too. That is how the app behaves, and the API matches it. If you want the todo recorded without the work starting, assign a person or leave the assignees off.

createTodo also takes run: true, which does the same thing and says so out loud. Prefer it when you mean it:

{ "title": "Summarise this week", "assignees": [{ "id": "3c8e...", "type": "agent" }], "run": true }

What agent work costs

Starting an agent spends Doozy Minutes, the workspace’s budget for agent work. They are drawn down by how long agents actually run, and they are held per organization rather than per key, so every integration in a workspace draws from the same pool as the people using the app.

Running out returns 402 with insufficient_minutes. Only agent work stops: reading and writing todos, chats, agents and captures all keep working, so an integration that files todos is unaffected by a workspace that has run down its minutes.

Agents also respect their working hours. An agent that is off duty will not pick up scheduled work until it is back on.

Last updated on