docs
Get startedTroubleshootingPaymentsWebhooksAgent paymentsSwop MCPOviIdentity (swop.id)SwivyRPC (preview)API reference

Verify signatures

Prove a webhook really came from Swop before you ship anything.

Anyone can POST to your webhook URL. The Swop-Signature header proves a request came from Swop and wasn't changed on the way. Always verify it before acting on an event.

How the signature works

Swop-Signature: t=1761760800,v1=5f2b8c…e41c9
PartMeaning
tWhen Swop signed it, in Unix seconds
v1Hex-encoded HMAC-SHA256 of the string "<t>.<raw request body>", keyed with your whsec_ signing secret

To verify:

  1. Read the raw request body as bytes. Don't parse and re-serialize the JSON first; any whitespace change breaks the signature.
  2. Split the header into t and v1.
  3. Reject it if t is more than 5 minutes from now. That stops someone replaying an old request.
  4. Compute HMAC-SHA256 of t + "." + rawBody with your secret, hex-encode it, and compare it to v1 in constant time.

Verifier code

These verifiers match Swop's own signing code and were tested against it.

// Node 18+ with Express. express.raw() keeps the exact bytes Swop signed.
import crypto from 'node:crypto';
import express from 'express';

export function verifySwopSignature(secret, header, rawBody, toleranceS = 300) {
  const parts = Object.fromEntries(
    String(header || '').split(',').map((kv) => {
      const i = kv.indexOf('=');
      return [kv.slice(0, i).trim(), kv.slice(i + 1).trim()];
    }),
  );
  const t = Number(parts.t);
  if (!Number.isFinite(t) || Math.abs(Date.now() / 1000 - t) > toleranceS) return false;
  const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
  const given = String(parts.v1 || '');
  if (given.length !== expected.length) return false;
  return crypto.timingSafeEqual(Buffer.from(given), Buffer.from(expected));
}

const app = express();

app.post('/webhooks/swop', express.raw({ type: 'application/json' }), (req, res) => {
  const raw = req.body.toString('utf8');
  if (!verifySwopSignature(process.env.SWOP_WEBHOOK_SECRET, req.get('Swop-Signature'), raw)) {
    return res.status(400).send('bad signature');
  }
  const event = JSON.parse(raw);
  switch (event.event) {
    case 'checkout.paid':   /* fulfil event.data.orderId */ break;
    case 'payout.released': /* book revenue for event.data.orderId */ break;
    case 'ping':            /* "Send test" from the Developer page */ break;
  }
  res.sendStatus(200); // any 2xx counts as delivered
});

app.listen(3000);

Common mistakes

SymptomCause
Every signature failsYour framework parsed the JSON before you read it. Use the raw body (express.raw, request.get_data()).
Fails only in productionWrong secret: each webhook has its own whsec_, and re-adding a webhook issues a new one.
Fails intermittentlyYour server clock is off by more than 5 minutes. Sync it with NTP.
Works locally, not deployedA proxy is rewriting the body or stripping the Swop-Signature header.

On this page