Webhooks setup

Step-by-step guide to registering a Certivant webhook endpoint, storing the signing secret, sending a test event, rotating secrets, and configuring per-form overrides.

Webhooks let Certivant push state changes to your backend in real time. Instead of polling GET /requests/:id until processing completes, Certivant POSTs to a URL you control whenever an event of interest happens.

This page covers the one-time setup. For the signature verification you have to implement before going to production, see Signature verification.

What you'll need

  • A publicly reachable HTTPS URL that accepts POST requests with a JSON body. Localhost is fine during development if you use a tunnel (ngrok, Cloudflare Tunnel, tailscale funnel).
  • The role Owner or Admin on the Certivant organization — Members cannot change webhook settings.
  • A way to store secrets — you'll get an HMAC signing secret you must keep alongside your other production credentials.

1 — Build your endpoint

A minimal handler looks like this (Node + Hono shown; any framework works):

app.post("/webhooks/certivant", async (c) => {
  const body = await c.req.text();   // Read RAW body — needed for signature check
  const signature = c.req.header("X-Certivant-Signature-V2");
  const payload = JSON.parse(body);

  // Connectivity ping (the dashboard "Test" button) — ack immediately.
  // It carries `test: true` and mutates nothing, so no signature is required.
  if (payload.test === true || payload.event === "kyc.webhook.test") {
    return c.json({ ok: true }, 200);
  }

  // Verify the signature BEFORE trusting a real event, and reject if it fails.
  // See /developer/webhooks/signature-verification for the verify helper
  // (checks `t=<unix>,v1=<hex>` over `${t}.${body}`, 300 s tolerance).
  if (!verifyCertivantSignature(body, signature, process.env.CERTIVANT_WEBHOOK_SECRET!)) {
    return c.json({ error: "invalid signature" }, 401);
  }

  // Acknowledge fast — Certivant will retry if you take > 10s
  c.executionCtx.waitUntil(handlePayload(payload));
  return c.json({ received: true }, 200);
});

The two non-negotiable rules:

  1. Respond with 2xx within 10 seconds. Certivant treats anything else (5xx, 4xx, timeout, connection error) as a delivery failure and retries with exponential backoff.
  2. Read the raw body before parsing JSON. Signature verification hashes the exact byte string Certivant sent. If your framework parses JSON first, you'll regenerate a different string and the signature won't match.

2 — Configure the URL in the dashboard

Sign in to the Certivant dashboard with an Owner or Admin account.
Navigate to KYC → Settings → Webhooks.
Paste your endpoint URL into Webhook URL, e.g. https://api.yourapp.com/webhooks/certivant. HTTPS only — plain HTTP is rejected.
Click Save. Certivant generates a signing secret on first save (visible only once — copy it into your environment now).
The secret has the prefix whsec_. Store it in your secrets manager as CERTIVANT_WEBHOOK_SECRET.

The secret is shown only once, when you generate or rotate it — there is no "show me the secret again" button, so store it immediately. If you lose it, rotate it (see below) to get a fresh one.

3 — Send a test event

Two buttons help you verify the integration:

  • Test (next to the webhook URL) sends a lightweight kyc.webhook.test connectivity ping carrying test: true. It confirms only that your endpoint is reachable and returns 2xx — it needs no request data and works even before you've stored a signing secret. Ack it immediately (see the handler above).

  • Send a test event (KYC → Developer → Webhook events) lets you pick the event — the kyc.webhook.test ping or a sample kyc.request.created / approved / rejected / escalated payload — and POSTs it signed exactly like a real delivery (X-Certivant-Signature-V2 plus the X-KYC-* headers, unix timestamp, your webhook secret). Sample payloads carry "test": true and a fake requestId. Use it to:

  • Confirm the URL is reachable from Certivant's infrastructure.

  • Confirm your signature verification accepts a valid signature.

  • See the exact payload shape before any real events fire.

You can also trigger a test programmatically — useful when you want to script your integration testing:

curl -X POST https://app.certivant.com/api/kyc/settings/webhook-test \
  -H "Cookie: better-auth.session_token=..." \
  -H "Content-Type: application/json" \
  -d '{"event":"kyc.request.approved"}'
# returns { status: 200, latencyMs: 142, event, signed: true, headers: [...], responsePreview } if your endpoint replied

The body is optional (default kyc.webhook.test). The endpoint takes a session cookie, not an API key — it's a dashboard-side action.

4 — Rotate the signing secret

When a secret is exposed (or annually as good hygiene):

Open KYC → Settings → Webhooks.
Click Rotate secret. Certivant generates a new whsec_… value and shows it once.
Update your environment variable with the new value.
Deploy. After deployment, the old secret is dead — any in-flight retries that still carry the old signature will fail verification on your side and Certivant will treat them as undeliverable.

There is no sender-side overlap window: new deliveries are signed with the new secret only. The verifier recipes in Signature verification accept a list of secrets and several v1= values, so you can deploy a verifier that trusts both the old and the new secret before you rotate. Otherwise, rotate during a quiet period and watch your logs for verification failures right after.

Form-level webhook override

KYC requests created via a form can override the org-default webhook URL with a per-form URL. Configure this on the form's settings page in the dashboard. Useful when you have multiple integrations sharing the same Certivant account — point each form at the team that owns it.

The signing secret is shared across both the default URL and form-level overrides. The signature header is identical regardless of which URL receives the event.

What's not webhook-able

A few things don't fire webhooks:

  • API key creation, revocation, or scope changes.
  • Settings changes (white-label edits, KYC threshold tweaks).
  • Document downloads.
  • Manual review comments.

If you need to react to these, look at the dashboard's Activity log instead — there's no public API for it yet.

Next steps

  • Signature verification — do this before production. Without it, anyone can forge requests to your webhook URL.
  • Events — the full catalogue of event types and payloads.
  • Retries — what happens when your endpoint is down.