SDKs
The TypeScript and Python SDKs: ask a person in one call, wait for the decision, act on any corrections, and handle errors without writing the polling.
Two packages carry the Deliverd API with the waiting, the retries and the polling written for you. They carry the same objects field for field, the same error codes and the same webhook verification. Everything they do is also available over REST.
| Language | Install | Import | Needs |
|---|---|---|---|
| TypeScript and JavaScript | npm install @deliverd/sdk | import { deliverd } from "@deliverd/sdk" | Node 20 or later |
| Python | pip install deliverd | from deliverd import deliverd | Python 3.9 or later |
Both have no dependencies. The packages are @deliverd/sdk on npm and deliverd on PyPI. Each reads its key from DELIVERD_API_KEY; see Authentication.
The core call
TypeScript
import { deliverd } from "@deliverd/sdk";
const decision = await deliverd.approve({
title: "Deploy to production"
});
if (decision.approved) await deploy();Python
from deliverd import deliverd
decision = deliverd.approve(
title="Deploy to production"
)
if decision.approved:
deploy()That is the whole integration. The key comes from the environment, the approvers default to your organisation's owners and administrators, and the request expires after a day so the wait always ends. Each approver is notified and emailed a page that works on a phone, where they approve, reject with a reason, or ask your agent a question.
approve() returns when somebody decides — including when they say no, because a refusal is an answer. It throws only when the wait itself failed: a timeout you set, an abort, or an API it could not reach.
Waiting for a decision
Everything the REST request takes, approve() takes too, plus a few conveniences for the wait:
expiresIn- How long the request stays open, as
"30m","4h"or"2d". Defaults to a day.expiresAttakes an exact time instead. timeout- Stop waiting after this and throw a timeout error. The request itself stays open until it expires.
onQuestion- Called when the approver asks something from the page. Return a string and it is posted as your answer; return null to leave it for a person.
onPoll- Called after each poll, for a progress line.
externalId- Your own reference. With one, a restarted agent finds the request it already made and waits on that rather than asking twice. Pass
reuse: falseto turn this off. signal- An
AbortSignalto stop waiting. In Python, pass athreading.Eventascancel.
const decision = await deliverd.approve({
title: "Create £14,280 purchase order",
description: "New server equipment for London infrastructure.",
risk: "medium",
approvers: ["Finance team"],
externalId: "po_12882",
expiresIn: "4h",
onQuestion: async (question) =>
question.question.includes("budget") ? "Within Q3 budget, signed off by Dana." : null,
});
if (!decision.approved) {
console.log(`${decision.status} by ${decision.decidedBy}: ${decision.note}`);
return;
}The wait polls quickly at first and backs off to a slow poll over time, with jitter, so a request somebody is watching settles at once and a long one does not burn your rate limit. A function that must not hold a process open can pass a callbackUrl and be told instead; see Webhooks and callbacks.
Approve with changes
Send the arguments you will act on as input, and name the ones a person may correct in editableFields. The approver sees each field and can change an editable one before approving, rather than rejecting the request over one figure.
const decision = await deliverd.approve({
title: "Refund £420 to Acme",
input: { orderId: "1042", amount: 420, currency: "GBP" },
editableFields: ["amount"],
});
if (decision.approved) {
// decision.input is the input with any corrections applied.
// decision.changes lists them as { field, from, to }.
await refund(decision.input);
}Reviews, requests and confirmations
// Ask people to read work, and wait for their verdicts.
const outcome = await deliverd.review({
title: "Q3 board pack",
reportId: report.id,
instructions: "Check the figures against the ledger.",
reviewers: ["partner@firm.example"],
});
if (!outcome.approved) await revise(outcome.verdicts);
// Ask people for facts you do not have.
const collected = await deliverd.collect({
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"] },
],
respondents: ["cfo@client.example"],
});
if (collected.complete) {
const headcount = collected.answers["Closing headcount at 30 September"];
}
// A yes or no on something small: low risk, an hour to answer.
if (await deliverd.confirm({ title: "Clear the staging cache?" })) await clearCache();review() resolves on changes requested as well as approval, with each reviewer's verdict and how many comment threads they opened. collect() returns the answers keyed by the question you asked; check complete before you use them, because a request that expired has only what arrived. Python has the same three under the same names.
Asking your organisation's policy before acting is deliverd.gate(), covered on its own page.
Flows
A job that needs several requests is still one job. A flow ties them together so the history reads as one sequence and exports as one evidence pack. The SDK gives you the flow as a handle:
const flow = await deliverd.flows.resume({
title: "Q3 close",
externalId: "close_2026_q3", // so a restart extends this one
});
const { answers } = await flow.collect({ title: "Before I start", questions, respondents });
const outcome = await flow.review({ title: "Draft pack", reportId, reviewers });
if (outcome.approved) await deliverd.publish({ title, content, flowId: flow.id });
await flow.close();Development mode
Before you have a key, run the same code against an imaginary approver: no network, nobody interrupted. It prints what the approver would have seen and returns the ending you choose.
import { Deliverd } from "@deliverd/sdk";
const deliverd = new Deliverd({
mode: "development",
development: { outcome: "rejected" },
});
const decision = await deliverd.approve({ title: "Deploy to production" });The outcome can be approved, rejected, denied, timeout or question — the last being the one people forget to handle, where the approver asks you something. Set DELIVERD_MODE=development to switch a whole process over without touching the code, and DELIVERD_DEV_OUTCOME to choose the ending. In Python it is Deliverd(mode="development"). Development mode swaps out the network and nothing else, so what you exercise is what will run.
Errors
Every failure is a DeliverdError carrying the API's own code, the HTTP status (0 when no response arrived), any details the API attached, and a hint saying what to do next when there is one. Branch on the code, never on the message.
import { DeliverdError, DeliverdTimeoutError } from "@deliverd/sdk";
try {
await deliverd.approve({ title: "Deploy to production", timeout: "30m" });
} catch (err) {
if (err instanceof DeliverdTimeoutError) {
// Nobody decided in time. The request stays open until it expires.
} else if (err instanceof DeliverdError && err.isAuth) {
console.error(err.code, err.hint); // e.g. insufficient_scope
} else {
throw err;
}
}isAuth,isRateLimit,isNotFoundandisConflictname the cases worth branching on. Python spells themis_authand so on.- A wait abandoned rather than answered throws
DeliverdTimeoutError, whose code istimeout. - Rate limits and server errors are retried with backoff, honouring
Retry-After, up to three attempts by default. A 4xx other than 429 is not retried, because it would be just as wrong a second later. - Every
POSTcarries an idempotency key the SDK generates and keeps across its retries, so a retried request is only done once.
The codes and what each means are on Errors and limits.
Also in the package
deliverd.publish()anddeliverd.reports.*for the publishing half. See Publishing reports.constructEvent()andconstructCallback()to verify webhooks and callbacks. See Webhooks and callbacks.deliverd.approvals.*,deliverd.reviews.*,deliverd.collections.*anddeliverd.gates.*for the full surface: create without waiting, list, cancel. Alist()with nolimitfollows the cursor to the end;listPage()hands you the cursor.@deliverd/sdk/adapterswires approvals into the hook each agent framework already has: the Claude Agent SDK'scanUseTool, the AI SDK's tool approval, the OpenAI Agents SDK's interruptions and LangGraph.