LIVE · AUDIT-CHAINED · EU-RESIDENT
SYSTEM · 99.99% UPTIME
v 1.0 ↗ MADE IN EU

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

EventWhen
session.createdNew session via SPA or public API
session.completedSession ended (manually or via API)
participant.joinedField user (or observer) joined a session
participant.leftParticipant disconnected
evidence.addedNew snapshot / annotation / whiteboard / clip / document captured
recording.readyPost-processed recording artefacts available
audit.anchoredChain head anchored at one of the configured TSAs
signature.completedA signed PDF (SES/AES/QES) is ready for download
webhook.testFired 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.