You just wired up a Pix checkout with Mercado Pago. Payment goes through, the buyer sees a confirmation, and then... nothing happens in your app. The payment.created notification is being POSTed somewhere — to a public URL that Mercado Pago requires — but your order-fulfillment code lives on localhost:3000, and Mercado Pago can't reach it.
The usual workarounds are all bad. Deploy to staging on every change and tail remote logs. Spin up a raw tunnel and hope the signature headers survive. Paste the payload into a curl script by hand and lose all fidelity with what production actually sends. None of these let you set a breakpoint in your handler and step through a real notification.
Relayers fixes this. It gives you a stable public ingest URL, forwards the exact request to your machine, and lets you filter and reshape events before they ever hit your code — with the original body and headers untouched, so HMAC signature verification works locally.
What you'll build
Mercado Pago sends its notification to a public URL it can always reach. Relayers receives it, applies your routing rules and transform, then pipes it down a WebSocket tunnel to whatever port your dev server runs on.
Prerequisites
- A Mercado Pago account with access to the developer panel (Your integrations → your app).
- Node, Go, Python — whatever your webhook handler runs in — listening on a local port (we'll use
3000). - The
relayersCLI. Download the desktop app or grab the CLI, then runrelayers login. - A free Relayers account. The free tier covers local development.
1Create an endpoint in Relayers
Open the desktop app, go to Endpoints, and click Create. Give it a name like mercadopago-dev. Relayers generates a public ingest URL:
https://api.relayers.app/v1/webhooks/wep_abc123
Copy it — you'll paste it into Mercado Pago next. Before leaving the app, hit Send test to fire a sample request at the endpoint so you can confirm it's live.
2Configure the notification URL in Mercado Pago
Mercado Pago supports two ways to tell it where to send notifications.
Option A — the developer panel (recommended for a fixed URL). Go to Your integrations → your app → Webhooks / Notificações, paste your Relayers URL as the Notification URL, and select the events you care about (Payments). Save.
Option B — per request. When you create a payment or preference through the API, pass notification_url:
{
"transaction_amount": 100.0,
"description": "Pix order #4821",
"payment_method_id": "pix",
"notification_url": "https://api.relayers.app/v1/webhooks/wep_abc123",
"payer": { "email": "buyer@example.com" }
}
Once configured, Mercado Pago POSTs a compact JSON body on each event:
{
"type": "payment",
"action": "payment.created",
"data": { "id": "1234567890" }
}
Newer webhooks also include an x-signature and x-request-id header so you can validate the request came from Mercado Pago via HMAC. Relayers forwards both headers untouched — more on that below.
Note that the body only carries the payment id. You still fetch the full payment object yourself with a GET /v1/payments/{id} call using that id — the notification is a nudge, not the record.
3Forward events to localhost
In your terminal, start the tunnel:
relayers listen --forward localhost:3000
The CLI opens a WebSocket to Relayers. From now on, every request that arrives at your endpoint is replayed against localhost:3000, method, path, body, and headers intact. Trigger a test payment in the Mercado Pago sandbox and watch it land in your handler in real time. Set a breakpoint — it'll hit.
If you prefer to route in the app, open the endpoint's Rules and set the destination to a tunnel pointing at localhost:3000.
4Route and filter with JQ
Mercado Pago fires notifications for several type values — payment, plan, subscription, invoice, and more. During development you usually only care about payments. Open Rules on your endpoint and add a JQ filter:
{ "type": "payment" }
This matches only requests whose body has type == "payment"; everything else is skipped. Rules are evaluated top-down and first match wins, so if you later add rules for subscriptions or invoices, drag them into the order you want and reorder as needed. Each matching rule has its own destination — one rule can go to your tunnel while another forwards to a public URL.
5Transform the payload
The raw Mercado Pago body is fine, but you can hand your handler exactly the shape it wants. Open Transformations, create one, and pick a language — jq, JSONata, JavaScript, or Go templates. A minimal jq transform:
{ event: .action, paymentId: .data.id }
Given the earlier payload, your handler receives:
{ "event": "payment.created", "paymentId": "1234567890" }
Use the built-in test playground to paste a sample body and see the output before you save. Attach the transform to your rule and the reshaped payload is what gets delivered.
Signature verification still works
This is the part that breaks with most tunneling hacks. Mercado Pago's x-signature header is an HMAC computed over specific request fields (including the data.id and the x-request-id value). If a proxy rewrites the body or drops headers, your local validation fails and you can't tell a real bug from a tooling artifact.
Relayers forwards the original body and headers byte-for-byte. Both x-signature and x-request-id arrive at localhost:3000 exactly as Mercado Pago sent them, so the HMAC check you run in production runs identically on your laptop. Test your signature-validation code for real — no if (isDev) skipVerification shortcuts.
Going to production
When you deploy, point Mercado Pago at your real endpoint. Two clean paths:
- Keep Relayers in front. Change the rule's destination from the localhost tunnel to your production URL. Your filter and transform logic carries over unchanged, and you keep Analytics and retry for free.
- Swap the notification URL. Update the URL in the Mercado Pago panel to hit your service directly.
Either way, use Import/Export to save your endpoint, rules, and transformations as a JSON bundle and replay the setup in another workspace or a teammate's machine.
Troubleshooting
- Respond fast with 2xx. Mercado Pago expects a quick
200or201. Do the heavy work (fetching the payment, updating your DB) after you acknowledge, or asynchronously — a slow handler triggers retries and duplicate deliveries. - Deduplicate by
data.id. Because retries happen, the samepayment.createdcan arrive more than once. Treatdata.id(plusaction) as an idempotency key and no-op on repeats. - Sandbox vs. production credentials. Test notifications come from your sandbox/test application; live ones from the production app. Make sure the notification URL is set on the right application, and check the Relayers Events view and payload inspector to confirm which environment a request came from.
- Nothing arriving? Open Events in the app. If requests show up in Relayers but not on localhost, your tunnel isn't running or is pointed at the wrong port. If nothing shows up at all, re-check the Notification URL in Mercado Pago and use Send test to isolate the problem.
Wrap-up
Mercado Pago requires a public notification URL, but your code lives on localhost — that's the whole friction. Relayers bridges it: a stable ingest URL, JQ filtering so you only see the events you want, transformations to reshape payloads, and byte-for-byte forwarding that keeps x-signature validation honest. You debug real Pix and card notifications against your dev server, breakpoints and all, then flip the destination to production when you're ready.
Ready to stop deploying just to see a webhook? Create a free endpoint at relayers.app and download the app to start forwarding in minutes.