Embed human approval in your product
Pause your own AI agent, ask your end-user to approve, and resume on their answer, with a typed SDK, verifiable webhooks, and an audit trail
The rest of these docs are about connecting your own agent to your own phone. This page is different: it is for teams building an AI product who want to pause their agent and ask their end-user to approve a consequential action, then resume on the answer. You keep your agent and your notification channel, and add one call. It runs on the Partner plan.
A human decision is asynchronous. It takes seconds, sometimes hours, so it is not
a normal function call. If your agent runs in a serverless function (Vercel,
Lambda, Cloud Run) you must use create-and-resume, not block-and-wait. Do not try
to await a human for 20 minutes inside a Lambda.
Two ways to wire it in
- Block and wait — call create with
wait: true; it holds the connection up to ~55s. Good for a long-lived agent (a desktop CLI agent, a worker you control). - Create and resume — create the decision, get an id back immediately, let your agent yield, and resume from a webhook or by polling. Use this for serverless.
Setup (once)
- Get a Partner API key from your dashboard (
pk_xxx.sk_xxx). - Make the human reachable, one of:
- Bring your own channel (recommended): you already notify this user, so map
them with
POST /api/v1/identifyusing a stableexternalIdfrom your system. - Use Pushary web push: drop the browser SDK into your web app to enroll the user. Heavier; only if you have no channel.
- Bring your own channel (recommended): you already notify this user, so map
them with
Quick start (SDK)
npm install @pushary/serverimport { createPusharyServer, verifyWebhookSignature } from "@pushary/server"
const pushary = createPusharyServer({ apiKey: process.env.PUSHARY_API_KEY! })
// Create (async by default). Always pass an idempotencyKey.
const decision = await pushary.decisions.create({
externalId: user.id,
question: "Publish this generated video to your public profile?",
type: "confirm", // confirm | select | input
callbackUrl: "https://yourapp.com/webhooks/pushary",
idempotencyKey: `run-${runId}-step-3`,
expiresInSeconds: 3600, // how long the human has (default 1h, max 24h)
})
// Resume from the webhook, or poll durably.
const state = await pushary.decisions.get(decision.decisionId, { wait: 30 })
if (state.answered) resume(state.value)Or call the REST API directly
curl -X POST https://pushary.com/api/v1/server/decisions \
-H "Authorization: Bearer pk_xxx.sk_xxx" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: run-8f2a-step-3" \
-d '{
"externalId": "your-user-id-123",
"question": "Publish this generated video to your public profile?",
"type": "confirm",
"callbackUrl": "https://yourapp.com/webhooks/pushary",
"expiresInSeconds": 3600
}'{ "decisionId": "b1c9...", "status": "pending", "answered": false, "pollUrl": "https://pushary.com/api/v1/server/decisions/b1c9...", "expiresInSeconds": 3600 }Make it a tool your agent can call
The decision call is just HTTP, so it becomes a tool in whatever your agent uses.
When the model calls the tool, your code runs the call. The model proposes the
question; your server attaches who to ask (externalId) and your API key. Never
let the model choose the recipient or hold the key.
Anthropic (Claude) / OpenAI (Codex)
Define a tool named e.g. ask_human_to_approve with a question (and optional
options) input. In your handler, inject the current user's externalId and call
decisions.create. For a serverless agent, return a pending result and resume
the run when the webhook fires; for a long-lived agent, call with wait: true and
return the answer inline.
Any MCP agent
If your agent speaks MCP, connect the Pushary MCP endpoint to get an ask-a-human
tool with no code, passing the end-user's externalId as an argument.
Resume: get the answer back
Webhook (fast path). On answer, Pushary POSTs your callbackUrl:
{ "correlationId": "b1c9...", "answer": "yes", "answeredAt": "2026-07-12T18:03:00Z" }Verify it. X-Pushary-Signature is HMAC-SHA256 of the raw body with your own
webhook secret. Fetch it once (decisions.getWebhookSecret()), cache it, and check
with verifyWebhookSignature(rawBody, header, secret). Rotate with
decisions.rotateWebhookSecret().
Poll (fallback). GET /decisions/{id}?wait=30 long-polls up to N seconds and
reads durably, so it resolves even after the live window closes.
Always implement poll as well as webhook. Webhook for speed, poll so a dropped delivery never strands a run.
Answer from your own app (headless)
If you render the approval in your own authenticated UI instead of letting Pushary deliver it, relay the user's choice from your backend:
await pushary.decisions.answer(decisionId, "yes") // confirm: yes/no; select: the option; input: textIt resolves the decision exactly as the hosted page would (fires your webhook, unblocks a poll, writes the audit record). You authenticate the user; we trust your key.
If nobody answers
The decision expires at expiresInSeconds. Your workflow decides what that means
(proceed, block, escalate). Handle the expired status; never hang a run forever.
What people get wrong
- Blocking a serverless agent on a human. It times out. Use create-and-resume.
- Reachability is a real step. No channel or
identify, no ask. - Who is answering? The hosted page link is bearer auth. For anything sensitive, have the user answer inside your own authenticated app and relay it.
- Verify webhooks. Never act on an unverified callback.
- Approval fatigue. Ask only for consequential actions; auto-allow the rest.
- Idempotency is not optional. Agents retry. Always send an
Idempotency-Key.
Limits
Partner plan: up to 25,000 end-users and 100,000 notifications/month, 365-day retention on the decision log. A decision stays open up to 24h; up to 200 pending at once per site.