Webhooks#
Every alert in the catalogue — failed verifications, stalled checkpoints, archived streams, API key changes, completed exports — can be delivered to your systems as a signed HTTPS POST. Endpoints are managed on the Webhooks page inside the app; this page documents the payload shape and how to verify the signature.
Delivery#
Deliveries are JSON POSTs to the https endpoint you configure. Failed deliveries are retried with backoff (1m, 5m, 30m, 2h, 8h); after the final attempt the delivery is marked failed in your delivery log, where it can be redelivered manually. Ten consecutive failed deliveries disable the endpoint and email your owners.
An endpoint can be scoped to selected streams. Alerts that are about a stream — verification outcomes, stalled checkpoints, exports — are then delivered only when their stream is in the endpoint's scope; tenant-wide alerts (usage thresholds, API key changes) always deliver. Like an API key's scope, an endpoint's scope is fixed at creation: streams added later stay outside it.
Each request carries two headers:
Sigilbase-Event: verification.failed
Sigilbase-Signature: t=1720512000,v1=a1b2c3d4e5f6…
Payload#
The body is stable across retries and redeliveries — the id field identifies the alert, so your receiver can deduplicate.
{
"id": "0197f3a2-6a2e-7c4b-9b1e-2f6f6f0a1c2d",
"event": "verification.failed",
"created_at": "2026-07-09T02:00:11+00:00",
"tenant": {
"id": "0197f000-1111-7aaa-8bbb-ccccdddd0000",
"slug": "meridian-fintech"
},
"data": {
"stream": { "slug": "prod-admin", "name": "Production Admin Actions" },
"sequence_from": 1,
"sequence_to": 4182,
"failure_reason": "entry hash mismatch at sequence 20"
}
}
Verifying the signature#
The Sigilbase-Signature header is t=<unix timestamp>,v1=<hex HMAC>. The HMAC is SHA-256 over the string <timestamp>.<raw request body> keyed with your endpoint's signing secret (shown once at creation). The header may carry more than one v1 — it does for 24 hours after a secret rotation — and a delivery is authentic if any v1 matches. Compute yours, compare in constant time, and reject stale timestamps:
$secret = getenv('SIGILBASE_WEBHOOK_SECRET');
$payload = file_get_contents('php://input');
$header = $_SERVER['HTTP_SIGILBASE_SIGNATURE']; // t=1720512000,v1=a1b2c3…[,v1=…]
$timestamp = null;
$signatures = [];
foreach (explode(',', $header) as $part) {
[$key, $value] = explode('=', $part, 2);
if ($key === 't') {
$timestamp = $value;
} elseif ($key === 'v1') {
$signatures[] = $value;
}
}
$expected = hash_hmac('sha256', $timestamp . '.' . $payload, $secret);
$matches = false;
foreach ($signatures as $signature) {
$matches = $matches || hash_equals($expected, $signature);
}
if (! $matches) {
http_response_code(400); // signature mismatch
exit;
}
if (abs(time() - (int) $timestamp) > 300) {
http_response_code(400); // stale: possible replay
exit;
}
http_response_code(200); // any 2xx marks the delivery successful
The scheme is the same one Stripe uses, deliberately — existing verification middleware for it works unchanged apart from the header name.
Rotating the secret#
The Webhooks page can rotate an endpoint's signing secret without recreating the endpoint. The new secret is revealed once, exactly like at creation. For 24 hours after the rotation every delivery carries two v1 signatures — one from each secret — so a receiver verifying against either passes throughout the switchover; after the window the old secret stops signing. Rotations are recorded on your sigilbase-system stream.
Chat formats#
An endpoint's format decides how deliveries are serialised. standard (the default) is the envelope documented above. slack renders the same events as Block Kit messages and teams as Adaptive Cards, so an endpoint can point straight at a Slack or Teams incoming-webhook URL with no glue code. Everything else is identical for every format: the same events, the same retries and delivery log, the same SSRF discipline, and the same signature — computed over the transformed body, exactly the bytes delivered (chat platforms ignore the header; it is there so you can verify a capture if you ever need to).
verification.failed arrives red-accented with the stream and sequence range; every other event is a single terse block. The format is chosen per endpoint at creation, and a tenant's two endpoints can use different formats — say, standard into your own systems and slack into the on-call channel.