Skip to main content
agentaos.webhooks makes no network call, verify() runs entirely in your process. It checks a payload’s HMAC-SHA256 signature against your webhook secret, rejects stale or malformed signatures, and returns the parsed, camelCased event. Configure the destination URL and copy the signing secret from Settings → Developers → Webhooks in the dashboard (or set webhookUrl per checkout/link, see Checkouts).

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

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 (or with a timestamp in the future) throws WebhookVerificationError.
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, there’s no partial or “unverified” event returned.

WebhookEvent

A discriminated union on type. switch/narrow on it to get typed data.
string
Unique event ID, evt_<uuid>. Present on every event, both on the wire and on the object verify() returns.
'checkout.session.completed' | 'send.completed' | 'send.failed' | 'subscription.created' | 'subscription.renewed' | 'subscription.payment_failed' | 'subscription.updated' | 'subscription.canceled'
Inside every event’s data, amount is a string, e.g. "49.99", not the number you get back from checkouts.create() or paymentLinks.create(). Parse it before doing math. See the money model.

checkout.session.completed

Fires when a checkout session’s payment confirms.
The payment link’s secure ID.
string
string
e.g. "49.99".
string
string
On-chain settlement hash.
string
Payer wallet address.
'human' | 'agent'
string
CAIP-2 network ID.
boolean
true in live mode, false in test mode.
object
Whatever metadata you passed at checkouts.create().

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 organization webhook endpoint.
string
The subscription ID.
string
Stripe status, verbatim: incomplete, active, trialing, past_due, unpaid, or canceled.
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.
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.
successUrl is best-effort, the customer might close their browser before the redirect lands. Fulfill orders from the webhook (server-to-server, reliable), not the redirect.
Treat delivery as at-least-once. Use event.data.sessionId (or transactionId) to check whether you’ve already processed it before fulfilling again.
If your endpoint is ever down, poll checkouts.retrieve() for status === 'completed' as a backstop. Don’t build your primary flow on polling, it’s slower and burns rate limit budget.

Next steps

Checkouts

Set webhookUrl to receive checkout.session.completed.

Errors

WebhookVerificationError and the rest of the error hierarchy.