What you will wire
A verified webhook endpoint for an AI agent, so your systems react the moment the agent is paid, pays, or its wallet goes live. You will create the webhook, verify every delivery, branch on the event type, and rotate the secret safely. By the end you have an endpoint that only acts on genuine, fresh events.
This is the dedicated webhook guide. Webhooks also show up inside the receive flow (how-to-receive-payments-as-ai-agent) and at wallet creation (how-to-add-wallet-to-my-agent); here the webhook itself is the subject. The payment API product page is the broader reference.
Why an agent needs webhooks
An agent's money moves asynchronously. A payment settles on chain a moment after the request, a wallet deploys a few seconds after you create the agent, and an inbound payment can arrive at any time. Polling for these is slow and wasteful. A webhook is the push: Blockchain0x calls your endpoint the instant something happens, so your system reacts in real time rather than on a timer.
For an agent specifically, that matters because the events drive logic. A payment.received might release a result or credit a balance; a payment.sent confirms an outbound payment cleared; a wallet.deployed tells you the agent is ready to transact. Without webhooks you are guessing when these happened; with them you know, and you act on the same record the platform has. The alternative, polling an endpoint on a timer, is both slower and more expensive: you either poll too often and waste calls, or too rarely and react late, and either way you are asking repeatedly for an answer the webhook would have pushed to you the instant it changed.
Prerequisites
- A Blockchain0x account with an agent created.
- A
sk_test_API key and a public HTTPS endpoint to receive deliveries (a tunnel like a dev proxy works for local testing). - Node 18 or newer. The SDK targets
>=18.
export B0X_API_KEY=sk_test_...Create the webhook
Register your endpoint and the events you want. Store the signingSecret it returns; it is shown only once.
import { createClient } from "@blockchain0x/node";
const client = createClient({ apiKey: process.env.B0X_API_KEY! });
const wh = await client.webhooks.create({
url: "https://api.your-app.com/webhooks/blockchain0x",
events: ["payment.received", "payment.sent", "wallet.deployed"],
});
console.log(wh.id);
console.log(wh.signingSecret); // STORE THIS NOW - returned only oncePut wh.signingSecret straight into your secret manager as B0X_WEBHOOK_SECRET. If it is ever lost, you rotate to get a new one rather than recovering the old.
Verify every delivery
Never act on a delivery you have not verified. Capture the raw body and pass it, the headers, and the secret into webhooks.verify.
import express from "express";
import { webhooks } from "@blockchain0x/node";
const app = express();
app.use(express.raw({ type: "application/json" })); // raw bytes, not parsed JSON
app.post("/webhooks/blockchain0x", (req, res) => {
const result = webhooks.verify({
headers: req.headers,
rawBody: req.body, // the Buffer of exact bytes
secret: process.env.B0X_WEBHOOK_SECRET!,
});
if (!result.ok) {
return res.status(400).json({ code: result.code }); // one of the dotted failure codes
}
// result.eventType, result.eventId, result.deliveryId are now populated.
handleEvent(result, JSON.parse(req.body.toString("utf8")));
return res.status(200).end();
});The verifier compares the HMAC in constant time, accepts both the structured t=,v1= signature and a bare-hex one, and rejects deliveries whose timestamp is more than five minutes old. A failed verify gives you a result.code naming exactly what was wrong, so log it.
Handle the events
Branch on result.eventType. Treat each event as a signal and keep the handler small and idempotent.
function handleEvent(result, payload) {
switch (result.eventType) {
case "payment.received":
creditAgent(payload); // the agent was paid
break;
case "payment.sent":
confirmOutbound(payload); // an outbound payment cleared
break;
case "wallet.deployed":
markAgentReady(payload); // the agent's wallet is live
break;
}
}Make each branch idempotent, keyed by result.eventId or result.deliveryId, and return 2xx only after you have durably recorded the event. Deliveries can repeat, and the five-minute replay window guards stale signatures, not your own double-processing, so dedupe on the id.
How the signature works
You normally just call webhooks.verify, but knowing what it checks helps when a delivery fails. Each delivery carries an X-Blockchain0x-Signature header, either in the structured form t=<unix>,v1=<hex> or as a bare hex signature, with the timestamp then coming from the dedicated X-Blockchain0x-Timestamp header. The signature is HMAC-SHA256 of timestamp.rawBody using your signing secret, hex-encoded.
webhooks.verify recomputes that HMAC over the bytes you pass and compares it in constant time, which is why the raw body matters: any change to the bytes changes the HMAC. It also reads the timestamp and rejects anything more than five minutes from its own clock, which is the replay protection. If your servers run with known clock skew, the verifier accepts a toleranceSec override, but the right fix is usually to sync the clock rather than widen the window. The other delivery headers, X-Blockchain0x-Event-Type, X-Blockchain0x-Event-Id, and X-Blockchain0x-Delivery-Id, surface on the verified result as eventType, eventId, and deliveryId.
Reliability and idempotency
Webhook delivery is at-least-once, not exactly-once, so build for repeats. If your endpoint is slow, errors, or returns a non-2xx, the delivery can be retried, and a network hiccup can cause a duplicate even after you processed one. That is normal for any webhook system, and the way to stay correct is idempotency rather than hoping for single delivery.
Key your processing on deliveryId (or eventId for event-level dedupe), record that you handled it durably, and make a second arrival a no-op that still returns 2xx. Return 2xx only after the work is safely recorded, so a crash mid-handler leaves the delivery eligible for a useful retry rather than acknowledged-but-lost. And keep the handler fast: do the minimal durable write inline, then push slower work onto a queue, so you ack within the delivery timeout and let retries cover genuine failures, not slowness.
Rotate the secret and test
Rotate the signing secret on a schedule or after any suspected exposure. Rotation is an immediate cutover, so deploy the new secret before you rely on it.
const rotated = await client.webhooks.rotateSecret(wh.id);
console.log(rotated.signingSecret); // the new secret; update B0X_WEBHOOK_SECRETTo confirm the whole path end to end without waiting for a real payment, send a test delivery and watch your endpoint receive a webhook.test event:
await client.webhooks.test(wh.id, { event: "webhook.test" });A webhook.test that arrives, verifies, and returns 2xx proves your URL, your secret, and your verification are all wired correctly before any real money is in play.
Common pitfalls
Three traps wiring agent webhooks.
Verifying the parsed body. The HMAC is over the raw bytes. Mount the raw-body parser and verify the Buffer, then parse JSON. Parsing first is the most common reason a correct secret still fails to verify.
Losing the signing secret. It is shown only at creation and rotation. Store it immediately; if you miss it, rotate rather than hunt for it.
Non-idempotent handlers. A redelivered event will double-process unless you dedupe on eventId or deliveryId. Record-then-ack, keyed by the id.
What to ship today
Create a webhook for payment.received and wallet.deployed, store the signingSecret, mount a raw-body route that calls webhooks.verify, and send a webhooks.test to confirm the path. Make the handler idempotent before you trust it with real events. For the receive flow that these events drive, see how-to-receive-payments-as-ai-agent. Pricing is on the pricing page.