# Facturas rectificativas

Emite la rectificativa de una factura de tu aplicación al devolver el dinero o tras un contracargo, ensáyala sin emitirla y sabe cuándo se puede rectificar.

Cuando una venta queda sin efecto, total o parcialmente, la factura no
se anula: se **rectifica**. Con `/external/v1/invoices/:id/rectifications`
tu aplicación emite la rectificativa de una factura que ella misma
emitió, en dos casos:

- **Has devuelto el dinero** con tu propia pasarela de pago (por ejemplo, tu cuenta de Stripe), también por desistimiento o por una baja con devolución.
- **Un contracargo deja la venta sin efecto**: lo has aceptado o perdido y el cliente no conserva lo que compró.

Si cobraste con la pasarela de UTILIA OS, no uses esta ruta para
devolver: usa el [reembolso](https://os.utilia.ai/docs/developers/api/invoice-actions#devolver-dinero-cobrado),
que mueve el dinero y puede emitir la rectificativa en el mismo acto.

**De la devolución en tu pasarela a la rectificativa**

1. **Devuelves el dinero** (Tu pasarela): El reembolso se hace en tu cuenta (re\_...).
2. **Pides la rectificativa** (POST …/rectifications): Con la causa, el importe si es parcial, y el bloque refund con la referencia de tu pasarela.
3. **UTILIA OS la emite** (Finanzas): Código legal, importes negativos, mención y registro de la devolución sobre la factura original.
4. [**Te llegan los avisos**](https://os.utilia.ai/docs/developers/webhooks/events) (Tu servidor): INVOICE_RECTIFICATION_CREATED, INVOICE_RECTIFIED e INVOICE_REFUNDED.

_UTILIA OS no mueve dinero: documenta una devolución que ya hiciste._

## Lo que decide UTILIA OS, no tu aplicación

| Qué | Cómo se decide |
| - | - |
| Código legal | **R5** si la original es una simplificada sin destinatario identificado; **R1** si es ordinaria o simplificada cualificada (la operación queda sin efecto total o parcialmente: art. 80.Dos de la Ley del IVA y art. 22.4 de la Ley 20/1991 del IGIC). R2, R3 y R4 no se emiten por esta ruta. |
| Modalidad | Siempre **por diferencias**, con importes negativos. |
| Líneas | Si rectificas todo y no hay rectificativas previas, copia las líneas de la original con la cantidad en negativo y la original queda en estado `RECTIFIED`. Si no, una línea por tramo fiscal de la original, con el importe repartido al céntimo. |
| Tratamiento fiscal | Conserva el tipo y la causa de exención de la original, así que repite su mención legal (por ejemplo, «Exención franquicia fiscal») aunque el régimen haya cambiado después. La de una factura de la ventanilla única del IVA («Próximamente») hereda su sistema `EU_OSS`, su país de consumo, su régimen y el porcentaje congelado de cada línea, aunque el país haya cambiado su tipo después; si la ventanilla única no está disponible para tu organización, responde `403 FEATURE_COMING_SOON`. |
| Fecha de expedición | **Siempre la de hoy**, en la zona horaria de tu organización. No se puede adelantar ni atrasar. La fecha en que devolviste el dinero va en `refund.refundedAt`. |
| Numeración | La serie de rectificativas de tu organización (`R-202600003`), **también** si la original se numeró en una serie de tu aplicación (por ejemplo, `APP-202600007`): la rectificativa no hereda la serie de la original y no se elige con `seriesCode`. Su `seriesCode` llega como `DEFAULT`, y el vínculo con la original queda en `originalInvoiceId`. |

Si mandas un campo que no está en el contrato, como `rectificationCode`,
`issueRefund` o `reissue`, la petición se rechaza con
`400 VALIDATION_ERROR`: así nadie cree que ha elegido algo que no eligió.

> **Atención: Un contracargo no siempre se rectifica**
>
> Rectifica un contracargo solo si la **operación queda sin efecto**: lo
> aceptas o lo pierdes y das de baja el servicio o reconoces el derecho al
> reembolso. Un contracargo por fraude o por impago en el que la venta
> sigue en pie es un impago, no una alteración del precio, y un
> contracargo que ganas no deja nada que rectificar. No hay un criterio
> oficial publicado: decidirlo es responsabilidad de tu aplicación, y
> UTILIA OS no lo comprueba.

## Plan y permisos

La ruta comprueba tres puertas en cada llamada:

1. **El plan.** Tu organización necesita el aplicativo **Finanzas**. No hace falta Pagos con tarjeta (Stripe): UTILIA OS no mueve dinero. Sin Finanzas, `403 MODULE_NOT_ENABLED`. La facturación de la aplicación tiene que estar encendida (`403 BILLING_NOT_ENABLED`).
2. **Los permisos de la aplicación (scopes)**, sin periodo de gracia:
   - `crm:invoices:rectify` para emitir. Es crítico financiero: solo lo concede un Super Administrador, con un motivo.
   - Además `crm:invoices:write` si mandas el bloque `refund`.
   - Además `crm:invoices:send` si pides el envío con `sendEmail: true`. Si el envío viene de la preferencia de la aplicación y falta el permiso, no se envía y la respuesta lo dice en `operationMessage`.
   - `crm:invoices:read` para la lista.
3. **La persona responsable de la aplicación.** La rectificativa se emite en su nombre, así que tiene que poder hacerlo en el panel: `INVOICES_VIEW` e `INVOICES_RECTIFY` con alcance sobre la factura original, `INVOICES_ISSUE`, `INVOICES_REFUND` si mandas `refund`, e `INVOICES_SEND` si pides el envío. Si le falta alguno, `403 APP_RESPONSIBLE_PERMISSION_DENIED`, sin decir cuál. Si no hay una persona responsable activa, `403 APP_RESPONSIBLE_INACTIVE`.

Ojo con un caso: si cambias de persona responsable y la nueva solo ve
sus propias facturas (alcance «propias»), no podrá rectificar las que
se emitieron con la anterior.

El alta se admite **solo de servidor a servidor**: con la clave en la
cabecera `X-Api-Key` (si llega en la dirección, `401 API_KEY_IN_URL_NOT_ALLOWED`)
y sin cabecera `Origin` de navegador (`403 ORIGIN_NOT_ALLOWED`). Tiene
un cupo propio de **10 peticiones por minuto y aplicación**, además del
general de 60.

## Emitir una rectificativa

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

Emite la rectificativa de la factura :id, que tiene que ser de tu aplicación. Responde 201 al crear y 200 al repetir una operación ya registrada con la misma clave. Permiso: crm:invoices:rectify.

Parámetro de ruta: `id`.

`:id` es la factura **original**, la que se rectifica. Se pueden
rectificar facturas ordinarias o simplificadas emitidas (estados
`ISSUED`, `SENT`, `OVERDUE`, `PAID`, `PARTIALLY_PAID`, `REFUNDED` o
`PARTIALLY_REFUNDED`); no los borradores, las anuladas, las ya
rectificadas del todo ni las propias rectificativas. Cuándo hay que
esperar está en [Cuándo se puede rectificar](#cuándo-se-puede-rectificar).

> **Consejo: Compruébala antes de emitirla**
>
> Una rectificativa es otro documento fiscal numerado. Con el mismo cuerpo,
> `POST /external/v1/invoices/:id/rectifications/validate` te dice si se
> emitiría ahora mismo y cómo sería, sin numerar nada: ver
> [Comprobar una rectificativa sin emitirla](#comprobar-una-rectificativa-sin-emitirla).

### Cuerpo

| Nombre | Tipo | Obligatorio | Descripción |
| - | - | - | - |
| `userId` | `string` | Sí | externalId del usuario propietario de la factura original (1 a 255 caracteres). Si no coincide, 404 INVOICE_NOT_ACCESSIBLE. |
| `idempotencyKey` | `string` | Sí | Clave de idempotencia (1 a 128 caracteres), OBLIGATORIA: un documento fiscal duplicado resta ingresos dos veces. Ver «Repetir y reanudar». |
| `cause` | `enum` | Sí | REFUND (devolución del precio, total o parcial, incluidos el desistimiento y la baja con devolución) o CHARGEBACK (contracargo aceptado o perdido CON la operación sin efecto). |
| `amount` | `number` | No | Importe a restar, CON impuestos, en la divisa de la factura y con 2 decimales como máximo (mínimo 0,01). Si lo omites, se rectifica todo lo que quede por rectificar. |
| `reason` | `string` | No | Causa que se imprime (hasta 500 caracteres). Si la omites: «Devolución del precio.» con REFUND o «Contracargo: la operación queda sin efecto.» con CHARGEBACK. |
| `refund` | `objeto` | No | La devolución que ya hiciste en tu pasarela. UTILIA OS la registra sobre la factura original como «Pasarela de pago externa», sin mover dinero. Exige además crm:invoices:write. Sin este bloque solo se emite el documento. |
| `refund.reference` | `string` | Sí | Obligatorio si mandas el bloque. Identificador de la devolución en tu pasarela (por ejemplo, re\_... o dp\_...): hasta 120 caracteres, empieza por una letra o un número y solo admite letras, números, guion bajo, punto, dos puntos y guion, sin espacios. No puede repetirse en otro reembolso de pasarela externa de tu organización. |
| `refund.refundedAt` | `string` | No | Fecha de la devolución (ISO 8601). Puede ser anterior a hoy, pero no futura, ni anterior a la factura original, ni de un periodo cerrado o con una declaración ya presentada. Si la omites, hoy. |
| `refund.method` | `enum` | No | Medio real con el que se devolvió: CARD (a la tarjeta, por defecto), TRANSFER o CASH. |
| `issueDate` | `string` | No | Solo se admite la fecha de hoy en la zona horaria de tu organización. Cualquier otra responde 400 VALIDATION_ERROR. Puedes omitirlo. |
| `sendEmail` | `boolean` | No | Enviar la rectificativa por correo al emitirla. Si lo omites, manda la preferencia de la aplicación. Con true exige crm:invoices:send; si falta, 403 antes de hacer nada. |
| `notes` | `string` | No | Notas visibles para el cliente, impresas en el PDF de la rectificativa. |
| `metadata` | `objeto` | No | Datos libres de la rectificativa. Las mismas claves reservadas que el alta de facturas. |

Sin el bloque `refund` se puede rectificar incluso una factura que no
consta cobrada: la rectificativa negativa compensa la deuda y, si es
total, la original queda `RECTIFIED`.

Con `refund`, la factura tiene que constar cobrada **fuera de la
pasarela de UTILIA OS** al menos por el importe de la rectificativa (por
ejemplo, con el bloque `payment` del alta o con `mark-paid`). Si no, la
respuesta es `409 REFUND_EXCEEDS_COLLECTED` con `refundableAmount`, y no
se crea nada. Lo que se cobró con la pasarela de UTILIA OS se devuelve
con el reembolso de [Acciones sobre una factura emitida](https://os.utilia.ai/docs/developers/api/invoice-actions).

### Ejemplo: devolución total

```bash
curl -X POST https://os.utilia.ai/api/external/v1/invoices/cksimp1234567890/rectifications \
  -H "X-Api-Key: utl_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "userId": "user_01HXYZ",
    "idempotencyKey": "devolucion-pedido-4471",
    "cause": "REFUND",
    "refund": { "reference": "re_3QabcDEF", "refundedAt": "2026-10-13" }
  }'
```

### Respuesta (201)

```json
{
  "rectification": {
    "id": "ckrect1234567890",
    "invoiceNumber": "R-202600003",
    "displayNumber": "R-2026-00003",
    "type": "RECTIFYING",
    "seriesCode": "DEFAULT",
    "status": "ISSUED",
    "rectificationCode": "R5",
    "rectificationType": "DIFFERENCE",
    "originalInvoiceId": "cksimp1234567890",
    "legalMentions": ["Exención franquicia fiscal"],
    "totalAmount": -39.99,
    "paidAmount": 0,
    "currency": "eur",
    "issueDate": "2026-10-13T00:00:00.000Z",
    "pdfUrl": "/api/external/v1/invoices/ckrect1234567890/pdf?userId=user_01HXYZ",
    "externalUserId": "user_01HXYZ",
    "lines": [
      {
        "name": "Plan Básico mensual",
        "quantity": -1,
        "unitPrice": 39.99,
        "taxType": "EXEMPT",
        "taxAmount": 0,
        "total": -39.99,
        "exemptionReason": "EXEMPT_IGIC_SMALL_BUSINESS",
        "exempt": true
      }
    ],
    "rectifications": []
  },
  "originalInvoice": {
    "id": "cksimp1234567890",
    "invoiceNumber": "T-202600001",
    "displayNumber": "T-2026-00001",
    "type": "SIMPLIFIED",
    "status": "RECTIFIED",
    "totalAmount": 39.99,
    "paidAmount": 0,
    "currency": "eur"
  },
  "refund": {
    "id": "ckrefund1234567890",
    "amount": 39.99,
    "currency": "eur",
    "status": "SUCCEEDED",
    "source": "EXTERNAL_GATEWAY",
    "reference": "re_3QabcDEF",
    "refundedAt": "2026-10-13T00:00:00.000Z"
  },
  "remainingRectifiableAmount": 0,
  "operationStatus": "complete",
  "operationMessage": "La rectificativa está emitida y la devolución registrada.",
  "idempotent": false
}
```

El ejemplo recorta campos de los dos documentos; llegan completos, con la
forma del [detalle de factura](https://os.utilia.ai/docs/developers/api/invoices#detalle-de-factura).

| Nombre | Tipo | Obligatorio | Descripción |
| - | - | - | - |
| `rectification` | `objeto` | Sí | La rectificativa emitida, con la forma del detalle de factura: type RECTIFYING, rectificationCode (R5 o R1), rectificationType DIFFERENCE, originalInvoiceId, total negativo, menciones y pdfUrl. |
| `originalInvoice` | `objeto` | Sí | La factura original tal como queda: su estado e importe cobrado. Si ya no queda nada por rectificar, RECTIFIED. |
| `refund` | `objeto \| null` | Sí | La devolución registrada, o nulo si no mandaste refund. amount va en la divisa (no en céntimos) y es el valor absoluto del total de la rectificativa. source es siempre EXTERNAL_GATEWAY. |
| `remainingRectifiableAmount` | `number` | Sí | Lo que aún se puede rectificar de la original, con impuestos. |
| `operationStatus` | `enum` | Sí | complete, processing o review_required. Ver «Repetir y reanudar». |
| `operationMessage` | `string` | Sí | Explicación en castellano del estado y del siguiente paso, si lo hay (por ejemplo, por qué no se envió el correo). |
| `emailSent` | `boolean` | No | Solo viaja si el envío procedía. false: la rectificativa está emitida y no se envió; reenvíala con POST /external/v1/invoices/:id/send. |
| `idempotent` | `boolean` | Sí | true si la respuesta corresponde a una operación ya registrada con la misma clave. |

### Ejemplo: devolución parcial sin registrar el dinero

Un cliente de una factura ordinaria de 107,00 € (100 € más IGIC al 7 %)
recibe una devolución de 53,50 € por un canal que no quieres registrar
en UTILIA OS. Solo emites el documento:

```json
{
  "userId": "user_01HXYZ",
  "idempotencyKey": "devolucion-parcial-ord-8812",
  "cause": "REFUND",
  "amount": 53.50,
  "reason": "Devolución de la mitad del importe por servicio prestado parcialmente."
}
```

La rectificativa sale con código **R1** (la original es ordinaria), una
línea de base −50,00 € y cuota −3,50 €, total −53,50 €.
`remainingRectifiableAmount` queda en 53,50 y la original conserva su
estado. Si el importe no se pudiera repartir al céntimo entre los tramos
fiscales de la original, la respuesta sería
`422 RECTIFICATION_AMOUNT_NOT_REPRESENTABLE`, con `closestSmallerGross` y
`closestLargerGross` cuando los hay.

### Repetir y reanudar

La rectificativa tiene varios pasos (documento, registro de la
devolución, emisión y correo). Si algo falla a medias, no se repite lo
que ya está hecho:

**¿Qué hago con operationStatus?**

¿Qué operationStatus trae la respuesta?

- complete: **Terminada**. Guarda el id de la rectificativa. Lee operationMessage por si el correo no salió.
- processing: **En curso**. Otra petición con la misma clave está trabajando ahora. Consulta de nuevo con la misma clave; no generes otra.
- `review_required`: **A medias**. Un paso falló y operationMessage dice cuál. Repite la petición con la misma clave y el mismo cuerpo: se reanuda desde el paso pendiente sin duplicar nada.

* La misma clave con el mismo cuerpo y la misma factura devuelve la misma operación, con `200` e `idempotent: true`, y reanuda lo pendiente.
* La misma clave con otro cuerpo u otra factura responde `409 BILLING_IDEMPOTENCY_CONFLICT`.
* Mientras una rectificación de la factura esté sin terminar, pedir otra con una clave distinta responde `409 RECTIFICATION_NOT_ALLOWED`: termina antes la primera.

### Cuándo se puede rectificar

Si la factura original no admite la rectificativa en ese momento, la
respuesta es `409 RECTIFICATION_NOT_ALLOWED` y `message` dice el motivo.
Cuando basta con esperar, el cuerpo del error lleva además, en la raíz,
`retryable: true` y `retryAfterSeconds`:

| Situación de la factura original | `retryable` | Qué haces |
| - | - | - |
| Es un borrador, está anulada o tiene una anulación sin terminar | — | No se rectifica. |
| Ya está rectificada por completo | — | No queda nada por rectificar. |
| Es una rectificativa o un abono | — | Solo se rectifican facturas ordinarias o simplificadas. |
| Su emisión sigue en curso (`issuanceDeliveryStatus: processing`) | `true`, con `retryAfterSeconds: 5` | Repite la misma petición, con la misma clave, pasados esos segundos. |
| Su emisión se interrumpió antes de terminar el cierre fiscal (`review_required`) | — | Tu equipo la comprueba y pulsa **Dar por revisada** en la ficha de la factura; después, repite la misma petición. Ver [Estado de la emisión](https://os.utilia.ai/docs/developers/api/invoices#estado-de-la-emisión). |
| Su PDF original se está terminando de preparar | `true`, con `retryAfterSeconds: 30` | Repite la misma petición, con la misma clave, pasados esos segundos. |
| Su PDF original necesita revisión | — | Lo revisa tu equipo desde la ficha de la factura; después, repite la misma petición. |
| Hay otra rectificación de la factura sin terminar | — | Termina antes esa, repitiendo su petición con su clave. |
| La fecha de expedición (hoy) o la de la devolución caen en un periodo cerrado o con una declaración presentada | — | Corrige `refund.refundedAt` o pide a tu equipo que lo revise desde UTILIA OS. |

El correo y el cobro con los que se emitió la factura **no** impiden
rectificarla: aunque `emailDelivery` no esté en `sent` o
`paymentRegistered` llegue en `false`, la rectificativa se emite.
Tampoco la impide un asiento contable pendiente de revisión.

### Si tu equipo ya registró la devolución desde UTILIA OS

Una devolución hecha en tu pasarela también se puede registrar a mano,
en la ficha de la factura: **Reembolsar › Registrar reembolso ya
emitido**, con el medio **Pasarela de pago externa**, la referencia de
la devolución y, si se quiere, la rectificativa a la vez. Esa acción y
el bloque `refund` de esta ruta usan el mismo registro, y una referencia
de pasarela solo puede figurar en un reembolso de la organización. Por
eso, si después tu aplicación pide la rectificativa con la misma
referencia en `refund.reference`:

- La respuesta es `409 REFUND_REFERENCE_ALREADY_USED` y no se crea nada.
- Si aquella acción ya rectificó la factura por completo, la respuesta es `409 RECTIFICATION_NOT_ALLOWED` («La factura ya está rectificada por completo»).

Elige **una sola vía** para cada devolución, preferiblemente esta
ruta. Si una petición de tu aplicación se cortó o quedó a medias, no la
completéis a mano: repítela con la misma `idempotencyKey`, que reanuda
la operación sin duplicar nada. Si tu equipo ya la registró, no pidas
otra: te llegan `INVOICE_REFUNDED`, con `refund.source:
"EXTERNAL_GATEWAY"`, y, si emitió la rectificativa,
`INVOICE_RECTIFICATION_CREATED` e `INVOICE_RECTIFIED`.

## Comprobar una rectificativa sin emitirla

**`POST /api/external/v1/invoices/:id/rectifications/validate`**

Dice si la rectificativa de la factura :id se emitiría ahora mismo con este cuerpo y cómo sería, sin numerarla, sin registrar la devolución y sin enviar nada. Mismo cuerpo que POST /external/v1/invoices/:id/rectifications. Permiso: crm:invoices:rectify.

Parámetro de ruta: `id`.

- **Mismo cuerpo y mismas reglas** que la rectificativa: un campo fuera del contrato responde `400 VALIDATION_ERROR` y una petición con cabecera `Origin` de navegador, `403 ORIGIN_NOT_ALLOWED`. Tiene un cupo propio de 30 peticiones por minuto.
- **Exige el mismo permiso que la rectificativa**, `crm:invoices:rectify`: sin él responde `403 INSUFFICIENT_SCOPE`, con el permiso en `requiredScopes`, y no enseña nada de la factura. La vista previa dice lo cobrado y lo que quedaría por rectificar, así que no se abre con menos permiso que emitirla.
- **Con el permiso, responde `200`** con `valid`, `livemode: false`, `blockers`, `warnings` y `preview`, salvo un cuerpo mal formado (`400`). Lo que impediría emitirla llega en `blockers`, con el mismo `errorCode` que daría la rectificativa (`INSUFFICIENT_SCOPE` si falta `crm:invoices:write` con `refund` o `crm:invoices:send` con correo, `BILLING_NOT_ENABLED`, `RECTIFICATION_NOT_ALLOWED` con su motivo, `REFUND_EXCEEDS_COLLECTED`, `REFUND_REFERENCE_ALREADY_USED`…) y sus datos en `details`, incluidos `retryable` y `retryAfterSeconds` cuando los hay.
- **No numera, no registra la devolución ni envía nada.** La factura original no cambia.
- Si la `idempotencyKey` ya tiene una operación, `warnings` trae `IDEMPOTENT_REPLAY` y `preview` llega en `null`: repetir la rectificativa con esa clave consultaría la operación existente en lugar de crear otra. Usa otra clave para una devolución distinta.

| Nombre | Tipo | Obligatorio | Descripción |
| - | - | - | - |
| `preview.cause` | `enum` | Sí | REFUND o CHARGEBACK, la causa que mandaste. |
| `preview.rectificationCode` | `enum` | Sí | El código legal que llevaría: R5 si la original es una simplificada no cualificada y R1 en los demás casos. |
| `preview.totalAmount` | `number` | Sí | Total de la rectificativa, impuestos incluidos, en negativo. |
| `preview.remainingAfter` | `number` | Sí | Lo que quedaría por rectificar de la original después de esta. |
| `preview.issueDate` | `string` | Sí | Fecha de expedición que tendría (AAAA-MM-DD): hoy en la zona horaria de tu organización. |
| `preview.refund` | `objeto \| null` | Sí | Con el bloque refund, la devolución que se registraría: reference, method, amount y refundedOn (día civil de tu organización). null sin él. |
| `preview.willSendEmail` | `boolean` | Sí | Si se enviaría por correo al emitirla. |

Un ejemplo completo, con la respuesta y el SDK, está en
[Probar la facturación sin emitir facturas](https://os.utilia.ai/docs/developers/api/invoice-testing#rectificativas-de-prueba).

## Consultar las rectificativas de una factura

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

Rectificativas emitidas de la factura :id, de la más antigua a la más reciente, con lo que aún se puede rectificar. Incluye las que haya emitido tu equipo desde UTILIA OS, nunca sus borradores. Permiso: crm:invoices:read.

Parámetro de ruta: `id`.

`userId` va como parámetro de consulta.

```json
{
  "originalInvoiceId": "ckinv1234567890",
  "rectifications": [
    {
      "id": "ckrect5550000001",
      "invoiceNumber": "R-202600004",
      "displayNumber": "R-2026-00004",
      "type": "RECTIFYING",
      "seriesCode": "DEFAULT",
      "status": "ISSUED",
      "rectificationCode": "R1",
      "rectificationType": "DIFFERENCE",
      "originalInvoiceId": "ckinv1234567890",
      "totalAmount": -53.5
    }
  ],
  "remainingRectifiableAmount": 53.5,
  "rectifiableWithRefundAmount": 20
}
```

`rectifiableWithRefundAmount` es lo máximo que puedes rectificar ahora
registrando a la vez la devolución: lo menor entre lo que queda por
rectificar y lo cobrado fuera de la pasarela de UTILIA OS que aún no se
ha devuelto. Solo viaja cuando es menor que `remainingRectifiableAmount`.

La rectificativa es una factura más: se lee con `GET /external/v1/invoices/:id`,
se descarga con `/pdf` y se envía con `POST /:id/send`, usando su
propio `id`. En el detalle de la original, `rectifications` la enumera y
`effectiveTotal` da el total vigente.

## Errores

| Situación | HTTP | `errorCode` |
| - | - | - |
| Cuerpo no válido, campo fuera del contrato o `issueDate` distinta de hoy | 400 | `VALIDATION_ERROR` |
| La clave llegó en la dirección | 401 | `API_KEY_IN_URL_NOT_ALLOWED` |
| Falta un permiso de la aplicación | 403 | `INSUFFICIENT_SCOPE` |
| El plan no incluye Finanzas | 403 | `MODULE_NOT_ENABLED` |
| La facturación de la aplicación está apagada | 403 | `BILLING_NOT_ENABLED` |
| No hay persona responsable activa | 403 | `APP_RESPONSIBLE_INACTIVE` |
| La persona responsable no puede rectificar esa factura | 403 | `APP_RESPONSIBLE_PERMISSION_DENIED` |
| La petición viene de un navegador | 403 | `ORIGIN_NOT_ALLOWED` |
| La original es de la ventanilla única del IVA y la función no está disponible para tu organización | 403 | `FEATURE_COMING_SOON` |
| La factura no existe, es de otra aplicación o de otro usuario | 404 | `INVOICE_NOT_ACCESSIBLE` |
| Borrador, anulada, ya rectificada por completo, rectificativa, emisión en curso (`retryable: true`) o con el cierre fiscal interrumpido, PDF original preparándose (`retryable: true`) o pendiente de revisión, anulación u otra rectificación sin terminar, o periodo cerrado. El mensaje dice el motivo; ver [Cuándo se puede rectificar](#cuándo-se-puede-rectificar) | 409 | `RECTIFICATION_NOT_ALLOWED` |
| `amount` supera lo que queda (llega `remainingRectifiableAmount`) | 409 | `RECTIFICATION_AMOUNT_EXCEEDS_REMAINING` |
| Las rectificaciones anteriores no cuadran y tu equipo tiene que revisarlas | 409 | `RECTIFICATION_HISTORY_REVIEW_REQUIRED` |
| Con `refund`, la factura no consta cobrada fuera de la pasarela de UTILIA OS por ese importe (llega `refundableAmount`) | 409 | `REFUND_EXCEEDS_COLLECTED` |
| `refund.reference` ya figura en otro reembolso de tu organización, también si tu equipo lo registró a mano | 409 | `REFUND_REFERENCE_ALREADY_USED` |
| La misma clave con otro cuerpo u otra factura | 409 | `BILLING_IDEMPOTENCY_CONFLICT` |
| El importe no se puede repartir al céntimo, o es parcial en una factura con retención | 422 | `RECTIFICATION_AMOUNT_NOT_REPRESENTABLE` |
| Más de 10 peticiones por minuto | 429 | — |

Los datos de negocio (`remainingRectifiableAmount`, `refundableAmount`,
`closestSmallerGross`, `closestLargerGross`, `retryable` y
`retryAfterSeconds`) llegan en la raíz del cuerpo del error. Con los SDK
4.3.0 o posteriores se leen en `error.details`. Un error con
`retryable: true` se repite tal cual, con la misma clave, pasados
`retryAfterSeconds`; el resto no se resuelve repitiendo hasta que cambie
lo que dice el mensaje.

## Avisos que produce

| Momento | Aviso |
| - | - |
| Se crea la rectificativa | `INVOICE_RECTIFICATION_CREATED` |
| Se emite y surte efectos | `INVOICE_RECTIFIED`, con la original en `invoice` y la rectificativa en `rectifyingInvoice` |
| Se registra la devolución (con `refund`) | `INVOICE_REFUNDED`, con `refund.source: "EXTERNAL_GATEWAY"` |

Los tres van dirigidos solo a tu aplicación. El contenido está en
[Avisos: contenido y catálogo de eventos](https://os.utilia.ai/docs/developers/webhooks/events).

## Con el SDK

### JavaScript / TypeScript

```typescript
const resultado = await sdk.invoices.rectifications.create('cksimp1234567890', {
  userId: 'user_01HXYZ',
  idempotencyKey: 'devolucion-pedido-4471',
  cause: 'REFUND',
  refund: { reference: 're_3QabcDEF' },
});

if (resultado.operationStatus === 'review_required') {
  // Repite la misma llamada, con la misma clave, para terminarla.
}

const lista = await sdk.invoices.rectifications.list('cksimp1234567890', 'user_01HXYZ');
console.log(lista.remainingRectifiableAmount);
```

### Python

```python
from utilia_sdk import (
    CreateExternalInvoiceRectificationInput,
    ExternalInvoiceRectificationCause,
    ExternalRectificationRefundInput,
)

resultado = await sdk.invoices.rectifications.create(
    "cksimp1234567890",
    CreateExternalInvoiceRectificationInput(
        user_id="user_01HXYZ",
        idempotency_key="devolucion-pedido-4471",
        cause=ExternalInvoiceRectificationCause.REFUND,
        refund=ExternalRectificationRefundInput(reference="re_3QabcDEF"),
    ),
)

lista = await sdk.invoices.rectifications.list("cksimp1234567890", "user_01HXYZ")
print(lista.remaining_rectifiable_amount)
```

## Siguiente paso

- [Facturar y cobrar con tu propia pasarela](https://os.utilia.ai/docs/developers/api/own-gateway-billing): El recorrido completo: factura cobrada en el acto, devolución y rectificativa.
- [Facturas](https://os.utilia.ai/docs/developers/api/invoices): El alta de facturas ordinarias y simplificadas.
- [Rectificativas y abonos en Finanzas](https://os.utilia.ai/docs/guides/finance/credit-notes): Cómo ve y gestiona tu equipo las rectificativas.
- [Contenido de los avisos](https://os.utilia.ai/docs/developers/webhooks/events): Los avisos de facturas, rectificativas y devoluciones.
