All articles

Integrations · July 25, 2026 · 12 min read

Mercado Pago and Pix webhooks: the complete integration and testing guide

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

  1. Go to Your integrations, select your application and open Webhooks → Configure notifications.
  2. 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.
  3. Select the events you want to receive. For payments (Pix, cards, boleto), the topic is payment, which fires the payment.created and payment.updated actions.
  4. Save the secret signature shown on that screen — it's what you'll use to verify the x-signature header. 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:

POST /webhooks/mercadopago?data.id=12345678901&type=payment
{
  "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

  1. You create the Pix payment and show the QR code to the customer.
  2. The customer pays in their banking app; Mercado Pago approves within seconds.
  3. Mercado Pago sends the notification (action: payment.updated) to your URL, with the signature in the x-signature header.
  4. Your endpoint verifies the signature and responds 200 immediately — Mercado Pago waits for the response for about 22 seconds; without it, the delivery counts as failed and is retried later.
  5. Outside the response cycle, your application calls GET /v1/payments/12345678901 with the account's access token.
  6. 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:

webhook handler — respond fast, process later
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 });
  }
});
in the worker — the API is the source of truth
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:

relevant headers
x-signature: ts=1704908010,v1=618c85345248dd820d5fd456117c2ab2ef8eda45...
x-request-id: bb56a2f1-6aae-46ac-982e-9dcd3581d08e

Verification takes four steps:

  1. Extract ts and v1 from the x-signature header.
  2. Build the manifest using the official template: id:[data.id];request-id:[x-request-id];ts:[ts]; — using the data.id from the query string and the x-request-id header.
  3. Compute the HMAC-SHA256 of the manifest in hexadecimal, using the secret signature from the panel as the key.
  4. Compare the result with v1 using a constant-time comparison.
signature verification in Node
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 firesWhat to do
payment.createdPayment created: Pix pending, boleto issued, card under reviewRecord the payment and wait for the update
payment.updatedStatus changed: approved, rejected, refunded, cancelledCall GET /v1/payments/{id} and update the order
chargebacksDispute opened or updatedPause fulfilment and gather evidence of the sale
subscription_preapprovalSubscription created, paused or cancelledSync 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-signature and 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-signature with the official manifest id:[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, a live_mode check, and consistent credentials between notification and lookup.
  • Test with sandbox accounts, the panel simulator and an inspection endpoint before writing code.