Verified, signed webhook delivery
Verify ownership
A destination must be a public HTTPS URL on port 443. Verification POSTs {"type":"endpoint.verification","challenge":"UNPREDICTABLE_NONCE"}. Return a 2xx application/json response containing the identical challenge within five seconds. Challenges expire after five minutes and are single use. Redirects are not followed. Save the signing secret returned when creating a destination.
Verify signatures before parsing
Delivery sends the exact retained event bytes. Headers: RerunLab-Event-Id, RerunLab-Delivery-Id, RerunLab-Timestamp (Unix seconds), and RerunLab-Signature (v1=hex). Compute HMAC-SHA256(secret, timestamp + "." + rawBody), compare in constant time, and reject timestamps older than five minutes or implausibly in the future. Preserve the raw body; reserializing JSON changes the signature.
import crypto from 'node:crypto';
export function verify(rawBody, headers, secret) {
const timestamp = headers['rerunlab-timestamp'];
if (!/^\d+$/.test(timestamp ?? '')) return false;
if (Math.abs(Date.now()/1000 - Number(timestamp)) > 300) return false;
const expected = Buffer.from('v1=' + crypto.createHmac('sha256', secret)
.update(timestamp + '.').update(rawBody).digest('hex'));
const supplied = Buffer.from(headers['rerunlab-signature'] ?? '');
return supplied.length === expected.length && crypto.timingSafeEqual(supplied, expected);
}
Acknowledge and deduplicate
Durably accept an event, then return 2xx quickly. Delivery is at least once: keep a unique constraint on event.id and acknowledge duplicates without repeating effects. An acknowledgement proves the receiver accepted the HTTP request; it does not prove a downstream database or business workflow completed.
Retries and replay
Up to eight attempts within 24 hours, scheduled around 0, 30 seconds, 2 minutes, 10 minutes, 1 hour, 6 hours, 12 hours and 23 hours from creation. Positive jitter avoids bursts. HTTP 408/425/429, 5xx and network failures retry. Other 4xx and redirects fail permanently. Retry-After is never shortened; a retry beyond the deadline fails. The overall request deadline is ten seconds and receiver response bodies are capped at 8 KiB.
Replay creates a new delivery ID with the original event bytes and event ID. It uses the original immutable destination URL revision. Editing a URL creates a new revision that must be verified for future events; already queued deliveries remain pinned. Deleting a destination cancels pending deliveries. Secret rotation has a 24-hour previous-secret overlap for receivers; keep both during that period.
Python verification example
import hashlib, hmac, re, time
def verify(raw_body, headers, secrets):
stamp = headers.get('rerunlab-timestamp', '')
signature = headers.get('rerunlab-signature', '')
if not re.fullmatch(r'[0-9]+', stamp):
return False
if abs(time.time() - int(stamp)) > 300:
return False
message = stamp.encode('ascii') + b'.' + raw_body
return any(hmac.compare_digest(
'v1=' + hmac.new(secret.encode(), message, hashlib.sha256).hexdigest(),
signature) for secret in secrets)
Keep the current and previous secret during rotation. Executable Node and Python helpers are in examples/polling and are tested against the same signed bytes, stale timestamps and modified bodies. Verification alone does not deduplicate: save the event ID with a unique constraint in the same transaction as enqueueing your work.