Blog · October 12, 2026

Stripe or Mercado Pago webhook not received

The customer paid, saw «payment approved», and the order is still «pending» in your system. It's almost always a webhook that never arrived, or one that arrived and failed. Here's how to tell which, the most common causes, code that answers it correctly, and how to find out before the customer writes to you.

1. What it looks like

The charge happens on Stripe or Mercado Pago. Your system learns about it through a webhook: a request they send to your server when the payment changes. If that request never arrives or your server answers with an error, this happens:

You find out when the customer writes «I already paid, where's my order?». The rest of this guide is about finding out sooner.

2. Was it sent? Check the dashboard

First, find out whether the provider sent it and what your server answered. Both keep a history.

Stripe

Mercado Pago

3. The most common causes

What the dashboard says points almost straight at the cause.

What you seeWhat it usually isHow to fix it
No deliveries at allThe URL isn't configured, the event isn't selected, or you're mixing test and live mode. In Mercado Pago, a notification_url on the preference sends it somewhere else.Check the URL and events for the right mode (test and live have different URLs and keys).
400 or 401Your server rejected the signature. On Stripe, almost always because the body was parsed as JSON before verification: «No signatures found matching the expected signature for payload».Verify against the untouched body, with that endpoint's secret for that mode. On Mercado Pago, the secret key of that same application.
Timed outYou do everything (save, send emails, invoice) before answering.Answer first, process afterwards (code below).
500Your code crashes: the order wasn't found, the API call used the other mode's token, a column changed.Search your logs for the event id. While you fix it, the provider keeps retrying.
301, 302 or 404The URL changed: http to https, with or without www, or a route that no longer exists. Stripe treats any redirect as a failure.Use the exact final URL, the one that answers without redirecting.
200, but the order didn't changeYou answered 200 and then processing failed; a different event arrived than the one you expected; or it arrived twice, or out of order.Record every event that arrives with its id, and don't assume order: check the payment's real status.
OXXO payments on Stripecheckout.session.completed fires when the customer gets their voucher, with payment_status: "unpaid". The money arrives hours or days later.Fulfill on checkout.session.async_payment_succeeded, not on completed.
A Mercado Pago detail that trips people up: the webhook doesn't include the payment status. It carries the type, the action and the id (data.id). To know whether it was approved, fetch it from /v1/payments/{id} with the access token from the same account and the same mode.

4. Answering it correctly

The rule is the same for both: verify the signature, answer fast, process afterwards. And log that it arrived, so you can look it up.

Stripe, with Express

// the untouched body ONLY on this route; express.json() goes after
app.post('/webhooks/stripe', express.raw({ type: 'application/json' }), (req, res) => {
  let event;
  try {
    event = stripe.webhooks.constructEvent(
      req.body, req.headers['stripe-signature'], process.env.STRIPE_WEBHOOK_SECRET);
  } catch (err) {
    console.error('stripe webhook: bad signature', err.message);
    return res.sendStatus(400);
  }
  res.sendStatus(200);   // answer first
  handle(event).catch((err) => console.error('stripe webhook', event.id, err));
});

Mercado Pago, with Express

app.post('/webhooks/mercadopago', express.json(), (req, res) => {
  // check x-signature with your application's secret key (the official SDKs do it)
  if (!validSignature(req)) return res.sendStatus(401);
  res.sendStatus(200);   // within 22 seconds
  if (req.body.type !== 'payment') return;
  checkPayment(req.body.data.id).catch((err) => console.error('mp webhook', req.body.data.id, err));
});

async function checkPayment(id) {
  const r = await fetch(`https://api.mercadopago.com/v1/payments/${id}`, {
    headers: { Authorization: `Bearer ${process.env.MP_ACCESS_TOKEN}` },
  });
  const payment = await r.json();
  // payment.status: approved, pending, rejected…  payment.external_reference: your order
  if (payment.status === 'approved') await markPaid(payment.external_reference);
}

Answering before processing has a cost: if handle fails, the provider won't retry, because you told it everything was fine. That's why the next step matters.

5. Don't rely on the webhook alone

The webhook is the fast path, not the only one. Two cheap backups:

6. Get alerted automatically

Everything above fixes the problem once you know it exists. What's missing is finding out the same day, not when the customer writes. The idea: record two moments and have something watch that the second one arrives.

With Estiaje it's two lines in your code:

// on your «thank you» page, when the payment comes back approved
estiaje.event('payment.approved_by_customer', { order_id, amount });

// in your webhook, once the order is marked paid
estiaje.outcome('payment.confirmed', { order_id });

And in the dashboard you declare what you expect, without touching any more code:

{
  "name":       "Payment approved → webhook processed",
  "when":       "payment.approved_by_customer",
  "expects":    "payment.confirmed",
  "match_by":   "attrs.order_id",
  "window_min": 30,
  "valor":      { "tipo": "atributo", "atributo": "amount" }
}

If an order is left without a close, your phone gets something like «$1,280 at risk: Payment approved → webhook processed», with the order that got stuck and, if you add a link to your system, a button to open it. If your webhook crashes, the error arrives grouped, with its stack trace and the order it left halfway.

Try Pro free for 14 days No card. Afterwards you stay on Free, with 3 watched processes.

Frequently asked questions

How often does Stripe retry a failed webhook?

In live mode, for up to three days with exponential backoff. You can resend an event by hand from Workbench up to 15 days after it was created, or with the Stripe CLI (stripe events resend) up to 30 days.

How often does Mercado Pago retry a webhook?

Mercado Pago waits 22 seconds for a 200 or 201. If it doesn't get one, it retries every 15 minutes; after the third attempt it spaces retries out, but keeps trying.

What does «No signatures found matching the expected signature for payload» mean?

The signature doesn't match the body you received. It's almost always because your framework parsed the body as JSON before verification (for example, express.json() before the webhook route), or because you're using the secret from another endpoint or from test mode. In Express, use express.raw({ type: 'application/json' }) on that route.

Does the Mercado Pago webhook include the payment status?

No. It carries the type, the action and the resource id (data.id). To know whether the payment was approved, fetch it from /v1/payments/{id} with the access token from the same account and the same mode (test or production).

Sources: the webhook documentation from Stripe (docs.stripe.com/webhooks) and Mercado Pago (mercadopago.com.mx/developers), checked October 12, 2026.

Keep reading