Skip to main content
HomeLanding pagesHow to add a webhook to an AI agent
LANDING PAGE

How to add a webhook to an AI agent

8 min read·Last updated June 2, 2026

Create a webhook with client.webhooks.create({ url, events }) and store the signingSecret it returns once. On each delivery, verify it with webhooks.verify against the raw body and B0X_WEBHOOK_SECRET, then branch on result.eventType (payment.received, payment.sent, wallet.deployed, webhook.test). Rotate the secret with rotateSecret and send a test event with webhooks.test. The verifier also blocks replays older than five minutes.

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.
BASH
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.

TYPESCRIPT
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 once

Put 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.

TYPESCRIPT
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.

TYPESCRIPT
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.

TYPESCRIPT
const rotated = await client.webhooks.rotateSecret(wh.id);
console.log(rotated.signingSecret); // the new secret; update B0X_WEBHOOK_SECRET

To 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:

TYPESCRIPT
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.

FAQ

Frequently asked questions.

What events does an agent webhook deliver?

The shipped events are payment.received (the agent was paid), payment.sent (the agent paid), wallet.deployed (the agent's wallet went live), and webhook.test (a manual test delivery). You subscribe by listing the events you want when you create the webhook, and branch on result.eventType in your handler.

How do I verify a delivery is genuine?

Call webhooks.verify with the request headers, the raw body bytes, and your B0X_WEBHOOK_SECRET. It returns a discriminated result: on result.ok you get eventType, eventId, and deliveryId; on failure you get a result.code naming the reason. It compares the HMAC in constant time and rejects deliveries more than five minutes old to stop replays.

Why must I use the raw body?

The signature is an HMAC over the exact bytes that arrived. If a JSON middleware parses and re-serializes the body before you verify, the bytes change and the signature will not match. Capture the raw body (for example express.raw) and pass that Buffer into webhooks.verify, then parse the JSON afterwards.

Where does the signing secret come from?

client.webhooks.create returns signingSecret once, and rotateSecret returns a new one. It is shown only at creation or rotation, so store it immediately in your secret manager as B0X_WEBHOOK_SECRET. If you lose it, rotate to get a fresh one; you cannot read the old one back.

What if my handler is down when an event fires?

Make your handler idempotent and return 2xx only once you have durably recorded the event, keyed by eventId or deliveryId, so a redelivery does not double-process. The five-minute replay window protects against stale signatures, not against your own duplicate handling, so dedupe on the id yourself.

Create your free agent wallet in 5 minutes.

First payment confirmed in under ten minutes. Free to start.