# Acepta Bizum en Stripe con Custom Payment Methods

El framework de [Custom Payment Methods (CPM)](https://docs.stripe.com/payments/payment-methods/custom-payment-methods) 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](https://docs.stripe.com/payments/payment-records) 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](#sample-repository).

## Cómo funciona[​](#how-it-works "Enlace directo al 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](#5-reconcile-with-stripe), y no la redirección, como el resultado autoritativo.

## Antes de empezar[​](#before-you-begin "Enlace directo al Antes de empezar")

* Una **cuenta de MONEI** con **Bizum activado** ([Panel → Configuración → Métodos de pago](https://dashboard.monei.com/settings/payment-methods)) y una [clave de API](https://dashboard.monei.com/settings/api-access). 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[​](#1-create-the-cpm-type "Enlace directo al 1. Crea el tipo de método de pago personalizado de Bizum en Stripe")

En el [Panel de Stripe](https://dashboard.stripe.com/settings/custom_payment_methods), 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`[​](#2-cpm-handler "Enlace directo al 2-cpm-handler")

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](https://docs.stripe.com/payments/payment-element/custom-payment-methods) de Stripe.

Con el tipo de visualización **static**, tu handler de envío llama a [`elements.submit()`](https://docs.stripe.com/payments/payment-element/custom-payment-methods) 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`[​](#3-create-the-monei-payment "Enlace directo al 3-create-the-monei-payment")

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](https://docs.stripe.com/api/payment_methods/create) 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](https://docs.monei.com/es/es/apis/rest/payments-create/.md) 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](#4-handle-the-redirect-return)).
* **`callbackUrl`** — el endpoint servidor a servidor al que MONEI envía por POST el [objeto de pago](https://docs.monei.com/es/es/apis/rest/schemas/payment/.md) 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

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"

  }

}'
```

server.js

```
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;
```

server.php

```
<?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();

?>
```

server.py

```
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`[​](#4-handle-the-redirect-return "Enlace directo al 4-handle-the-redirect-return")

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`[​](#5-reconcile-with-stripe "Enlace directo al 5-reconcile-with-stripe")

MONEI envía por POST el **[objeto de pago](https://docs.monei.com/es/es/apis/rest/schemas/payment/.md) 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](https://docs.stripe.com/payments/payment-records) que informa el resultado.

1. **Verifica la cabecera `MONEI-Signature`** para confirmar que la petición vino de MONEI. Consulta [Verificar firmas](https://docs.monei.com/es/es/guides/verify-signature/.md).
2. **Devuelve `200 OK`** de inmediato, o MONEI reintentará.
3. Mapea el `status` de MONEI a una llamada de [Payment Records](https://docs.stripe.com/payments/payment-records) de Stripe (consulta la [tabla de mapeo](#status-mapping)).
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`](https://docs.stripe.com/api/payment-record/report-refund/report) y las llamadas `report_payment_attempt_*` actúan sobre ese registro. El [repositorio de ejemplo](#sample-repository) 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[​](#refunds "Enlace directo al 6. Reembolsos")

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

* cURL
* Node.js
* PHP
* Python

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}'
```

server.js

```
import {Monei} from '@monei-js/node-sdk';



const monei = new Monei('YOUR_API_KEY');



await monei.payments.refund('PAYMENT_ID', {amount: 1000});
```

server.php

```
<?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])

);

?>
```

server.py

```
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`](https://docs.stripe.com/api/payment-record/report-refund/report) 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[​](#status-mapping "Enlace directo al 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`](https://docs.stripe.com/api/payment-record/report-payment-attempt-failed/report) / [`report_payment_attempt_canceled`](https://docs.stripe.com/api/payment-record/report-payment-attempt-canceled/report) **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](https://docs.monei.com/es/es/apis/rest/payments-capture/.md) más tarde (Bizum retiene 30 días); esos pagos se informan como `AUTHORIZED` hasta que capturas, y luego `SUCCEEDED`.

## Prueba tu integración[​](#testing "Enlace directo al Prueba tu integración")

* Usa tu clave de API de MONEI en [modo de prueba](https://docs.monei.com/es/es/testing/.md) y las claves de Stripe en modo de prueba.
* Usa los [números de teléfono de prueba de Bizum](https://docs.monei.com/es/es/testing/.md#n%C3%BAmeros-de-tel%C3%A9fono-de-prueba-para-bizum).
* Comprueba el resultado en [Panel de MONEI → Pagos](https://dashboard.monei.com/payments) y confirma que el Payment Record de Stripe muestra **Bizum — Succeeded** con `payment_reference` igual al id de pago de MONEI y tu metadata intacta.

## Repositorio de ejemplo[​](#sample-repository "Enlace directo al 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`](https://github.com/MONEI/stripe-bizum-cpm-example). Refleja exactamente el contrato de esta página.

## Preguntas frecuentes[​](#common-questions "Enlace directo al Preguntas frecuentes")

### ¿Necesito OAuth o un conector alojado por MONEI?[​](#do-i-need-oauth "Enlace directo al ¿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?[​](#why-not-no-code "Enlace directo al ¿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](https://support.monei.com/hc/requests/new).

### ¿A qué URL retorna un pago fallido?[​](#failed-payment-url "Enlace directo al ¿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".
