Suscripciones de Mercado Pago bien hechas: preapproval, webhooks y pagos fallidos (2026) — Cesar Ayala
← Todos los artículos

Suscripciones de Mercado Pago bien hechas: preapproval, webhooks y pagos fallidos (2026)

Crea la suscripción con POST /preapproval usando un card_token_id y status "authorized" (con o sin un preapproval_plan reutilizable). Luego trata los webhooks como verdad: subscription_preapproval para el ciclo de vida y subscription_authorized_payment para cada cobro. Consulta cada recurso por id, da acceso solo con un cobro aprobado confirmado y sincroniza las cancelaciones en ambos sentidos.

¿Con plan o sin plan? Las dos formas de armar suscripciones en MP

Antes de escribir una sola línea de código, tienes que decidir algo que define toda tu integración: ¿vas a usar un plan reutilizable o vas a crear cada suscripción directa? Mercado Pago te da dos modelos y la mayoría de la gente elige mal porque los docs no enmarcan la decisión, solo la describen.

Modelo 1 — con plan asociado. Creas un preapproval_plan reutilizable con POST /preapproval_plan, que pre-cocina la configuración de recurrencia (frecuencia, monto, moneda). Después suscribes a cada payer a ese plan. Este es el camino para tiers estandarizados: tu plan “Pro $299/mes” existe una vez y todos tus clientes Pro cuelgan de él. Cuando cambias el plan, cambias un objeto y no mil suscripciones.

Modelo 2 — sin plan. Creas la suscripción directa con POST /preapproval y metes la configuración de auto_recurring inline en esa misma llamada. Este es el camino para montos custom por cliente, cobros one-off o cadencias que no se repiten entre usuarios. Cada suscripción es su propio mundo.

Lo importante que nadie te dice: ambos caminos terminan en /preapproval para suscribir al payer. La diferencia está en cómo le das la recurrencia. En el modelo con plan, el preapproval de suscripción se crea referenciando el preapproval_plan_id (no repites el auto_recurring inline, eso ya vive en el plan) más el card_token_id y status: "authorized". En el modelo sin plan, ese mismo preapproval lleva el auto_recurring completo dentro. Así que no es “dos integraciones distintas”, es una integración con una decisión de configuración arriba.

Si vienes del modelo de suscripciones de Stripe, el mapa mental es idéntico: el preapproval_plan de MP es tu Price reutilizable, y el /preapproval sin plan es un subscription item ad-hoc con monto inline. Misma idea, otra API.

La regla de decisión, sin rodeos:

preapproval_plan vs /preapproval directo

CON plan (preapproval_plan)

  • POST /preapproval_plan una vez, reutilizable
  • Recurrencia pre-cocida en el plan
  • El preapproval referencia el preapproval_plan_id
  • Ideal para tiers fijos y estandarizados
  • Cambias el plan, no cada suscripción

SIN plan (/preapproval directo)

  • POST /preapproval con auto_recurring inline
  • Monto y cadencia por cada cliente
  • Ideal para montos custom u one-off
  • Cada suscripción es independiente
  • Más flexible, más estado que mantener
Las dos formas de crear suscripciones en Mercado Pago y cuándo usar cada una. Ambas convergen en /preapproval para suscribir al payer.

Si tu pricing son tiers fijos, ve con plan. Si cada cliente negocia su monto, ve directo a /preapproval. Punto.

Crear la suscripción: tokeniza la tarjeta y haz POST /preapproval como “authorized”

Aquí es donde se gana o se pierde la integración, así que vamos con nombres de campo exactos.

Primero tokenizas la tarjeta. Los datos de la tarjeta nunca tocan tu servidor más allá del token. Usas el SDK de frontend de Mercado Pago (o Bricks) para generar un card_token_id desde el navegador del cliente. Ese token es de un solo uso y de vida corta, así que tokenizas justo antes de crear la suscripción, no horas antes (confirma la ventana de validez vigente en los docs/SDK a fecha de 2026).

Después haces POST /preapproval con este body. Los campos de nivel raíz son reason, external_reference, payer_email, card_token_id, back_url, status, y el objeto auto_recurring, que carga frequency, frequency_type, transaction_amount, currency_id, start_date y end_date.

// Crea la suscripcion sin plan asociado. El card_token_id viene del frontend.
const res = await fetch("https://api.mercadopago.com/preapproval", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.MP_ACCESS_TOKEN}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    reason: "Plan Pro mensual - Mi SaaS",
    external_reference: "user_8842",          // tu llave de union a tu DB
    payer_email: "cliente@correo.com",
    card_token_id: cardTokenId,               // tokenizado en el navegador
    back_url: "https://miapp.com/suscripcion/ok",
    status: "authorized",                     // <-- cobra/autoriza de inmediato
    auto_recurring: {
      frequency: 1,
      frequency_type: "months",
      transaction_amount: 299,
      currency_id: "MXN",
      start_date: "2026-07-01T00:00:00.000-06:00",
      end_date: "2027-07-01T00:00:00.000-06:00",
    },
  }),
});

const preapproval = await res.json();
// Guarda preapproval.id JUNTO con tu external_reference. Esa es tu ancla.

Si en cambio usas el modelo con plan, ese mismo POST /preapproval no lleva auto_recurring inline: lleva preapproval_plan_id apuntando al plan que ya creaste, más el card_token_id, el payer_email y status: "authorized".

Tres gotchas que cuestan horas:

  • status tiene que ser "authorized" para cobrar o autorizar de inmediato. Si lo dejas en pending, creaste una suscripción que existe pero no cobra nada. Mucha gente debuggea “¿por qué no me cobró?” durante una hora y la respuesta es esta línea.
  • external_reference es tu llave de unión a tu propio usuario. Configúrala desde el día uno. Cuando llegue un webhook diciendo “el cobro de tal suscripción se aprobó”, necesitas saber a qué usuario tuyo corresponde sin hacer arqueología.
  • El card_token_id debe estar adjunto. Una suscripción con plan asociado siempre se crea con tu card_token_id y con status authorized. Sin token, no hay medio de pago, no hay cobro.

Si todavía no tienes los fundamentos de la API de MP, primero pasa por cómo integrar Mercado Pago y luego regresa aquí.

Flujo en runtime de una suscripción

Tokeniza la tarjetaFrontend genera card_token_id, de un solo uso
POST /preapproval status authorizedCrea la suscripción y autoriza el medio de pago
Webhook por cada ciclosubscription_authorized_payment dispara en cada cobro
Fetch por idConsultas el recurso real, no confías en el body
Grant / revokeAcceso solo con cobro approved; revocas tras fallas repetidas
Del token al acceso: cada ciclo dispara un webhook que confirmas por id antes de tocar el acceso del usuario.

¿En qué estado está mi suscripción? Los estados: pending, authorized, paused, cancelled

Una suscripción de MP se mueve por estados y tu DB tiene que modelarlos sin adivinar.

  • pending — creada pero sin autorizar. No cobra. Si te quedaste aquí sin querer, revisa tu status en el POST.
  • authorized — activa y facturable. Este es el estado feliz: el payer tiene acceso y MP está cobrando en cada ciclo.
  • paused — temporalmente sin cobrar. La suscripción sigue viva pero no genera cargos hasta que la reactivas.
  • cancelled — terminal. Ya no se cobra y no se reactiva.

No mutes estos estados adivinando. Para cambiar el estado usas los endpoints de gestión de la suscripción (update / pause / cancel) contra el preapproval_id. Si quieres pausar a un cliente, llamas al endpoint de pausa; no escribes paused en tu DB y rezas.

La disciplina clave: espeja estos estados en tu propia DB, pero trata a Mercado Pago como la fuente de la verdad, reconciliado vía webhooks (la siguiente sección). Tu DB es una caché de la verdad de MP, no la verdad misma.

Y como siempre con pagos: estados, fees y límites son sensibles al tiempo. Confirma los valores vigentes en el dashboard a fecha de 2026 antes de hardcodear nada.

Los webhooks son la fuente de verdad: subscription_preapproval vs subscription_authorized_payment

Esta es la sección que separa una integración que aguanta producción de una que genera chargebacks.

Configuras tu notification_url y te suscribes a dos topics (así se llaman en el panel de notificaciones; ojo, el campo que llega en el JSON del webhook es type, con su action y su data.id):

  • subscription_preapproval — el ciclo de vida de la suscripción: cuando pasa a authorized, paused o cancelled. Esto te dice “el estado de la suscripción cambió”.
  • subscription_authorized_payment — cada intento de cobro recurrente. Esto te dice “se intentó un cargo y este fue el resultado”. Es el evento que te interesa para dar o quitar acceso ciclo a ciclo.

Activa también el topic de payments para alcanzar las notificaciones del pago subyacente que cuelga de esos cargos. Te da un segundo punto de verificación cuando necesitas el detalle del pago.

Antes de procesar nada, valida la firma de la notificación. MP manda una cabecera x-signature (junto con x-request-id) que firmas contra tu secret del panel para confirmar que el POST viene de Mercado Pago y no de cualquiera que sepa POSTear ids falsos a tu endpoint. La firma es la primera capa; el fetch-by-id es la segunda. En producción no te saltes ninguna de las dos.

Ahora la regla de oro, la que los docs mencionan de pasada y que es la razón de existir de este post: en cada notificación, consultas el recurso por su id desde la API y actúas sobre el estado CONFIRMADO. Nunca confíes en el body de la notificación. Nunca confíes en un redirect o back_url.

El body del webhook solo trae un id:

{ "data": { "id": "999999999" } }

Eso es todo lo que debes creerle: que existe un recurso con ese id. El estado real lo traes tú con un GET. Y un detalle que rompe integraciones en silencio: el recurso de authorized_payment (la factura de cada cobro recurrente) trae el preapproval_id de la suscripción a la que pertenece, además del external_reference. Usa el preapproval_id como llave robusta para resolver tu usuario contra el mapeo que guardaste al crear la suscripción; no dependas únicamente de que el external_reference venga eco en cada cobro.

// Handler para subscription_authorized_payment.
// El body solo trae el id. Vas a la API por el estado real.
app.post("/webhooks/mercadopago", async (req, res) => {
  // 0) Valida la firma x-signature contra tu secret ANTES de procesar.
  if (!verifyMpSignature(req)) return res.sendStatus(401);

  // 1) Responde 200 rapido para que MP no reintente la notificacion.
  res.sendStatus(200);

  const { type, data } = req.body; // el JSON trae "type", no "topic"
  if (type !== "subscription_authorized_payment") return;

  // 2) NUNCA confies en el body: trae el recurso por id.
  const r = await fetch(
    `https://api.mercadopago.com/authorized_payments/${data.id}`,
    { headers: { "Authorization": `Bearer ${process.env.MP_ACCESS_TOKEN}` } }
  );
  const charge = await r.json();

  // 3) Resuelve TU usuario por el preapproval_id de la suscripcion,
  //    no por el external_reference suelto del cobro.
  const userRef = await resolveUserByPreapprovalId(charge.preapproval_id);

  // 4) Actua SOLO sobre el estado confirmado por la API.
  if (charge.status === "approved") {
    await grantAccess(userRef, charge);        // extiende el periodo pagado
  } else {
    await registerFailedCharge(userRef, charge); // dunning, no cortes aun
  }
});

Das o quitas acceso solo después de un evento verificado, consultado y confirmado. Si concedes acceso porque el navegador del cliente aterrizó en tu back_url de éxito, acabas de regalar tu producto a cualquiera que sepa copiar una URL.

Los dos webhooks y la disciplina que los docs no gritan

subscription_preapprovalCiclo de vida: authorized / paused / cancelled
subscription_authorized_paymentCada intento de cobro recurrente
payments (topic extra)El pago subyacente, segundo punto de verificación
Regla 1Valida la firma x-signature antes de procesar
Regla 2Siempre fetch del recurso por id antes de actuar
Regla 3Nunca confíes en el body ni en un redirect/back_url
La regla que sostiene todo el sistema: valida la firma, consulta por id, ignora el body, sincroniza cancelaciones en ambos sentidos.

La parte que nadie escribe: pagos fallidos, dunning y reintentos

El happy path es el 20% fácil. El dinero real se salva o se pierde aquí.

Una tarjeta va a fallar. Se vence, se queda sin fondos, el banco la bloquea por “actividad sospechosa” un martes cualquiera. Mercado Pago reintenta los cobros recurrentes fallidos según su propia agenda, y tú te enteras del resultado de cada intento a través de eventos subscription_authorized_payment. No hay un endpoint mágico que te avise “este cliente está moroso”; lo construyes tú escuchando esos eventos.

La lógica correcta, portada directo de la disciplina de dunning que uso en Stripe:

  • Concede o extiende acceso con un cobro approved confirmado. Cada cobro exitoso empuja la fecha de fin del periodo pagado.
  • No cortes acceso en la primera falla. Construye una ventana de gracia que iguale la cadencia de reintentos de MP. Cortarle a alguien cuyo banco rechazó un cargo que se va a reintentar mañana es la forma más cara de generar churn voluntario.
  • Tras fallas repetidas, revoca o limita el acceso y avísale al payer que actualice su tarjeta. Aquí es donde recuperas ingresos: un correo a tiempo con un link para actualizar el medio de pago salva suscripciones que ya dabas por muertas.
  • Loguea cada intento de cobro contra tu external_reference. Cuando soporte reciba “me cobraron pero dicen que no pagué”, quieres ver la línea de tiempo completa del dunning de ese usuario en un query, no reconstruirla a mano.

El gotcha grande: la cadencia exacta de reintentos de MP no está documentada a gritos. No la hardcodees con un número que adivinaste. Confírmala en el dashboard/docs a fecha de 2026 y haz que tu ventana de gracia la siga, no al revés. Si MP reintenta durante varios días, tu ventana de gracia debe cubrir esos días o vas a cortar acceso a clientes que sí iban a pagar.

Mantén las cancelaciones sincronizadas — en ambos sentidos

Este es el segundo gap silencioso, y es la póliza de seguro más barata que vas a escribir.

Si el payer cancela dentro de Mercado Pago, tu app tiene que enterarse vía subscription_preapproval con status: "cancelled" y revocar el acceso. No esperes al siguiente cobro fallido para darte cuenta; para entonces el cliente lleva semanas usando algo que ya canceló, y si por error le cobras de nuevo, es un chargeback.

Si el usuario cancela en TU app, llamas al endpoint de cancelación de gestión contra el preapproval_id para que MP deje de cobrar. Si no lo haces, MP sigue facturando a un usuario que ya se fue, y le estás cobrando a alguien que cree que canceló. Eso es un chargeback y un golpe directo a tu confianza.

Trata el estado cancelled como terminal e idempotente en ambos lados: si llega un segundo evento de cancelación, no truena nada, simplemente confirmas que ya estaba cancelado.

Un usuario al que le cobras después de “cancelar” no es un bug menor: es una disputa, una reseña de una estrella y un cliente que nunca regresa. Esta sincronización de dos vías es trivial de escribir y carísima de omitir.

Conectándolo todo de punta a punta: la checklist

Checklist de integración de punta a punta

  1. Elige el modeloplan reutilizable para tiers fijos, /preapproval directo para custom
  2. Tokeniza la tarjetacard_token_id desde el frontend, de un solo uso
  3. POST /preapproval status authorizedcon auto_recurring o preapproval_plan_id; sin authorized no cobra
  4. Guarda las referenciasexternal_reference + el preapproval_id de MP en tu DB
  5. Configura webhooksnotification_url + los dos topics + payments
  6. Valida la firmax-signature contra tu secret en cada notificacion
  7. Verifica por idGET del recurso en cada notificación, ignora el body
  8. Concede accesosolo con cobro approved confirmado
  9. Implementa dunning y graciaventana que iguale la cadencia de reintentos de MP
  10. Sincroniza cancelacionesen ambos sentidos, terminal e idempotente
  11. Prueba y reconciliasandbox + tarjeta real; job diario contra MP como backstop
La secuencia completa, del modelo al reconcile diario. Cada paso depende del anterior; no te saltes el fetch-by-id.

Ese job diario de reconciliación es tu red de seguridad. Los webhooks se pierden, los servidores se caen, las notificaciones llegan fuera de orden. Una vez al día barres tus suscripciones activas contra el estado real en MP y corriges cualquier deriva. Es aburrido y te salva el mes.

Preguntas frecuentes

¿Con plan o sin plan, cuál uso? Tiers reutilizables y estandarizados, ve con preapproval_plan y referencia el preapproval_plan_id al suscribir. Montos custom por cliente o cobros one-off, ve directo a POST /preapproval con auto_recurring inline. Si dudas, empieza sin plan: es más flexible y siempre puedes migrar a planes cuando tus tiers se estabilicen.

¿Por qué no me cobró la suscripción? Casi siempre dos cosas: tu status no era "authorized" (se quedó en pending, que no cobra), o el card_token_id no quedó adjunto al crear el preapproval. Revisa esas dos líneas antes de cualquier otra cosa.

¿Puedo confiar en el body del webhook? No. El body solo trae un id. Valida la firma x-signature, luego haz GET del recurso por ese id y actúa sobre el estado confirmado por la API. El body te dice qué mirar, no qué hacer.

¿Qué pasa cuando una tarjeta falla? MP reintenta según su agenda y tú reaccionas vía eventos subscription_authorized_payment. Corre tu lógica de dunning: ventana de gracia primero, correo para actualizar la tarjeta, y revocas acceso solo tras fallas repetidas. Confirma la cadencia de reintentos en el dashboard a fecha de 2026.

El usuario canceló en MP pero mi app lo sigue mostrando activo. No estás manejando subscription_preapproval con status: "cancelled". Suscríbete a ese topic, consulta el preapproval por id, y revoca acceso cuando confirmes el estado cancelado.

¿Esto aplica a un marketplace con split de comisiones? Las suscripciones y el split son piezas distintas pero compatibles. Si cobras recurrente a nombre de tus vendedores, revisa la contraparte de marketplace con split payments.

A producir: trata el evento verificado como la única verdad

Todo el sistema descansa sobre una sola regla: concedes y revocas acceso solo sobre un evento verificado, consultado y confirmado. Ni un redirect, ni un body de webhook, ni una corazonada. Una firma válida, un GET por id, un estado confirmado, una acción.

El happy path —tokenizar, crear, cobrar— es el 20% fácil que cualquiera arma en una tarde. Los pagos fallidos, el dunning con ventana de gracia y la sincronización de cancelaciones en dos vías son el 80% donde realmente se gana o se pierde el ingreso recurrente. Constrúyelos desde el día uno, no como un parche después del primer chargeback.

Para los campos vigentes, los estados y la cadencia de reintentos, la fuente oficial es la documentación de Suscripciones de Mercado Pago y la referencia del endpoint /preapproval. Confírmalos ahí antes de lanzar, porque los fees y los límites cambian y tu integración no debería enterarse en producción.