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:

  1. Read the raw bytes exactly as received.
  2. Extract the signature header.
  3. Recompute the digest using the same secret.
  4. Use timing-safe comparison.
  5. 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-signature was not sent.
  • Missing replay timestamp header: x-event-timestamp was not sent.
  • Missing replay nonce header: x-event-nonce was not sent.
  • Replay detected for nonce: the nonce was already used recently.
  • Invalid ingest signature: the computed signature does not match the raw body.