Integration
Callbacks are where integrations break
Not because they are hard, but because three things get skipped. Here they are, in the order they bite.
- 01Verify the signature
Every callback carries a signature computed over the raw body with your secret key. Compute it yourself and compare in constant time. If it does not match, answer 400 and change nothing: anyone can post to a public address.
// Compute over the raw body, before any JSON parsing. const expected = crypto .createHmac('sha256', process.env.MP_SECRET) .update(rawBody) .digest('hex'); if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(received))) { return res.status(400).end(); } - 02Be idempotent
The same notification can arrive twice, and it will. Key your handler on the payment identifier, not on the fact that a request came in. Processing the same callback twice must not ship the order twice or refund it twice.
const already = await orders.findPayment(body.id); if (already?.status === body.status) return res.status(200).end();
- 03Answer fast, work later
Answer 200 as soon as you have stored the fact. Do the slow work afterwards. If you answer late or with an error, we retry with growing intervals, and a slow handler turns one payment into a queue of duplicates.
Confirm before you ship
A callback tells you something changed. Before releasing goods, read the payment from the API and act on that answer. It costs one request and removes a whole class of fraud.
The exact header name, the algorithm and the retry schedule are in the specification issued with your keys, so that this page can never drift away from what the gateway really sends.