Saltar al contenido principal

Acepta Bizum en Stripe con Custom Payment Methods

El framework de Custom Payment Methods (CPM) de Stripe te permite mostrar un método de pago en el Payment Element de Stripe que Stripe no procesa por sí mismo. Esta guía muestra cómo ofrecer Bizum a tus clientes españoles dentro de tu checkout de Stripe, mientras MONEI procesa el pago de Bizum con su propia licencia de adquirencia de Bizum. El resultado se informa a la Payment Records API de Stripe para que Bizum aparezca junto al resto de tus pagos en los informes de Stripe.

MONEI dispone de una licencia directa de adquirencia de Bizum, por lo que puede liquidar fondos de Bizum a tu cuenta, algo que un procesador nativo de Stripe no puede hacer hoy con Bizum.

Qué construyes

Aceptas Bizum a través del framework de Custom Payment Methods de Stripe, por lo que el flujo se ejecuta en tu propio backend: un pequeño endpoint de servidor crea el pago en MONEI e informa el resultado a Stripe, junto con el handler en el cliente que el Payment Element de Stripe exige para cualquier método personalizado. MONEI se encarga del procesamiento y la liquidación de Bizum; tú te autenticas en ambas APIs con tus propias claves. Tienes una referencia ejecutable en el repositorio de ejemplo.

Cómo funciona

La experiencia del cliente la marca la redirección (pasos 6–7); el informe a Stripe (pasos 8–9) es una reconciliación servidor a servidor que se ejecuta unos segundos después. Trata el callback, y no la redirección, como el resultado autoritativo.

Antes de empezar

  • Una cuenta de MONEI con Bizum activado (Panel → Configuración → Métodos de pago) y una clave de API. Bizum está disponible para clientes cuya cuenta bancaria esté en España (también AD/IE) y dada de alta en Bizum.
  • Una cuenta de Stripe con el Payment Element y acceso a Custom Payment Methods.
  • Un servidor que pueda llamar a las APIs de MONEI y Stripe y exponer dos URLs públicas (una página de retorno de la redirección y un endpoint de callback).

1. Crea el tipo de método de pago personalizado de Bizum en Stripe

En el Panel de Stripe, crea un tipo de método de pago personalizado para Bizum (nombre, logo, los países/divisas a los que aplica). Stripe devuelve un id de tipo de método de pago personalizado (cpmt_…). Referencias este id tanto al renderizar el Payment Element como al crear el PaymentMethod personalizado.

Activa el nuevo tipo en la configuración de tu Payment Element para que Bizum aparezca a los clientes elegibles.

2. Construye el handler de CPM Cliente

Stripe exige un handler en el cliente para cada método de pago personalizado: CPM no tiene un modo nativo de "redirigir al proveedor", así que tu handler es lo que conecta Stripe con MONEI. Cuando el cliente selecciona Bizum y confirma, tu handler llama a tu servidor y luego envía al cliente a la página alojada de MONEI devuelta en el paso 3.

info

Cómo te integras depende del tipo de visualización de CPM. Para type: 'static', llama a elements.submit() en tu propio handler de envío y bifurca según el selectedPaymentMethod devuelto. Para type: 'embedded', implementa los callbacks handleRender y handleDestroy. En ambos casos, Stripe no pasa el importe ni una URL de retorno a tu código: esos los controla tu servidor. Consulta métodos de pago personalizados en el Payment Element de Stripe.

Con el tipo de visualización static, tu handler de envío llama a elements.submit() y, cuando el cliente eligió tu CPM de Bizum, delega en tu servidor y redirige a la página alojada de MONEI:

client.js — tu handler de envío del Payment Element
const {submitError, selectedPaymentMethod} = await elements.submit();

if (submitError) {
// muestra submitError.message
} else if (selectedPaymentMethod === 'cpmt_xxx') {
// tu endpoint /pay crea el pago en MONEI (Sección 3) y devuelve su URL
const {redirectUrl} = await fetch('/pay', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({orderId: 'stripe-order-123'})
}).then((r) => r.json());

window.location.href = redirectUrl; // página alojada de Bizum de MONEI
}

3. Crea el pago de Bizum en MONEI Servidor

Cuando tu handler de CPM llame a tu servidor, haz dos cosas: crea el PaymentMethod personalizado de Stripe y luego crea el pago en MONEI.

Primero, crea el PaymentMethod personalizado a partir del id de tu tipo de CPM. El Payment Element no lo crea por ti: lo creas en el servidor, y su id pm_… es lo que informarás a Stripe más adelante.

server.js
const paymentMethod = await stripe.paymentMethods.create({
type: 'custom',
custom: {type: process.env.STRIPE_CPMT_ID} // cpmt_… del paso 1
});
// guarda paymentMethod.id (pm_…): va en el metadata de MONEI de abajo
// y se convierte en payment_method_details.payment_method al informar a Stripe

Luego crea un Pago con MONEI. Fija siempre el importe en el servidor, nunca desde el cliente. Los importes usan la unidad mínima de la divisa (1000 = 10,00 €).

Define la URL de retorno y un callback, y lleva los identificadores de Stripe en metadata (usa el id pm_… de arriba como stripe_payment_method_id) para poder reconciliar más tarde:

  • completeUrl — a dónde devuelve MONEI al cliente cuando el pago finaliza, tanto en éxito como en fallo. Lee el parámetro status de la query para distinguirlos (consulta el contrato de redirección).
  • callbackUrl — el endpoint servidor a servidor al que MONEI envía por POST el objeto de pago final. Es lo que reconcilias con Stripe.

Un failUrl aparte es opcional: defínela solo para enviar los pagos fallidos o cancelados a una página distinta de la de éxito; si la omites, esos vuelven a completeUrl. (cancelUrl no tiene relación: es solo el destino del botón "atrás" / "volver al comercio".)

POST https://api.monei.com/v1/payments
curl --request POST 'https://api.monei.com/v1/payments' \
--header 'Authorization: YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data-raw '{
"amount": 1000,
"currency": "EUR",
"orderId": "stripe-order-123",
"paymentMethod": {
"bizum": {
"phoneNumber": "+34600000000"
}
},
"completeUrl": "https://example.com/return",
"callbackUrl": "https://example.com/monei/callback",
"metadata": {
"stripe_payment_method_id": "pm_xxx",
"stripe_cpmt_id": "cpmt_xxx",
"integration": "stripe_cpm"
}
}'

El número de teléfono es opcional: si lo omites, el cliente lo introduce en la página alojada. La respuesta incluye el id del pago (guárdalo; se convierte en el payment_reference de Stripe) y una URL de redirección a la página alojada. Devuelve esa URL a tu handler de CPM para que el navegador la abra.

4. Gestiona el retorno de la redirección Cliente

Después de que el cliente termine (o abandone) el flujo de Bizum en la página alojada, MONEI lo devuelve a tu completeUrl — en ambos casos — con el resultado en la query string:

?id=<paymentId>&status=<STATUS>&amount=<amount>&currency=<currency>&orderId=<orderId>&message=<message>
  • El parámetro del id de pago es id (no payment_id).
  • status es el enum de estado en mayúsculas: SUCCEEDED, FAILED o CANCELED.
  • message es una descripción legible del estado — útil para mostrar al cliente en caso de fallo.
  • El éxito y el fallo vuelven ambos a completeUrl, así que bifurca según status — no trates la redirección en sí como éxito. (Define un failUrl opcional solo si quieres los fallos en una página aparte.)
Bifurca siempre según status

Como tanto los pagos correctos como los fallidos aterrizan en completeUrl, una redirección por sí sola no significa éxito: lee el status en mayúsculas y renderiza en consecuencia.

Muestra una pantalla de éxito o fallo según el status, pero no completes el pedido a partir de la redirección: la redirección puede interrumpirse (el cliente cierra la pestaña). El cumplimiento del pedido y el informe a Stripe ocurren en el callback de abajo.

5. Reconcilia con Stripe Servidor

MONEI envía por POST el objeto de pago completo (status, metadata, importe…) a tu callbackUrl cuando el pago alcanza un estado final. Este es el resultado autoritativo.

Un pago CPM no pasa por un PaymentIntent de Stripe — Stripe no lo procesa. En su lugar, creas una entrada de Payment Records que informa el resultado.

  1. Verifica la cabecera MONEI-Signature para confirmar que la petición vino de MONEI. Consulta Verificar firmas.
  2. Devuelve 200 OK de inmediato, o MONEI reintentará.
  3. Mapea el status de MONEI a una llamada de Payment Records de Stripe (consulta la tabla de mapeo).
  4. Usa el id de pago de MONEI como clave de idempotencia para que los reintentos y callbacks duplicados no informen por duplicado.
server.js — handler del callback
// tras verificar MONEI-Signature y parsear req.body como el objeto de pago
const payment = req.body;
const reference = payment.id; // id de pago de MONEI == payment_reference de Stripe
const now = Math.floor(Date.now() / 1000);

if (payment.status === 'SUCCEEDED') {
const record = await stripe.paymentRecords.reportPayment(
{
amount_requested: {value: payment.amount, currency: payment.currency.toLowerCase()},
initiated_at: now,
// el PaymentMethod personalizado (pm_…) que creaste en el handler de CPM
payment_method_details: {payment_method: payment.metadata.stripe_payment_method_id},
outcome: 'guaranteed',
guaranteed: {guaranteed_at: now},
processor_details: {type: 'custom', custom: {payment_reference: reference}},
metadata: {monei_authorization_code: payment.authorizationCode ?? ''}
},
{idempotencyKey: `report_payment_${reference}`}
);
// Guarda record.id (pr_…) asociado a `reference`: lo necesitas para informar reembolsos.
}
// FAILED / CANCELED: llama a report_payment con outcome 'failed'; reembolsos: consulta la Sección 6

report_payment devuelve un Payment Record (pr_…). Guarda su id asociado al id de pago de MONEI: report_refund y las llamadas report_payment_attempt_* actúan sobre ese registro. El repositorio de ejemplo tiene el handler completo (todos los estados + reembolsos).

tip

outcome es guaranteed solo una vez que los fondos están garantizados (un pago SUCCEEDED capturado). Para un pago en curso o preautorizado, infórmalo como iniciado (llama a report_payment sin un outcome terminal) y finaliza cuando el callback informe el estado terminal.

6. Reembolsos

Reembolsa a través de MONEI y luego informa el reembolso a Stripe:

POST https://api.monei.com/v1/payments/{id}/refund
curl --request POST 'https://api.monei.com/v1/payments/PAYMENT_ID/refund' \
--header 'Authorization: YOUR_API_KEY' \
--header 'Content-Type: application/json' \
--data-raw '{"amount": 1000}'

MONEI admite reembolsos de Bizum completos y parciales: para un reembolso parcial, envía un amount menor. Cuando el reembolso se liquide (vía el callback de callbackUrl, o consultando GET /v1/payments/{id}), llama a report_refund de Stripe sobre el Payment Record que guardaste en la Sección 5:

server.js — handler del callback (reembolso)
// payment.status es REFUNDED o PARTIALLY_REFUNDED.
// recordId = el pr_… que guardaste al informar este pago por primera vez.
await stripe.paymentRecords.reportRefund(recordId, {
amount: {value: payment.lastRefundAmount, currency: payment.currency.toLowerCase()},
outcome: 'refunded',
refunded: {refunded_at: Math.floor(Date.now() / 1000)},
processor_details: {
type: 'custom',
// refund_reference debe ser único por reembolso: combina el id de pago con el
// total reembolsado acumulado para que cada reembolso parcial se informe una vez
custom: {refund_reference: `${payment.id}_${payment.refundedAmount}`}
}
});

Si no recibes el callback, consulta GET /v1/payments/{id} y reconcilia a partir del estado devuelto: aplica el mismo mapeo.

Tabla de mapeo de estados

Mapea cada status de cargo de MONEI a una llamada de Stripe Payment Records. La clave de idempotencia es el id de pago de MONEI (processor_details.custom.payment_reference).

status de MONEILlamada de StripeNotas
SUCCEEDEDreport_payment con outcome: guaranteedFondos garantizados
AUTHORIZEDreport_payment iniciadoguaranteed al capturarPreautorización de Bizum de 30 días
PENDING / PENDING_PROCESSINGreport_payment iniciado (sin outcome terminal)Asíncrono / redirección en curso
FAILEDreport_payment con outcome: failedPrimer/único intento — consulta la nota de abajo
CANCELED / EXPIREDreport_payment con outcome: failedLa expiración cuenta como no completado
REFUNDEDreport_refund con outcome: refunded (completo)Sobre el id de Payment Record existente
PARTIALLY_REFUNDEDreport_refund con amount.value parcialSe admiten reembolsos de Bizum completos y parciales

El enum outcome de report_payment de Stripe es solo failed o guaranteed. report_payment crea un Payment Record; los endpoints report_payment_attempt_failed / report_payment_attempt_canceled actualizan uno existente (un pago que informaste previamente como iniciado). Como un pago Bizum entrega un único callback terminal, informa el estado terminal en una sola llamada a report_payment (outcome: guaranteed o failed); usa los endpoints attempt_* solo para finalizar un pago que primero informaste como iniciado (las filas AUTHORIZED / PENDING).

Los ejemplos de creación de la Sección 3 hacen una venta directa. Bizum también admite preautorización: una retención que capturas más tarde (Bizum retiene 30 días); esos pagos se informan como AUTHORIZED hasta que capturas, y luego SUCCEEDED.

Prueba tu integración

Repositorio de ejemplo

Una referencia ejecutable con Express —crea el PaymentMethod personalizado de Stripe y el pago de Bizum en MONEI, gestiona el retorno de la redirección e informa a Stripe— está disponible en MONEI/stripe-bizum-cpm-example. Refleja exactamente el contrato de esta página.

Preguntas frecuentes

¿Necesito OAuth o un conector alojado por MONEI?

No. Esta integración es dirigida por el comercio: llamas a las APIs de MONEI y Stripe con tus propias claves desde tu propio servidor. No hay handshake de OAuth ni conector alojado por MONEI.

¿Por qué no puede ser totalmente sin código?

Stripe exige un handler de CPM en el cliente, y CPM no tiene modo nativo de redirección al proveedor, así que siempre hace falta algo de código. Para opciones de menor esfuerzo en plataformas de e-commerce compatibles, contacta con MONEI.

¿A qué URL retorna un pago fallido?

A la misma completeUrl que un pago correcto: MONEI pone el resultado en el parámetro status de la query, así que bifurca según él. Define un failUrl opcional solo para enviar los fallos a una página distinta. cancelUrl no tiene relación: es solo el destino del botón "atrás" / "volver al comercio".