Every webhook delivery is signed with HMAC-SHA256 using the per-subscription secret returned at create time. Always verify the signature before trusting the body. An unverified webhook handler is a public unauthenticated endpoint that an attacker can spoof at will.Documentation Index
Fetch the complete documentation index at: https://vantagesolutions.mintlify.app/llms.txt
Use this file to discover all available pages before exploring further.
Signature headers
Two headers accompany every delivery:| Header | Value |
|---|---|
X-VC-Signature | sha256=<hex> — HMAC-SHA256 digest. |
X-VC-Timestamp | Unix timestamp in milliseconds at which the event was signed. |
<timestamp> is the literal value of the X-VC-Timestamp header (digits only), the separator is a single literal dot character, and <body> is the exact bytes of the request body as received over the wire — no re-serialization, no whitespace adjustment.
The HMAC key is the subscription secret returned by POST /api/v1/partner/webhooks (the value starting with whsec_).
Verification algorithm
- Read the raw body bytes before any JSON parsing or middleware that might reformat them.
- Read the
X-VC-SignatureandX-VC-Timestampheaders. Reject if either is missing. - Strip the
sha256=prefix from the signature header; reject if not present. - Parse the timestamp as an integer (milliseconds since epoch). Reject if non-numeric.
- Reject if
abs(now_ms - timestamp_ms) > 300_000(5 minute replay window). - Compute
HMAC-SHA256(secret, f"{timestamp}." + body_bytes).hexdigest(). - Constant-time compare the computed digest against the received digest. Reject on mismatch.
Reference implementations
Python
Node.js
What the checks defend against
| Check | Defends against |
|---|---|
sha256= prefix | Algorithm-confusion attacks (forcing HMAC-MD5 or no algorithm). |
| Timestamp parseable | Malformed-header crash / type confusion. |
| Timestamp within 5 min | Replay attacks — an attacker who captures a valid delivery cannot replay it later. |
| HMAC compare | Forgery — without the secret, an attacker cannot produce a valid signature. |
| Constant-time compare | Timing oracle — an attacker probing one byte at a time learns nothing about the expected digest. |
Secret storage
Store the subscription secret as you would any high-value credential:- Environment variable or secret manager. Never commit it to a code repository.
- One per subscription. If you have multiple subscription URLs (e.g., staging + production), each has its own independent secret.
- Rotate by deletion + recreation. v1 has no in-place secret rotation. To rotate: create a second subscription with the new URL/secret, verify traffic flows, delete the old one.
What to do if verification fails
Failed verification is always a hard reject — return a non-2xx status from your handler (recommended:401 Unauthorized so failures are obvious in monitoring). Do not log the body, do not enqueue for retry on your side, do not “process the event but flag it.” A forged or replayed webhook should be discarded with the same finality as an unauthenticated API call.
Persistent verification failures on legitimate traffic almost always trace to one of three causes:
- Wrong secret. The handler is configured with a different secret than the subscription it’s receiving. Re-check against
GET /api/v1/partner/webhooksto confirm the subscription ID matches. - Body mutation. A middleware layer parsed and re-serialized the JSON before your handler saw it. Reroute the raw bytes.
- Clock skew. Your server’s clock drifted more than 5 minutes from UTC. Run
ntpdorchronyd.