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.
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. 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. |
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 and externalId, 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. Anyone else is refused at creation, so share the report before naming them.
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 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.allSettledmeans nothing is outstanding at this moment, not that the job is finished.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. They needreports:read,approvals:read,reviews:readandcollections:read;?format=csvreturns the timeline alone.
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.