Webhook Signature Verification
Verify signed event ingestion requests with HMAC-SHA256.
Why this matters
POST /events/ingest is a public endpoint. Signature verification is what proves the request came from your system and was not modified in transit.
Without signature verification:
- anyone who knows the endpoint can inject events
- replayed payloads can poison your event history
- forged events can distort risk decisions
Required headers
x-event-signature: required. The HMAC-SHA256 hex digest of the raw body.x-event-timestamp: required. Use the current Unix timestamp in seconds.x-event-nonce: required. Send a fresh unique value for every request.
Signing algorithm
plaintext
signature = HMAC-SHA256(rawRequestBody, webhookSigningSecret).hexDigest()Important details:
- sign the raw request body
- do not reformat JSON between signing and sending
- compare signatures using a timing-safe equality function
Node.js example
javascript
import crypto from 'crypto';
function signBody(rawBody, secret) {
return crypto.createHmac('sha256', secret).update(rawBody).digest('hex');
}
const body = JSON.stringify({
companyId: process.env.VERTEXY_COMPANY_ID,
eventSource: 'checkout-service',
externalEventId: 'evt_100001',
idempotencyKey: 'evt_100001',
userId: 'user_123',
eventType: 'payment_succeeded',
timestamp: new Date().toISOString(),
metadata: {},
});
const signature = signBody(body, process.env.VERTEXY_WEBHOOK_SECRET);Python example
python
import hashlib
import hmac
import json
import os
body = json.dumps({
"companyId": os.environ["VERTEXY_COMPANY_ID"],
"eventSource": "checkout-service",
"externalEventId": "evt_100001",
"idempotencyKey": "evt_100001",
"userId": "user_123",
"eventType": "payment_succeeded",
"timestamp": "2026-04-07T12:00:00.000Z",
"metadata": {},
}, separators=(",", ":"))
signature = hmac.new(
os.environ["VERTEXY_WEBHOOK_SECRET"].encode(),
body.encode(),
hashlib.sha256,
).hexdigest()Receiver-side verification checklist
On the receiving side of your own systems, you should:
- Read the raw bytes exactly as received.
- Extract the signature header.
- Recompute the digest using the same secret.
- Use timing-safe comparison.
- Reject any mismatch with
401 Unauthorized.
Replay protection
VertexY also validates timestamp freshness and nonce uniqueness.
To stay compatible:
- generate a fresh nonce for every request
- use current time
- retry with a new nonce if you need to resend
Common mistakes
- Signing parsed JSON instead of the raw JSON text: the signature will not match.
- Reformatting or pretty-printing JSON after signing: the signature will not match.
- Reusing the same nonce on retries: the request will be treated as a replay.
- Using the wrong company secret: the signature will not match.
Troubleshooting
Missing ingest signature:x-event-signaturewas not sent.Missing replay timestamp header:x-event-timestampwas not sent.Missing replay nonce header:x-event-noncewas not sent.Replay detected for nonce: the nonce was already used recently.Invalid ingest signature: the computed signature does not match the raw body.