Quickstart
From zero to a human-approved agent action in about five minutes. If you're on Node, use the SDK below. On another stack, jump to raw HTTP; the flow is identical.
1. Get an API key
Sign in — your first login creates a workspace — then open Dashboard → API Keys and create a key. Copy it immediately; keys are stored hashed and shown exactly once.
CONFIRM_API_KEY=cfm_live_...2. Install the SDK
npm i @confirm/sdkThe SDK reads CONFIRM_API_KEY from the environment. Node 18+, ESM, zero runtime dependencies.
3. Pause for a human, then resume
At the moment your agent is about to do something irreversible, call createAndWait. It creates the request, emails the approver, and blocks until a human decides. Use your own email as notify so you get the link.
import { ConfirmClient } from "@confirm/sdk";
const confirm = new ConfirmClient(); // reads CONFIRM_API_KEY
const decision = await confirm.requests.createAndWait({
summary: "Refund $10,000 to customer #4821",
payload: { action: "refund", amount: 10000, customerId: "4821" },
notify: "you@yourcompany.com",
reasoning: "Ticket #5521 reports a billing error; order history shows a $10.00 charge.",
recentActions: ["Read ticket #5521", "Looked up customer #4821: newest order is $10.00"],
ttlMinutes: 60, // fail safe if nobody answers
});
if (decision.status === "APPROVED") {
await issueRefund(decision.effectivePayload); // the human-approved action
} else {
console.log(`Not approved: ${decision.status}`); // REJECTED or EXPIRED
}Run it. The call hangs on purpose: your agent is paused inside createAndWait, polling until the verdict lands.
4. Decide as the approver
Check the inbox you passed in notify and open the review link. Because the request carried reasoning and recentActions, the page shows the agent's trail and its explanation above the payload, then three choices: Approve, Reject, or Edit payload first. Edit the amount to 10.00 and approve, to see corrections flow back.
Within a few seconds createAndWait returns. Because you edited it, decision.effectivePayload.amount is 10.00, and that's what issueRefund runs.
RULE
effectivePayload, never payload. It resolves to the human's edited version when one exists — that's the entire "fix the hallucination before it ships" feature, in one field.5. Gate tools automatically
Calling createAndWait by hand works, but you have to remember to do it at every risky call site. guard() wraps your whole tool layer with policies instead, so a tool nobody wrote a rule for is still gated:
import { guard } from "@confirm/sdk";
const tools = guard(agentTools, {
policies: [
{ when: (t) => t.name === "refund" && t.args.amount > 100, notify: "finance@acme.com" },
{ when: (t) => t.name === "search", decision: "allow" }, // read-only, never gated
],
default: "require", // fail-closed: unmatched tools still need a human
});See the SDK reference for guard(), withApproval(), and webhook verification, or the Vercel AI SDK guide for wrapping real tool() definitions. MCP agent? The MCP server adds an approval tool with no code.
Prefer raw HTTP?
No SDK required. Create a request with a POST, then poll the returned id (or skip polling with a webhook, step below).
# create
curl -X POST https://confirm.dev/api/v1/requests \
-H "Authorization: Bearer $CONFIRM_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"summary": "Refund $10,000 to customer #4821",
"payload": { "action": "refund", "amount": 10000, "customerId": "4821" },
"notify": "you@yourcompany.com",
"reasoning": "Ticket #5521 reports a billing error; order history shows a $10.00 charge.",
"recentActions": ["Read ticket #5521", "Checked order history"]
}'
# poll (until status leaves PENDING)
curl https://confirm.dev/api/v1/requests/REQUEST_ID \
-H "Authorization: Bearer $CONFIRM_API_KEY"{
"id": "clx…",
"status": "APPROVED",
"payload": { "action": "refund", "amount": 10000, "customerId": "4821" },
"effectivePayload": { "action": "refund", "amount": 10.00, "customerId": "4821" },
"resolvedByEmail": "you@yourcompany.com"
}The full contract is in the API reference.
Go event-driven
For anything longer than a few minutes, don't block. Add an endpoint under Dashboard → Webhooks and we'll POST you request.approved / request.rejected / request.expired the instant a decision lands. Verify it in a few lines with constructEvent from the SDK — see Webhooks.