Tutorial: Receiving Webhooks & Validating Signatures
Tutorial: Receiving Webhooks & Validating Signatures
Tutorial: Receiving Webhooks & Validating Signatures
In this tutorial, you’ll build a working webhook receiver that listens for Verifa events, validates their HMAC-SHA256 signatures, and processes them safely. By the end, you’ll have a production-ready webhook handler you can adapt to your stack.
Without signature verification, anyone who discovers your webhook URL can send fake events to your server — potentially granting unauthorized access to users, triggering incorrect business logic, or corrupting your data. Verifying the HMAC-SHA256 signature on every request ensures the event genuinely came from Verifa and hasn’t been tampered with in transit.
webhooks:read and webhooks:write scopesDuring development, your webhook receiver needs a public URL that Verifa can reach. Start an ngrok tunnel (or any similar tool):
Copy the forwarding URL (e.g., https://a1b2c3.ngrok.io). You’ll use this when
creating your webhook endpoint.
You can create a webhook endpoint from the dashboard or via the API.
From the dashboard, expand Webhooks under the Developers section in the left sidebar, then click Webhooks.

Click + Add Endpoint in the top right corner.

Fill in your endpoint details in the Add Webhook Endpoint modal:
https://a1b2c3.ngrok.io/webhooks/verifa)Tutorial Webhooksession.approved, session.declined, and session.requires-reviewYou can also configure API Version, Key Inflection, and Check Type Filter — leave them as defaults for now.

Click Add Endpoint. A Webhook Created modal appears with your signing secret and a Copy button.

Copy the signing secret now using the Copy button. The modal warns: “Save this signing secret now. It will not be shown again.” If you lose it, you’ll need to rotate to get a new one.
Save the secret value immediately. It is only returned when the endpoint is
created and when you rotate the secret. You cannot retrieve it later. (In
sandbox mode, the secret remains visible on GET requests.)
Whichever method you used, store the secret as an environment variable:
Choose your language and build a handler that receives the raw request body, verifies the HMAC-SHA256 signature, and processes the event.
Every webhook Verifa sends includes an X-Verifa-Signature header in the
Stripe-style t=<unix_ts>,v1=<hex_hmac> format. The HMAC-SHA256 covers the
timestamp and body joined by a literal .:
whsec_... value)f"{timestamp}.{raw_request_body}" — the Unix timestamp from
t=, a literal period, then the raw HTTP request body bytes Verifa sentTo verify, you parse t= and v1= out of the header, recompute the HMAC over
{t}.{body}, and compare it to v1 using a constant-time comparison. You then
reject any delivery whose t= value is more than five minutes away from your
current clock — this is the replay-protection window enforced by every Verifa
client (tolerance_seconds=300).
For backwards compatibility during the deprecation window, the verification
helper in src/core/security.py:verify_webhook_signature also accepts the
legacy bare-hex HMAC over the raw body. New outbound deliveries always use the
timestamped format, so you should implement the new format in your receiver
and only fall back to the legacy form if you have in-flight deliveries to
drain.
You must use the raw request body bytes for verification, not a parsed-and-re-serialized JSON object. Parsing and re-serializing can change key order, spacing, or encoding, which will produce a different hash.
The recipe receivers should follow:
Create a file called server.js:
Run it:
With your server running and ngrok forwarding traffic, send a test event to verify everything is wired up.
On the Webhooks list page, click the … menu on your endpoint’s row and select Send test event.

A Test Delivery Result modal appears showing the outcome — a green checkmark with “Delivery successful” and the HTTP status code, or an error if your server rejected it.

The test sends a verification.test event to your endpoint, signed with your
endpoint’s secret:
Any 2xx response from your server counts as success.
If the test fails, check:
VERIFA_WEBHOOK_SECRET set correctly?Create a verification session in sandbox mode to trigger a real webhook.
Open the capture_url from the response in your browser and walk through the
verification steps (document upload, selfie, etc.). In sandbox mode, sessions
move to sandbox_pending status after capture, and you’ll be prompted to
choose a verification outcome (e.g., approve or decline).
Once you select an outcome, Verifa dispatches the corresponding webhook event
(e.g., session.approved or session.declined) to your endpoint.
Sandbox sessions do not auto-complete. You must finish the capture flow and select an outcome to trigger webhook events.
Verifa retries failed deliveries with exponential backoff. Your handler should be prepared for this.
After 5 failed attempts, the delivery is marked as failed. If 10 consecutive deliveries fail (across all events), the endpoint is automatically disabled and your org admins receive an email notification.
Every webhook payload includes an idempotency_key — a unique identifier for
that specific delivery (e.g. idk_a1b2c3d4e5f6...). If Verifa retries a
delivery, the same idempotency_key is sent again, so you can use it to detect
and skip duplicates:
If your webhook secret is compromised, rotate it immediately.
On the Webhooks list page, click the … menu on your endpoint’s row and select Rotate secret.

A Rotate Signing Secret modal warns that this will immediately invalidate your current secret. Enter your account password and click Confirm.

A Secret Rotated modal appears with the new signing secret and a Copy button. The modal warns: “Your old secret is now invalid. Update your server before closing.”

Update your environment variable with the new secret. The old secret stops working immediately, so deploy the new secret to your server before or immediately after rotation.
For zero-downtime rotation, clone the endpoint first (POST .../clone), update
your server to accept signatures from either secret, rotate the original
endpoint’s secret, then delete the clone.
Monitor your webhook deliveries to catch failures early.
Navigate to Developers > Webhooks > Webhook Events in the sidebar. This page shows all deliveries across your endpoints, with filters for endpoint, status, date range, and event type.

Click the expand arrow on any delivery row to see the full payload, including
the event type and idempotency_key. If a delivery failed, a Retry
button appears on the expanded row.

To retry a failed delivery:
Before going live, make sure you’ve covered these:
/test endpoint)t= and v1= from the X-Verifa-Signature headerf"{t}.{body}" (timestamp, literal ., then raw body bytes)t= outside the 5-minute tolerance window200 immediately and processing asynchronouslyX-Verifa-Signature header unmodified