Approvals, reviews and requests
The REST API for asking a person: approvals before an action, reviews of finished work, requests for information, and flows that tie them together.
Four requests put a person in your agent's loop. Each is created with one POST, answered by a person on a page Deliverd hosts, and read back by polling, a webhook or a callback. The SDKs do the waiting for you; this page is the API underneath, and the interactive reference has every field and response.
| Request | Asks for | Create | Scope |
|---|---|---|---|
| Approval | Permission, before an action | POST /api/v1/approvals | approvals:write |
| Review | Judgement on work already done | POST /api/v1/reviews | reviews:write |
| Request for information | Facts you do not have | POST /api/v1/collections | collections:write |
| Flow | Nothing: it ties the others together | POST /api/v1/flows | flows:write |
Approvals
curl -X POST "$DELIVERD_URL/api/v1/approvals" \
-H "Authorization: Bearer $DELIVERD_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"title": "Create €14,280 purchase order",
"description": "New server equipment for London infrastructure.",
"risk": "medium",
"factors": [
{ "label": "Within the quarter'"'"'s budget", "status": "ok" },
{ "label": "Supplier not on the preferred list", "status": "warning" }
],
"approvers": ["Finance team"],
"expiresAt": "2026-10-20T17:00:00Z",
"externalId": "po_12882",
"metadata": { "supplier": "HPE", "amount": 14280 }
}'The response is 201 with the approval, its id, status: "pending" and the url of the page the approvers decide on. Each approver is notified and emailed that page.
title- Required. What is being asked, as a headline. Up to 200 characters. It is also the push notification's title, so write it as the thing to decide.
description- Why, what it affects, what it costs: what the approver reads first.
risklow,medium,highorcritical. Medium when omitted. Only low and medium can be decided from the sign-in-free link in an email.factors- Evidence, one line each, shown as a checklist:
{ label, status, detail }, wherestatusisok,warningorinfo. Up to 20. links- Where to read more:
{ label, url }. Up to 10. reportId- A published report the request is about.
approvers- Who may decide: members by email or id, a directory group or workspace by name or id,
"everyone", or an admitted guest where the organisation lets guests approve. Up to 20. Your owners and administrators when omitted. expiresAt- When the request expires undecided. ISO 8601. The SDKs take
expiresInand default to a day. externalId- Your own reference, returned unchanged and searchable with
?externalId=. flowId- The open flow this request belongs to — shown as a Project in the app.
agent- Which agent is asking, in your words:
{ name, provider, externalId }, each optional. Recorded, returned and shown as declared; nothing is decided on it. model- The model the agent is running on, such as
claude-opus-5. Recorded and shown as declared, likeagent. metadata- Any JSON object. Shown to the approver and returned unchanged.
input- The arguments you will run with if this is approved, shown to the approver field by field.
editableFields- Fields of
inputthe approver may correct before approving. See Approve with changes. recommendation- What your model or decision engine recommends:
{ verdict, summary, confidence, rationale, source }, whereverdictisapproveorrejectandconfidenceruns from 0 to 1. Shown to the approver as context, never as the decision. Where the organisation has blind review on (Records → Analytics → Approval analytics, or Organisation → Security), a high or critical risk request's recommendation is shown only after the approver records their own view, and a decision from Slack, Teams, email or a notification is sent to the page. callbackUrl- An HTTPS address to be told when a person answers, instead of polling. See Callbacks.
An approver who is not an active member (or an admitted guest) is refused with 400 unknown_approver rather than notified into nothing, and a name matching both a person and a team is refused with ambiguous_approver rather than guessed. A team is expanded once, when the request is made.
Reading the decision
Poll GET /api/v1/approvals/{id} (approvals:read) until status leaves pending:
| Status | Meaning |
|---|---|
pending | Waiting on a person. Compare approvalsSoFar with requiredApprovals when a rule asked for more than one. |
approved | Decided yes. approvedInput is what to run with. |
rejected | Decided no. note carries the reason. One rejection settles it, however many approvals were required. |
changes_requested | Sent back to be revised, with note saying what to change. Nothing was approved: revise and ask again, and the new request records which one it replaces. |
expired | Nobody decided before expiresAt. |
cancelled | You withdrew it. The row stays as evidence. |
A settled approval carries decidedBy (the person's name, or their email when they have not set one), decidedById, decidedAt and note. When an organisation rule asked for more than one person, requiredApprovals and approvalsSoFar show the progress; poll on those rather than on status alone. GET /api/v1/approvals lists your requests, filterable by status, externalId and approver (one person's inbox — see In your own product), and pages with limit and cursor like every list.
Questions and answers
An approver can ask the requester something from the page. It appears under questions with answer: null, fires the approval.question event, and nobody decides until it is answered. Answer it with:
curl -X POST "$DELIVERD_URL/api/v1/approvals/$ID/answers" \
-H "Authorization: Bearer $DELIVERD_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "questionId": "…", "answer": "Yes, signed off by the CFO on Friday." }'Only the requester can answer. A questionId that is not on this approval is 404 question_not_found; one already answered is 409 already_answered, and the answer is there to read. POST /api/v1/approvals/{id}/cancel withdraws a pending request.
Approve with changes
Send the arguments you will act on as input, and name the ones a person may correct in editableFields:
{
"title": "Refund €420 to Acme",
"input": { "orderId": "1042", "amount": 420, "currency": "GBP" },
"editableFields": ["amount"]
}The approver can change an editable field before approving. When the request settles, approvedInput is input with their corrections, and changes lists each one as { field, from, to }. The approval.approved webhook and callback carry the same. Run with `approvedInput`, never with what you proposed.
- Only flat values — strings, numbers, true or false — can be editable, and a correction keeps the value's type. Nothing can be added or removed.
- Corrections apply only when one approval settles the request. A request needing several approvers is approved as proposed, because an earlier approver agreed to the proposal, not to a correction they never saw.
- An approval raised by the gate offers the fields the registered action names as editable, each held to the action's schema. Fields an administrator has labelled sensitive (restricted fields) are neither shown nor editable on the page reached from an email link without signing in, and are never sent to the model screen.
- Slack and Teams buttons approve as proposed.
Concerns
When your organisation's ethics rules match a request, the approval carries them in concerns, each { principle, rule, concern }. A request with concerns reaches its approvers with the concern first and can only be approved with a written reason. concerns is empty when nothing matched. See Ethics concerns for what that means for an agent.
Reviews
A review asks named people to read work that already exists and return a verdict of approved or changes_requested.
curl -X POST "$DELIVERD_URL/api/v1/reviews" \
-H "Authorization: Bearer $DELIVERD_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"title": "Q3 board pack",
"reportId": "…",
"instructions": "Check the figures against the ledger.",
"reviewers": ["partner@firm.example"]
}'Poll GET /api/v1/reviews/{id} until status is approved, changes_requested, expired or cancelled. requiredApprovals sets how many approvals settle it (one by default); a single changes_requested settles it regardless. versionId pins the version under review, so publishing again mid-review does not move it. threadCount is how many comment threads the reviewers opened — read them with the comments API.
A reviewer must be an active member, a team, or a guest who can already open the report, where an administrator has turned on Guests may review. Anyone else is refused at creation, so share the report before naming them. An agent can never name a guest it invited.
Requests for information
A request for information (a collection in the API) asks named people typed questions and returns their answers as data.
curl -X POST "$DELIVERD_URL/api/v1/collections" \
-H "Authorization: Bearer $DELIVERD_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"title": "Before I can finish the Q3 pack",
"questions": [
{ "prompt": "Closing headcount at 30 September", "kind": "number" },
{ "prompt": "Revenue recognition basis", "kind": "choice",
"options": ["Accrual", "Cash"] },
{ "prompt": "Anything the board should know?", "kind": "long_text",
"required": false }
],
"respondents": ["cfo@client.example"],
"dueAt": "2026-10-01T09:00:00Z"
}'The kinds are text, long_text, number, date, choice, multi_choice, boolean. A value is checked against its question's kind before it is stored, so a number comes back as a number. Questions are required unless you say otherwise. Up to 40 questions and 50 respondents.
Poll GET /api/v1/collections/{id} until status is complete, expired or cancelled. requiredResponses decides how many submitted replies complete it (everyone asked, by default). A reply that has not been submitted carries no answers. dueAt is what the person sees and what the single reminder is measured against; expiresAt closes the request unanswered.
Flows and evidence
A flow — shown as a Project in the app — ties one job's requests and publishes together, so the history reads as one sequence. Create one, then name its flowId when you create each member:
curl -X POST "$DELIVERD_URL/api/v1/flows" \
-H "Authorization: Bearer $DELIVERD_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "title": "Q3 close", "externalId": "close_2026_q3" }'flowIdis accepted on approvals, reviews, collections and reports. Attaching takes the member's own write scope, notflows:write.- A flow groups and does nothing else: no steps, no dependencies, nothing waits on it.
GET /api/v1/flows/{id}returns the members and atimelineof every audited step, in order — with the events of the tasks started under it when the key holdstasks:read, and of its gates when it holdsgates:read.allSettledmeans nothing is outstanding at this moment, not that the job is finished.- Approvals, reviews and collections take an optional
agent({ name, provider, externalId }) andmodel, for which agent asked and on which model. They are stored, returned, written into the request's audit event and shown on the request and the project page, always as declared: one key is often shared by several agents, and the credential alone cannot say which. POST /api/v1/flows/{id}/completesays the work is done (only whoever started the flow);POST /api/v1/flows/{id}/cancelcalls it off.GET /api/v1/flows/{id}/evidenceandGET /api/v1/reports/{idOrSlug}/evidenceexport the whole record — who was asked, what they were told and what they said — with a digest. A flow's pack also carries its task tree, findings, gates and the governance of any governed objective; a key withouttasks:readorgates:readgets the pack without those sections, andwithheldsays which. They needreports:read,approvals:read,reviews:readandcollections:read;?format=csvreturns the timeline alone.
Objectives and side tasks
A task keeps the goal of long work apart from the conversation doing it, so debugging and research cannot bury it. Start an objective, branch side tasks for secondary problems, record findings, then complete and apply each side task's result back to its parent. The guide covers what people see.
curl -X POST "$DELIVERD_URL/api/v1/tasks" \
-H "Authorization: Bearer $DELIVERD_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "title": "Entra onboarding",
"objective": "Organisations provision users and groups from Entra without inviting each person.",
"externalId": "entra-onboarding" }'POST /api/v1/tasksstarts an objective. WithexternalId, an open objective you already started under it comes back instead (resumed: true), so a restarted agent resumes its goal.GET /api/v1/tasks/{id}/contextis what to read before continuing, instead of the history: the objective, the state, confirmed and suggested decisions, open questions, side-task results, the next action and areanchorparagraph.PATCH /api/v1/tasks/{id}updates the working state (currentState,openQuestions,addOpenQuestions,resolveOpenQuestions,constraints,blockedBy,nextAction) or the status. Changingobjectiveis refused to an agent key. PassexpectedVersionto refuse a write over somebody else's.POST /api/v1/tasks/{id}/side-tasksbranches a side task. It starts with a snapshot of the parent;relevantContextreplaces what Deliverd selects, andresolvesQuestionnames the parent's open question it answers.POST /api/v1/tasks/{id}/findingsrecords a hypothesis, test, evidence, conclusion, recommendation, risk, question or decision. A decision is a suggestion until a person confirms it in Deliverd, orPOST /api/v1/tasks/{id}/findings/{findingId}/approvalputs it to an approval (approvals:write).POST /api/v1/tasks/{id}/completefinishes with a structured result;POST /api/v1/tasks/{id}/applycarries it into the parent (outcome:applied,keptordiscarded) and answers with the parent's context and a message that returns to the objective. Both run as one transaction.POST /api/v1/tasks/{id}/reopenkeeps the earlier result inpreviousResults;POST /api/v1/tasks/{id}/cancelcalls a task off, and an objective's open side tasks with it.GET /api/v1/tasks/{id}/graphreturns every node under the objective, metadata only.- Scopes are
tasks:readandtasks:write. A task in a workspace is invisible — not forbidden — to anyone who cannot see that workspace, through any of its side tasks too.
Retries and idempotency
Every POST accepts an Idempotency-Key header. Keep it across your own retries and a request that arrives twice is done once; the replay comes back with Idempotency-Replayed: true. A key lasts 24 hours. The same key with a different body is 422 idempotency_key_reused, and with the first attempt still running 409 idempotency_in_progress. externalId is the other half: the SDKs use it to find a request a crashed agent already made, rather than asking twice.
Try it on your own agent.
Free to start, no card. Sign up with Google, Microsoft or GitHub, and the first page after hands you an API key.