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.

RequestAsks forCreateScope
ApprovalPermission, before an actionPOST /api/v1/approvalsapprovals:write
ReviewJudgement on work already donePOST /api/v1/reviewsreviews:write
Request for informationFacts you do not havePOST /api/v1/collectionscollections:write
FlowNothing: it ties the others togetherPOST /api/v1/flowsflows: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.
risk
low, medium, high or critical. 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 }, where status is ok, warning or info. 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 expiresIn and 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, like agent.
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 input the approver may correct before approving. See Approve with changes.
recommendation
What your model or decision engine recommends: { verdict, summary, confidence, rationale, source }, where verdict is approve or reject and confidence runs 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:

StatusMeaning
pendingWaiting on a person. Compare approvalsSoFar with requiredApprovals when a rule asked for more than one.
approvedDecided yes. approvedInput is what to run with.
rejectedDecided no. note carries the reason. One rejection settles it, however many approvals were required.
changes_requestedSent 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.
expiredNobody decided before expiresAt.
cancelledYou 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" }'
  • flowId is accepted on approvals, reviews, collections and reports. Attaching takes the member's own write scope, not flows: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 a timeline of every audited step, in order — with the events of the tasks started under it when the key holds tasks:read, and of its gates when it holds gates:read. allSettled means nothing is outstanding at this moment, not that the job is finished.
  • Approvals, reviews and collections take an optional agent ({ name, provider, externalId }) and model, 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}/complete says the work is done (only whoever started the flow); POST /api/v1/flows/{id}/cancel calls it off.
  • GET /api/v1/flows/{id}/evidence and GET /api/v1/reports/{idOrSlug}/evidence export 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 without tasks:read or gates:read gets the pack without those sections, and withheld says which. They need reports:read, approvals:read, reviews:read and collections:read; ?format=csv returns 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/tasks starts an objective. With externalId, 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}/context is 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 a reanchor paragraph.
  • PATCH /api/v1/tasks/{id} updates the working state (currentState, openQuestions, addOpenQuestions, resolveOpenQuestions, constraints, blockedBy, nextAction) or the status. Changing objective is refused to an agent key. Pass expectedVersion to refuse a write over somebody else's.
  • POST /api/v1/tasks/{id}/side-tasks branches a side task. It starts with a snapshot of the parent; relevantContext replaces what Deliverd selects, and resolvesQuestion names the parent's open question it answers.
  • POST /api/v1/tasks/{id}/findings records a hypothesis, test, evidence, conclusion, recommendation, risk, question or decision. A decision is a suggestion until a person confirms it in Deliverd, or POST /api/v1/tasks/{id}/findings/{findingId}/approval puts it to an approval (approvals:write).
  • POST /api/v1/tasks/{id}/complete finishes with a structured result; POST /api/v1/tasks/{id}/apply carries it into the parent (outcome: applied, kept or discarded) 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}/reopen keeps the earlier result in previousResults; POST /api/v1/tasks/{id}/cancel calls a task off, and an objective's open side tasks with it. GET /api/v1/tasks/{id}/graph returns every node under the objective, metadata only.
  • Scopes are tasks:read and tasks: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.