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.
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.
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:
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.
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ámetrostatusde 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".)
- cURL
- Node.js
- PHP
- Python
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"
}
}'
import {Monei} from '@monei-js/node-sdk';
const monei = new Monei('YOUR_API_KEY');
const payment = await monei.payments.create({
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'
}
});
// Envía al cliente a la página alojada de Bizum
const redirectUrl = payment.nextAction.redirectUrl;
<?php
require_once 'vendor/autoload.php';
use Monei\Model\CreatePaymentRequest;
use Monei\Model\PaymentPaymentMethodInput;
use Monei\Model\PaymentPaymentMethodBizumInput;
use Monei\MoneiClient;
$monei = new MoneiClient('YOUR_API_KEY');
$payment = $monei->payments->create(
new CreatePaymentRequest([
'amount' => 1000,
'currency' => 'EUR',
'order_id' => 'stripe-order-123',
'payment_method' => new PaymentPaymentMethodInput([
'bizum' => new PaymentPaymentMethodBizumInput([
'phone_number' => '+34600000000'
])
]),
'complete_url' => 'https://example.com/return',
'callback_url' => 'https://example.com/monei/callback',
'metadata' => [
'stripe_payment_method_id' => 'pm_xxx',
'stripe_cpmt_id' => 'cpmt_xxx',
'integration' => 'stripe_cpm'
]
])
);
// Envía al cliente a la página alojada de Bizum
$redirectUrl = $payment->getNextAction()->getRedirectUrl();
?>
import Monei
from Monei import (
CreatePaymentRequest,
PaymentPaymentMethodInput,
PaymentPaymentMethodBizumInput,
)
monei = Monei.MoneiClient(api_key="YOUR_API_KEY")
payment = monei.payments.create(
CreatePaymentRequest(
amount=1000,
currency="EUR",
order_id="stripe-order-123",
payment_method=PaymentPaymentMethodInput(
bizum=PaymentPaymentMethodBizumInput(phone_number="+34600000000")
),
complete_url="https://example.com/return",
callback_url="https://example.com/monei/callback",
metadata={
"stripe_payment_method_id": "pm_xxx",
"stripe_cpmt_id": "cpmt_xxx",
"integration": "stripe_cpm",
},
)
)
# Envía al cliente a la página alojada de Bizum
redirect_url = payment.next_action.redirect_url
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>¤cy=<currency>&orderId=<orderId>&message=<message>
- El parámetro del id de pago es
id(nopayment_id). statuses el enum de estado en mayúsculas:SUCCEEDED,FAILEDoCANCELED.messagees 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únstatus— no trates la redirección en sí como éxito. (Define unfailUrlopcional solo si quieres los fallos en una página aparte.)
statusComo 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.
- Verifica la cabecera
MONEI-Signaturepara confirmar que la petición vino de MONEI. Consulta Verificar firmas. - Devuelve
200 OKde inmediato, o MONEI reintentará. - Mapea el
statusde MONEI a una llamada de Payment Records de Stripe (consulta la tabla de mapeo). - Usa el id de pago de MONEI como clave de idempotencia para que los reintentos y callbacks duplicados no informen por duplicado.
// 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).
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:
- cURL
- Node.js
- PHP
- Python
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}'
import {Monei} from '@monei-js/node-sdk';
const monei = new Monei('YOUR_API_KEY');
await monei.payments.refund('PAYMENT_ID', {amount: 1000});
<?php
require_once 'vendor/autoload.php';
use Monei\Model\RefundPaymentRequest;
use Monei\MoneiClient;
$monei = new MoneiClient('YOUR_API_KEY');
$refund = $monei->payments->refund(
'PAYMENT_ID',
new RefundPaymentRequest(['amount' => 1000])
);
?>
import Monei
from Monei import RefundPaymentRequest
monei = Monei.MoneiClient(api_key="YOUR_API_KEY")
refund = monei.payments.refund("PAYMENT_ID", RefundPaymentRequest(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:
// 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 MONEI | Llamada de Stripe | Notas |
|---|---|---|
SUCCEEDED | report_payment con outcome: guaranteed | Fondos garantizados |
AUTHORIZED | report_payment iniciado → guaranteed al capturar | Preautorización de Bizum de 30 días |
PENDING / PENDING_PROCESSING | report_payment iniciado (sin outcome terminal) | Asíncrono / redirección en curso |
FAILED | report_payment con outcome: failed | Primer/único intento — consulta la nota de abajo |
CANCELED / EXPIRED | report_payment con outcome: failed | La expiración cuenta como no completado |
REFUNDED | report_refund con outcome: refunded (completo) | Sobre el id de Payment Record existente |
PARTIALLY_REFUNDED | report_refund con amount.value parcial | Se 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
- Usa tu clave de API de MONEI en modo de prueba y las claves de Stripe en modo de prueba.
- Usa los números de teléfono de prueba de Bizum.
- Comprueba el resultado en Panel de MONEI → Pagos y confirma que el Payment Record de Stripe muestra Bizum — Succeeded con
payment_referenceigual al id de pago de MONEI y tu metadata intacta.
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".