Skip to content
Reference

Webhooks, signed

Konnect ID pushes an event to your server when a session needs a decision or has one. Every delivery is signed so you can prove it is ours.

Events

EventSent when
verification.completedAutomatic checks all passed. Session status is FINISHED.
verification.review_requiredSomething needs a human. Session status is VERIFICATION. data.reasons explains why.
verification.approvedA reviewer approved the session.
verification.declinedA reviewer declined the session.

Automatic events are sent only on a real status transition, so re-evaluations do not repeat them. A verification.status_updated event may also be sent for other status changes; ignore event types you do not handle.

Payload

json
{
  "id": "0d9a6c1e-6f55-4e0c-9a77-2f8c1f3f4b10",
  "event": "verification.review_required",
  "created": "2026-09-30T10:42:11.000Z",
  "data": {
    "sessionId": "b7c1…",
    "applicationId": "order-10293",
    "clientId": "acme",
    "flow": "KYC",
    "status": "VERIFICATION",
    "decision": "MANUAL_REVIEW",
    "riskScore": 55,
    "reasons": ["front: Editing software trace found (Adobe Photoshop)"],
    "source": "auto_verification"
  }
}
FieldDescription
idUnique delivery id (same value as X-Konnect-Delivery). Use it to deduplicate.
eventEvent name.
createdISO 8601 creation time.
data.sessionId / applicationId / clientId / flowIdentify the session and your own references.
data.statusSession status: FINISHED, VERIFICATION, APPROVED, DECLINED.
data.decisionAUTO_APPROVED, MANUAL_REVIEW, APPROVED or DECLINED.
data.riskScore0-100, lower is safer (automatic events only).
data.reasonsHuman-readable reasons for manual review (automatic events only).
data.sourceauto_verification, reviewer or result_link.
The webhook is a notification, not the full record. Fetch /info for extracted data.

Headers

http
Content-Type: application/json
User-Agent: KonnectID-Webhooks/1
X-Konnect-Event: verification.review_required
X-Konnect-Delivery: 0d9a6c1e-6f55-4e0c-9a77-2f8c1f3f4b10
X-Konnect-Signature: t=1790764931,v1=5b1c…e9

Verify the signature

The signature header is t=<unix seconds>,v1=<hex>, where v1 = HMAC-SHA256(secret, "<t>.<rawBody>"). The secret is the App's secret key; if the session has no app, the company API key is used instead. If neither is available the signature header is omitted.

import crypto from "node:crypto";
import express from "express";

const app = express();
const seen = new Set(); // use Redis or your database in production

// IMPORTANT: sign over the RAW body, not re-serialized JSON.
app.post("/webhooks/konnect", express.raw({ type: "application/json" }), (req, res) => {
  const header = req.get("X-Konnect-Signature") || "";
  const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
  const t = Number(parts.t);
  const raw = req.body.toString("utf8");

  if (!t || Math.abs(Date.now() / 1000 - t) > 300) return res.sendStatus(400);

  const expected = crypto
    .createHmac("sha256", process.env.KONNECT_APP_SECRET)
    .update(`${t}.${raw}`)
    .digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(parts.v1 || "");
  if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) return res.sendStatus(401);

  const deliveryId = req.get("X-Konnect-Delivery");
  if (seen.has(deliveryId)) return res.sendStatus(200); // duplicate: already handled
  seen.add(deliveryId);

  res.sendStatus(200); // answer fast, then work
  const event = JSON.parse(raw);
  queueMicrotask(() => handle(event));
});
Raw bodyCompute the HMAC over the exact bytes received. Parsing and re-serializing JSON changes whitespace and key order and will cause a mismatch.

Replay protection

  • Reject the request if |now − t| > 5 minutes.
  • Store X-Konnect-Delivery ids you have processed and ignore repeats. Retries reuse the same id.
  • Make handlers idempotent: updating a record to the same status twice must be harmless.

Delivery and retries

Each event is attempted up to four times: immediately, then after 2 s, 10 s and 60 s. Attempts time out after 8 seconds and redirects are not followed.

  • Any 2xx response ends the delivery.
  • A 4xx response other than 408 or 429 is treated as a permanent rejection and is not retried.
  • 408, 429, 5xx, network errors and timeouts are retried.

Respond with 2xx as soon as the signature is verified and the event is stored, then process asynchronously. Delivery is in-process and best effort: after the last attempt it is not queued again, so reconcile occasionally by calling /info for sessions that stay open for too long.

Webhook URL rulesWebhook URLs must be public HTTP(S) endpoints. Private and loopback addresses are refused, so use a tunnel to test locally.