Skip to content

Webhooks & delivery

BatchRouter delivers a signed webhook to your endpoint when a batch reaches a terminal status, so you don’t have to poll. This guide shows how to register a webhook (per-batch or as an org-wide default), verify the X-BatchRouter-Signature HMAC, write an idempotent handler, and fall back to polling.

You can register a delivery endpoint in two ways. A per-batch webhook always overrides the org-level default for that batch.

Pass a webhook object with url and secret when you create the batch. The URL must be HTTPS; the secret is 8–256 characters and is used to sign every delivery.

Terminal window
curl -X POST https://api.batchrouter.com/v1/batches \
-H "Authorization: Bearer $BATCHROUTER_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: batch-2026-06-18-001" \
-d '{
"quote_id": "qlock_abc123",
"items": [
{"customer_item_id":"item-1","operation":"responses","model":"gpt-4o-mini","input":{"messages":[{"role":"user","content":"Summarize: BatchRouter routes batch-AI workloads across providers."}]}}
],
"webhook": {
"url": "https://example.com/hooks/batchrouter",
"secret": "a-long-random-shared-secret"
}
}'

Set a default endpoint once with PUT /v1/auth/account/delivery-webhook. Every batch you create without its own webhook block delivers here. Same constraints apply: HTTPS url, 8–256-char secret.

Terminal window
curl -X PUT https://api.batchrouter.com/v1/auth/account/delivery-webhook \
-H "Authorization: Bearer $BATCHROUTER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/hooks/batchrouter",
"secret": "a-long-random-shared-secret"
}'

Every webhook request carries an X-BatchRouter-Signature header: the HMAC-SHA256 of the raw request body, keyed with your webhook secret, hex-encoded. Compute the same HMAC over the bytes you received and compare in constant time. Never parse or trust the JSON before the signature checks out.

import express from "express";
import crypto from "node:crypto";
const SECRET = process.env.BATCHROUTER_WEBHOOK_SECRET;
const app = express();
// Capture the RAW body — required for an accurate HMAC.
app.use(express.raw({ type: "application/json" }));
app.post("/hooks/batchrouter", (req, res) => {
const signature = req.get("X-BatchRouter-Signature") ?? "";
const expected = crypto
.createHmac("sha256", SECRET)
.update(req.body) // req.body is a Buffer of the raw bytes
.digest("hex");
const sigBuf = Buffer.from(signature);
const expBuf = Buffer.from(expected);
if (sigBuf.length !== expBuf.length || !crypto.timingSafeEqual(sigBuf, expBuf)) {
return res.status(401).send("invalid signature");
}
const event = JSON.parse(req.body.toString("utf8"));
// ... handle event.batch (id, status, …)
res.sendStatus(200);
});

BatchRouter retries failed deliveries (non-2xx responses or timeouts) with backoff, so the same event can arrive more than once. Your endpoint must be safe to call repeatedly.

  1. Return 2xx fast. Acknowledge with 200 once the payload is verified and persisted. Do heavy work asynchronously — a slow handler looks like a failure and triggers a retry.

  2. Dedupe by batch + status. Key processing on the batch.id and status (and your own delivery record) so a redelivered event is a no-op. Treat the webhook as a signal to act, not the sole source of truth.

  3. Fetch authoritative state. On receipt, call GET /v1/batches/{batchId} to confirm the current status before acting on results.

You can inspect delivery attempts, retry state, and last-failure details for a batch with GET /v1/batches/{batchId}/webhooks — useful when an endpoint was down and you need to see whether a delivery was retried or dead-lettered.

Webhooks are optional. If you don’t configure one, polling is the default path — and it’s a good backstop even when webhooks are on (for example, if your endpoint had an outage).

  1. Poll GET /v1/batches/{batchId} until status is terminal: completed, failed, cancelled, or expired.

  2. On completed, read results with GET /v1/batches/{batchId}/results (paginate with ?limit and ?cursor), or for large batches get a signed file via GET /v1/batches/{batchId}/artifact-url.