Skip to main content
webhooks.verify() runs in your process and makes no network call. It checks the payload’s HMAC-SHA256 signature against your webhook secret, rejects a stale or malformed signature, and returns the parsed event with camelCase keys. Set the destination URL and copy the signing secret in Settings → Developers → Webhooks, or set webhookUrl on a checkout or link.

webhooks.verify(payload, signature, secret, toleranceSec?)

The samples on this page run on Express 4 and 5.
string | Buffer
required
The raw, unparsed request body. If it’s a Buffer, it’s decoded as UTF-8 before verifying.
string
required
The x-agentaos-signature header value.
string
required
Your webhook signing secret (whsec_...).
number
default:"300"
Max age of the signature’s timestamp, in seconds. A signature older than this throws WebhookVerificationError. A timestamp in the future throws too, by any amount: if your server’s clock runs behind AgentaOS’s, every event fails, so keep the clock synced (NTP).
Returns a typed WebhookEvent. Throws WebhookVerificationError on any failure.

Signature format

The header is t=<unix_timestamp>,v1=<hmac_hex>. verify():
  1. Parses t and v1 out of the header, throws if either is missing.
  2. Checks now - t is between 0 and toleranceSec seconds, throws Webhook signature expired if not (this rejects both stale replays and clock-skewed future timestamps).
  3. Recomputes HMAC-SHA256(secret, "${t}.${payload}") and compares it to v1 with crypto.timingSafeEqual (constant-time, no early-exit on mismatch).
  4. JSON.parses the payload and deep-transforms it from snake_case to camelCase before returning it as a WebhookEvent.
Every failure at any step throws WebhookVerificationError. verify() never returns a partial or unverified event.

WebhookEvent

A discriminated union on type. switch/narrow on it to get typed data.
'checkout.session.completed' | 'send.completed' | 'send.failed' | 'subscription.created' | 'subscription.renewed' | 'subscription.payment_failed' | 'subscription.updated' | 'subscription.canceled' | 'dispute.created' | 'dispute.closed' | 'account.updated' | 'webhook.test'
string
The id of the business the event happened in: your own, or one of the businesses you manage. Present on every event.
The wire JSON also carries an id for each event, but the WebhookEvent type does not declare it yet. Deduplicate on the keys in Deduplication. Amounts follow the money model: amount in data is a string, and amountMinor is an integer in minor units.
verify() camelCases every key, including the keys inside metadata. Use camelCase keys in metadata: a key like user_id comes back as userId.

checkout.session.completed

Fires when a checkout is paid or a free trial starts. The full field list, with the wire names, is in Events.
The payment link’s public token from its checkoutUrl, not its id. null for a checkout created without a link, although the type says string.
string
string
e.g. "49.99".
string
The currency the buyer was charged in: the fiat currency (EUR, USD) for a card or bank checkout, the token (USDC) for a wallet checkout.
string
On-chain settlement hash. null for a card or bank sale, although the type says string.
string
Payer wallet address. null for a card or bank sale, although the type says string.
'human' | 'agent'
string
card, bank_transfer, or a CAIP-2 network ID for a stablecoin payment.
boolean
true in live mode, false in test mode.
object
The metadata you set on the checkout or payment link, with camelCase keys. metadata.customerId is your own id if you set one. On a renewal, metadata is AgentaOS’s own: see Event order for subscriptions.

Fields the type does not declare yet

verify() also returns rail, vendorReference, customerId, buyerEmail, buyerName and overpaidByCents on every checkout event, and trial and trialEnd when a free trial starts. customerId is AgentaOS’s id for the buyer, not your user id. CheckoutCompletedData does not declare these fields yet, so read them through a wider type:
metadata.trial is also true on a trial start, and needs no cast.

send.completed

Fires when an outbound send (a payout or transfer you initiated) is broadcast successfully.
string
string
string
string
string
string
number
string
boolean
true in live mode, false in test mode.
string | null

send.failed

Same shape as send.completed, but txHash is always null and no broadcast succeeded.
string
null
string
string
string
string
number
string
boolean
true in live mode, false in test mode.
string | null

subscription.*

All five subscription.* events carry the same SubscriptionData, delivered to your account’s webhook URL. For when each one fires, and in what order, see Event order for subscriptions.
string
The subscription’s id, a UUID: the id that subscriptions.list() returns.
string
The status after the change, one of the subscription statuses. A subscription that never activated sends no event, so incomplete and incomplete_expired do not appear.
string | null
The plan name, from the backing payment link.
string
number
Per-cycle price in integer minor units. 1000 means €10.00.
string | null
ISO 8601 timestamp, or null before the first cycle is set.
boolean
true once a cancellation is scheduled for period end.
The current plan’s payment link id, a UUID. Not the token in the checkout URL that checkout.session.completed sends as linkId.
object | null
Null unless a downgrade is scheduled. Then { linkId, planName, amountMinor, effectiveAt }: the plan the subscription switches to at effectiveAt. subscription.updated fires when it is scheduled, reverted, and when it takes over.
string | null
string | null
boolean
true in live mode, false in test mode.

dispute.created / dispute.closed

A buyer’s bank opened a chargeback on a sale, or closed it in your favor. A lost dispute sends no event. Both carry the same DisputeData.
string
string
The dispute’s status as the card processor reports it, e.g. needs_response, or won on dispute.closed.
string
The sale the dispute is about.
number
Integer minor units. 1240 means €12.40.
string
boolean
true in live mode, false in test mode.

account.updated

A business’s verification changed: its identity check landed, or we approved, held, resumed or rejected it, or asked it for changes. For a platform, business names the client it happened to. See Platform webhooks.
'unverified' | 'in_review' | 'verified' | 'on_hold' | 'rejected'
Where the business’s review stands, in the words the dashboard uses.
boolean
We have asked the business something and are waiting on it.
boolean
The identity check is done: its own, or its company’s for another app of the same company.
boolean
Always true: verification is about live money.

webhook.test

The sample event the dashboard’s Send test event button fires at your endpoint.
string
boolean
true in live mode, false in test mode.
The customer can close the browser before the successUrl redirect lands. Fulfill orders from the webhook, which your server receives directly, not from the redirect.
Record a key per event, such as event.data.sessionId or event.data.transactionId, and skip an event you have already handled. The key for each event type is in Deduplication.
If your endpoint is down, poll checkouts.retrieve() for status === 'completed' as a backstop. Do not build your main flow on polling: it is slower, and every call counts against your rate limit.

Next steps

Checkouts

Set webhookUrl to receive checkout.session.completed.

Errors

WebhookVerificationError and the rest of the error hierarchy.