Webhooks
Webhooks are how your backend learns about things that happen asynchronously — sessions ending, recordings finishing post-processing, audit chains anchoring at the TSA. Register a URL, verify the signature on every POST, dispatch on event type.
Event types
| Event | When |
|---|---|
session.created | New session via SPA or public API |
session.completed | Session ended (manually or via API) |
participant.joined | Field user (or observer) joined a session |
participant.left | Participant disconnected |
evidence.added | New snapshot / annotation / whiteboard / clip / document captured |
recording.ready | Post-processed recording artefacts available |
audit.anchored | Chain head anchored at one of the configured TSAs |
signature.completed | A signed PDF (SES/AES/QES) is ready for download |
webhook.test | Fired by the "Test fire" button in the SPA admin, with a tiny synthetic payload |
Envelope shape
{
"id": "evt_01HGB9...",
"type": "session.completed",
"created_at": "2026-05-23T11:42:15Z",
"org_id": 42,
"data": {
/* event-specific payload — full resource shape */
}
}
The id is stable across retries — use it as your
idempotency key on the receiver side.
Signature header
Every POST carries a NB-Signature header in Stripe-style format:
NB-Signature: t=1716461235,v1=5257a8...3e2c1f
The v1 value is HMAC-SHA256(secret, "{timestamp}.{body}") in hex. The t
value is a Unix timestamp at signing time. A 5-minute skew window
rejects replays of older payloads.
Verifying in code
Node
import { NexBasira, InvalidSignatureError } from "@nexbasira/node";
const nb = new NexBasira({ apiKey: "...", apiSecret: "..." });
// Express handler — important: use express.raw() so the body is the
// untouched bytes the signature was computed over.
app.post("/nb-webhook", express.raw({ type: "application/json" }), (req, res) => {
const sig = req.header("NB-Signature")!;
const secret = process.env.NB_WEBHOOK_SECRET!;
let event;
try {
event = nb.webhooks.constructEvent(req.body, sig, secret);
} catch (err) {
if (err instanceof InvalidSignatureError) {
return res.status(401).send("bad signature");
}
throw err;
}
// event is now type-narrowed by event.type
return handleEvent(event, res);
}); Python
from nexbasira import NexBasira, InvalidSignatureError
nb = NexBasira(api_key="...", api_secret="...")
# Flask handler — get the raw body, not request.get_json()
@app.post("/nb-webhook")
def webhook():
sig = request.headers["NB-Signature"]
secret = os.environ["NB_WEBHOOK_SECRET"]
try:
event = nb.webhooks.construct_event(request.data, sig, secret)
except InvalidSignatureError:
return ("bad signature", 401)
return handle_event(event) Retry behaviour
A delivery is considered successful when your endpoint returns a 2xx within 30s. Anything else triggers exponential backoff:
30s → 5m → 1h → 6h → 24h → DROPPED After 50 consecutive failures across all events, the endpoint auto-disables. The org admin can re-enable from the SPA admin once the receiver is back.
Idempotency on your side
Webhook deliveries can repeat. Treat event.id as the
deduplication key:
-- Postgres example
INSERT INTO webhook_deliveries (event_id, processed_at)
VALUES ($1, NOW())
ON CONFLICT (event_id) DO NOTHING
RETURNING event_id;
-- If RETURNING is empty, this is a duplicate — skip the side-effect. Signing secrets
Each registered webhook endpoint has its own whsec_* signing secret.
The plaintext is shown exactly once when you register the
endpoint (or rotate it); after that we store a Fernet-encrypted copy and
surface only the first 6 chars for identification.
Rotate from SPA → Admin → Webhooks → Rotate secret. Existing receivers will reject signed events until they're updated with the new secret — schedule the rotation with a deploy window.
Test fire
Every registered endpoint has a "Test fire" button in the SPA admin
that ships a webhook.test envelope so you can verify your
receiver before going live. The test envelope has the same shape as a
real event but with a synthetic payload tagged
"test": true in data.
Delivery log
SPA → Admin → Webhooks shows the last 100 deliveries per endpoint with HTTP status, attempts, last response body (truncated), and timestamps. Filter by status / endpoint to debug receiver issues.