Every delivery is signed with your environment’s webhook signing secret (sf_test_whsec_… / sf_live_whsec_…). Verification proves the event came from SwitchFlow and wasn’t tampered with — never fulfill an order from an unverified webhook.

The signature header

SwitchFlow-Signature: t=1751798551,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
  • t — Unix timestamp (seconds) of the delivery attempt
  • v1 — hex-encoded HMAC-SHA256(secret, "{t}.{raw request body}")

Verification steps

  1. Read the raw request body — before any JSON parsing or middleware re-serialization (a re-serialized body may not be byte-identical).
  2. Parse t and v1 from the header.
  3. Compute HMAC-SHA256 over the string {t}.{rawBody} with your signing secret and compare to v1 using a constant-time comparison.
  4. Reject if the timestamp is outside your replay window (±5 minutes is a sensible default).

Example (Node.js)

const crypto = require('crypto');

function verifySwitchFlowSignature(rawBody, signatureHeader, secret) {
  const parts = Object.fromEntries(
    signatureHeader.split(',').map((kv) => kv.split('='))
  );
  const { t, v1 } = parts;
  if (!t || !v1) return false;

  // Bound the replay window to ±5 minutes
  if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;

  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${t}.${rawBody}`)
    .digest('hex');

  return (
    expected.length === v1.length &&
    crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1))
  );
}

// Express: capture the raw body for the webhook route
app.post(
  '/webhooks/switchflow',
  express.raw({ type: 'application/json' }),
  (req, res) => {
    const ok = verifySwitchFlowSignature(
      req.body.toString('utf8'),
      req.get('SwitchFlow-Signature') || '',
      process.env.SWITCHFLOW_WEBHOOK_SECRET
    );
    if (!ok) return res.status(400).send('invalid signature');

    const event = JSON.parse(req.body);
    // enqueue for async processing, keyed on event.id
    res.sendStatus(200);
  }
);

Pitfalls

  • Body-parsing middleware that runs before your webhook route will break verification — mount a raw-body parser for the webhook path specifically.
  • Rotating the signing secret revokes the old one immediately. Deploy the new secret before rotating, or you will reject legitimate deliveries (they will retry, but a 400 from your side is terminal — see retry policy).
  • Return 2xx only after the signature check passes; return 400 for bad signatures so the delivery is flagged in your dashboard rather than retried pointlessly.