SDKs
The TypeScript, Python and Rust SDKs: ask a person in one call, wait for the decision, act on any corrections, and handle errors without writing the polling.
Three 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 |
| Rust | cargo add deliverd | use deliverd::Deliverd | Rust 1.89 or later, on tokio |
The TypeScript and Python SDKs have no dependencies; the Rust SDK uses reqwest, serde and tokio. The packages are @deliverd/sdk on npm, deliverd on PyPI and deliverd on crates.io. 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()Rust
use deliverd::{ApprovalRequest, Deliverd};
let deliverd = Deliverd::from_env()?;
let decision = deliverd
.approve(ApprovalRequest::new("Deploy to production"))
.await?;
if decision.approved {
deploy().await;
}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 — a Project in the app — 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.
Inside an agent framework
@deliverd/sdk/adapters wires approvals into the hook each agent framework already has: the Claude Agent SDK's canUseTool, the AI SDK's tool approval, the OpenAI Agents SDK's interruptions and LangGraph. Each tool call is one request keyed by its call id, so a retried call does not ask twice.
From deliverd 0.11.0, deliverd.adapters does the same in Python for Microsoft's agent stack. deliverd.adapters.agent_framework answers Microsoft Agent Framework's function_approval_request for tools marked approval_mode="always_require", on the session that asked:
Microsoft Agent Framework
from deliverd import Deliverd
from deliverd.adapters.agent_framework import run_with_deliverd_approvals
session = agent.create_session()
result = await run_with_deliverd_approvals(
agent,
"Refund order 1042",
session,
Deliverd(),
approvers=["finance"],
)deliverd.adapters.foundry, also from deliverd 0.11.0, answers Foundry Agent Service's mcp_approval_request for an MCP tool with require_approval, sending each answer on a new response chained by previous_response_id:
Microsoft Foundry
from deliverd import Deliverd
from deliverd.adapters.foundry import run_with_deliverd_approvals
response = run_with_deliverd_approvals(
project.get_openai_client(),
Deliverd(),
create_kwargs={
"input": "Close the issues nobody has touched since June",
"extra_body": {"agent_reference": {"name": agent.name, "type": "agent_reference"}},
},
approvers=["platform-team"],
)Both take approvers and a when(tool, input) that picks the calls needing a person, and a timeout after which a call is left pending rather than refused, to be resolved later without asking twice. Neither framework can run a call a person corrected, so a call approved only with changes is declined and the model is told the approved values. Neither framework becomes a dependency.
For AWS and Google, from deliverd 0.19.0, the next release: deliverd.adapters.bedrock answers Amazon Bedrock Agents' return of control for action group functions with user confirmation on, sending CONFIRM, or DENY with the person's reason, on the same session:
Amazon Bedrock Agents
import boto3
from deliverd import Deliverd
from deliverd.adapters.bedrock import invoke_with_deliverd_approvals
run = invoke_with_deliverd_approvals(
boto3.client("bedrock-agent-runtime"),
Deliverd(),
invoke_kwargs={
"agentId": AGENT_ID,
"agentAliasId": AGENT_ALIAS_ID,
"sessionId": "ticket-1042",
"inputText": "Refund order 1042",
},
approvers=["finance"],
)deliverd.adapters.adk, from deliverd 0.19.0, is a before_tool_callback for Google's Agent Development Kit, wherever the agent runs, Vertex AI Agent Engine included. A call that was not approved does not run, and the model reads why. run_with_deliverd_confirmations answers ADK's adk_request_confirmation from the client instead, for tools built with require_confirmation:
Google ADK
from google.adk.agents import LlmAgent
from deliverd import Deliverd
from deliverd.adapters.adk import deliverd_before_tool_callback
agent = LlmAgent(
model="gemini-2.5-flash",
name="billing",
tools=[issue_refund, lookup_order],
before_tool_callback=deliverd_before_tool_callback(Deliverd(), approvers=["finance"]),
)On Amazon Bedrock AgentCore, where agents are most often built with Strands, DeliverdApprovals from deliverd.adapters.strands (deliverd 0.19.0) goes in the agent's hooks and cancels any call a person did not approve, with their reason. Agents on either cloud can also call Deliverd's MCP server directly: see Amazon Bedrock and Google Vertex AI.
Also in the package
deliverd.publish()anddeliverd.reports.*for publishing. 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.
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.