Webhooks
Register an endpoint, read the event envelope and verify the signature.
Register an HTTPS endpoint with your sk_ key (scope webhooks:manage):
POST /api/v2/webhooks { "url": "https://…/webhooks/sekyro", "events": ["session.completed", "session.failed", "session.review_pending", "session.review_decided", "session.expired"] }
→ { "endpoint": {…}, "secret": "whsec_…" } # the secret is returned ONCE — vault it
POST /api/v2/webhooks/test # sends a `webhook.test` delivery (scope webhooks:test)
POST /api/v2/webhooks/:id/rotate-secretEnvelope (per-event version, no global API version):
{
"id": "0192f1a0-…", // UUIDv7, stable across retries — de-duplicate on it
"event": "session.completed",
"version": 1,
"createdAt": "2026-10-17T09:12:33.120Z",
"orgId": "66…",
"env": "sandbox" | "live",
"data": { "sessionId": "…", "status": "completed", "outcome": "approved", "steps": [{ "checkType": "liveness", "state": "passed" }] }
}| Event | data | When |
|---|---|---|
session.completed, session.failed | { sessionId, status, outcome, steps } | Terminal verdict |
session.review_pending | { sessionId, status, steps } | Every executable check is done or blocked on a human review |
session.review_decided | { sessionId, outcome: 'pass' | 'fail', reason } | A reviewer (or the 48 h review timeout, reason: 'review_timeout') decided. Only this event fires for a reviewed session — no second completed/failed. Note the decision vocabulary (pass/fail) differs from the session outcome: re-read the session. |
session.expired | { sessionId, status, steps } | TTL lapsed |
session.created, session.started, session.step.completed | lifecycle hints | Optional |
webhook.test | — | From POST /webhooks/test |
Treat every envelope as a hint: on receipt call sessions.get(id) and run
the same decision code as step 3 of the recipe.
Signature
Header x-sekyro-signature (primary). The legacy name
x-ibtikar-signature is still sent for older integrations — accept either
until you have migrated. Value: t=<unix seconds>,v1=<hex>[,v1=<hex>]; each
v1 is HMAC-SHA256(secret, "<t>.<rawBody>"). During a secret rotation two
v1 are present (current + previous) and any match is accepted.
verifyWebhook(rawBody, header, secret, { toleranceSec = 300 }) does all of
this in constant time and returns the typed envelope, or throws
SekyroWebhookError with code invalid_signature | stale | malformed.
signatureFromHeaders(headers) picks the primary header and falls back to the
legacy one — pass it a Headers object (request.headers in Next.js) or a
plain header record (req.headers in Express), as the snippets do.
@sekyro/node also exports SEKYRO_SIGNATURE_HEADER and
LEGACY_SIGNATURE_HEADER if you read the header yourself.
Raw body. The signature covers the bytes as sent. request.text() in
Next.js; express.raw({ type: 'application/json' }) on this route in
Express (a global express.json() mounted first re-serialises the body and
every delivery fails).
- Next.js route handler (Node runtime):
snippets/webhook-next.ts - Express:
snippets/webhook-express.ts
Reply 2xx quickly and do the work afterwards; non-2xx deliveries are retried
with a fresh t and the same envelope id.