Documentation Index
Fetch the complete documentation index at: https://docs.minisend.xyz/llms.txt
Use this file to discover all available pages before exploring further.
Each request includes X-Minisend-Signature — an HMAC-SHA256 hex of the raw JSON body, keyed with your webhook_secret. Verify it before processing.
Find your webhook_secret in Settings. Treat it like a password.
Use a timing-safe comparison. === is vulnerable to timing attacks.
Sign over the raw body
The signature covers the exact bytes Minisend sent. Re-stringifying the parsed body (JSON.stringify(req.body)) can produce different bytes and break verification.
Capture the raw body before parsing. In Express, use express.raw({ type: 'application/json' }) for the webhook route.
Verify
const crypto = require('crypto');
function verifyWebhook(rawBody, signature, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(rawBody, 'utf8')
.digest('hex');
const a = Buffer.from(signature, 'hex');
const b = Buffer.from(expected, 'hex');
if (a.length !== b.length) return false;
return crypto.timingSafeEqual(a, b);
}
Full Express handler
const express = require('express');
const crypto = require('crypto');
const app = express();
// Raw body required for signature verification
app.use(
'/webhooks/minisend',
express.raw({ type: 'application/json' }),
);
function verifyWebhook(rawBody, signature, secret) {
const expected = crypto.createHmac('sha256', secret).update(rawBody, 'utf8').digest('hex');
const a = Buffer.from(signature, 'hex');
const b = Buffer.from(expected, 'hex');
if (a.length !== b.length) return false;
return crypto.timingSafeEqual(a, b);
}
app.post('/webhooks/minisend', (req, res) => {
const signature = req.headers['x-minisend-signature'];
if (!signature) return res.status(401).json({ error: 'Missing signature' });
if (!verifyWebhook(req.body, signature, process.env.MINISEND_WEBHOOK_SECRET)) {
return res.status(401).json({ error: 'Invalid signature' });
}
const payload = JSON.parse(req.body.toString('utf8'));
// Acknowledge immediately, then process
res.status(200).send('OK');
switch (payload.event) {
case 'checkout.completed':
markOrderPaid(payload.external_id, {
session_id: payload.session_id,
receipt: payload.receipt,
amount_local: payload.amount_local,
currency: payload.currency,
});
break;
case 'checkout.failed':
handleFailedPayment(payload.external_id, payload.session_id);
break;
case 'checkout.expired':
handleExpiredSession(payload.external_id, payload.session_id);
break;
}
});
app.listen(3000);
Return 2xx before running business logic. Anything taking >10s triggers a retry.