Skip to content

Webhook events

BatchRouter delivers webhooks to tell your systems when work changes state — most importantly when a batch finishes — without you polling. Every webhook is an HTTPS POST with a JSON body and an HMAC-SHA256 signature you verify against a shared secret. This page documents the event envelope, the event types, how to verify signatures, how retries work, and how to inspect past deliveries.

For configuring where webhooks go (per-batch versus your org default), see the webhooks guide. For pushing result artifacts to your own S3-compatible bucket instead of receiving a callback, see delivery targets.

A webhook fires to the URL you configure, with the secret you supply at the same time:

  • Per-batch — pass a webhook object ({ "url", "secret" }) in the POST /v1/batches body. This applies to that batch only. The secret must be 8–256 characters.
  • Org default — set a default URL and secret once with PUT /v1/auth/account/delivery-webhook. Every batch that does not override it delivers here, and billing events (below) are delivered here too.

A per-batch webhook overrides the org default for that batch.

Every webhook request carries the same set of headers and a JSON body. Headers are case-insensitive.

HeaderDescription
X-BatchRouter-EventThe event type, e.g. billing.balance_low. Lets you route by type without parsing the body first.
X-BatchRouter-TimestampThe timestamp signed alongside the body. Part of the signed string — do not ignore it.
X-BatchRouter-Signaturebase64url HMAC-SHA256 of {timestamp}.{body}, keyed with your webhook secret.
Content-Typeapplication/json.

The signed message is the timestamp, a literal ., and the raw, unmodified request body — concatenated as {timestamp}.{body}. You must compute the HMAC over the exact bytes you received, before any JSON parsing or re-serialization, or the signature will not match.

Acknowledge receipt by returning any 2xx status. Any non-2xx response (or a timeout) is treated as a failed delivery and scheduled for retry.

When a batch reaches a terminal state, BatchRouter delivers a signed event to the batch’s webhook (or your org default). The body carries the batch identifier and its final status so you can fetch results. Terminal statuses are completed, failed, cancelled, and expired; the live progression is pending → queued → routing → dispatched → processing → completing → completed.

On a successful completion event, fetch the output with GET /v1/batches/{batchId}/results (paginated) or pull a signed download with GET /v1/batches/{batchId}/artifact-url.

For the exact field list of the batch event body, see the interactive API reference and the raw spec.

If you set an org delivery webhook and enable webhook delivery for billing alerts (alert_webhook_enabled via PUT /v1/billing/controls), spending and auto top-up events are delivered to the same URL, signed the same way. Each carries an id prefixed evt_, a created_at, the organization, and a typed billing object.

Spending alertsX-BatchRouter-Event is one of:

Event typeFires when
billing.balance_lowAvailable balance fell below your configured low-balance threshold.
billing.limit_reachedCommitted spend met a daily or monthly limit.
billing.limit_threshold_reachedSpend crossed a configured percent-of-limit or absolute-spend threshold.

Auto top-up outcomesX-BatchRouter-Event is one of:

Event typeFires when
billing.autotopup.succeededAn automatic credit refill succeeded.
billing.autotopup.failedA refill attempt failed.
billing.autotopup.disabledAuto top-up was disabled after repeated failures.

Monetary fields in billing events are decimal USD strings (for example "12.50"). See the API reference BillingAlertEvent and AutoTopupOutcomeEvent schemas for the full per-status shape.

Recompute the HMAC over {timestamp}.{body} with your webhook secret and compare it to the X-BatchRouter-Signature header using a constant-time comparison. Always work from the raw request body — most frameworks parse JSON by default, which changes the bytes and breaks verification.

import crypto from 'node:crypto';
// rawBody must be the exact bytes/string received, NOT a re-serialized object.
function verifyBatchRouterWebhook(rawBody, headers, secret) {
const timestamp = headers['x-batchrouter-timestamp'];
const received = headers['x-batchrouter-signature'];
if (!timestamp || !received) return false;
const expected = crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${rawBody}`)
.digest('base64url');
const a = Buffer.from(received);
const b = Buffer.from(expected);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
// Express example — capture the raw body for this route.
import express from 'express';
const app = express();
app.post(
'/webhooks/batchrouter',
express.raw({ type: 'application/json' }),
(req, res) => {
const rawBody = req.body.toString('utf8');
if (!verifyBatchRouterWebhook(rawBody, req.headers, process.env.BATCHROUTER_WEBHOOK_SECRET)) {
return res.status(400).send('invalid signature');
}
const event = JSON.parse(rawBody);
const type = req.headers['x-batchrouter-event'];
// ... handle event, then fetch authoritative state via the API.
res.sendStatus(200);
},
);

Webhook delivery runs through a durable queue with its own dead-letter queue, so a transient failure on your side does not lose the event.

  • A delivery that does not return 2xx (including timeouts and connection errors) is recorded as failed and retried automatically. Each delivery tracks an attempt_count, the last_response_status, last_error, and the next_attempt_at.
  • After repeated failures a delivery is dead-lettered (status: dead_lettered) and stops retrying.

The platform records these transitions as delivery events — for example webhook.retry_scheduled and webhook.dead_lettered — which you can read back from the inspection endpoint below.

Because deliveries are retried, your handler should be idempotent: the same event may arrive more than once. Deduplicate on a stable identifier (the event id for billing events, or the batch_id plus status for lifecycle events) and make repeated processing a no-op.

Use GET /v1/batches/{batchId}/webhooks to see the customer-visible delivery status, retry state, last failure, and persisted delivery events for a single batch — useful when a callback didn’t arrive and you need to know whether it was attempted.

Terminal window
curl https://api.batchrouter.com/v1/batches/bat_123/webhooks \
-H "Authorization: Bearer $BATCHROUTER_API_KEY"

The response has two arrays:

  • data — one entry per delivery, each with id, batch_id, target url, status (pending, delivering, succeeded, failed, or dead_lettered), attempt_count, last_attempt_at, next_attempt_at, last_response_status, and last_error.
  • events — the persisted timeline of delivery events, each with its id, the event name (such as webhook.retry_scheduled and webhook.dead_lettered), status, webhook_id, batch_id, org_id, and created_at.

See the BatchWebhooksResponse schema in the API reference for the full field list.