Skip to content

Webhooks ​

Arkyc delivers signed, per-project webhooks for verification events, with retries and a delivery log.

Configure an endpoint ​

In the dashboard: Project → Webhooks → add endpoint. Provide a URL and the events to subscribe to. The signing secret is shown once; store it. You can also send a test delivery, or use the Dashboard API.

Events ​

verification.started
verification.document_submitted
verification.processing
verification.requires_review
verification.approved
verification.rejected
verification.completed
verification.expired
verification.cancelled

Events fire from a single transition choke point, covering the session service, the biometric worker, and review actions. The terminal decision events (verification.approved, verification.rejected, and the verification.completed summary) carry the full checks block and decision_reason; progress events (started, document_submitted, processing) carry the status transition only. See Verification lifecycle for what each status means.

Payload ​

json
{
  "event": "verification.approved",
  "session_id": "…",
  "tenant_id": "…",
  "project_id": "…",
  "user_reference": "user_123",
  "status": "approved",
  "checks": {
    "document": { "quality_score": 0.93, "ocr_confidence": 0.88, "expired": false, "ocr_parse_stage": "mrz" },
    "liveness": { "passed": true, "score": 0.97 },
    "face_match": { "passed": true, "similarity_score": 0.91 }
  },
  "decision_reason": null,
  "created_at": "2026-06-21T20:05:00Z"
}

Signature ​

Each delivery is a POST with:

HeaderValue
Content-Typeapplication/json
X-Arkyc-SignatureHMAC-SHA256 of ${timestamp}.${rawBody}
X-Arkyc-TimestampUnix seconds when the delivery was signed

Verify with the SDK (constant-time compare + timestamp tolerance):

ts
import { WebhookSigner } from '@arkyc/sdk'

const ok = WebhookSigner.verify({
  payload: rawBody, // the exact bytes received
  secret: process.env.ARKYC_WEBHOOK_SECRET!,
  signature: req.headers['x-arkyc-signature'] as string,
  timestamp: Number(req.headers['x-arkyc-timestamp']),
  toleranceSec: 300, // optional, default 300
})
if (!ok) return res.status(400).end()

Read the raw request body; verifying a re-serialized JSON object will fail.

Express receiver example ​

ts
import express from 'express'
import { WebhookSigner } from '@arkyc/sdk'

const app = express()

app.post('/webhooks/arkyc', express.raw({ type: 'application/json' }), (req, res) => {
  const raw = req.body.toString('utf8')
  const ok = WebhookSigner.verify({
    payload: raw,
    secret: process.env.ARKYC_WEBHOOK_SECRET!,
    signature: req.get('x-arkyc-signature')!,
    timestamp: Number(req.get('x-arkyc-timestamp')),
  })
  if (!ok) return res.status(400).end()

  const event = JSON.parse(raw)
  // …handle event.event / event.status…
  res.json({ ok: true })
})

Handling deliveries safely ​

Delivery is at-least-once, so build your receiver to tolerate duplicates and out-of-order arrivals:

  • Be idempotent. The same event can arrive more than once (a retry after your endpoint was slow, even if it actually succeeded). Key your handling on session_id + event (or status) and make repeats a no-op; don't grant access or charge twice.
  • Respond fast. Return a 2xx quickly (within a few seconds) and do heavy work asynchronously. A slow endpoint is treated as failed and retried.
  • Don't assume ordering. A retried processing can land after approved. Trust the session's current state, not the arrival order: ignore a transition that would move a terminal session backwards, or re-retrieve the session to reconcile.
  • Verify before trusting. Check the signature (above) before acting on any payload, and treat the webhook (or a server-side retrieve) as the source of truth over the widget's onComplete.

Delivery & retries ​

A queue-backed worker signs and POSTs each delivery, recording webhook_deliveries (attempts, response status/body). Failures retry with backoff and dead-letter after the max attempts. Run the worker with ark queue:work webhook (see Self-hosting).

The signing secret is stored as-is to re-sign deliveries; encryption-at-rest is a planned hardening item.

Released under the MIT License.