Webhooks
Get notified when payouts and collections change, and verify every delivery.
Webhooks tell your server when money moves, so you never have to poll. Every event, field and header is listed in the webhook events reference.
Add an endpoint
Register endpoints in the dashboard under Developers → Webhooks: enter the HTTPS URL and pick the events to receive. The endpoint's signing secret (it starts with whsec_) is shown when you create it; store it next to your API key.
Endpoints belong to an environment like everything else: endpoints you add while the dashboard is in Sandbox mode receive Sandbox events only.
Verify the signature
Join X-MigraPay-Timestamp, a . and the raw body bytes (before any JSON parsing). Compute the HMAC-SHA256 of that string using the full signing secret, including its whsec_ prefix, as the key. Hex-encode it and compare with X-MigraPay-Signature-V2 in constant time.
Reject the request if the signatures differ, or if the timestamp is more than 5 minutes from your clock. The timestamp check stops someone replaying an old delivery they captured.
import crypto from 'node:crypto';
import express from 'express';
const app = express();
const secret = process.env.MIGRAPAY_WEBHOOK_SECRET; // whsec_...
// Read the raw body: the signature covers the exact bytes sent.
app.post('/webhooks/migrapay', express.raw({ type: 'application/json' }), (req, res) => {
const timestamp = req.get('X-MigraPay-Timestamp') ?? '';
const fresh = Math.abs(Date.now() / 1000 - Number(timestamp)) < 300;
const expected = crypto
.createHmac('sha256', secret)
.update(`${timestamp}.`)
.update(req.body)
.digest('hex');
const received = req.get('X-MigraPay-Signature-V2') ?? '';
const valid =
fresh &&
received.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected));
if (!valid) return res.sendStatus(400);
const { event, data } = JSON.parse(req.body.toString('utf8'));
// Deduplicate on req.get('X-MigraPay-Delivery'), then hand off to a queue.
res.sendStatus(200);
});Verify against the raw body. Parsing the JSON and serialising it again can change spacing or key order, and the signature will not match.
Responding and retries
- Return any 2xx status within 10 seconds to acknowledge. Do the real work asynchronously.
- Any other status, a timeout or a connection error is a failed attempt. MigraPay retries up to 5 attempts in total, waiting 2, 4, 8 and 16 minutes between them.
- After the fifth failed attempt the delivery is marked failed and not retried automatically.
- Deliveries can arrive more than once and out of order. Deduplicate on
X-MigraPay-Delivery, and treat the payout or collection status from the API as the source of truth when ordering matters.
Best practices
- Use HTTPS and verify every signature.
- Acknowledge fast, then process from a queue.
- Make your handler idempotent: processing the same delivery twice must not credit a customer twice.
- If you suspect you missed events, reconcile with
GET /transactionsorGET /collections.