Sekyro AIDocs

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-secret

Envelope (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" }] }
}
EventdataWhen
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.completedlifecycle hintsOptional
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).

Reply 2xx quickly and do the work afterwards; non-2xx deliveries are retried with a fresh t and the same envelope id.

On this page