# Cobro de facturas con el usuario delante

Cobra una factura con el usuario delante, con tarjeta guardada o nueva, y consulta el historial de intentos de cobro.

Con estos endpoints tu aplicación cobra una factura existente mientras
el usuario está delante, y consulta los intentos de cobro anteriores.
Están bajo `/external/v1/invoices/:id/*`, junto con el resto de la API
de facturación.

No los confundas con el cobro sin nadie delante: son dos endpoints
distintos y no se pueden intercambiar.

**Dos formas de cobrar una factura**

## Con el usuario delante (POST /invoices/:id/pay)

La persona confirma el pago en ese momento desde tu aplicación, con una tarjeta guardada o una nueva. Exige Finanzas y Pagos con tarjeta (Stripe) en el plan, y el permiso billing:charge.

## Sin nadie delante (POST /invoices/:id/charge)

UTILIA OS cobra contra una autorización de cobro que el usuario firmó antes. Exige además la capacidad «Cobro automático de facturas». Lo explica [Acciones sobre una factura emitida](https://os.utilia.ai/docs/developers/api/invoice-actions).

## Plan y permisos

Antes que el permiso de la aplicación se comprueba el plan de tu
organización. Sin los aplicativos **Finanzas** y **Pagos con tarjeta
(Stripe)**, la respuesta es `403` con `errorCode: 'MODULE_NOT_ENABLED'`.

| Endpoint | Aplicativos del plan | Permiso de la API (scope) |
| - | - | - |
| `POST /external/v1/invoices/:id/pay` | Finanzas y Pagos con tarjeta (Stripe) | `billing:charge` |
| `GET /external/v1/invoices/:id/payment-intents` | Finanzas y Pagos con tarjeta (Stripe) | `billing:payment-methods:read` |

Sin el permiso adecuado, los endpoints responden `403 INSUFFICIENT_SCOPE`
con el permiso que
falta en `requiredScopes`, desde la primera
llamada y sin periodo de gracia. El catálogo completo está en
[Permisos de la API](https://os.utilia.ai/docs/developers/api/scopes).

`billing:charge` mueve dinero real de la tarjeta del cliente, así que
solo lo concede un Super Administrador, con un motivo de al menos
veinte caracteres que queda en la auditoría. La consulta del historial
de cobros, en cambio, solo lee y exige `billing:payment-methods:read`:
una aplicación puede revisar el estado de los cobros sin poder
iniciarlos.

## Iniciar un cobro

**`POST /api/external/v1/invoices/:id/pay`**

Crea un PaymentIntent sobre la factura. Con tarjeta guardada lo confirma con el usuario presente; sin ella devuelve el clientSecret para Stripe Elements. Permiso: billing:charge.

Parámetro de ruta: `id`.

### Cuerpo

| Nombre | Tipo | Obligatorio | Descripción |
| - | - | - | - |
| `userId` | `string` | Sí | externalId del usuario propietario de la factura. |
| `paymentMethodId` | `string` | No | Identificador en UTILIA OS de un método de pago guardado. Si lo omites, se devuelve el clientSecret para una tarjeta nueva. |
| `saveCard` | `boolean` | No | Solo al cobrar con tarjeta nueva (sin paymentMethodId). Con true, la tarjeta queda guardada tras el cobro. Guardarla NO autoriza cobros futuros: para eso hace falta una autorización de cobro. |

### Ejemplo: cobro con tarjeta guardada

```bash
curl -X POST https://os.utilia.ai/api/external/v1/invoices/inv-uuid/pay \
  -H "X-Api-Key: utl_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "userId": "user_01HXYZ",
    "paymentMethodId": "pm-uuid-1"
  }'
```

Respuesta (200):

```json
{
  "clientSecret": "pi_..._secret_...",
  "paymentIntentId": "pi-utilia-uuid",
  "status": "succeeded",
  "alreadySucceeded": true,
  "publishableKey": "pk_live_...",
  "stripeAccountId": "acct_..."
}
```

Con `alreadySucceeded: true`, el cobro se confirmó sin autenticación adicional y tu interfaz no tiene que hacer nada más. La factura pasa a `PAID` en cuanto UTILIA OS recibe la confirmación de la pasarela, normalmente en segundos.

### Ejemplo: cobro con tarjeta nueva

```bash
curl -X POST https://os.utilia.ai/api/external/v1/invoices/inv-uuid/pay \
  -H "X-Api-Key: utl_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "userId": "user_01HXYZ",
    "saveCard": true
  }'
```

Respuesta (200):

```json
{
  "clientSecret": "pi_..._secret_...",
  "paymentIntentId": "pi-utilia-uuid",
  "status": "requires_payment_method",
  "alreadySucceeded": false,
  "publishableKey": "pk_live_...",
  "stripeAccountId": "acct_..."
}
```

En este caso tu interfaz recibe el `clientSecret` y monta Stripe Elements:

```tsx
import { Elements, PaymentElement, useStripe, useElements } from '@stripe/react-stripe-js';
import { loadStripe } from '@stripe/stripe-js';

const stripePromise = loadStripe(publishableKey, { stripeAccount: stripeAccountId });

<Elements stripe={stripePromise} options={{ clientSecret }}>
  <PayForm />
</Elements>

function PayForm() {
  const stripe = useStripe();
  const elements = useElements();

  async function handleSubmit() {
    const result = await stripe!.confirmPayment({
      elements: elements!,
      confirmParams: { return_url: window.location.href },
      redirect: 'if_required',
    });
    if (result.error) {
      // mostrar error al usuario
    }
    // Si no hay error, el aviso INVOICE_PAID confirmará el cobro
  }

  return <form onSubmit={handleSubmit}><PaymentElement /><button>Pagar</button></form>;
}
```

### Qué hacer según la respuesta

**¿Qué hago con la respuesta de pay?**

¿Qué trae la respuesta?

- alreadySucceeded: true: **Pago hecho**. No hagas nada más. La factura pasa a PAID en cuanto la pasarela lo confirma.
- `requires_action`: **Falta 3D Secure**. Llama a stripe.confirmCardPayment() con el clientSecret.
- `requires_payment_method`: **Tarjeta nueva**. Monta Stripe Elements con el clientSecret para que el usuario la introduzca.
- processing: **En curso**. También con requires_confirmation: la pasarela lo está procesando. Espera el aviso.

Con `canceled` o `failed`, el cobro no ha salido; el motivo está en el
campo `errorCode` del historial de intentos de cobro. En todos los
casos, lo que confirma el pago es el aviso `INVOICE_PAID`.

### Errores del cobro

- `403 BILLING_NOT_ENABLED`: la facturación de la aplicación está apagada.
- `409 STRIPE_ACCOUNT_NOT_READY`: tu organización no tiene la cuenta Stripe conectada o no tiene los cobros habilitados (en algunos casos llega con `400` o `503`).
- `409 BILLING_CURRENCY_NOT_SUPPORTED`: la cuenta de cobro no admite la divisa de la factura.
- `404 EXTERNAL_USER_NOT_FOUND`: el `userId` indicado no está registrado en la aplicación.
- `404 INVOICE_NOT_ACCESSIBLE`: la factura no existe, no es de esta aplicación o no es del usuario indicado (siempre la misma respuesta, para no revelar si existe).
- `400 INVOICE_STATE_INVALID`: la factura no se puede cobrar (borrador o sin importe pendiente).
- `400 INVOICE_ALREADY_CANCELLED`: la factura está anulada.
- `400 INVOICE_ALREADY_PAID`: la factura ya estaba pagada.
- `404 PAYMENT_METHOD_NOT_FOUND`: el método guardado no pertenece al usuario.
- `400 PAYMENT_METHOD_INACTIVE`: la tarjeta elegida no está activa (caducada o desvinculada). Añade una nueva.
- `400 VALIDATION_ERROR`: la pasarela rechazó el cobro (tarjeta rechazada, fondos insuficientes, CVC incorrecto, 3D Secure necesario…). El cuerpo incluye el mensaje de la pasarela, recortado. El código original (`card_declined`, `insufficient_funds`, `expired_card`…) no viaja en este error: está en el campo `errorCode` del historial de intentos de cobro.

## Historial de intentos de cobro

**`GET /api/external/v1/invoices/:id/payment-intents`**

Devuelve los intentos de cobro (PaymentIntents) de una factura, del más reciente al más antiguo. Permiso: billing:payment-methods:read.

Parámetro de ruta: `id`.

### Parámetros

| Nombre | Tipo | Obligatorio | Descripción |
| - | - | - | - |
| `id` | `path` | Sí | Identificador de la factura en UTILIA OS. |
| `userId` | `query` | Sí | externalId del usuario. |

### Respuesta (200)

```json
[
  {
    "id": "pi-utilia-1",
    "amount": 53.45,
    "currency": "EUR",
    "status": "succeeded",
    "initiatedBy": "CUSTOMER",
    "createdAt": "2026-04-20T11:22:00.000Z",
    "succeededAt": "2026-04-20T11:22:33.000Z",
    "failedAt": null,
    "errorCode": null,
    "errorMessage": null
  },
  {
    "id": "pi-utilia-0",
    "amount": 53.45,
    "currency": "EUR",
    "status": "failed",
    "initiatedBy": "CUSTOMER",
    "createdAt": "2026-04-20T11:10:00.000Z",
    "succeededAt": null,
    "failedAt": "2026-04-20T11:10:05.000Z",
    "errorCode": "card_declined",
    "errorMessage": "Tu banco ha rechazado la tarjeta."
  }
]
```

### Campos

| Nombre | Tipo | Obligatorio | Descripción |
| - | - | - | - |
| `id` | `string` | Sí | Identificador del intento de cobro en UTILIA OS. |
| `amount` | `decimal` | Sí | Importe del intento. |
| `currency` | `string` | Sí | Divisa (código ISO 4217). |
| `status` | `enum` | Sí | requires_payment_method, requires_action, processing, succeeded, canceled o failed. |
| `initiatedBy` | `enum` | Sí | CUSTOMER (portal o aplicación externa), STAFF (panel de UTILIA OS) o SYSTEM_RECURRING (cobro automático programado). |
| `createdAt` | `datetime` | Sí | Cuándo se creó el intento. |
| `succeededAt` | `datetime` | No | Cuándo se confirmó el cobro. |
| `failedAt` | `datetime` | No | Cuándo se detectó el fallo. |
| `errorCode` | `string` | No | Código del error de la pasarela (card_declined, expired_card…). |
| `errorMessage` | `string` | No | Mensaje de error ya traducido al castellano. |

## Flujo recomendado para tu interfaz

**Cobro con el usuario delante y tarjeta guardada**

Participantes: Tu usuario, Tu aplicación y UTILIA OS.

1. Tu usuario → Tu aplicación: Abre la factura y pulsa Pagar. Preselecciona su tarjeta predeterminada (GET /payment-methods).
2. Tu aplicación → UTILIA OS: `POST /api/external/v1/invoices/:id/pay`. Con el paymentMethodId de la tarjeta elegida.
3. UTILIA OS → Tu aplicación (respuesta): alreadySucceeded: true. El banco no ha pedido nada más.
4. UTILIA OS → Tu aplicación: Aviso INVOICE_PAID. Actualiza tu estado con el aviso.

Si la respuesta trae otra cosa, sigue el árbol de
[Qué hacer según la respuesta](#qué-hacer-según-la-respuesta).

## Cuadrar con los avisos

El aviso `INVOICE_PAID` es la fuente de verdad: da una factura por pagada solo cuando lo recibas y su firma HMAC sea válida. Tu interfaz puede mostrar «Procesando pago» mientras espera, pero lo que guardes en tu base de datos tiene que esperar al aviso.

## Cabeceras y límite de uso

Los mismos que el resto de endpoints `/external/v1/*`: `X-Api-Key` y 60 peticiones por minuto por clave.

## Siguiente paso

- [Avisos de facturación](https://os.utilia.ai/docs/developers/webhooks/events): Eventos INVOICE_PAID, INVOICE_PAYMENT_FAILED y más.
- [Firma HMAC](https://os.utilia.ai/docs/developers/webhooks/hmac-signature): Cómo validar la firma de los avisos.
- [Cobrar sin el usuario delante](https://os.utilia.ai/docs/developers/api/invoice-actions): Cobro contra una autorización firmada, para renovaciones desatendidas.
- [Códigos de error](https://os.utilia.ai/docs/guides/finance/invoicing-error-codes): Referencia de errorCode de facturación por categoría.
