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
POSTrequests 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:
- Respond with
2xxwithin 10 seconds. Certivant treats anything else (5xx, 4xx, timeout, connection error) as a delivery failure and retries with exponential backoff. - 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
https://api.yourapp.com/webhooks/certivant. HTTPS only — plain HTTP is rejected.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.testconnectivity ping carryingtest: true. It confirms only that your endpoint is reachable and returns2xx— 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.testping or a samplekyc.request.created/approved/rejected/escalatedpayload — and POSTs it signed exactly like a real delivery (X-Certivant-Signature-V2plus theX-KYC-*headers, unix timestamp, your webhook secret). Sample payloads carry"test": trueand a fakerequestId. 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):
whsec_… value and shows it once.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.
Reused verifications API
Let an applicant who already passed a Certivant verification elsewhere reuse it with you. Create a consent link, receive a single-use share token, and redeem it into a new approved verification.
Signature verification
Verify the X-Certivant-Signature-V2 header (HMAC-SHA256 over timestamp.body, 300 s tolerance) so you trust webhook payloads. Includes Node, Python, Go, Ruby, PHP examples.