Checking webhook signatures
Verify the data CryptoSwift sends to your webhook endpoint
CryptoSwift signs tenant webhooks with the webhookSecret for the receiving environment. Retrieve it from GET /tenant/me (Sandbox) or GET /tenant/me (Production). Updating your webhook URL generates a new secret; refresh the stored value after each URL change.
For Partner webhooks, use the Partner webhookSecret from GET /partners/me. The signature format and verification steps below are the same.
The header has this format:
CryptoSwift-Signature: t=<Unix timestamp in milliseconds>,s=<lowercase hex HMAC-SHA256>
The signed input is the timestamp exactly as supplied in the header, followed by ".", followed by the original request body bytes. Capture the body before JSON middleware parses it. Do not parse and reserialize the body before verification, or trust its contents until verification succeeds. The current backend signs the same JSON serialization that its HTTP client transmits.
This Express example uses express.raw on the webhook route. If your app has global JSON parsing middleware, mount this route before that middleware or use its raw-body capture option.
const express = require('express');
const crypto = require('node:crypto');
const app = express();
const webhookSecret = process.env.CRYPTOSWIFT_WEBHOOK_SECRET;
if (!webhookSecret) throw new Error('CRYPTOSWIFT_WEBHOOK_SECRET is required');
app.post('/webhooks/cryptoswift', express.raw({ type: 'application/json' }), (req, res) => {
const match = /^t=(\d+),s=([a-f0-9]{64})$/.exec(req.get('CryptoSwift-Signature') || '');
if (!match || !Buffer.isBuffer(req.body)) return res.sendStatus(400);
const [, timestamp, signature] = match;
const timeMs = Number(timestamp); // CryptoSwift uses Unix milliseconds, like Date.now().
if (!Number.isSafeInteger(timeMs) || Math.abs(Date.now() - timeMs) > 5 * 60 * 1000) {
return res.sendStatus(400); // Recommended five-minute replay tolerance.
}
const expected = crypto.createHmac('sha256', webhookSecret)
.update(timestamp).update('.').update(req.body).digest();
const received = Buffer.from(signature, 'hex');
if (!crypto.timingSafeEqual(received, expected)) return res.sendStatus(401);
let event;
try {
event = JSON.parse(req.body.toString('utf8'));
} catch {
return res.sendStatus(400);
}
// Demo receipt only; replace with durable, idempotent processing before 2xx.
console.info('Verified CryptoSwift webhook', { id: event.id, type: req.get('X-Event-Type') });
res.sendStatus(204);
});
app.listen(3000);
The fixed 64-character hex check ensures the buffers have equal length before timingSafeEqual. The timestamp check limits replay to a five-minute window; it does not prevent duplicate delivery within that window. Replace the demo receipt with durable, idempotent processing before acknowledging, while allowing legitimate updates to the same transaction. Use webhook handling for status reconciliation. The same signature format is used for transaction and wallet verification notifications.