Before you start
- Node.js 20.6 or later, and
@agentaos/payand Express installed (npm install @agentaos/pay express). The samples run on Express 4 and 5. - A URL AgentaOS can reach over HTTPS. AgentaOS does not deliver to
localhostor a private address, so for local development run a tunnel service that giveshttp://localhost:3000a public HTTPS address. - A payment link or checkout to test against. See Accept your first payment if you don’t have one yet.
1
Register a webhook URL and reveal the signing secret
In app.agentaos.ai, open Settings → Developers → Webhooks, enter the HTTPS URL you want events sent to, for example
https://myshop.com/webhooks, and click Save. Click Reveal signing secret, copy the whsec_... value, and add it to your server’s environment as AGENTAOS_WEBHOOK_SECRET. Never commit it, never log it, never send it to the client.2
Set up the raw-body route
Signature verification signs the raw request body. If a JSON body parser runs first, the reserialized body won’t byte-for-byte match what was signed, and every event will fail verification. Mount
express.raw() on the webhook route only, and keep express.json() for everything else.3
Verify the signature
webhooks.verify() parses the t=...,v1=... header, rejects a timestamp older than 5 minutes or in the future, recomputes the HMAC-SHA256 digest in constant time, and returns a typed WebhookEvent. If any check fails, it throws WebhookVerificationError. Keep your server’s clock synced (NTP): a clock that runs behind rejects every event.4
React to checkout.session.completed, idempotently
Key your fulfillment logic on
event.data.sessionId, and check whether you’ve already processed it before doing anything. Retries mean the same event can arrive more than once, so your handler must be safe to run twice.metadata holds what you set on checkouts.create, so metadata.customerId is your own user id. verify() camelCases every key, including the keys inside metadata: use camelCase keys in metadata, because a key like user_id comes back as userId. The event’s customerId field is different: it is AgentaOS’s own id for the buyer, not yours. A subscription renewal fires this event without your metadata. See Event order for subscriptions and Sell a subscription.event.data.amount is a string, such as "49.99". See the money model.5
Respond 200 quickly
Verify, queue or record the event, then respond. Do slow work, emails, external API calls, outside the request so AgentaOS doesn’t time out waiting for you.
Full example
webhook-server.ts
Test it end to end
1
Start your server
Save the example as
webhook-server.ts, put AGENTAOS_API_KEY and AGENTAOS_WEBHOOK_SECRET in a .env file, and run npx tsx --env-file=.env webhook-server.ts. Make sure your tunnel or production URL points at it.2
Create a checkout with a webhookUrl
Use the link or checkout from Accept your first payment, or create a new one with
webhookUrl set to your endpoint.3
Pay with the test card
4242 4242 4242 4242, any future expiry, any CVC, typed directly into the hosted checkout page.4
Watch your server log the order as paid
You should see
Order <sessionId> paid: 49.99 EUR in your logs within seconds of the payment clearing.Verify it worked
- Your endpoint returned
200for the delivery. The event list in Settings → Developers → Webhooks shows each delivery’s status and response code. - Your logs show exactly one fulfillment for that
sessionId, even if AgentaOS retries the delivery. - An invalid or missing
X-AgentaOS-Signatureheader gets rejected with400, not silently processed. Try POSTing a fake payload without a valid signature to confirmwebhooks.verify()throws as expected.
Next steps
Event reference
Every event type, its full payload, and a JSON example.
Webhooks (concept)
Manual verification in Python, Go, and PHP, plus retry and delivery details.
Accept your first payment
Create the checkout that triggers this handler.
Payouts
What happens to your balance after the payment lands.