# Acciones sobre una factura emitida

Enviar por correo, registrar un cobro externo, devolver dinero, obtener el enlace público y cobrar sin el usuario delante desde /external/v1/invoices/:id.

Crear la factura es el principio. Lo que viene después (mandársela al cliente, cobrarla, registrar una transferencia, devolver el dinero) está en este grupo de endpoints, todos bajo `/external/v1/invoices/:id`.

Todos exigen la cabecera `X-Api-Key`, comparten un límite de 60 peticiones por minuto y buscan la factura por su identificador, la aplicación dueña y el usuario indicado. Si la factura es de otra aplicación o de otro usuario, la respuesta es un `404 INVOICE_NOT_ACCESSIBLE` idéntico al de una factura inexistente: nunca revela que existe.

## Plan y permisos

Antes que el permiso de la aplicación se comprueba el plan de tu organización. Si falta el aplicativo, la respuesta es `403` con `errorCode: 'MODULE_NOT_ENABLED'` y su `moduleKey`; si falta la capacidad, `403` con `errorCode: 'CAPABILITY_NOT_ENABLED'` y su `capabilityKey`. Los dos son estados comerciales esperados, no fallos: no se reintentan.

| Endpoint | Aplicativos del plan | Capacidad del plan | Permiso de la API (scope) |
| - | - | - | - |
| `POST /invoices/:id/send` | Finanzas | — | `crm:invoices:send` |
| `POST /invoices/:id/mark-paid` | Finanzas | — | `crm:invoices:write` |
| `POST /invoices/:id/refund` | Finanzas y Pagos con tarjeta (Stripe) | — | `billing:refund` |
| `GET /invoices/:id/public-link` | Finanzas | — | `crm:invoices:read` |
| `POST /invoices/:id/public-link` | Finanzas | — | `crm:invoices:write` |
| `POST /invoices/:id/charge` | Finanzas y Pagos con tarjeta (Stripe) | Cobro automático (`finance.direct_debit`) | `billing:charge` |

Estos endpoints no tienen periodo de gracia: sin el permiso, la respuesta es `403` con `errorCode: 'INSUFFICIENT_SCOPE'` y el permiso que falta en `requiredScopes`. Concédelo en la ficha de la aplicación antes de integrar. `billing:refund` y `billing:charge` son críticos financieros: solo un Super Administrador puede concederlos, con un motivo.

## Enviar la factura por correo

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

Envía la factura al usuario final por correo, con el PDF adjunto, usando la plantilla y el remitente de tu organización. La factura pasa a estado Enviada.

Parámetro de ruta: `id`.

Es el mismo envío que usa tu equipo desde el panel: misma plantilla, mismo remitente, misma vía de salida y mismo registro de comunicación. Úsalo para enviar una factura cuyo correo al emitirla quedó en `failed` o `withheld` (lo dice su `emailDelivery`), o cuando prefieras decidir tú el momento del envío en lugar de dejarlo al interruptor de la aplicación. Si su `emailDelivery` está en `retrying`, no hace falta: UTILIA OS ya la está reenviando sola. Cuando sale por esta vía, `emailDelivery` pasa a `sent` y el reintento automático se detiene, así que la factura no sale dos veces. Ver [El correo de la factura](https://os.utilia.ai/docs/developers/api/invoices#el-correo-de-la-factura).

### Cuerpo

| Nombre | Tipo | Obligatorio | Descripción |
| - | - | - | - |
| `userId` | `string` | Sí | Identificador del usuario en tu aplicación (externalId) al que pertenece la factura. |
| `to` | `string[]` | No | Destinatarios. Máximo 10 correos, y todos tienen que ser direcciones ya conocidas de esta factura. Sin este campo, el destinatario es el contacto de facturación principal del cliente y, si no hay, el correo del usuario externo. |
| `cc` | `string[]` | No | Copia. Máximo 10 correos, con la misma restricción que to. |
| `message` | `string` | No | Mensaje personal, máximo 2.000 caracteres. Sustituye el texto de la plantilla y conserva el bloque de datos de la factura. |
| `templateId` | `string` | No | Plantilla de correo concreta. Sin este campo se usa la predeterminada de tu organización. |

> **Atención: Solo direcciones conocidas de esta factura**
>
> `send` no es un servicio de correo general: escribe en nombre de tu organización, con su remitente y su plantilla, así que solo llega a la otra parte de la factura. Son direcciones aceptadas:
>
> - los **contactos de facturación activos** del cliente puente del usuario,
> - los **contactos del CRM** de ese cliente marcados para recibir facturas,
> - el **correo del usuario externo**,
> - el **destinatario con el que se emitió** la factura.
>
> Cualquier otra dirección en `to` o en `cc` rechaza el envío entero con `422 INVOICE_EMAIL_RECIPIENT_NOT_ALLOWED`, sin enviar nada. La comparación no distingue mayúsculas de minúsculas, y el mensaje explica la regla pero nunca enumera las direcciones registradas. Para escribir a una dirección nueva, regístrala antes como contacto de facturación del cliente.

Cada aplicación puede enviar **100 facturas por hora** dentro de tu organización, y el cupo es el mismo que gastan los correos que salen al emitir. Superado el cupo, la respuesta es `429` con `errorCode: 'RATE_LIMIT_EXCEEDED'`, la cabecera `Retry-After` y el campo `retryAfterSeconds` en el cuerpo. La ventana es la hora en curso: espera los segundos que indique en lugar de reintentar sin parar.

### Respuesta (200)

```json
{
  "sent": true,
  "invoiceId": "ckinv1234567890",
  "recipients": ["administracion@estudiomarea.example"],
  "sentAt": "2026-10-01T09:13:10.000Z"
}
```

### Errores

| Situación | HTTP | `errorCode` |
| - | - | - |
| La factura es de otra aplicación o de otro usuario | 404 | `INVOICE_NOT_ACCESSIBLE` |
| La factura está en borrador o anulada | 409 | `INVOICE_STATE_INVALID` |
| No hay ningún destinatario al que enviarla | 422 | `INVOICE_EMAIL_NO_RECIPIENT` |
| Un destinatario no es una dirección conocida de la factura | 422 | `INVOICE_EMAIL_RECIPIENT_NOT_ALLOWED` |
| La aplicación superó su cupo de envíos por hora | 429 | `RATE_LIMIT_EXCEEDED` |
| El PDF original de la factura se está terminando de preparar (`retryable: true`, `retryAfterSeconds: 15`) o el almacén de documentos no responde (`retryable: true`, `retryAfterSeconds: 120`) | 503 | `INVOICE_PDF_NOT_AVAILABLE` |
| El PDF original necesita que lo revise tu equipo antes de enviarlo (`retryable: false`) | 409 | `INVOICE_PDF_NOT_AVAILABLE` |
| El servidor de correo no responde (`retryable: true`, `retryAfterSeconds: 120`) o el servidor de salida rechazó el envío (`retryable: false`) | 502 | `INVOICE_EMAIL_NOT_SENT` |

En todos estos casos **la factura sigue emitida**: lo único que no ha
salido es el correo. `retryable` y `retryAfterSeconds` viajan en la raíz
del cuerpo del error. Con `retryable: true`, repite el envío pasados esos
segundos; con `false`, no lo repitas igual: revisa la dirección o pide a
tu equipo que revise el PDF. No vuelvas a crear la factura nunca.

## Registrar un cobro fuera de la pasarela

**`POST /api/external/v1/invoices/:id/mark-paid`**

Marca la factura como cobrada por una vía que no pasa por la pasarela, como una transferencia o un pago en efectivo. Usa el mismo proceso que el panel, así que el justificante (y la contabilidad, cuando esté disponible) se comporta igual.

Parámetro de ruta: `id`.

### Cuerpo

| Nombre | Tipo | Obligatorio | Descripción |
| - | - | - | - |
| `userId` | `string` | Sí | Identificador del usuario en tu aplicación (externalId). |
| `method` | `enum` | Sí | Forma de pago: TRANSFER, CASH, CHECK, CARD u OTHER. Un cobro con tarjeta en la pasarela propia de tu aplicación es CARD. Las formas de la pasarela de UTILIA OS (STRIPE\_\*) se rechazan a propósito: ese cobro lo registra el propio pago y volver a registrarlo duplicaría el asiento. |
| `amount` | `number` | No | Importe cobrado, con dos decimales y mínimo 0,01. Sin este campo la factura se marca como pagada por completo; con él se registra un cobro parcial. |
| `paidAt` | `string` | No | Fecha del cobro en formato ISO 8601. Sin ella, el momento de la llamada. |
| `reference` | `string` | No | Referencia bancaria o de tu aplicación, máximo 200 caracteres. Se guarda en la auditoría y se añade a las notas internas de la factura, que es donde la busca quien concilia. |

La respuesta es el resumen de la factura ya actualizada, con el mismo formato que devuelve `GET /external/v1/invoices`.

Si ya sabes al emitir que la factura está cobrada, no hace falta esta
llamada: manda el bloque `payment` en el [alta de la factura](https://os.utilia.ai/docs/developers/api/invoices#una-factura-que-ya-cobraste-con-tu-pasarela)
y queda cobrada en el mismo acto.

Es también la forma de completar un cobro del bloque `payment` que no
se pudo registrar al emitir (`paymentRegistered: false`): en cuanto
queda registrado, por esta llamada o desde la ficha de la factura,
`paymentRegistered` pasa a `true` en las lecturas siguientes. Si el
correo de la emisión quedó retenido por ese motivo
(`PAYMENT_NOT_REGISTERED`), envíalo después con `send`.

### Errores

| Situación | HTTP | `errorCode` |
| - | - | - |
| La factura es de otra aplicación o de otro usuario | 404 | `INVOICE_NOT_ACCESSIBLE` |
| La factura está en borrador o anulada | 409 | `INVOICE_STATE_INVALID` |
| La factura ya está cobrada por completo | 409 | `INVOICE_ALREADY_PAID` |

## Devolver dinero cobrado

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

Reembolsa por la pasarela de UTILIA OS, entero o en parte, el dinero ya cobrado de una factura. Anular una factura no devuelve dinero; esto sí.

Parámetro de ruta: `id`.

Este reembolso mueve el dinero que se cobró con la **pasarela de UTILIA
OS**. Si cobraste con tu propia pasarela y ya devolviste el dinero allí,
no lo uses: emite la [rectificativa](https://os.utilia.ai/docs/developers/api/invoice-rectifications)
con el bloque `refund`, que registra la devolución sin mover dinero.

### Cuerpo

| Nombre | Tipo | Obligatorio | Descripción |
| - | - | - | - |
| `userId` | `string` | Sí | Identificador del usuario en tu aplicación (externalId). |
| `amountCents` | `integer` | No | Importe a devolver en céntimos, mínimo 1. Sin este campo se devuelve todo lo cobrado. |
| `reason` | `enum` | No | duplicate, fraudulent o requested_by_customer. Los mismos valores que la pasarela. |
| `createRectifyingInvoice` | `boolean` | No | Emitir además la factura rectificativa del reembolso, por diferencias y con importes negativos, total o parcial. El código legal lo decide UTILIA OS: R1 si la factura es ordinaria o simplificada cualificada, R5 si es simplificada sin destinatario identificado. Por defecto, no. |
| `paymentIntentId` | `string` | No | Cobro concreto sobre el que devolver, en facturas con varios. |
| `idempotencyKey` | `string` | No | Clave de idempotencia, válida 24 horas. Repetir la misma clave devuelve el resultado de la primera llamada en lugar de reembolsar dos veces. |

### Respuesta (200)

```json
{
  "id": "ckref1234567890",
  "invoiceId": "ckinv1234567890",
  "amountCents": 5345,
  "currency": "eur",
  "status": "SUCCEEDED",
  "isFullRefund": true,
  "reason": "requested_by_customer",
  "rectifyingInvoiceId": null,
  "createdAt": "2026-10-15T11:02:31.000Z"
}
```

Produce el aviso `INVOICE_REFUNDED`, dirigido a tu aplicación. Con
`createRectifyingInvoice: true`, la rectificativa se calcula **antes** de
mover el dinero: si no se puede documentar exacta al céntimo, el
reembolso se rechaza con `422` y no se devuelve nada. Cuando se emite,
llegan además `INVOICE_RECTIFICATION_CREATED` e `INVOICE_RECTIFIED`, y
`rectifyingInvoiceId` de la respuesta la identifica.

### Errores

| Situación | HTTP | `errorCode` |
| - | - | - |
| La factura es de otra aplicación o de otro usuario | 404 | `INVOICE_NOT_ACCESSIBLE` |
| No hay cobros completados, o el importe supera lo cobrado | 409 | `REFUND_NOT_ALLOWED` |
| La cuenta de la pasarela no está lista para operar | 409 | `STRIPE_ACCOUNT_NOT_READY` |
| Con `createRectifyingInvoice`, la rectificativa no se puede calcular exacta al céntimo o hay que revisar las anteriores | 422 | Un código de la familia `RECTIFICATION_*`, por ejemplo `RECTIFICATION_AMOUNT_NOT_REPRESENTABLE` |

## Enlace público de la factura

Es la única dirección que puedes entregar a tu usuario final. La ruta del PDF de la API externa exige la clave de API y nunca debe salir de tu servidor.

**`GET /api/external/v1/invoices/:id/public-link`**

Consulta si la factura tiene un enlace público vigente, hasta cuándo y qué acciones permite. Solo lee: nunca devuelve la dirección.

Parámetro de ruta: `id`.

**`POST /api/external/v1/invoices/:id/public-link`**

Emite un enlace público nuevo y devuelve su dirección. Es la única vía para obtener una dirección utilizable.

Parámetro de ruta: `id`.

> **Atención: La dirección se entrega una sola vez**
>
> De un enlace emitido, UTILIA OS guarda solo su huella, nunca el valor en claro. Por eso la lectura devuelve siempre `publicUrl: null` y solo el `POST` trae la dirección. Y por eso **emitir revoca el enlace anterior de esa factura**: si ya habías repartido uno a tu usuario, deja de funcionar y tendrás que repartir el nuevo. No consultes una y otra vez esperando que aparezca la dirección, ni emitas un enlace nuevo por costumbre: guárdalo en tu base de datos la primera vez.

### Parámetros

| Nombre | Tipo | Obligatorio | Descripción |
| - | - | - | - |
| `userId` | `query o cuerpo` | Sí | Identificador del usuario en tu aplicación (externalId). Va en la cadena de consulta en el GET y en el cuerpo en el POST. |
| `expiresInDays` | `integer` | No | Solo en el POST. Días de vigencia del enlace nuevo, entre 1 y 365. Sin este campo se aplica la vigencia predeterminada de tu organización. |

### Respuesta del POST (200)

```json
{
  "invoiceId": "ckinv1234567890",
  "publicUrl": "https://os.utilia.ai/public/invoices/9f3c1a6e4b2d",
  "expiresAt": "2026-12-30T23:59:59.000Z",
  "allowedActions": ["VIEW", "PAY"],
  "createdAt": "2026-10-01T09:12:45.000Z"
}
```

La respuesta del `GET` tiene la misma forma con `publicUrl` a nulo.

`allowedActions` es siempre `["VIEW", "PAY"]` en los enlaces que emite la API externa, también cuando la factura viene de una suscripción. Firmar o retirar una autorización de cobro recurrente no cabe aquí: son actos del usuario final que se recogen por su camino propio, con su texto de consentimiento y su prueba legal, en [Autorizaciones de cobro](https://os.utilia.ai/docs/developers/api/payment-authorizations). El enlace que emite tu equipo desde el panel sí puede habilitar esas acciones, porque lo reparte una persona con sesión iniciada.

### Errores

| Situación | HTTP | `errorCode` |
| - | - | - |
| La factura es de otra aplicación o de otro usuario | 404 | `INVOICE_NOT_ACCESSIBLE` |
| El `GET` no encuentra ningún enlace vigente | 404 | `INVOICE_PUBLIC_LINK_NOT_AVAILABLE` |

## Cobrar sin el usuario delante

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

Cobra la factura contra la autorización de cobro vigente del usuario, sin que haya nadie delante. Es lo que hace posible la renovación mensual desatendida.

Parámetro de ruta: `id`.

No lo confundas con el cobro con el usuario delante: con `POST /invoices/:id/pay` tu aplicación monta la interfaz de la pasarela con el `clientSecret` y la persona confirma. `POST /invoices/:id/charge` es el cobro **sin** nadie delante, y exige que el usuario haya firmado antes una [autorización de cobro](https://os.utilia.ai/docs/developers/api/payment-authorizations).

**Cobro sin nadie delante**

Participantes: Tu servidor y UTILIA OS.

1. Tu servidor → UTILIA OS: `POST /api/external/v1/invoices/:id/charge`. Con userId e idempotencyKey.
2. UTILIA OS (por su cuenta): Cobra la tarjeta que respalda la autorización vigente.
3. UTILIA OS → Tu servidor (respuesta): status: SUCCEEDED. El dinero ya está cobrado.
4. UTILIA OS → Tu servidor: Aviso CHARGE_SUCCEEDED. Con invoiceFullyPaid, que dice si la factura queda cobrada entera.

### Cuerpo

| Nombre | Tipo | Obligatorio | Descripción |
| - | - | - | - |
| `userId` | `string` | Sí | Identificador del usuario en tu aplicación (externalId). |
| `paymentAuthorizationId` | `string` | No | Autorización de cobro concreta. Sin este campo se usa la vigente y compatible del usuario. |
| `paymentMethodId` | `string` | No | Solo de comprobación. Sin el usuario delante se cobra siempre la tarjeta de la autorización; si indicas otra, la llamada se rechaza con PAYMENT_AUTHORIZATION_NOT_ACTIVE y tienes que recoger una autorización nueva con esa tarjeta. |
| `idempotencyKey` | `string` | No | Clave de idempotencia, válida 24 horas. Envía siempre la misma para reintentar la MISMA operación: una clave nueva es un cobro nuevo. |

### Respuesta (200)

```json
{
  "status": "REQUIRES_ACTION",
  "paymentIntentId": "pi_3QabcDEFghiJKLmn",
  "invoiceId": "ckinv1234567890",
  "amountCents": 5345,
  "currency": "eur",
  "publicUrl": "https://os.utilia.ai/public/invoices/9f3c1a6e4b2d",
  "clientSecret": "pi_3QabcDEFghiJKLmn_secret_XyZ",
  "failureCode": null,
  "failureMessage": null
}
```

| Nombre | Tipo | Obligatorio | Descripción |
| - | - | - | - |
| `status` | `enum` | Sí | SUCCEEDED (dinero cobrado), REQUIRES_ACTION (el banco pide autenticación reforzada), PROCESSING (en curso) o FAILED (rechazado). |
| `publicUrl` | `string` | No | Con REQUIRES_ACTION, la dirección a la que llevar al usuario para que complete la autenticación. Es un enlace adicional, limitado a ver y pagar: no revoca el que ya tuviera repartido de esa factura. |
| `clientSecret` | `string` | No | Con REQUIRES_ACTION, el secreto para completar la autenticación desde tu propia interfaz si prefieres no usar el enlace público. |
| `failureCode` | `string` | No | Con FAILED, el código de rechazo de la pasarela. |
| `failureMessage` | `string` | No | Con FAILED, la explicación legible del rechazo. |

No siempre sale a la primera. Qué hacer según el `status` de la respuesta:

**¿Qué hago con la respuesta de un cobro?**

¿Qué status trae la respuesta?

- `SUCCEEDED`: **Cobro hecho** (`CHARGE_SUCCEEDED`). El dinero está cobrado. El aviso lo confirma.
- `REQUIRES_ACTION`: **Falta la autenticación del banco** (`CHARGE_REQUIRES_ACTION`). No es un error: lleva al usuario a publicUrl, o complétala en tu interfaz con clientSecret. Cuando pague, llega INVOICE_PAID.
- `PROCESSING`: **En curso**. Espera el aviso. No repitas el cobro con otra clave de idempotencia.
- `FAILED`: **Rechazado** (`CHARGE_FAILED`). failureCode y failureMessage dicen por qué. UTILIA OS no lo reintenta: decides tú.

### Errores

| Situación | HTTP | `errorCode` |
| - | - | - |
| El usuario no tiene autorización de cobro vigente | 409 | `PAYMENT_AUTHORIZATION_REQUIRED`, con `nextStep` |
| La autorización indicada ya no está vigente | 409 | `PAYMENT_AUTHORIZATION_NOT_ACTIVE` |
| La tarjeta indicada no existe o ya no está activa | 404 o 409 | `PAYMENT_METHOD_NOT_FOUND` o `PAYMENT_METHOD_INACTIVE` |
| La factura ya está cobrada | 409 | `INVOICE_ALREADY_PAID` |
| La divisa de la factura no la admite la cuenta de la pasarela | 409 | `BILLING_CURRENCY_NOT_SUPPORTED` |
| La cuenta de la pasarela no está lista para operar | 409 | `STRIPE_ACCOUNT_NOT_READY` |

El error de autorización ausente es accionable: dice qué hacer a continuación.

```json
{
  "errorCode": "PAYMENT_AUTHORIZATION_REQUIRED",
  "message": "La suscripción no puede cobrarse sola porque este usuario todavía no ha autorizado el cobro.",
  "nextStep": {
    "operation": "paymentAuthorizations.getConsentText",
    "then": "paymentAuthorizations.create",
    "scope": "RECURRING"
  }
}
```

## Avisos que producen estas acciones

| Acción | Aviso |
| - | - |
| Envío por correo | `INVOICE_SENT` |
| Cobro fuera de la pasarela | `INVOICE_PAID` o `INVOICE_PARTIALLY_PAID`, con `source: 'manual'` |
| Reembolso | `INVOICE_REFUNDED`; con rectificativa, además `INVOICE_RECTIFICATION_CREATED` e `INVOICE_RECTIFIED` |
| Cobro sin el usuario delante | `CHARGE_STARTED` y después `CHARGE_SUCCEEDED`, `CHARGE_FAILED` o `CHARGE_REQUIRES_ACTION` |
| Enlace público | ninguno |

## Con el SDK

### JavaScript / TypeScript

```typescript
import { UtiliaSDK } from '@utilia-os/sdk-js';

const sdk = new UtiliaSDK({
  baseURL: 'https://os.utilia.ai/api',
  apiKey: process.env.UTILIA_API_KEY!,
});

await sdk.invoices.send('ckinv1234567890', {
  userId: 'user_01HXYZ',
  message: 'Adjuntamos la factura del mes de octubre.',
});

await sdk.invoices.markPaid('ckinv1234567890', {
  userId: 'user_01HXYZ',
  method: 'TRANSFER',
  reference: 'TRF-2026-10-0042',
});

const enlace = await sdk.invoices.regeneratePublicLink('ckinv1234567890', {
  userId: 'user_01HXYZ',
});

const cobro = await sdk.invoices.charge('ckinv1234567890', {
  userId: 'user_01HXYZ',
  idempotencyKey: 'cobro_ckinv1234567890',
});

if (cobro.status === 'REQUIRES_ACTION') {
  llevarAlUsuario(cobro.publicUrl);
}

await sdk.invoices.refunds.create('ckinv1234567890', {
  userId: 'user_01HXYZ',
  reason: 'requested_by_customer',
  idempotencyKey: 'devolucion_ckinv1234567890',
});
```

### Python

```python
import os
from utilia_sdk import UtiliaSDK
from utilia_sdk.models import (
    ChargeExternalInvoiceInput,
    MarkExternalInvoicePaidInput,
    RefundExternalInvoiceInput,
    RegenerateExternalInvoicePublicLinkInput,
    SendExternalInvoiceInput,
)

sdk = UtiliaSDK(
    base_url="https://os.utilia.ai/api",
    api_key=os.environ["UTILIA_API_KEY"],
)

await sdk.invoices.send(
    "ckinv1234567890",
    SendExternalInvoiceInput(
        user_id="user_01HXYZ",
        message="Adjuntamos la factura del mes de octubre.",
    ),
)

await sdk.invoices.mark_paid(
    "ckinv1234567890",
    MarkExternalInvoicePaidInput(
        user_id="user_01HXYZ",
        method="TRANSFER",
        reference="TRF-2026-10-0042",
    ),
)

enlace = await sdk.invoices.regenerate_public_link(
    "ckinv1234567890",
    RegenerateExternalInvoicePublicLinkInput(user_id="user_01HXYZ"),
)

cobro = await sdk.invoices.charge(
    "ckinv1234567890",
    ChargeExternalInvoiceInput(
        user_id="user_01HXYZ",
        idempotency_key="cobro_ckinv1234567890",
    ),
)

if cobro.status == "REQUIRES_ACTION":
    llevar_al_usuario(cobro.public_url)

await sdk.invoices.refunds.create(
    "ckinv1234567890",
    RefundExternalInvoiceInput(
        user_id="user_01HXYZ",
        reason="requested_by_customer",
        idempotency_key="devolucion_ckinv1234567890",
    ),
)
```

## Rectificativas

Hay dos caminos, según por dónde cobraste:

- **Con la pasarela de UTILIA OS**: el reembolso de esta página con `createRectifyingInvoice: true` devuelve el dinero y emite la rectificativa en el mismo acto.
- **Con tu propia pasarela**: devuelve el dinero allí y emite la rectificativa con [`POST /external/v1/invoices/:id/rectifications`](https://os.utilia.ai/docs/developers/api/invoice-rectifications), con el bloque `refund` para dejar registrada la devolución.

En los dos casos el código legal (R1 o R5) lo decide UTILIA OS, siempre
por diferencias y con importes negativos. Las rectificativas con otros
motivos (un error en los datos, un descuento posterior) las emite tu
equipo desde Finanzas o con la herramienta `crm_invoices_rectifications`
del servidor MCP de finanzas.

## Siguiente paso

- [Autorizaciones de cobro](https://os.utilia.ai/docs/developers/api/payment-authorizations): Recoger el consentimiento que habilita cobrar sin el usuario delante.
- [Suscripciones](https://os.utilia.ai/docs/developers/api/subscriptions): Cuotas mensuales con factura y cobro automáticos.
- [Cobros programados](https://os.utilia.ai/docs/developers/api/scheduled-charges): Cobrar una factura suelta en una fecha futura.
- [Guía de facturación](https://os.utilia.ai/docs/developers/api/billing-guide): El recorrido completo, de la primera factura al cobro recurrente.
