A Pix payment — Brazil's instant payment system — is confirmed within seconds, and your application needs to know right away, without polling the Mercado Pago API in a loop. That's what notification webhooks are for. In this guide you'll set up notifications from scratch, verify the x-signature header, build the complete flow of a Pix payment and test everything before going live — including the errors that most often stall real-world integrations.
Why Mercado Pago notifies you via webhook
When a customer pays with Pix, approval happens in seconds; with a card, the status may move from in_process to approved or rejected minutes later; with a boleto — a widely used Brazilian bank slip paid in cash or through bank apps — days later. Polling the API every few seconds for every open order doesn't scale and quickly runs into rate limits. The inverted model solves it: Mercado Pago sends a POST to your URL whenever something relevant happens, and your application reacts immediately.
Historically there were two mechanisms: IPN (Instant Payment Notification), now deprecated, and Webhooks, which are the recommended path and the only one you should use in new integrations. The official documentation lives on the Mercado Pago Webhooks notifications page. If you're not yet comfortable with how webhooks work in general (retries, idempotency, fast responses), start with our guide What is a webhook and how does it work.
Setting up notifications
Through the developer panel
- Go to Your integrations, select your application and open Webhooks → Configure notifications.
- Register your endpoint URL — it must be valid HTTPS and reachable from the internet. There are two separate fields: test mode (used with test credentials) and production mode.
- Select the events you want to receive. For payments (Pix, cards, boleto), the topic is
payment, which fires thepayment.createdandpayment.updatedactions. - Save the secret signature shown on that screen — it's what you'll use to verify the
x-signatureheader. It can be regenerated at any time (in which case you must update your environment).
Via API, per payment
Alternatively (or in addition), you can set the notification_url field when creating the payment or the checkout preference. This is useful when each store or tenant in your system has its own endpoint. The notification arrives in the same format; the only difference is where the URL was defined.
What arrives at your endpoint
The notification is a POST with the resource identifier in the query string (data.id and type) and a compact JSON body:
{
"id": 987654321,
"live_mode": true,
"type": "payment",
"date_created": "2026-07-25T14:32:07.000-04:00",
"user_id": 44444,
"api_version": "v1",
"action": "payment.updated",
"data": { "id": "12345678901" }
}The complete flow of a Pix payment, step by step
- You create the Pix payment and show the QR code to the customer.
- The customer pays in their banking app; Mercado Pago approves within seconds.
- Mercado Pago sends the notification (
action: payment.updated) to your URL, with the signature in thex-signatureheader. - Your endpoint verifies the signature and responds
200immediately — Mercado Pago waits for the response for about 22 seconds; without it, the delivery counts as failed and is retried later. - Outside the response cycle, your application calls
GET /v1/payments/12345678901with the account's access token. - If
status = "approved", the order is released; any other status just updates your internal record.
In code (Node + Express), the handler skeleton looks like this:
app.post("/webhooks/mercadopago", async (req, res) => {
// 1. Reject requests without a valid signature (see section below)
if (!isValidSignature(req)) return res.sendStatus(401);
// 2. Acknowledge right away — don't wait for processing
res.sendStatus(200);
// 3. Heavy work goes to a queue, never inside the response cycle
const { type, data } = req.body;
if (type === "payment") {
await queue.add("process-payment", { paymentId: data.id });
}
});const resp = await fetch(
`https://api.mercadopago.com/v1/payments/${paymentId}`,
{ headers: { Authorization: `Bearer ${process.env.MP_ACCESS_TOKEN}` } },
);
const payment = await resp.json();
if (payment.status === "approved" && payment.live_mode) {
await releaseOrder(payment.external_reference);
}Verifying the x-signature header
Your endpoint is public — anyone who discovers the URL can send a POST with a JSON body identical to Mercado Pago's. The signature is what separates a legitimate notification from a forged one. The header arrives in this format:
x-signature: ts=1704908010,v1=618c85345248dd820d5fd456117c2ab2ef8eda45...
x-request-id: bb56a2f1-6aae-46ac-982e-9dcd3581d08eVerification takes four steps:
- Extract
tsandv1from thex-signatureheader. - Build the manifest using the official template:
id:[data.id];request-id:[x-request-id];ts:[ts];— using thedata.idfrom the query string and thex-request-idheader. - Compute the HMAC-SHA256 of the manifest in hexadecimal, using the secret signature from the panel as the key.
- Compare the result with
v1using a constant-time comparison.
import crypto from "node:crypto";
function isValidSignature(req) {
const signature = req.headers["x-signature"]; // "ts=...,v1=..."
const requestId = req.headers["x-request-id"];
const dataId = req.query["data.id"];
if (!signature || !requestId || !dataId) return false;
const parts = Object.fromEntries(
signature.split(",").map((p) => p.trim().split("=")),
);
if (!parts.ts || !parts.v1) return false;
const manifest = `id:${dataId};request-id:${requestId};ts:${parts.ts};`;
const expected = crypto
.createHmac("sha256", process.env.MP_WEBHOOK_SECRET)
.update(manifest)
.digest("hex");
return (
expected.length === parts.v1.length &&
crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1))
);
}Events worth subscribing to
| Event (action) | When it fires | What to do |
|---|---|---|
payment.created | Payment created: Pix pending, boleto issued, card under review | Record the payment and wait for the update |
payment.updated | Status changed: approved, rejected, refunded, cancelled | Call GET /v1/payments/{id} and update the order |
chargebacks | Dispute opened or updated | Pause fulfilment and gather evidence of the sale |
subscription_preapproval | Subscription created, paused or cancelled | Sync the subscription status in your system |
How to test before going live
- Test accounts. Create a seller/buyer pair under "Test accounts" in the panel. The test seller has its own credentials — that's what you use to create sandbox payments and receive notifications on the test-mode endpoint.
- Notification simulator. The Webhooks configuration screen has a simulation button that sends a sample notification to your URL — the fastest way to check connectivity and signature verification.
- Inspect the real payload first. Before writing the handler, point the test URL at an inspection endpoint (a temporary URL that captures and displays every request, headers and body included). Seeing a real
x-signatureand body eliminates half of the integration doubts. - Local development. Mercado Pago can't reach
localhost— you'll need a tunnel or event forwarding. The options are compared in How to test webhooks on localhost. - Test cards and payers. Use the documented test cards (approval, declined for insufficient funds, declined for security) to exercise every path in your flow, and sandbox Pix for the instant flow.
Troubleshooting: the classic problems
The notification never arrives
Almost always infrastructure: a URL with invalid HTTPS (expired or self-signed certificate), a firewall/WAF blocking Mercado Pago's IPs, or the URL registered in the wrong mode (test × production). Confirm with the panel simulator and make sure your endpoint responds in under 22 seconds — a slow response counts as a failure even if the status code is 200.
401 when fetching the payment
The data.id you receive belongs to the account whose credentials created the payment. A common mistake is receiving the notification from the test account and calling the API with the production access token (or one from another application). Notification and lookup must use the same credential pair.
Duplicate notifications
Mercado Pago retries unacknowledged notifications in cycles of up to 15 minutes — and sometimes the same event arrives twice even when everything is fine. Make processing idempotent: use the data.id + the fetched status as the key and ignore repeats. It's the same general principle as any webhook, detailed in The 10 most common webhook implementation mistakes.
Test notification processed as a real sale
Every notification carries live_mode in the body, and the payment you fetch has the field too. Check it before releasing anything — orders "paid" with test credentials in production are an embarrassing classic.
"Late" boleto
Pix notifies within seconds; a boleto only changes status when the bank clears it (up to 3 business days). It's not a bug — it's the payment method. Model your internal states to live with payments that stay pending for days.
Summary
- Use Webhooks (not IPN): topic
payment, one HTTPS URL per mode (test/production), secret signature stored safely. - Verify
x-signaturewith the official manifestid:[data.id];request-id:[x-request-id];ts:[ts];and HMAC-SHA256. - Respond 200 in under 22 seconds and process in a queue; the real status comes from
GET /v1/payments/{id}, never from the webhook body. - Idempotency keyed on
data.id, alive_modecheck, and consistent credentials between notification and lookup. - Test with sandbox accounts, the panel simulator and an inspection endpoint before writing code.