Saltar al contenido
ApiPay Hub · Docs

MercadoPago#

gateway_id: "mercadopago". Es la pasarela con dos modos de operación y con el catálogo de estados más amplio de las tres: MercadoPago tiene medios de pago genuinamente asíncronos (cupones de pago en efectivo, transferencias) que pueden tardar días en resolverse, y eso se nota en cómo hay que integrarla.

CapacidadMercadoPago
FlujoBricks en la página (IFRAME_FLOW) o preferencia con redirección
Captura diferidaSí en el adaptador (SEPARATE_CAPTURE), sin endpoint público todavía
Reembolso parcialSí (PARTIAL_REFUND)
Webhooks nativosSí (NATIVE_WEBHOOKS), firmados con x-signature
DivisasLas de la cuenta del tenant (CLP, ARS, BRL, MXN…)

Los dos modos#

1. Card Payment Brick (en tu página)#

El Brick de MercadoPago renderiza el formulario de tarjeta dentro de su propio iframe, tokeniza contra la infraestructura de MercadoPago con la public_key del tenant y devuelve un token de un solo uso. El comprador no sale de tu sitio. Es el modo por defecto.

tienda-andina.cl
└─ iframe checkout.apipay.io          <- checkout-widget
   └─ iframe sdk.mercadopago.com      <- Card Payment Brick: la tarjeta sólo existe aquí

2. Preferencia de checkout (con redirección)#

Se activa con la metadata reservada mercadopago_use_preference al crear el intent. El hub crea una preferencia (POST /checkout/preferences) y el comprador va a la página de pago de MercadoPago, donde existen medios que el Brick de tarjeta no cubre (dinero en cuenta, cupones, cuotas sin tarjeta).

Crear el intent#

import { ApiPay } from '@apipay/node';

const apipay = new ApiPay({ apiKey: process.env.APIPAY_SECRET_KEY ?? '' });

const intent = await apipay.paymentIntents.create({
amount_minor: 1499000, // CLP tiene exponente 0
currency: 'CLP',
gateway_id: 'mercadopago',
description: 'Orden 4831 - Tienda Andina',
customer_email: 'cliente@example.com',
// Necesaria para el modo preferencia: alimenta las tres back_urls.
return_url: 'https://tienda-andina.cl/checkout/retorno',
metadata: {
  order_id: '4831',
  // Omitir esta clave usa el Brick en la pagina; ponerla en "true" crea la preferencia.
  mercadopago_use_preference: 'true',
},
});

back_urls: las tres apuntan al mismo sitio#

MercadoPago exige tres URLs de retorno y ApiPay las rellena las tres con tu return_url, más auto_return: "approved" para que el retorno sea automático cuando el pago se aprueba:

{
  "back_urls": {
    "success": "https://tienda-andina.cl/checkout/retorno",
    "pending": "https://tienda-andina.cl/checkout/retorno",
    "failure": "https://tienda-andina.cl/checkout/retorno"
  },
  "auto_return": "approved"
}

Además del retorno, el hub envía en la preferencia:

Campo de MercadoPagoValor que pone ApiPay
external_referenceEl id público del intent (pi_…)
metadata.apipay_intent_idEl mismo pi_…, para búsquedas en el panel de MercadoPago
items[0].unit_priceEl monto en unidades mayores (conversión por exponente de la divisa)
items[0].currency_idLa divisa ISO 4217
items[0].titlemetadata.description si existe; si no, el pi_…

Tu resto de metadata viaja tal cual, salvo la clave de control mercadopago_use_preference, que se elimina antes de enviarla.

Estados: MercadoPago tiene más que las otras#

status de MercadoPagoEstado normalizadoEstado del intent
pendingPENDINGPROCESSING
in_processPENDINGPROCESSING
in_mediationPENDINGPROCESSING
authorizedAUTHORIZEDPROCESSING
approvedSUCCEEDEDSUCCEEDED
rejectedFAILEDFAILED
cancelledCANCELEDCANCELED
expiredEXPIREDEXPIRED
refundedSUCCEEDEDsin cambio
charged_backSUCCEEDEDsin cambio
cualquier otroPENDINGPROCESSING

in_mediation merece un párrafo propio: es una disputa abierta en MercadoPago. Se proyecta como PROCESSING porque no es terminal y puede resolverse en cualquiera de los dos sentidos. No lo trates como un fallo ni devuelvas la mercadería.

Rechazos: status_detail#

status_detailcode de ApiPaygateway_error_code
cc_rejected_insufficient_amountcard_declinedINSUFFICIENT_FUNDS
cc_rejected_bad_filled_card_numbercard_declinedCARD_DECLINED
cc_rejected_bad_filled_security_codecard_declinedCARD_DECLINED
cc_rejected_call_for_authorizecard_declinedCARD_DECLINED
cc_rejected_other_reasoncard_declinedCARD_DECLINED
cc_rejected_bad_filled_datecard_declinedEXPIRED_CARD
cc_rejected_card_expiredcard_declinedEXPIRED_CARD
cc_rejected_high_riskgateway_rejectedFRAUD_SUSPECTED
cc_rejected_blacklistgateway_rejectedFRAUD_SUSPECTED
cc_rejected_card_disabledgateway_rejectedFRAUD_SUSPECTED
cualquier otrogateway_rejectedUNKNOWN

El status_detail crudo siempre viaja en gateway_raw_code para soporte:

{
  "type": "https://api.apipay.io/problems/card-declined",
  "title": "Payment Required",
  "status": 402,
  "detail": "mercadopago rejected the payment",
  "code": "card_declined",
  "gateway_id": "mercadopago",
  "gateway_error_code": "INSUFFICIENT_FUNDS",
  "gateway_raw_code": "cc_rejected_insufficient_amount"
}

cc_rejected_bad_filled_card_number y cc_rejected_bad_filled_security_code son errores de tecleo, no rechazos del emisor: el mensaje correcto para el comprador es "revisa los datos", no "usa otra tarjeta". Distínguelos por gateway_raw_code.

Webhooks de MercadoPago hacia ApiPay#

No tienes que implementarlos, pero explican de dónde sale el estado final de un pago asíncrono:

  • Endpoint: POST /v1/gateways/mercadopago/webhooks, público y sin API key.

  • Cabecera: x-signature, con partes ts (segundos unix) y v1 (HMAC hex), más x-request-id.

  • El manifiesto que se firma no es el cuerpo, a diferencia de ApiPay:

    id:{data.id};request-id:{x-request-id};ts:{ts};
    
  • Tolerancia anti-replay: 300 s.

  • El tenant se deduce del identificador de cuenta de MercadoPago (user_id del payload) contra la configuración de pasarelas: el webhook llega sin API key.

  • Sólo se modela el topic payment; el resto se persiste en inbound_webhook_events y no transiciona nada.

Con medios asíncronos, el webhook no es una optimización, es el único camino: un cupón de pago en efectivo puede tardar días en pagarse. Tu integración tiene que aceptar que un intent viva en PROCESSING mucho tiempo y resolverse cuando llegue payment_intent.succeeded o payment_intent.expired.

Reembolsos#

POST /v1/refunds, total o parcial, igual que en el resto de pasarelas. El hub usa el id del refund (re_…) como clave de idempotencia hacia MercadoPago, así que un reintento del orquestador no devuelve dos veces.

status del refund en MercadoPagoEstado del refund
approvedSUCCEEDED
pending, in_processPENDING
rejected, cancelledFAILED

Un refund PENDING se resuelve más tarde por webhook (refund.succeeded o refund.failed). No asumas que POST /v1/refunds devuelve un desenlace terminal.

Modo test#

Las claves sk_test_ operan contra la misma api.apipay.io. Para MercadoPago necesitas además usuarios de prueba en su plataforma: vendedor y comprador, cada uno con sus credenciales.

Las tarjetas de prueba y el procedimiento para crear usuarios de prueba no se copian aquí: se desactualizan. Consulta la documentación oficial de pruebas de MercadoPago (se abre en una pestaña nueva).

La única tabla de escenarios que este portal mantiene como propia es la de sandbox, que se controla con los dos últimos dígitos del monto. Ver Modo test.

Lo que MercadoPago tampoco te va a dar#

  • payment_method es siempre null. El adaptador no captura metadatos de tarjeta por alcance PCI DSS SAQ A, aunque MercadoPago los publique.
  • Cuotas decididas por ti sin acuerdo previo. installments es un parámetro del pago que el Brick negocia con la cuenta del tenant; no es algo que se fuerce desde tu servidor.
  • Campos de tarjeta propios. Ni número, ni CVV, ni autocomplete="cc-number". La captura ocurre dentro del Brick o en la página de MercadoPago.

Checklist de integración#

  • return_url presente si usas el modo preferencia (alimenta las tres back_urls).
  • La página de retorno lee el estado con un GET, no confía en los query params.
  • PROCESSING prolongado aceptado como normal: hay medios asíncronos.
  • refunded y charged_back conciliados con GET /v1/transactions, no con el estado del intent.
  • La orden se libera con payment_intent.succeeded.
  • Ningún campo de tarjeta en tu página, ni en un ejemplo.