Guide to Vanta Webhooks: Features and Best Practices
Vanta webhooks notify your systems when something changes in your compliance programme: a security questionnaire is created or exported, someone requests access to your Trust Center, a vendor decision is recorded, or, for audit firms, evidence and information requests move during an audit. If you build on Vanta, webhooks let you react to those changes without polling the API.
This guide covers how Vanta webhooks are delivered and signed, the event types and their payloads, the retry schedule, and the best practices for running a Vanta receiver in production.
What are Vanta webhooks?
Vanta delivers webhooks through Svix. You register an HTTPS endpoint in the Vanta dashboard, choose the event types it should receive, and Vanta sends each event as a POST with the event payload as the JSON body.
Every request carries three Svix headers: svix-id, svix-timestamp and svix-signature. The signature is an HMAC-SHA256 over the message ID, the timestamp and the raw body, keyed with the endpoint's whsec_ signing secret.
Vanta's payloads are deliberately small. They hold the IDs of the objects that changed, and Vanta's guidance is to treat those IDs as opaque strings and fetch the full object from the Manage Vanta API.
Vanta webhook features
| Feature | Details |
|---|---|
| Delivery platform | Svix |
| Configuration | Vanta dashboard, Settings > Webhooks > Add Endpoint |
| Endpoint | Must be HTTPS |
| Subscription | Chosen event types per endpoint. An empty selection receives all events |
| Event names | v1.<domain>.<entity>.<action> |
| Headers | svix-id, svix-timestamp, svix-signature |
| Scheme | Base64 HMAC-SHA256 over {svix-id}.{svix-timestamp}.{raw body} |
| Secret | One per endpoint, starts with whsec_ |
| Timestamp tolerance | Vanta recommends rejecting timestamps more than 5 minutes from your clock |
| Response window | A 2xx within 15 seconds |
| Retries | 8 attempts over about 5 days, then marked as failed |
| Recovery | Retry and Bulk Retry in the dashboard |
| Source IPs | Not published |
Common events
Vanta documents 24 event types across six domains:
| Domain | Event types | Delivered to |
|---|---|---|
| Questionnaire | v1.questionnaire.created, .deleted, .status-changed, .export-completed, .export-failed | Your Vanta account |
| Trust Center | v1.trust-center.access-request.received, .approved, .denied | Your Vanta account |
| Vendor | v1.vendor.decision.created | Your Vanta account |
| Information Request | v1.information-request.status-changed, .comment.created, .comment.updated, .comment.deleted, .evidence.created, .evidence.deleted | Audit Partner accounts, during an active audit |
| Evidence | v1.evidence.created, .deleted, .status-changed, .comment.created, .comment.updated, .comment.deleted | Audit Partner accounts, during an active audit |
| Control | v1.control.comment.created, .comment.updated, .comment.deleted | Audit Partner accounts, during an active audit |
The version is part of the event name (the v1. prefix), not a separate header.
Vanta's reference documents each event's payload as the body, and the payloads are objects of IDs, for example:
{ "questionnaire": { "id": "123" } }
None of the documented payloads includes the event type, and the three headers do not carry it either. The top-level key tells you which kind of object changed, but not what happened to it, so v1.questionnaire.created and v1.questionnaire.deleted look the same. Give event types that need different handling their own endpoints, and inspect a real delivery with the dashboard's Send Example before you depend on any field.
Setting up Vanta webhooks
- In the Vanta dashboard, go to Settings > Webhooks and click Add Endpoint.
- Enter your HTTPS endpoint URL.
- Select the event types you need, or leave the selection blank to receive every event.
- Click Create, then open the endpoint and copy its Signing Secret. It starts with
whsec_. Store it server-side, for example asVANTA_.WEBHOOK_ SIGNING_ SECRET - On the endpoint's Testing tab, choose an event type and click Send Example to send a test message.
Vanta also reminds you to disable CSRF protection on the webhook route, because webhook requests do not carry CSRF tokens.
Securing Vanta webhooks
Vanta recommends the official Svix libraries, and documents the manual steps as well:
- Read the
svix-id,svix-timestampandsvix-signatureheaders. - Build the signed content
{svix-id}.{svix-timestamp}.{body}from the raw request body. - Remove the
whsec_prefix from the signing secret and base64-decode the rest. - Compute an HMAC-SHA256 of the signed content with the decoded secret.
- Base64-encode the result and compare it with each
v1,entry insvix-signature.
const crypto = require('crypto');
const express = require('express');
function verifySvixSignature(rawBody, headers, secret) {
const msg_id = headers['svix-id'];
const msg_timestamp = headers['svix-timestamp'];
const signature_header = headers['svix-signature'];
if (!msg_id || !msg_timestamp || !signature_header) return false;
if (typeof secret !== 'string' || !secret.startsWith('whsec_')) return false;
// The HMAC key is the base64-decoded part of the secret after "whsec_".
const key = Buffer.from(secret.slice('whsec_'.length), 'base64');
const signed_content = Buffer.concat([Buffer.from(`${msg_id}.${msg_timestamp}.`, 'utf8'), rawBody]);
const expected = Buffer.from(
crypto.createHmac('sha256', key).update(signed_content).digest('base64'),
'utf8'
);
// The header can hold several space-separated "v1,<signature>" entries.
return signature_header.split(' ').some((entry) => {
const [version, signature] = entry.split(',');
if (version !== 'v1' || !signature) return false;
const received = Buffer.from(signature, 'utf8');
return received.length === expected.length && crypto.timingSafeEqual(received, expected);
});
}
const app = express();
app.post('/webhooks/vanta', express.raw({ type: '*/*' }), (req, res) => {
const secret = process.env.VANTA_WEBHOOK_SIGNING_SECRET;
if (!secret) return res.sendStatus(500); // fail closed
if (!verifySvixSignature(req.body, req.headers, secret)) {
return res.sendStatus(400);
}
// Optional: Vanta recommends rejecting timestamps more than 5 minutes old.
// const age = Math.abs(Date.now() / 1000 - Number(req.get('svix-timestamp')));
// if (age > 300) return res.sendStatus(400);
const payload = JSON.parse(req.body.toString('utf8'));
// Deduplicate on req.get('svix-id'), queue the event, then respond.
res.sendStatus(200);
});
The same check in Python:
import base64
import hashlib
import hmac
import os
def verify_svix_signature(raw_body: bytes, headers, secret: str) -> bool:
msg_id = headers.get("svix-id")
msg_timestamp = headers.get("svix-timestamp")
signature_header = headers.get("svix-signature")
if not (msg_id and msg_timestamp and signature_header):
return False
if not secret or not secret.startswith("whsec_"):
return False
key = base64.b64decode(secret[len("whsec_"):])
signed_content = f"{msg_id}.{msg_timestamp}.".encode("utf-8") + raw_body
expected = base64.b64encode(hmac.new(key, signed_content, hashlib.sha256).digest())
for entry in signature_header.split(" "):
version, _, signature = entry.partition(",")
if version == "v1" and hmac.compare_digest(signature.encode("utf-8"), expected):
return True
return False
# In FastAPI: raw_body = await request.body()
# verify_svix_signature(raw_body, request.headers, os.environ["VANTA_WEBHOOK_SIGNING_SECRET"])
To test your verifier, Svix publishes this example: secret whsec_, message ID msg_loFOjxBNrRLzqYUf, timestamp 1731705121 and body {"event_ should produce the signature v1,rAvfW3dJ/X/qxhsaXPOyyCGmRKsaKWcsNccKXlIktD0=. The timestamp is old, so leave any freshness check off while you test.
Make Vanta webhooks production-ready. Hookdeck Event Gateway verifies Vanta's Svix signatures, acknowledges each delivery inside the 15-second window, and queues every event until your service has processed it.
Hookdeck's Vanta source type verifies the three svix-* headers with the whsec_ signing secret you enter. Hookdeck does not apply a timestamp window, so a delayed retry still verifies. If you want Vanta's 5-minute check, apply it in your own receiver.
Vanta webhook limitations and pain points
Payloads don't say what happened
The Problem: Your handler receives {"questionnaire": {"id": "123"}} and can't tell whether the questionnaire was created, deleted or exported.
Why It Happens: Vanta's documented payloads contain only the IDs of the affected objects, and no documented header carries the event type.
Workarounds:
- Give each event type that needs different handling its own endpoint, so the URL tells you the type.
- Fetch the object from the Manage Vanta API using the ID, and act on its current state.
How Hookdeck Can Help: Several Hookdeck sources can deliver to the same service with different paths, so each Vanta endpoint keeps its own event type while your code handles them in one place.
Raw body handling breaks verification
The Problem: Every signature check fails even though the secret is right.
Why It Happens: The signature covers the exact body bytes. Frameworks that parse JSON before your handler runs, such as express.json(), change the bytes, and Vanta's troubleshooting guide names this as the cause.
Workarounds:
- Use
express.raw()on the webhook route and parse after verification. - Check that you copied the signing secret for the right endpoint.
How Hookdeck Can Help: Hookdeck verifies against the bytes Vanta sent, so the services behind it don't each need raw-body handling.
A 15-second timeout
The Problem: Handlers that call the Manage Vanta API inline time out and get retried.
Why It Happens: Vanta marks a delivery as failed if it doesn't get a 2xx within 15 seconds, and retries it.
Workarounds:
- Return
200once the event is stored, then fetch from the API in a background job.
How Hookdeck Can Help: Hookdeck acknowledges Vanta immediately and delivers to your service with a timeout you configure.
Failed messages are dropped after about five days
The Problem: A long outage loses events once the retry schedule runs out.
Why It Happens: Vanta retries at 5 seconds, 5 minutes, 30 minutes, 2 hours, 8 hours, 1 day and 2 days after the first attempt, then marks the message as failed.
Workarounds:
- Use Retry or Bulk Retry in Settings > Webhooks to replay failed messages for a time range after recovery.
How Hookdeck Can Help: Hookdeck stores every delivery as it arrives, so an outage on your side doesn't burn Vanta's retries, and you can replay from Hookdeck whenever your service is back.
Best practices
Verify against the raw body
Hash the exact bytes received. Use express.raw() in Express, Buffer.from(await request.arrayBuffer()) in a Next.js route handler, and await request.body() in FastAPI.
Deduplicate on svix-id
Delivery is at least once. Vanta's docs say to use svix-id to deduplicate, and Svix keeps the same ID when it resends a message. See our guide to webhook idempotency.
Acknowledge fast and process asynchronously
Return a 2xx straight after verification and handle the event in a background job. See why to process webhooks asynchronously.
Treat IDs as opaque and fetch current state
Don't parse or validate IDs. Use them to fetch the full object from the Manage Vanta API, which also protects you from acting on a stale event that arrived after a retry.
Test with Send Example
The Testing tab sends a real, signed example for any event type. Use it to confirm verification and to see the exact body before you rely on its shape.
Conclusion
Vanta sends small, ID-only payloads through Svix, signed with base64 HMAC-SHA256 over svix-id, svix-timestamp and the raw body. Verify every request, deduplicate on svix-id, answer within 15 seconds, and fetch the full object from the Manage Vanta API before you act.
Hookdeck Event Gateway sits in front of your receiver, verifies Vanta's signatures, and holds each event until your service has processed it.
Get started with Hookdeck for free and handle Vanta webhooks reliably in minutes.