# Facturas

Emite facturas ordinarias y simplificadas desde tu aplicación, sigue su emisión y su correo, y consúltalas, descárgalas o anúlalas, con el plan y los permisos.

Con `/external/v1/invoices` tu aplicación emite facturas en nombre de
sus usuarios, con la numeración de tu organización o en una serie que
tu organización le haya reservado, y las consulta, descarga o anula.
Todas las peticiones llevan la cabecera
`X-Api-Key` con una clave activa, exigen que la facturación de la
aplicación esté encendida y tienen un límite de 60 peticiones por
minuto.

Cada clave solo opera sobre las facturas de su propia aplicación, y el
`userId` filtra además por el usuario indicado.

## Plan y permisos

Antes que el permiso de la aplicación se comprueba el plan de tu
organización. Sin el aplicativo **Finanzas** contratado, todos estos
endpoints responden `403` con `errorCode: 'MODULE_NOT_ENABLED'` y la
clave del aplicativo que falta.

| Endpoint | Aplicativo del plan | Permiso de la API (scope) |
| - | - | - |
| `POST /external/v1/invoices` | Finanzas | `crm:invoices:write` y `crm:invoices:issue` |
| `POST /external/v1/invoices/validate` y `/validate/pdf` | Finanzas | Cualquier permiso `crm:invoices:*` |
| `GET /external/v1/invoices` | Finanzas | `crm:invoices:read` |
| `GET /external/v1/invoices/:id` | Finanzas | `crm:invoices:read` |
| `GET /external/v1/invoices/:id/pdf` | Finanzas | `crm:invoices:read` |
| `GET /external/v1/invoices/stats` | Finanzas | `crm:invoices:read` |
| `GET /external/v1/invoices/series` | Finanzas | `crm:invoices:read` |
| `GET /external/v1/invoices/eu-vat-rates` (Próximamente) | Finanzas | `crm:invoices:read` |
| `POST /external/v1/invoices/:id/cancel` | Finanzas | `crm:invoices:cancel` |
| `POST /external/v1/invoices/:id/rectifications` y `/rectifications/validate` | Finanzas | `crm:invoices:rectify` |
| `GET /external/v1/invoices/:id/rectifications` | Finanzas | `crm:invoices:read` |

Sin el permiso adecuado, los endpoints responden `403 INSUFFICIENT_SCOPE`
con un mensaje genérico que no dice cuál falta. No hay periodo de
gracia: el permiso se exige desde la primera llamada, también en
aplicaciones creadas hace tiempo, así que concédelos antes de integrar.
`crm:invoices:cancel` y `crm:invoices:rectify` son críticos financieros:
solo los concede un Super Administrador, con un motivo. El catálogo
completo y las reglas de concesión están en
[Permisos de la API](https://os.utilia.ai/docs/developers/api/scopes).

Las acciones sobre una factura ya emitida (enviarla por correo,
cobrarla, marcarla pagada, devolver el dinero o pedir su enlace
público) están en [Acciones sobre una factura emitida](https://os.utilia.ai/docs/developers/api/invoice-actions).
Las rectificativas, cuando devuelves el dinero con tu propia pasarela o
un contracargo deja la venta sin efecto, están en
[Facturas rectificativas](https://os.utilia.ai/docs/developers/api/invoice-rectifications).
Las facturas con el IVA del país del consumidor por la ventanilla única
(`taxSystem: "EU_OSS"`) están en «Próximamente»: hasta que estén disponibles
para tu organización, responden `403 FEATURE_COMING_SOON`.

## Ordinaria o simplificada

Por esta ruta se emiten dos tipos de factura, que se eligen con `type`:

| | Ordinaria (`STANDARD`) | Simplificada (`SIMPLIFIED`) |
| - | - | - |
| Cuándo | Por defecto, si omites `type`. Para empresas y profesionales, o cuando el cliente la pide. | Ventas a consumidores, como una suscripción o una compra en tu aplicación (art. 7 del RD 1619/2012). |
| Numeración | La serie correlativa de tu organización (`202600042`) o una serie de ordinarias de tu aplicación (`APP-202600001`). | Su propia serie, separada de la anterior como exige la norma (`T-202600001`), o una serie de simplificadas de tu aplicación. |
| Importe | Sin tope legal. | Hasta **400 € impuestos incluidos**. Por encima, `422 SIMPLIFIED_INVOICE_AMOUNT_EXCEEDED`: emite una ordinaria. |
| Destinatario | Obligatorio y completo. | Opcional (ver la tabla siguiente). |
| Mención impresa | La que toque por sus líneas. | Además, «Factura simplificada (RD 1619/2012 art. 7)». |

El destinatario de una simplificada depende de lo que mandes en `recipient`:

| Lo que mandas | Lo que se imprime | Qué exige |
| - | - | - |
| Sin `recipient` | «Consumidor final». El correo, si se envía, sale a `user.email`. | Nada más. |
| `recipient.name` sin NIF | El nombre, tal cual. Sigue siendo simplificada. | Nada más. |
| `recipient.taxId` | Una **simplificada cualificada**, con el destinatario identificado (art. 7.2 y 7.3). | Nombre, dirección, ciudad, código postal y país. Si falta alguno, `400 SIMPLIFIED_RECIPIENT_INCOMPLETE` con la lista en `missingFields`. |

UTILIA OS no copia en la factura datos del usuario que no vengan en
`recipient`. Una ordinaria sin `recipient` responde
`400 INVOICE_RECIPIENT_REQUIRED`, y con datos incompletos,
`400 INVOICE_RECIPIENT_INCOMPLETE` con `missingFields`. Las
rectificativas no se crean aquí: tienen su propia ruta.

## Crear una factura

**`POST /api/external/v1/invoices`**

Emite una factura ordinaria o simplificada en nombre de la aplicación con la numeración de tu organización. Como crea y emite en el mismo paso, exige a la vez crm:invoices:write y crm:invoices:issue.

> **Consejo: Antes de tu primera factura real**
>
> Cada llamada que responde `201` emite una factura numerada que no se puede borrar. Para probar el cuerpo sin emitir nada, usa `POST /external/v1/invoices/validate` y su PDF de muestra: ver [Probar la facturación sin emitir facturas](https://os.utilia.ai/docs/developers/api/invoice-testing).

### Cuerpo de la petición

| Nombre | Tipo | Obligatorio | Descripción |
| - | - | - | - |
| `user` | `objeto` | Sí | Datos del usuario externo propietario de la factura. |
| `user.externalId` | `string` | Sí | Identificador estable del usuario en tu sistema (máximo 255 caracteres). Con él se crea o se actualiza el usuario. |
| `user.email` | `string` | Sí | Correo del usuario. Se valida su formato. |
| `user.name` | `string` | No | Nombre o razón social. |
| `user.taxId` | `string` | No | NIF o CIF del usuario. Imprescindible si el receptor es el propio usuario y la factura es entre empresas. También puedes mandarlo antes con POST /external/v1/users/identify. |
| `user.billingAddress` | `objeto` | No | Dirección de facturación: street, city, postalCode y country obligatorios, province opcional. Si la pasas, sustituye a la guardada. También puedes mandarla antes con identify. |
| `user.metadata` | `objeto` | No | Datos libres asociados al usuario. |
| `type` | `enum` | No | STANDARD (ordinaria, por defecto) o SIMPLIFIED (simplificada). RECTIFYING y CREDIT_NOTE se rechazan con 400: las rectificativas tienen su propia ruta. |
| `seriesCode` | `string` | No | Serie en la que se numera la factura: el código de una serie que tu organización ha reservado a la aplicación (consúltalas con GET /external/v1/invoices/series) o DEFAULT para la serie de siempre del tipo. Mayúsculas y cifras, hasta 10 caracteres. Si lo omites, la serie por defecto de la aplicación para el tipo y, si no tiene, la de siempre. Ver «Series disponibles». |
| `recipient` | `objeto` | No | Datos fiscales del receptor que se imprimen en la factura. Obligatorio en la ordinaria; opcional en la simplificada (ver «Ordinaria o simplificada»). |
| `recipient.name` | `string` | Sí | Nombre o razón social del receptor. Obligatorio si mandas el bloque. |
| `recipient.taxId` | `string` | No | NIF o CIF. Obligatorio en facturas entre empresas. En una simplificada la convierte en cualificada y obliga a mandar la dirección completa. |
| `recipient.address` | `string` | No | Dirección (calle y número). Obligatoria en la ordinaria y en la simplificada con NIF. |
| `recipient.city` | `string` | No | Ciudad. Obligatoria en la ordinaria y en la simplificada con NIF. |
| `recipient.postalCode` | `string` | No | Código postal. Obligatorio en la ordinaria y en la simplificada con NIF. |
| `recipient.country` | `string` | No | País (mejor en castellano, por ejemplo «España»). Obligatorio en la ordinaria y en la simplificada con NIF. |
| `recipient.email` | `string` | No | Correo del receptor. |
| `recipient.phone` | `string` | No | Teléfono del receptor. |
| `issueDate` | `string` | No | Fecha de expedición (AAAA-MM-DD). Si la omites, hoy en la zona horaria de tu organización. No puede ser futura ni anterior a la de la última factura ya expedida en la misma serie, y su año es el de la numeración: si no, 400 INVOICE_ISSUE_DATE_OUT_OF_ORDER sin numerar. |
| `dueDate` | `string` | No | Fecha de vencimiento (AAAA-MM-DD). |
| `taxSystem` | `enum` | No | IVA, IGIC, IPSI, EXEMPT, INTERNATIONAL o EU_OSS. Si lo omites, se toma del régimen fiscal de tu organización (nunca EU_OSS). EU_OSS es el IVA del país del consumidor por la ventanilla única, que está en «Próximamente»: sus reglas están en Ventanilla única del IVA (OSS) y, mientras no esté disponible para tu organización, responde 403 FEATURE_COMING_SOON. |
| `notes` | `string` | No | Notas visibles al cliente (se imprimen en el PDF). |
| `discountPercent` | `number` | No | Descuento global (de 0 a 100). Un valor mayor que cero junto con pricesIncludeTax: true responde 400 TAX_INCLUSIVE_GLOBAL_DISCOUNT_UNSUPPORTED. |
| `pricesIncludeTax` | `boolean` | No | Si es true, unitPrice y discount de cada línea son importes CON impuestos y el total de cada línea es exactamente lo cobrado. Si lo omites, los precios son sin impuestos. |
| `payment` | `objeto` | No | Cobro que tu aplicación ya hizo fuera de UTILIA OS, por ejemplo con su propia pasarela. Con él, la factura queda cobrada por su total en el mismo acto. |
| `payment.method` | `enum` | Sí | Obligatorio si mandas el bloque. CARD (una tarjeta cobrada con tu pasarela), TRANSFER, CASH, CHECK u OTHER. Las formas de la pasarela de UTILIA OS (STRIPE\_\*) no se admiten. |
| `payment.paidAt` | `string` | No | Fecha y hora del cobro (ISO 8601). No puede ser futura. Si la omites, el momento de la llamada. |
| `payment.reference` | `string` | No | Referencia del cobro en tu pasarela o en tu banco (por ejemplo, pi\_...), hasta 200 caracteres. Queda registrada para la conciliación. |
| `consumerResidence` | `objeto` | No | Residencia del consumidor final y su prueba. Se coteja con el sistema fiscal de la factura y no se imprime. |
| `consumerResidence.region` | `enum` | Sí | Obligatorio si mandas el bloque. CANARIAS (IGIC), PENINSULA_BALEARES (IVA; con la ventanilla única, también EU_OSS en el régimen exterior), CEUTA_MELILLA, OTHER_EU (otro país de la Unión Europea; con la ventanilla única, EU_OSS) o NON_EU (fuera de la Unión Europea). |
| `consumerResidence.countryCode` | `string` | No | País en código ISO 3166-1 de dos letras. Obligatorio con OTHER_EU y NON_EU. |
| `consumerResidence.evidence` | `string` | No | Resumen de la prueba de residencia que conserva tu aplicación, por ejemplo «país de la tarjeta: ES; código postal de facturación: 38», hasta 500 caracteres. No envíes la dirección IP ni la dirección postal completa: basta con el resumen. |
| `consumerResidence.evidenceItems` | `array` | No | Solo con EU_OSS, y obligatoria en ese caso (Próximamente): prueba estructurada de la ubicación del consumidor, de 1 a 10 elementos con type, countryCode y, si es España, region y thirdParty. Fuera de EU_OSS, 400 VALIDATION_ERROR. Ver Ventanilla única del IVA (OSS). |
| `metadata` | `objeto` | No | Datos libres asociados a la factura. Las claves que empiezan por external y que usa UTILIA OS están reservadas: no se pueden fijar. |
| `lines` | `array` | Sí | Líneas de la factura. Mínimo una. |
| `lines[].name` | `string` | Sí | Concepto breve. |
| `lines[].description` | `string` | No | Descripción extendida. |
| `lines[].quantity` | `number` | Sí | Cantidad (hasta 4 decimales). |
| `lines[].unitPrice` | `number` | Sí | Precio unitario sin impuestos. Con pricesIncludeTax: true, precio con impuestos. |
| `lines[].taxType` | `enum` | Sí | Tipo de impuesto (por ejemplo, IVA_21, IGIC_7 o EXEMPT). Debe pertenecer al sistema fiscal de la factura. Con EU_OSS, todas las líneas son EU_VAT. |
| `lines[].vatRateClass` | `enum` | No | Solo en líneas EU_VAT (Próximamente): clase del tipo de IVA del país del consumidor, STANDARD (por defecto), REDUCED, REDUCED_SECOND, SUPER_REDUCED o PARKING. El porcentaje lo resuelve y congela UTILIA OS. En otras líneas, 400 VALIDATION_ERROR. |
| `lines[].ossSupplyType` | `enum` | No | Solo en líneas EU_VAT (Próximamente): SERVICES (por defecto) o GOODS (venta a distancia de bienes, solo en el régimen de la Unión). En otras líneas, 400 VALIDATION_ERROR. |
| `lines[].discount` | `number` | No | Descuento en importe por línea. Con pricesIncludeTax: true, importe con impuestos. |
| `lines[].igicOperationType` | `enum` | No | Solo para el régimen especial del comerciante minorista del IGIC: IGIC_RETAIL (venta de la actividad minorista, exenta; exige taxType EXEMPT) o IGIC_GENERAL (resto). Exige que tu organización tenga ese régimen declarado en su configuración fiscal. |
| `lines[].exemptionReason` | `enum` | No | Causa de la exención: EXEMPT_IGIC_RETAILER, EXEMPT_IGIC_INTERNAL, EXEMPT_EXPORT, EXEMPT_OTHER o EXEMPT_IGIC_SMALL_BUSINESS (régimen del pequeño empresario del IGIC). Obligatoria cuando taxType es EXEMPT con IGIC, salvo en un periodo del régimen del pequeño empresario, en el que UTILIA OS pone esa causa por ti. Decide la mención legal que se imprime. |
| `lines[].exemptionNote` | `string` | No | Explicación de la exención (máximo 300 caracteres). Obligatoria con EXEMPT_OTHER; se imprime en la mención legal de la factura. |
| `sendEmail` | `boolean` | No | Decide si la factura sale por correo al emitirla y manda sobre el interruptor «Enviar las facturas por correo al emitirlas» de la aplicación en los dos sentidos: true la envía aunque esté apagado y false la retiene aunque esté encendido. Si lo omites, decide el interruptor. El envío ocurre después del cierre fiscal: un fallo de correo nunca tumba una factura ya numerada, y su resultado llega en emailDelivery (ver «El correo de la factura»). Enviar el correo requerirá el permiso crm:invoices:send: mientras dure la transición, si tu aplicación no lo tiene el correo sale igual y la respuesta lleva la cabecera X-Deprecation-Warning; pide ya ese permiso para no quedarte sin envíos cuando termine. |
| `idempotencyKey` | `string` | No | Clave de idempotencia (hasta 128 caracteres). Si llega la misma con el mismo cuerpo, se devuelve la factura original; con otro contenido, 409 BILLING_IDEMPOTENCY_CONFLICT. Si la primera petición todavía se está ejecutando, la repetición devuelve esa misma factura con issuanceDeliveryStatus: processing, nunca una segunda. Se compara el cuerpo tal como llega: mandar un campo con su valor por defecto (por ejemplo, "type": "STANDARD" o "seriesCode": "DEFAULT") cuenta como un cuerpo distinto del que lo omite. Repetir una clave ya emitida devuelve su factura aunque la serie se haya desactivado después. |

Si el bloque `user` no trae `taxId` o `billingAddress` y el usuario
tampoco los tiene guardados, se toman del `recipient` que se imprime en
el documento y se escriben en su ficha. Lo que declare `user` manda
siempre, y un dato ya guardado nunca se pisa con el del receptor. Es
una red, no la vía principal: manda los datos fiscales con
[`identify`](https://os.utilia.ai/docs/developers/api/users) en cuanto los tengas,
porque una factura emitida a un cliente sin identificador fiscal obliga
a rectificarla.

### Ejemplo de petición

```bash
curl -X POST https://os.utilia.ai/api/external/v1/invoices \
  -H "X-Api-Key: utl_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "user": {
      "externalId": "user_01HXYZ",
      "email": "cliente@empresa.com",
      "name": "Empresa de Canarias SL",
      "taxId": "B76543210"
    },
    "recipient": {
      "name": "Empresa de Canarias SL",
      "taxId": "B76543210",
      "address": "Avenida de Canarias 12",
      "city": "Las Palmas",
      "postalCode": "35001",
      "country": "España"
    },
    "issueDate": "2026-04-19",
    "dueDate": "2026-05-19",
    "taxSystem": "IGIC",
    "lines": [
      {
        "name": "Plan Premium mensual",
        "quantity": 1,
        "unitPrice": 49.95,
        "taxType": "IGIC_7"
      }
    ],
    "sendEmail": true,
    "idempotencyKey": "ord_01HXYZ-invoice"
  }'
```

### Respuesta correcta (201)

```json
{
  "id": "ckinv1234567890",
  "invoiceNumber": "202600042",
  "displayNumber": "2026-00042",
  "type": "STANDARD",
  "seriesCode": "DEFAULT",
  "taxSystem": "IGIC",
  "consumerCountryCode": null,
  "ossScheme": null,
  "status": "SENT",
  "legalMentions": [],
  "originalInvoiceId": null,
  "issuancePending": false,
  "issuanceDeliveryStatus": "complete",
  "issuanceMessage": "La factura está emitida y su entrega ha terminado.",
  "emailDelivery": {
    "status": "sent",
    "message": "La factura se ha enviado por correo.",
    "attempts": 1,
    "lastAttemptAt": "2026-10-01T09:12:45.000Z",
    "sentAt": "2026-10-01T09:12:46.000Z"
  },
  "cancellationPending": false,
  "totalAmount": 53.45,
  "paidAmount": 0,
  "currency": "eur",
  "issueDate": "2026-10-01T00:00:00.000Z",
  "dueDate": "2026-10-08T00:00:00.000Z",
  "pdfUrl": "/api/external/v1/invoices/ckinv1234567890/pdf?userId=user_01HXYZ",
  "externalUserId": "user_01HXYZ",
  "createdAt": "2026-10-01T09:12:44.000Z",
  "idempotent": false,
  "emailSent": true
}
```

| Nombre | Tipo | Obligatorio | Descripción |
| - | - | - | - |
| `invoiceNumber` | `string` | Sí | Número registrado de la factura: AAAA00000 o PREFIJO-AAAA00000 según la serie (por ejemplo, T-202600001), sin guion. Es el de los libros registro y, si la factura lo lleva, el de su código QR. Guárdalo, búscalo y concilia con él. |
| `displayNumber` | `string` | Sí | El número tal como se imprime en el PDF y se enseña en los correos y en el panel: invoiceNumber con un guion entre el año y el número de orden (T-202600001 se imprime T-2026-00001). Es solo presentación: para guardar y buscar, usa invoiceNumber. |
| `type` | `enum` | Sí | STANDARD, SIMPLIFIED, RECTIFYING (rectificativa) o CREDIT_NOTE (abono emitido por tu equipo). Las facturas anteriores a este campo son STANDARD. |
| `seriesCode` | `string` | Sí | Serie en la que se numeró: el código de una serie de tu aplicación o DEFAULT para la serie de siempre de su tipo (también en las rectificativas, que van siempre a la serie de rectificativas). |
| `taxSystem` | `enum` | Sí | Sistema impositivo del documento, EU_OSS incluido. Una rectificativa conserva el de su original. |
| `consumerCountryCode` | `string \| null` | Sí | Estado miembro de consumo (ISO 3166-1 alfa-2): el del consumidor de otro país de la Unión y, en EU_OSS, el país cuyo IVA se aplica. Nulo en el resto. |
| `ossScheme` | `enum \| null` | Sí | Solo en EU_OSS: régimen de la ventanilla única con que se emitió, OSS_UNION_SCHEME u OSS_NON_UNION_SCHEME. Nulo en el resto. |
| `legalMentions` | `string[]` | Sí | Menciones legales impresas, en su orden. Las calcula UTILIA OS a partir del tipo de factura y de las causas de exención de sus líneas; tu aplicación no las fija. |
| `originalInvoiceId` | `string \| null` | Sí | Factura que corrige este documento. Nulo salvo en rectificativas y abonos. |
| `pricesIncludeTax` | `boolean` | No | Solo viaja, y en true, si la factura se creó con pricesIncludeTax: true. Entonces el unitPrice de las líneas es el precio sin impuestos que calculó UTILIA OS. |
| `paymentRegistered` | `boolean` | No | Solo viaja si mandaste payment. true: el cobro quedó registrado, en el acto o después. false: la factura está emitida pero el registro del cobro falló (o la emisión se cortó antes de llegar a registrarlo); complétalo con POST /external/v1/invoices/:id/mark-paid o desde la ficha de la factura en UTILIA OS, y en las lecturas siguientes pasa a true. Repetir la clave de idempotencia no lo reintenta. |
| `issuancePending` | `boolean` | No | true solo mientras la emisión está en curso (processing) o si se interrumpió antes de terminar su cierre fiscal (review_required). Un correo o un cobro que no salieron no la dejan pendiente. |
| `issuanceDeliveryStatus` | `enum` | No | Estado de la emisión: processing, review_required o complete. Ver «Estado de la emisión». Viaja en las facturas que emite la API externa. |
| `issuanceMessage` | `string` | No | Qué falta, si falta algo, y cómo completarlo sin duplicar la factura ni sus envíos, en castellano y listo para tu registro. |
| `emailDelivery` | `objeto` | No | Solo viaja si el correo se pidió: con sendEmail: true o, si lo omites, con el interruptor de la aplicación encendido. Dice si la factura ha llegado al cliente por correo, por qué no y qué hacer. Se actualiza solo. Ver «El correo de la factura». |
| `emailSent` | `boolean` | No | Resumen de emailDelivery en un booleano: true si el correo salió. Viaja cuando tocaba enviar, también en las repeticiones con la misma clave. En falso, la factura SÍ está emitida y numerada y solo falta el correo: mira emailDelivery.status para saber si lo reintenta UTILIA OS (retrying) o tienes que enviarla tú (failed o withheld). No la vuelvas a crear nunca. |
| `pdfUrl` | `string` | Sí | Ruta relativa, con el userId, para descargar el PDF desde tu servidor con la misma clave de API. No es un enlace para tu usuario final: para eso está el enlace público. |
| `idempotent` | `boolean` | Sí | Indica si la respuesta es una factura ya existente, reutilizada por la clave de idempotencia. |

`displayNumber`, `issuanceDeliveryStatus`, `issuanceMessage` y
`emailDelivery` vuelven en todas las lecturas de la factura (el detalle,
el listado y las respuestas de las acciones), con su valor de ese
momento. `emailSent` e `idempotent` solo los trae la respuesta del alta.

El PDF, los correos y el panel presentan el número con un guion entre
el año y el número de orden (`T-2026-00001`): es `displayNumber`. El
número registrado, el de los libros registro y, si la factura lo lleva,
el de su código QR, va sin guion (`T-202600001`): es `invoiceNumber`, y
es el que debes guardar, buscar y conciliar. El guion solo es
presentación.

> **Atención: El PDF de la API externa no es un enlace público**
>
> `pdfUrl` exige la cabecera `X-Api-Key` y no debe salir de tu servidor.
> Para entregar la factura a tu usuario final, emite el enlace público con
> `POST /external/v1/invoices/:id/public-link`.

### Precios con impuestos incluidos

Si tu aplicación cobra un precio cerrado, por ejemplo 39,99 €, manda
`pricesIncludeTax: true` y ese precio en `unitPrice`. UTILIA OS busca la
base exacta al céntimo para que el total de la línea sea exactamente lo
cobrado:

| Línea | Base | Cuota | Total |
| - | - | - | - |
| 39,99 con `IGIC_7` | 37,37 | 2,62 | 39,99 |
| 39,99 con `EXEMPT` | 39,99 | — | 39,99 |

En las líneas exentas o no sujetas, la base es el propio importe. Si un
importe no tiene una base exacta al céntimo con su tipo, la respuesta
es `422 TAX_INCLUSIVE_PRICE_NOT_REPRESENTABLE` con el número de línea en
`lineNumber` (empieza en 1): ajusta un céntimo o usa cantidad 1. Nunca
se emite una factura cuyo total difiera del cobrado.

### Una factura que ya cobraste con tu pasarela

Si tu aplicación cobra con su propia pasarela (por ejemplo, su cuenta
de Stripe) y usa UTILIA OS solo para facturar, manda el bloque
`payment`. La factura se emite y queda **cobrada por su total** en el
mismo acto, y si se envía por correo sale con la plantilla de factura
pagada: tu cliente recibe un único correo.

Si el registro del cobro falla, la factura ya está emitida y su emisión
está completa: `paymentRegistered` llega en `false` e
`issuanceDeliveryStatus` en `complete`. Para no pedirle a tu cliente que
pague algo que ya pagó, el correo no sale (`emailDelivery.status` en
`withheld`, con `reason: PAYMENT_NOT_REGISTERED`). Registra el cobro con
[`mark-paid`](https://os.utilia.ai/docs/developers/api/invoice-actions#registrar-un-cobro-fuera-de-la-pasarela)
(o tu equipo, con **Registrar cobro** en la ficha de la factura):
`paymentRegistered` pasa a `true` y después la envías con `send`.
Repetir la clave de idempotencia no lo reintenta. Un cobro sin registrar
no impide rectificar ni anular la factura. Sin `payment`, la factura
queda pendiente de cobro, como hasta ahora.

### Residencia del consumidor

Con `consumerResidence` dejas constancia de dónde reside el consumidor
y de cómo lo sabes. Es tu aplicación quien lo conoce: UTILIA OS no lo
deduce si no lo mandas. Si lo mandas, se coteja con el sistema fiscal de
la factura:

- `CANARIAS` exige `IGIC`.
- `PENINSULA_BALEARES` exige `IVA`.
- `CEUTA_MELILLA`, `OTHER_EU` y `NON_EU` no admiten `IGIC`.

Si no cuadra, la respuesta es `400 TAX_SYSTEM_RESIDENCE_MISMATCH`. La
residencia se guarda con la factura, no se imprime y no se devuelve en
`metadata`. Con un consumidor de otro país de la Unión (`OTHER_EU`), la
respuesta trae su país en `consumerCountryCode`.

Cuando la ventanilla única del IVA
esté disponible para tu organización (hoy está en «Próximamente») y una
venta a un consumidor de la Unión ya tribute en el país del consumidor,
una factura con IVA español a ese consumidor se rechazará con
`400 OSS_THRESHOLD_EXCEEDED` u `OSS_DESTINATION_VAT_REQUIRED`. Para las
demás organizaciones no cambia nada.

### Régimen del pequeño empresario del IGIC (REPEP)

Si tu organización es un autónomo canario y tiene declarado el
**Régimen especial del pequeño empresario o profesional (REPEP)** en
**Finanzas › Ajustes › Fiscal › Regímenes especiales**,
sus ventas con IGIC dentro de un periodo del régimen están exentas:

- Manda las líneas con `taxType: "EXEMPT"` y sin `exemptionReason`. UTILIA OS les pone la causa `EXEMPT_IGIC_SMALL_BUSINESS` («Pequeño empresario (franquicia fiscal)») y la factura lleva la mención «Exención franquicia fiscal».
- Una línea con cuota de IGIC, también `IGIC_0`, responde `400 IGIC_SMALL_BUSINESS_TAXED_LINE` con `lineNumber`. El tipo cero no es una exención.
- Solo afecta a facturas con `taxSystem: "IGIC"`. Las ventas a consumidores de Península y Baleares llevan IVA y no cambian; mandar la causa del REPEP en una factura que no es IGIC responde `400 IGIC_SMALL_BUSINESS_REQUIRES_IGIC`.
- Mandar la causa con una fecha fuera de cualquier periodo del régimen responde `400 IGIC_SMALL_BUSINESS_NOT_ACTIVE`.

Qué es el régimen, quién puede declararlo y qué cambia en los modelos
está en [Impuestos](https://os.utilia.ai/docs/guides/finance/taxes#régimen-especial-del-pequeño-empresario-o-profesional-repep).

### Ejemplo completo: simplificada de 39,99 € en el REPEP, cobrada en el acto

Un consumidor residente en Tenerife paga 39,99 € con la tarjeta en la
pasarela de tu aplicación. Tu organización está en el REPEP.

```bash
curl -X POST https://os.utilia.ai/api/external/v1/invoices \
  -H "X-Api-Key: utl_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "type": "SIMPLIFIED",
    "user": { "externalId": "user_01HXYZ", "email": "ana@example.com" },
    "taxSystem": "IGIC",
    "pricesIncludeTax": true,
    "lines": [
      { "name": "Plan Básico mensual", "quantity": 1, "unitPrice": 39.99, "taxType": "EXEMPT" }
    ],
    "payment": { "method": "CARD", "reference": "pi_3QabcDEF" },
    "consumerResidence": {
      "region": "CANARIAS",
      "evidence": "Dirección de facturación y tarjeta en Santa Cruz de Tenerife."
    },
    "sendEmail": true,
    "idempotencyKey": "pedido-4471"
  }'
```

Con la línea exenta, `pricesIncludeTax` no cambia nada (la base es el
total), pero conviene mandarlo si tu aplicación siempre trabaja con
precios cerrados: si algún día la venta lleva cuota, el total seguirá
siendo el cobrado.

```json
{
  "id": "cksimp1234567890",
  "invoiceNumber": "T-202600001",
  "displayNumber": "T-2026-00001",
  "type": "SIMPLIFIED",
  "seriesCode": "DEFAULT",
  "status": "PAID",
  "legalMentions": [
    "Factura simplificada (RD 1619/2012 art. 7)",
    "Exención franquicia fiscal"
  ],
  "pricesIncludeTax": true,
  "paymentRegistered": true,
  "originalInvoiceId": null,
  "issuancePending": false,
  "issuanceDeliveryStatus": "complete",
  "issuanceMessage": "La factura está emitida y su entrega ha terminado.",
  "emailDelivery": {
    "status": "sent",
    "message": "La factura se ha enviado por correo.",
    "attempts": 1,
    "lastAttemptAt": "2026-10-13T09:30:13.000Z",
    "sentAt": "2026-10-13T09:30:14.000Z"
  },
  "totalAmount": 39.99,
  "paidAmount": 39.99,
  "currency": "eur",
  "issueDate": "2026-10-13T00:00:00.000Z",
  "dueDate": null,
  "pdfUrl": "/api/external/v1/invoices/cksimp1234567890/pdf?userId=user_01HXYZ",
  "externalUserId": "user_01HXYZ",
  "createdAt": "2026-10-13T09:30:12.000Z",
  "idempotent": false,
  "emailSent": true
}
```

El PDF imprime «T-2026-00001» como número, «Consumidor final» como
destinatario, sin dirección, «IGIC: exento» en lugar de un tipo y la
mención «Exención franquicia fiscal». Si más adelante devuelves el
dinero, emite su rectificativa con
[Facturas rectificativas](https://os.utilia.ai/docs/developers/api/invoice-rectifications).

### Reintentar sin duplicar la factura

Emitir una factura hace varias cosas en el mismo paso (numerarla,
registrarla, generar su PDF y, si lo pides, registrar el cobro y enviar
el correo), así que la respuesta puede tardar varios segundos. Pon a tu
cliente HTTP un tiempo de espera de **al menos 30 segundos** para
`POST /external/v1/invoices`: uno más corto corta peticiones que sí
terminan.

Si aun así se corta la conexión, no sabes si la factura llegó a
emitirse. Repite la petición con la **misma** `idempotencyKey` y el
mismo cuerpo, nunca con una clave nueva: durante 24 horas, UTILIA OS
reconoce la clave y no emite otra. Si la primera petición todavía se
está ejecutando, la repetición devuelve esa misma factura con
`issuanceDeliveryStatus: processing`; consulta de nuevo con la misma
clave hasta que llegue `complete`.

**Reintentar con la misma clave de idempotencia**

Participantes: Tu servidor y UTILIA OS.

1. Tu servidor → UTILIA OS: `POST /api/external/v1/invoices`. Con idempotencyKey: ord_01HXYZ-invoice.
2. UTILIA OS (por su cuenta): Numera la factura, la emite y responde, pero la respuesta se pierde por el camino.
3. Tu servidor → UTILIA OS: `POST /api/external/v1/invoices`. La misma clave y el mismo cuerpo.
4. UTILIA OS → Tu servidor (respuesta): La misma factura, con idempotent: true. No se emite ninguna factura nueva. Si la primera ejecución sigue en marcha, llega con issuanceDeliveryStatus: processing.

**¿Qué pasa si repites una clave de idempotencia?**

¿Usaste esa idempotencyKey en las últimas 24 horas?

- No: **Factura nueva** (`idempotent: false`). Se numera y se emite otra factura.
- Sí: ¿Envías el mismo cuerpo?
  - Sí: **La misma factura** (`idempotent: true`). No se emite ninguna factura nueva.
  - No: **Conflicto** (`409 BILLING_IDEMPOTENCY_CONFLICT`). Una clave sirve para una sola factura. Usa una nueva para otra factura.

### Estado de la emisión

`issuanceDeliveryStatus` dice si la emisión ha terminado. Habla solo del
**cierre fiscal** (el número, el sello, el registro y los efectos del
documento, como su PDF y su código QR), no de la entrega:

| `issuanceDeliveryStatus` | Qué significa | Qué haces |
| - | - | - |
| `processing` | La emisión sigue en curso. `issuancePending` llega en `true`. | Repite la misma petición, con la misma clave, en unos segundos. No crees otra factura. |
| `review_required` | La ejecución se interrumpió **antes de terminar el cierre fiscal**. `issuancePending` llega en `true`. | Lo revisa el equipo de tu organización en la ficha de la factura, con el botón **Dar por revisada**. Hasta entonces la factura no se puede rectificar ni anular. Conserva la factura y la clave: repetir la petición no reemite nada ni envía correos. |
| `complete` | La factura está emitida. | Mira `emailDelivery` y `paymentRegistered` para saber cómo fue la entrega. |

Un correo que no sale o un cobro del bloque `payment` que no se registra
**no** dejan la emisión pendiente: la factura queda `complete`, y su
resultado viaja aparte, en `emailDelivery` y `paymentRegistered`, para
completarlo sin tocar la factura. Tampoco impiden rectificarla ni
anularla.

> **Atención: Cambio del 19 de octubre de 2026**
>
> Hasta esa fecha, un correo que no salía o un cobro que no se registraba
> dejaban la factura en `review_required` para siempre, y su rectificativa
> y su anulación respondían `409`. Si tu integración usaba
> `issuanceDeliveryStatus: review_required` para detectar un correo
> fallido, lee ahora `emailDelivery`. Las facturas anteriores que estaban
> en revisión solo por el correo o por el cobro se leen ya como
> `complete`, con el resultado del correo en `emailDelivery` y el del
> cobro en `paymentRegistered`.

**Dar por revisada** está en el aviso «Emisión pendiente de revisión» de
la ficha de la factura, en [Finanzas › Facturas](https://os.utilia.ai/dashboard/finance/invoices),
y lo ve quien tiene los permisos «Editar aplicaciones externas» y
«Emitir facturas». Solo se acepta si la factura tiene número y su PDF
original está generado y verificado; no reemite ni envía nada y queda
en el registro de auditoría con el nombre de quien lo hizo. Después, la
aplicación ya puede rectificarla o anularla repitiendo su petición. La
ruta del panel que usa está en
[Panel de aplicaciones externas](https://os.utilia.ai/docs/developers/api/apps-panel#facturación-de-la-aplicación).

### El correo de la factura

Si pides el correo (`sendEmail: true`, o sin `sendEmail` con el
interruptor de la aplicación encendido), UTILIA OS intenta enviarla
dentro de la misma petición, con la plantilla, el remitente y el PDF de
tu organización. El destinatario es el contacto de facturación
principal del cliente del CRM; si no hay, el correo del usuario
(`user.email`) y, en último lugar, el del destinatario de la factura
(`recipient.email`). El resultado llega en `emailDelivery`:

```json
{
  "emailDelivery": {
    "status": "retrying",
    "reason": "PDF_NOT_READY",
    "message": "Todavía no se ha enviado por correo porque su PDF original se estaba terminando de preparar. UTILIA la enviará sola en unos minutos; no hace falta que la reenvíes.",
    "attempts": 1,
    "lastAttemptAt": "2026-10-19T10:00:09.000Z"
  }
}
```

| Nombre | Tipo | Obligatorio | Descripción |
| - | - | - | - |
| `status` | `enum` | Sí | sent, retrying, failed o withheld (ver la tabla siguiente). |
| `reason` | `enum` | No | Motivo estable cuando no se ha enviado. No viaja con sent. |
| `message` | `string` | Sí | Qué ha pasado y qué hacer, en castellano y listo para mostrar a tu equipo o a tu soporte. |
| `attempts` | `number` | No | Intentos de envío hechos hasta ahora. |
| `lastAttemptAt` | `string` | No | Último intento, en ISO 8601. |
| `sentAt` | `string` | No | Momento en que salió, en ISO 8601. Solo con sent. |

| `status` | Qué significa | Qué haces |
| - | - | - |
| `sent` | El correo salió: al emitirla, en un reintento o porque alguien la envió después desde la ficha o con `POST /send`. | Nada. |
| `retrying` | Todavía no ha salido por un fallo pasajero y UTILIA OS la reenvía sola. | Espera. No la reenvíes ni la vuelvas a crear. |
| `failed` | No se ha podido enviar. | Envíala con [`POST /external/v1/invoices/:id/send`](https://os.utilia.ai/docs/developers/api/invoice-actions#enviar-la-factura-por-correo) o desde su ficha, después de resolver el motivo. |
| `withheld` | No se ha enviado a propósito. | Resuelve el motivo y envíala con `send`. |

| `reason` | `status` | Qué ha pasado |
| - | - | - |
| `PDF_NOT_READY` | `retrying` | El PDF original de la factura se estaba terminando de preparar. |
| `STORAGE_UNAVAILABLE` | `retrying` | El almacén de documentos no respondía. |
| `MAIL_SERVER_UNAVAILABLE` | `retrying` | El servidor de correo no respondía. |
| `SEND_QUOTA_EXCEEDED` | `retrying` | La aplicación agotó su cupo de 100 envíos por hora, el mismo que comparte con `POST /send`. Sale sola en cuanto haya cupo. |
| `SEND_FAILED` | `failed` | El envío falló de forma definitiva, por ejemplo porque el servidor de salida lo rechazó. |
| `RETRIES_EXHAUSTED` | `failed` | Fallaron todos los reintentos automáticos. |
| `NO_RECIPIENT` | `failed` | No hay ninguna dirección a la que enviarla. Registra un contacto de facturación del usuario y envíala con `send`. |
| `INTERRUPTED` | `failed` | La emisión se cortó mientras se enviaba y no consta si salió. Comprueba el historial de comunicaciones de la factura antes de reenviarla. |
| `SEND_SCOPE_MISSING` | `withheld` | La aplicación no tiene `crm:invoices:send` y el envío venía del interruptor. Cuando se lo concedan, envíala con `send`. |
| `PAYMENT_NOT_REGISTERED` | `withheld` | El cobro del bloque `payment` no se registró, y el correo pediría pagar una factura ya pagada. Registra el cobro con `mark-paid` y envíala después. |

Cómo reintenta UTILIA OS un correo en `retrying`:

1. Dentro de la petición de alta espera al PDF original unos 8 segundos, como mucho. Si no está listo, no retiene la respuesta: el correo queda en `retrying`.
2. Si el fallo fue del PDF, del almacén o del servidor de correo, hace un reintento rápido a los 45 segundos, más o menos.
3. Después lo recoge la recuperación de envíos automáticos, que pasa cada 10 minutos, hasta 12 intentos en total (unas dos horas).
4. Si se agotan, `emailDelivery` pasa a `failed` con `RETRIES_EXHAUSTED` y la persona que figura como creadora de la factura (la persona responsable de la aplicación cuando se emitió) recibe en UTILIA OS el aviso «Factura sin enviar».

Cada intento, y también cada decisión de no enviarla, queda en el
bloque **Comunicaciones con el cliente** de la ficha de la factura, como
«Factura enviada automáticamente», con su resultado. Si alguien la envía
mientras tanto desde la ficha o con `POST /send`, `emailDelivery` pasa a
`sent` y el reintento automático se detiene: la factura nunca sale dos
veces por esta vía. Mientras la emisión está en `processing`,
`emailDelivery` no viaja.

Para ensayar el correo sin emitir nada, `POST /external/v1/invoices/validate`
dice en `preview.email` si saldría y a qué dirección: ver
[Probar la facturación sin emitir facturas](https://os.utilia.ai/docs/developers/api/invoice-testing#el-correo-de-la-factura).

### Errores al crear

- `403 MODULE_NOT_ENABLED`: el plan de tu organización no incluye el aplicativo Finanzas. Es una decisión comercial, no un fallo técnico: no reintentes.
- `403 INSUFFICIENT_SCOPE`: la aplicación no tiene concedido `crm:invoices:write` o `crm:invoices:issue`. La creación exige los dos, porque crea y emite en el mismo paso.
- `403 BILLING_NOT_ENABLED`: la facturación de la aplicación está apagada. La enciende un Super Administrador con **Activar la facturación**, en **Dinero › Facturación** de la ficha de la aplicación. Para descubrirlo antes de la primera factura real, `POST /external/v1/invoices/validate` lo devuelve como bloqueo, y el **Resumen** y la pestaña **Pruebas** de la aplicación lo marcan como paso pendiente.
- `403 BILLING_AMOUNT_EXCEEDS_LIMIT`: el total supera el tope por factura configurado para la aplicación.
- `409 BILLING_IDEMPOTENCY_CONFLICT`: la clave de idempotencia ya se usó con otro contenido.
- `400 INVOICE_SERIES_NOT_AVAILABLE`: la serie de `seriesCode` no existe o no es de tu aplicación. El mensaje es el mismo en los dos casos.
- `400 INVOICE_SERIES_TYPE_MISMATCH`: la serie es de otro tipo de documento (una de ordinarias para una simplificada, o al revés).
- `409 INVOICE_SERIES_INACTIVE`: la serie está desactivada. Usa otra o pide a tu organización que la reactive.
- `400 INVOICE_ISSUE_DATE_OUT_OF_ORDER`: `issueDate` es futura o anterior a la de la última factura de la misma serie. Llegan `issueDate` y `today` o `lastIssueDate` y `seriesCode`.
- `400 INVOICE_RECIPIENT_REQUIRED`: factura ordinaria sin `recipient`.
- `400 INVOICE_RECIPIENT_INCOMPLETE`: factura ordinaria con el destinatario incompleto. Los campos que faltan llegan en `missingFields`.
- `400 SIMPLIFIED_RECIPIENT_INCOMPLETE`: simplificada con NIF sin el resto de datos del destinatario. Los que faltan llegan en `missingFields`.
- `422 SIMPLIFIED_INVOICE_AMOUNT_EXCEEDED`: simplificada por encima de 400 € impuestos incluidos. Emite una ordinaria.
- `422 TAX_INCLUSIVE_PRICE_NOT_REPRESENTABLE`: con `pricesIncludeTax`, el importe de la línea `lineNumber` no tiene base exacta al céntimo.
- `400 TAX_INCLUSIVE_GLOBAL_DISCOUNT_UNSUPPORTED`: `pricesIncludeTax` con un `discountPercent` mayor que cero.
- `400 TAX_SYSTEM_RESIDENCE_MISMATCH`: la residencia del consumidor no cuadra con el sistema fiscal.
- `400 IGIC_SMALL_BUSINESS_TAXED_LINE`, `IGIC_SMALL_BUSINESS_REQUIRES_IGIC` o `IGIC_SMALL_BUSINESS_NOT_ACTIVE`: reglas del régimen del pequeño empresario del IGIC (ver arriba).
- `403 FEATURE_COMING_SOON`: la petición lleva algo de la ventanilla única del IVA, que todavía no está disponible para tu organización. No es un bloqueo de plan: no lo reintentes.
- `400 OSS_*`: reglas de la ventanilla única del IVA, que está en «Próximamente»; antes, `403 FEATURE_COMING_SOON`.
- `400 VALIDATION_ERROR`: uno o más campos no cumplen las restricciones.

## Series disponibles

Sin hacer nada, las facturas ordinarias de tu aplicación se numeran en
la serie general de tu organización, intercaladas con las que emite el
equipo, y las simplificadas en `T-`. Si quieres que las de tu aplicación
vayan aparte (por ejemplo, `APP-202600001`), una persona de tu
organización con el permiso «Editar organización» crea una serie para
la aplicación en [Ajustes › Organización](https://os.utilia.ai/dashboard/settings/organization),
pestaña **Facturas**, bloque **Series de facturación**, y puede fijarla
como serie por defecto de la aplicación en su pantalla **Facturación**.
Tu aplicación no crea ni cambia series con la clave de API: solo las
consulta y elige entre las suyas.

**`GET /api/external/v1/invoices/series`**

Series en las que la aplicación puede numerar sus facturas: la de siempre de cada tipo, las que tu organización le ha reservado y, solo informativa, la de rectificativas. Permiso: crm:invoices:read.

### Respuesta (200)

```json
{
  "fiscalYear": 2026,
  "series": [
    { "code": "DEFAULT", "documentType": "STANDARD", "prefix": null, "example": "202600001", "description": null, "isAppDefault": false, "selectable": true },
    { "code": "APP", "documentType": "STANDARD", "prefix": "APP", "example": "APP-202600001", "description": "Facturas de la aplicación de reservas", "isAppDefault": true, "selectable": true },
    { "code": "DEFAULT", "documentType": "SIMPLIFIED", "prefix": "T", "example": "T-202600001", "description": null, "isAppDefault": true, "selectable": true },
    { "code": "DEFAULT", "documentType": "RECTIFYING", "prefix": "R", "example": "R-202600001", "description": null, "isAppDefault": false, "selectable": false }
  ]
}
```

| Nombre | Tipo | Obligatorio | Descripción |
| - | - | - | - |
| `fiscalYear` | `number` | Sí | Año en curso en la zona horaria de tu organización. |
| `series[].code` | `string` | Sí | El valor que mandas en seriesCode. DEFAULT es la serie de siempre de su tipo. |
| `series[].documentType` | `enum` | Sí | STANDARD (ordinarias), SIMPLIFIED (simplificadas) o RECTIFYING (la de rectificativas, solo informativa). |
| `series[].prefix` | `string \| null` | Sí | Prefijo impreso delante del número. La serie general puede no tener prefijo. |
| `series[].example` | `string` | Sí | El primer número del año con el formato real de la serie. No es el siguiente número: la respuesta no lo da, porque revelaría cuántas facturas emite tu organización. |
| `series[].description` | `string \| null` | Sí | Motivo de la serie que indicó tu organización al crearla. |
| `series[].isAppDefault` | `boolean` | Sí | Es la que se usa para su tipo cuando el alta no trae seriesCode: la serie por defecto de la aplicación o, si no tiene, la de siempre. |
| `series[].selectable` | `boolean` | Sí | Se puede elegir en seriesCode. La de rectificativas llega en false: las rectificativas van siempre a ella, sin elegirla. |

Solo aparecen las series activas reservadas a tu aplicación: nunca las
de otra aplicación ni las que el equipo usa en sus facturas.

### Qué serie lleva cada factura

1. Si mandas `seriesCode`, esa serie. `DEFAULT` fuerza la de siempre aunque la aplicación tenga una por defecto.
2. Si no, la serie por defecto de la aplicación para el tipo (ordinaria o simplificada).
3. Si no tiene, la de siempre: la general para las ordinarias y `T-` para las simplificadas.

La serie tiene que ser de tu aplicación (`400 INVOICE_SERIES_NOT_AVAILABLE`),
del mismo tipo que la factura (`400 INVOICE_SERIES_TYPE_MISMATCH`) y
estar activa (`409 INVOICE_SERIES_INACTIVE`). Todo se comprueba **antes
de numerar**: un rechazo nunca consume un número ni deja hueco en la
serie. Cada serie lleva su propia numeración correlativa, que empieza de
nuevo cada año, y la respuesta dice en `seriesCode` en cuál se numeró.

Las rectificativas van siempre a la serie de rectificativas (`R-`),
aunque la factura original esté en una serie de tu aplicación. Las
suscripciones también admiten `seriesCode`: ver
[Suscripciones](https://os.utilia.ai/docs/developers/api/subscriptions).

### Fecha de expedición

Con la serie, `issueDate` queda acotada para que la numeración nunca
retroceda:

- No puede ser futura en la zona horaria de tu organización.
- No puede ser anterior a la de la última factura ya expedida en la misma serie.
- Su año es el de la numeración: una factura con fecha de 2026 lleva un número de 2026.

Si no se cumple, la respuesta es `400 INVOICE_ISSUE_DATE_OUT_OF_ORDER`,
sin numerar, con `issueDate` y, según el caso, `today` o `lastIssueDate`
y `seriesCode`. Si omites `issueDate`, se usa el día de hoy en la zona
horaria de tu organización.

## Listar facturas

**`GET /api/external/v1/invoices`**

Lista paginada de facturas de la aplicación, con filtros opcionales por usuario, estado, tipo y serie. Permiso: crm:invoices:read.

### Parámetros de consulta

| Nombre | Tipo | Obligatorio | Descripción |
| - | - | - | - |
| `userId` | `string` | No | Filtra por externalId del usuario. |
| `status` | `enum` | No | DRAFT, ISSUED, SENT, PAID, PARTIALLY_PAID, OVERDUE, CANCELLED, DISPUTED, OVERDUE_UNCOLLECTABLE, REFUNDED, PARTIALLY_REFUNDED o RECTIFIED. |
| `type` | `enum` | No | STANDARD, SIMPLIFIED, RECTIFYING o CREDIT_NOTE. |
| `seriesCode` | `string` | No | Código de una serie de tu aplicación o DEFAULT (las series de siempre de cada tipo; combínalo con type para acotar). |
| `fromDate` | `date` | No | Fecha de emisión mínima (AAAA-MM-DD). |
| `toDate` | `date` | No | Fecha de emisión máxima (AAAA-MM-DD). |
| `page` | `integer` | No | Página (por defecto, 1). |
| `limit` | `integer` | No | Elementos por página (por defecto 20, máximo 100). |

### Respuesta (200)

```json
{
  "invoices": [
    {
      "id": "inv-uuid",
      "invoiceNumber": "202600001",
      "displayNumber": "2026-00001",
      "type": "STANDARD",
      "seriesCode": "DEFAULT",
      "status": "PAID",
      "issuancePending": false,
      "issuanceDeliveryStatus": "complete",
      "issueDate": "2026-04-19T00:00:00.000Z",
      "dueDate": "2026-05-19T00:00:00.000Z",
      "totalAmount": 53.45,
      "paidAmount": 53.45,
      "currency": "eur",
      "pdfUrl": "/api/external/v1/invoices/inv-uuid/pdf?userId=user_01HXYZ",
      "externalUserId": "user_01HXYZ"
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 20,
    "total": 1,
    "totalPages": 1
  }
}
```

## Detalle de factura

**`GET /api/external/v1/invoices/:id`**

Datos completos de una factura: líneas, desglose de impuestos, receptor y rectificativas. Permiso: crm:invoices: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 propietario. |

Además de los campos de la respuesta del alta, el detalle trae:

| Nombre | Tipo | Obligatorio | Descripción |
| - | - | - | - |
| `lines[].exemptionReason` | `enum \| null` | Sí | Causa de la exención de la línea, o nulo si no está exenta. EXEMPT_IGIC_SMALL_BUSINESS es el régimen del pequeño empresario del IGIC. |
| `lines[].exempt` | `boolean` | Sí | true en líneas exentas o no sujetas, que no repercuten cuota. Úsalo en lugar de taxRate: un tipo cero no es una exención. |
| `lines[].vatRateClass / lines[].ossSupplyType` | `enum \| null` | Sí | Solo en líneas EU_VAT de la ventanilla única: clase del tipo y servicios o bienes. En ellas, taxRate es el tipo del país del consumidor congelado al emitir. Nulos en el resto. |
| `taxes[].vatRateClass` | `enum \| null` | Sí | Solo en tramos EU_VAT: clase del tipo del tramo. Nulo en el resto. |
| `rectifications` | `array` | Sí | Rectificativas de la factura, de la más antigua a la más reciente, con id, invoiceNumber, displayNumber (el número impreso, con guion entre el año y el número de orden), issueDate, totalAmount (negativo), rectificationCode y status. Vacía si no tiene o si el documento es a su vez una rectificativa. |
| `effectiveTotal` | `number` | No | Total vigente: el de la factura más el de sus rectificativas emitidas, que restan. Solo en facturas que no son rectificativas ni abonos. |
| `rectificationCode` | `enum` | No | Solo en rectificativas y abonos: R1, R2, R3, R4 o R5. Las que emite la API externa son R1 o R5. |
| `rectificationType` | `enum` | No | Solo en rectificativas y abonos: DIFFERENCE (por diferencias) o COMPLETE (sustitución, solo desde UTILIA OS). |

En una simplificada sin destinatario, `recipientName` es «Consumidor
final» y la dirección, la ciudad y el código postal llegan con un guion.

### Errores del detalle

- `403 INSUFFICIENT_SCOPE`: la aplicación no tiene concedido `crm:invoices:read`.
- `404 EXTERNAL_USER_NOT_FOUND`: no hay ningún usuario de la aplicación con ese `userId`.
- `404 INVOICE_NOT_ACCESSIBLE`: la factura no existe, es de otra aplicación o no es de ese usuario. Es siempre la misma respuesta, para no revelar si la factura existe.

## Descargar PDF

**`GET /api/external/v1/invoices/:id/pdf`**

PDF fiscal de la factura (application/pdf). Permiso: crm:invoices:read.

Parámetro de ruta: `id`.

### Parámetros

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

Devuelve el binario con `Content-Type: application/pdf` y `Content-Disposition: attachment; filename="factura-...pdf"`.

## Anular factura

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

Anula una factura ordinaria o simplificada emitida. Permiso: crm:invoices:cancel, que solo concede un Super Administrador.

Parámetro de ruta: `id`.

Anular es para una factura que **no debió emitirse** (un duplicado, un
error). Si la venta existió y devuelves el dinero, no la anules: emite
una [rectificativa](https://os.utilia.ai/docs/developers/api/invoice-rectifications).

### Cuerpo

| Nombre | Tipo | Obligatorio | Descripción |
| - | - | - | - |
| `reason` | `string` | No | Motivo de la anulación. Se guarda en las notas internas de la factura. |

`userId` (el `externalId` del propietario) va como parámetro de consulta, igual que en el detalle.

### Errores de la anulación

- `403 INSUFFICIENT_SCOPE`: la aplicación no tiene concedido `crm:invoices:cancel`.
- `409 INVOICE_STATE_INVALID`: no se pueden anular borradores.
- `409 INVOICE_ALREADY_CANCELLED`: ya estaba anulada.
- `409 BILLING_IDEMPOTENCY_CONFLICT`: la emisión sigue en curso (repite la anulación en unos segundos), su cierre fiscal se interrumpió y tu equipo tiene que darla por revisada antes (ver [Estado de la emisión](#estado-de-la-emisión)) o una anulación anterior aún no ha terminado. Un correo o un cobro del alta que no salieron no impiden anularla.

## Estadísticas del usuario

**`GET /api/external/v1/invoices/stats`**

Totales de facturación de un usuario externo. Permiso: crm:invoices:read.

### Parámetros de consulta

| Nombre | Tipo | Obligatorio | Descripción |
| - | - | - | - |
| `userId` | `string` | Sí | externalId del usuario. |

### Respuesta (200)

```json
{
  "total": 12,
  "paid": 8,
  "pending": 3,
  "overdue": 1,
  "totalAmount": 641.40,
  "totalPaid": 427.60,
  "totalPending": 213.80,
  "currency": "eur"
}
```

| Nombre | Tipo | Obligatorio | Descripción |
| - | - | - | - |
| `total` | `number` | Sí | Total de facturas emitidas a ese usuario externo. |
| `paid` | `number` | Sí | Facturas en estado PAID. |
| `pending` | `number` | Sí | Facturas en estado ISSUED, SENT o PARTIALLY_PAID. |
| `overdue` | `number` | Sí | Facturas en estado OVERDUE. |
| `totalAmount` | `number` | Sí | Suma del importe total facturado. |
| `totalPaid` | `number` | Sí | Suma del importe ya cobrado. |
| `totalPending` | `number` | Sí | Diferencia entre lo facturado y lo cobrado (nunca negativa). |
| `currency` | `string` | Sí | Divisa de tu organización en minúsculas (por ejemplo, `eur`). |

## Cabeceras comunes

| Nombre | Tipo | Obligatorio | Descripción |
| - | - | - | - |
| `X-Api-Key` | `header` | Sí | Clave de API de la aplicación externa. |
| `X-Request-Id` | `header` | No | Identificador para seguir la petición; UTILIA OS lo devuelve en la respuesta. |
| `Content-Type` | `header` | Sí | application/json en las peticiones con cuerpo. |

## Límite de uso

Todos los endpoints de este grupo comparten un límite de **60 peticiones por minuto por clave**, además del límite general de la clave (1.000 peticiones por hora). Al superarlo, la respuesta es `429 Too Many Requests` con la cabecera `Retry-After`, que indica los segundos que hay que esperar.

## Siguiente paso

- [Acciones sobre una factura](https://os.utilia.ai/docs/developers/api/invoice-actions): Enviar, cobrar, marcar pagada, devolver y enlace público.
- [Facturas rectificativas](https://os.utilia.ai/docs/developers/api/invoice-rectifications): Documentar una devolución o un contracargo que deja la venta sin efecto.
- [Suscripciones](https://os.utilia.ai/docs/developers/api/subscriptions): Cuotas que se generan, envían y cobran solas cada periodo.
- [Guía de facturación](https://os.utilia.ai/docs/developers/api/billing-guide): El recorrido completo, del alta del usuario al cobro recurrente.
