webhookUrl when you create a checkout or a payment link, or set one org-wide in the dashboard (Settings → Developers → Webhooks). A checkout’s own webhookUrl wins if set, then its payment link’s, then your org default, whichever is found first is the only one that fires, they don’t all fire.
Events
checkout.session.completed
checkout.session.completed
Fired when a checkout’s payment is confirmed, on any rail (card, wallet, stablecoin). Resolved via the checkout’s own webhook, then its payment link’s, then your org default.
send.completed
send.completed
Fired when an outbound on-chain send you initiated is confirmed. Only your org-level webhook URL is checked for this event (there’s no per-checkout scope for outbound sends).
send.failed
send.failed
Fired when an outbound send’s broadcast fails. Same org-level-only resolution as
send.completed.checkout.session.completed
string | null
The payment link’s
secure_link_id, null for a standalone checkout.string
The checkout’s public
session_id.string
Currency units, as a string (
"49.99"), not a number. See Money model.string
string
card, sepa, bank_transfer, bridge, or wallet. How the buyer actually paid.string | null
On-chain hash.
null for card/bank rails, use vendor_reference instead.string | null
Off-chain audit reference (card-processor payment reference, bank reference).
null for on-chain rails.string | null
On-chain payer address.
null for card/bank rails.string
human or agent.string
CAIP-2 network ID for on-chain rails, or
stripe for card/bank.boolean
true on a test-mode checkout.number | null
Set when a bank-transfer buyer sent more than the amount due (beyond a 1-cent tolerance).
null otherwise.object
Whatever you set on the checkout (or its link), unchanged.
send.completed
send.failed
Identical shape to send.completed, with tx_hash: null.
The raw payload above is the literal snake_case JSON body AgentaOS
POSTs to your webhookUrl, this is what you’ll parse in any language other than the SDK. agentaos.webhooks.verify() (Node.js) parses it and returns a camelCased, typed object instead: linkId, sessionId, txHash, payerType, etc. rail, vendor_reference, testnet, and overpaid_by_cents are additive fields not yet reflected in the SDK’s TypeScript types, they arrive on the wire regardless of language.Delivery
- Delivered as
POST,Content-Type: application/json, body is the exact JSON shown above. - A
10second timeout per attempt, up to3attempts total, exponential backoff between retries. - Redirects are not followed (a
3xxresponse counts as a failed attempt). - URLs resolving to private or internal addresses are rejected before any delivery attempt (SSRF guard).
- Return
2xxquickly. Do slow work (emails, fulfillment) asynchronously after responding, a slow handler risks the delivery timing out and retrying.
Signature verification
Every delivery includes anX-AgentaOS-Signature header:
Get your signing secret (
whsec_...) from app.agentaos.ai → Settings → Developers → Webhooks → Reveal signing secret. It’s stable across URL changes; rotate it from the same screen if it’s ever exposed.
- SDK (Node.js)
- Manual (any language)
Best practices
Don't trust the redirect
A checkout’s
successUrl is best-effort, the buyer’s browser might close first. Webhooks are the source of truth for “did this actually get paid.”Handle idempotently
A retried delivery can arrive more than once. Deduplicate on
id (the event id) or data.session_id/data.transaction_id.Respond fast
Return
200 immediately, then do the slow work. A handler that blocks risks a retry and a duplicate delivery.Verify before you trust
Never act on the raw body without checking
X-AgentaOS-Signature first, anyone can POST to a public URL.Next steps
Checkouts
Set
webhookUrl when creating a checkout.Payment Links
Set a default
webhookUrl for every checkout created from a link.