Backend

Webhooks

Signed, retried event deliveries — the authoritative way to learn a session's decision.

View as Markdown

Add an endpoint

In the dashboard open Developers → Webhooks, add your HTTPS URL and choose events (*, session.* or single types). The signing secret (whsec_…) is shown when you create the endpoint and can be revealed later. Or use the API: POST /v1/webhook_endpoints with { "url", "events" } — the secret is returned once.

Endpoints belong to a mode: test sessions deliver only to test-mode endpoints. In production, URLs must be https and publicly reachable (no private or loopback addresses).

Events

TypeWhen
session.createdA session was created.
session.startedThe applicant opened the flow.
session.processingAll steps were submitted; checks and rules are running.
session.approvedApproved, by the rules or by a reviewer.
session.declinedDeclined, by the rules or by a reviewer.
session.reviewWaiting for a person in your team to decide.
session.resubmission_requestedA reviewer asked the applicant to redo some steps.
session.expiredThe session expired before it was finished.
session.cancelledThe session was cancelled.
check.completedAn async direct check finished.
screening.hitSanctions or PEP screening found a potential match.

Payload

{
  "id": "evt_…",
  "type": "session.approved",
  "created": 1790000000,
  "mode": "test",
  "data": {
    "session": {
      "id": "ses_…",
      "reference_id": "user_123",
      "status": "approved",
      "type": "kyc",
      "decision": "approved",
      "decision_source": "auto",
      "parent_session_id": null
    }
  }
}

Payloads carry ids and statuses only, never personal data. Fetch the session for details.

Verify the signature

Every delivery has an OKYC-Signature header:

OKYC-Signature: t=1790000000,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd

v1 is hex(HMAC_SHA256(secret, t + "." + raw_body)). With @okyc/node, pass the raw body (not parsed JSON) to webhooks.constructEvent; it checks the signature in constant time and rejects timestamps older than 5 minutes:

server.mjs
// Webhooks: verify against the RAW body, dedupe on event.id, then fetch details from the API.
const seen = new Set();
app.post('/okyc/webhook', express.raw({ type: 'application/json' }), async (req, res) => {
  let event;
  try {
    event = await okyc.webhooks.constructEvent(
      req.body,
      req.header('okyc-signature'),
      process.env.OKYC_WEBHOOK_SECRET,
    );
  } catch {
    return res.sendStatus(400);
  }
  if (seen.has(event.id)) return res.sendStatus(200);
  seen.add(event.id);
  if (event.type === 'session.approved' || event.type === 'session.declined') {
    const session = await okyc.sessions.retrieve(event.data.session.id);
    console.log(`user ${session.reference_id} → ${session.status}`);
    // mark the user verified / rejected in your database
  }
  res.sendStatus(200);
});

In other languages:

  1. Split the header on ,; read t and every v1.
  2. Reject if |now − t| > 300 seconds.
  3. Compute HMAC_SHA256(secret, "<t>.<raw body>") as lowercase hex.
  4. Accept if it equals any v1, compared in constant time.

To test your handler, generateTestHeader(secret, body) from @okyc/node builds a valid header, and the dashboard can send a test event to an endpoint.

Delivery

  • Respond with any 2xx within 10 seconds. Anything else is retried with exponential backoff for up to 24 hours.
  • Delivery is at least once and not ordered: dedupe on the event id, and treat the session you fetch as the truth.
  • Every attempt is in the dashboard's delivery log, and you can replay events from there. GET /v1/events lists recent events for reconciliation.

On this page