kyve.dev

Webhooks

Event envelope, signature verification, and delivery semantics.

Webhooks are the authoritative way to receive verification results. Polling /v1/verifications/:id works, but webhooks are instant and cheaper.

Event envelope

interface WebhookEventEnvelope<T> {
  id: string;                 // evt_...
  object: 'event';
  type: string;               // e.g. "verification.completed"
  api_version: '2026-04-01';  // pinned per endpoint
  created: number;            // unix seconds
  livemode: boolean;
  data: { object: T };
  request?: {                 // present on tenant-initiated events
    id: string | null;
    idempotency_key: string | null;
  };
}

This mirrors Stripe's event shape. id is globally unique; type identifies the event; data.object is the full resource at the time the event fired.

Headers

Every delivery includes:

  • Content-Type: application/json
  • KYC-Signature: t=<unix_ts>,v1=<hex_hmac_sha256>
  • KYC-Event-Id: evt_... (idempotency key for your receiver)
  • User-Agent: KYC-Webhooks/1.0

Verifying the signature

  1. Read the KYC-Signature header and parse out t and v1.
  2. Compute HMAC-SHA256(secret, t + "." + raw_body).
  3. Compare the hex digest against v1 using a constant-time comparison.
  4. Reject if |now - t| > 300 seconds.
import crypto from 'node:crypto';
 
function verify(secret: string, header: string, rawBody: string) {
  const parts = Object.fromEntries(
    header.split(',').map((kv) => kv.split('=')),
  );
  const t = parts.t;
  const v1 = parts.v1;
  if (!t || !v1) return false;
 
  const skew = Math.abs(Date.now() / 1000 - Number(t));
  if (skew > 300) return false;
 
  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${t}.${rawBody}`)
    .digest('hex');
 
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
}

Delivery guarantees

  • At-least-once. Expect occasional duplicates; dedupe on KYC-Event-Id.
  • Retries. Exponential backoff up to 24 hours.
  • Expected response. 2xx within 10 seconds. Anything else is a retry.

Event types (MVP)

Only terminal-state events fire today. There is no verification.created or verification.updated; intermediate progress is read from GET /v1/verifications/{id} or the iframe kyc.status_changed postMessage.

TypeFires when
verification.completedAll checks pass and the session reaches a terminal verified state.
verification.failedAny check fails and the session reaches a terminal failed state.
verification.requires_reviewSession lands in requires_review (manual reviewer attention needed).
verification.canceledTenant or admin canceled an in-flight session.
verification.expiredSession aged out (~30 min) without reaching a terminal state.
billing.topup.completedWallet credited (Stripe checkout or AIO USDT settled).

On this page