# Facturas

Emite facturas ordinarias y simplificadas desde tu aplicación, envíalas, cóbralas, anúlalas, rectifícalas, devuelve el dinero y programa cobros.

El servicio de facturas emite facturas ordinarias y simplificadas en nombre de tu organización para los usuarios de tu aplicación: con numeración correlativa (la de tu organización o la de una serie reservada a tu aplicación) y el PDF con los datos fiscales del cliente. Las facturas aparecen en [Finanzas](https://os.utilia.ai/dashboard/finance/invoices) como cualquier otra, vinculadas a la ficha del cliente en el CRM. Desde el mismo servicio las envías por correo, las cobras, entregas su enlace público, las anulas, las rectificas, devuelves dinero y programas cobros. Funciona con la clave de tu aplicación externa, siempre desde tu servidor.

**De la factura emitida al cobro confirmado**

1. **Emitir** (invoices.create): Numeración correlativa y PDF con los datos fiscales.
2. **Entregar** (regeneratePublicLink o send): Enlace público para verla y pagarla, o envío por correo con el PDF.
3. [**Cobrar**](https://os.utilia.ai/docs/developers/sdk/payments) (payInvoice o charge): Con el usuario delante, o sin él contra su autorización de cobro.
4. [**Confirmar**](https://os.utilia.ai/docs/developers/webhooks/events) (Aviso INVOICE_PAID): Tu servidor recibe el aviso y da la factura por pagada.

_Guarda el id y el número de la factura en tu base de datos al emitirla: te sirven para enseñarla y para reconciliar los avisos._

## Antes de empezar

- La clave de tu aplicación debe tener la **facturación activada** y los scopes de cada acción (en la tabla siguiente). Úsala solo desde tu servidor.
- La organización necesita el aplicativo **Finanzas** en su plan. Para cobrar con tarjeta hace falta además **Pagos con tarjeta (Stripe)** y la cuenta de Stripe conectada; para emitir, no. Puedes comprobar si todo está listo para cobrar con `paymentMethods.getReadiness()` (`payment_methods.get_readiness()`), explicado en [Métodos de pago](https://os.utilia.ai/docs/developers/sdk/payment-methods).
- El usuario tiene que estar [identificado](https://os.utilia.ai/docs/developers/sdk/users), a ser posible con su NIF y su dirección de facturación, que son los que salen en el PDF.

| JavaScript | Python | Para qué | Scope |
| - | - | - | - |
| `create(datos)` | `create(datos)` | Emitir una factura | `crm:invoices:write` y `crm:invoices:issue` |
| `list(userId, filtros?)` | `list(user_id, filtros)` | Facturas de un usuario | `crm:invoices:read` |
| `get(id, userId)` | `get(invoice_id, user_id)` | Detalle con sus líneas | `crm:invoices:read` |
| `downloadPdf(id, userId)` | `download_pdf(invoice_id, user_id)` | PDF (`Blob` en JavaScript, `bytes` en Python) | `crm:invoices:read` |
| `getStats(userId)` | `get_stats(user_id)` | Totales del usuario | `crm:invoices:read` |
| `listSeries()` | `list_series()` | Series en las que tu aplicación puede numerar (4.4.0) | `crm:invoices:read` |
| `getEuVatRates(filtros?)` | `get_eu_vat_rates(date=..., country=...)` | Tipos de IVA vigentes de la Unión para la ventanilla única (4.4.0, Próximamente) | `crm:invoices:read` |
| `send(id, datos)` | `send(invoice_id, datos)` | Enviarla por correo con su PDF | `crm:invoices:send` |
| `charge(id, datos)` | `charge(invoice_id, datos)` | Cobrarla sin el usuario delante | `billing:charge` |
| `markPaid(id, datos)` | `mark_paid(invoice_id, datos)` | Registrar un cobro fuera de la pasarela | `crm:invoices:write` |
| `getPublicLink(id, { userId })` | `get_public_link(invoice_id, user_id=...)` | Vigencia del enlace público | `crm:invoices:read` |
| `regeneratePublicLink(id, datos)` | `regenerate_public_link(invoice_id, datos)` | Emitir un enlace público nuevo | `crm:invoices:write` |
| `cancel(id, userId, motivo?)` | `cancel(invoice_id, user_id, reason)` | Anularla | `crm:invoices:cancel` |
| `refunds.create(id, datos)` | `refunds.create(invoice_id, datos)` | Devolver lo cobrado con la pasarela de UTILIA OS | `billing:refund` |
| `rectifications.create(id, datos)` | `rectifications.create(invoice_id, datos)` | Emitir una rectificativa (4.3.0) | `crm:invoices:rectify`, y `crm:invoices:write` con `refund` |
| `rectifications.list(id, userId)` | `rectifications.list(invoice_id, user_id)` | Rectificativas de una factura y lo que queda por rectificar (4.3.0) | `crm:invoices:read` |
| `scheduledCharges.*` | `scheduled_charges.*` | Programar cobros en una fecha | `billing:scheduled-charges:read` / `:write` |

## Emitir una factura

`create` emite directamente: no hay borradores ni un paso posterior de emisión. Los datos principales:

| JavaScript | Python | Obligatorio | Qué es |
| - | - | - | - |
| `user` | `user` | Sí | El usuario: `externalId` y `email` obligatorios; `name`, `taxId` y `billingAddress` opcionales. |
| `type` | `type` | No | `STANDARD` (ordinaria, por defecto) o `SIMPLIFIED` (simplificada, hasta 400 € impuestos incluidos y con su propia serie `T-`). Desde la 4.3.0. |
| `seriesCode` | `series_code` | No | Código de una serie de tu aplicación (de `listSeries()`) o `DEFAULT` para la de siempre. Sin él, la serie por defecto de tu aplicación para el tipo o, si no tiene, la de siempre. Desde la 4.4.0. |
| `recipient` | `recipient` | En la ordinaria | Datos fiscales del receptor: `name` obligatorio si lo mandas; `taxId`, `address`, `city`, `postalCode` y `country`. En la simplificada es opcional: sin él se imprime «Consumidor final»; con NIF exige la dirección completa. |
| `lines` | `lines` | Sí | Al menos una línea: `name`, `quantity`, `unitPrice` (`unit_price`) y `taxType` (`tax_type`), entre otros. |
| `issueDate`, `dueDate` | `issue_date`, `due_date` | No | Fechas `AAAA-MM-DD`. Sin fecha de expedición, hoy en la zona horaria de tu organización. Desde la 4.4.0 la expedición no puede ser futura ni anterior a la de la última factura de la misma serie (`INVOICE_ISSUE_DATE_OUT_OF_ORDER`). |
| `taxSystem` | `tax_system` | No | `IVA`, `IGIC`, `IPSI`, `EXEMPT`, `INTERNATIONAL` o, desde la 4.4.0, `EU_OSS` (ventanilla única del IVA, Próximamente; ver más abajo). |
| `pricesIncludeTax` | `prices_include_tax` | No | Los precios de las líneas llevan impuestos y el total de cada línea es exactamente lo cobrado. Desde la 4.3.0. |
| `payment` | `payment` | No | Cobro que ya hiciste con tu propia pasarela: `method`, `paidAt` (`paid_at`) y `reference`. La factura nace cobrada. Desde la 4.3.0. |
| `consumerResidence` | `consumer_residence` | No | Residencia del consumidor (`region`, `countryCode`, `evidence`), cotejada con el sistema fiscal. Desde la 4.3.0. |
| `sendEmail` | `send_email` | No | Enviarla por correo al emitirla. Sin el campo, manda la preferencia de la aplicación. |
| `idempotencyKey` | `idempotency_key` | No | Clave estable de esta factura para no emitirla dos veces. |

Las reglas de cada campo nuevo, los errores y un ejemplo completo de una
simplificada de 39,99 € cobrada en el acto están en
[Facturas de la API REST](https://os.utilia.ai/docs/developers/api/invoices#ordinaria-o-simplificada).

En cada línea, `discount` es un **importe** que se resta a esa línea (dos unidades de 100 € con descuento 10 dan 190 € antes de impuestos); `discountPercent` (`discount_percent`) es el porcentaje global de la factura. Para el IGIC hay además `igicOperationType` (`IGIC_RETAIL` o `IGIC_GENERAL`, solo si el emisor tiene declarado el régimen minorista) y, en líneas exentas, `exemptionReason` (`EXEMPT_IGIC_RETAILER`, `EXEMPT_IGIC_INTERNAL`, `EXEMPT_EXPORT`, `EXEMPT_OTHER`, que exige una `exemptionNote` de hasta 300 caracteres, o `EXEMPT_IGIC_SMALL_BUSINESS`, la del régimen del pequeño empresario del IGIC, que UTILIA OS pone sola en las líneas exentas sin causa cuando tu organización está en ese régimen). El servidor comprueba la coherencia fiscal: que el SDK acepte un campo no hace válida cualquier combinación.

### JavaScript

```typescript
const invoice = await sdk.invoices.create({
  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 de Gran Canaria',
    postalCode: '35001',
    country: 'España',
  },
  dueDate: '2026-11-08',
  taxSystem: 'IGIC',
  lines: [{ name: 'Plan Premium mensual', quantity: 1, unitPrice: 49.95, taxType: 'IGIC_7' }],
  idempotencyKey: 'order_01HXYZ-invoice',
});

console.log(invoice.invoiceNumber, invoice.idempotent);
```

### Python

```python
from utilia_sdk.models import (
    CreateInvoiceInput,
    CreateInvoiceLine,
    CreateInvoiceRecipient,
    CreateInvoiceUser,
)

invoice = await sdk.invoices.create(
    CreateInvoiceInput(
        user=CreateInvoiceUser(
            external_id="user_01HXYZ",
            email="cliente@empresa.com",
            name="Empresa de Canarias SL",
            tax_id="B76543210",
        ),
        recipient=CreateInvoiceRecipient(
            name="Empresa de Canarias SL",
            tax_id="B76543210",
            address="Avenida de Canarias 12",
            city="Las Palmas de Gran Canaria",
            postal_code="35001",
            country="España",
        ),
        due_date="2026-11-08",
        tax_system="IGIC",
        lines=[CreateInvoiceLine(name="Plan Premium mensual", quantity=1, unit_price=49.95, tax_type="IGIC_7")],
        idempotency_key="order_01HXYZ-invoice",
    )
)

print(invoice.invoice_number, invoice.idempotent)
```

La respuesta trae el `id`, el número de factura, el estado y `idempotent`, que vale `true` cuando la clave ya se había usado y el servidor te devuelve la factura existente en lugar de emitir otra. Que la factura se haya creado no significa por sí solo que la Agencia Tributaria haya aceptado su registro.

> **Atención: Una emisión dudosa no se repite a ciegas**
>
> `create` hace un único intento aunque configures reintentos: un fallo de red puede llegar después de emitir la factura. Conserva la `idempotencyKey` y los datos originales y vuelve a llamar con la misma clave, o consulta la factura antes. Nunca cambies la clave para «desbloquear» una emisión: crearías una segunda factura.

Si la respuesta trae `emailSent` (`email_sent`) en falso, la factura **está emitida y numerada**: solo falta el correo. Nunca vuelvas a crearla. El servidor dice además en `emailDelivery` qué ha pasado: con `retrying`, UTILIA OS la está reenviando sola y no tienes que hacer nada; con `failed` o `withheld`, resuelve el motivo y envíala con `send`. Los motivos están en [El correo de la factura](https://os.utilia.ai/docs/developers/api/invoices#el-correo-de-la-factura). Los tipos del SDK declaran `emailDelivery` y `displayNumber` (`email_delivery` y `display_number`) a partir de la 4.5.0.

Comprobaciones locales: en JavaScript, si falta `user`, su `externalId` o su `email`, si una factura que no es simplificada no lleva `recipient`, o si `recipient` llega sin `name`, el SDK lanza un `UtiliaSDKError` de validación sin enviar nada; si `lines` está vacío, lanza un `Error` corriente con el mensaje «La factura debe tener al menos una línea». En Python esas comprobaciones las hacen los modelos de entrada: una ordinaria sin `recipient` falla al construir el modelo con `ValueError`.

La respuesta trae también, desde la 4.3.0, `type`, `legalMentions` (`legal_mentions`), las menciones impresas, y `originalInvoiceId` (`original_invoice_id`); si mandaste `payment`, `paymentRegistered` (`payment_registered`) dice si el cobro quedó registrado. Desde la 4.4.0, `seriesCode` (`series_code`) dice en qué serie se numeró (`DEFAULT` en la de siempre). En el detalle, cada línea trae `exempt`: úsalo en lugar de `taxRate`, porque un tipo cero no es una exención.

## Series de tu aplicación

Desde la 4.4.0, tu organización puede reservar a tu aplicación una o varias series (por ejemplo, `APP-202600001`) para que sus facturas no se intercalen con las del equipo, y fijar cuál se usa por defecto. Las crea una persona de tu organización en UTILIA OS; tu aplicación no puede crearlas ni cambiarlas, solo consultarlas con `listSeries()` y elegir entre ellas con `seriesCode`. Cómo se elige la serie, los errores (`INVOICE_SERIES_NOT_AVAILABLE`, `INVOICE_SERIES_TYPE_MISMATCH`, `INVOICE_SERIES_INACTIVE`) y la fecha de expedición están en [Series disponibles](https://os.utilia.ai/docs/developers/api/invoices#series-disponibles).

### JavaScript

```typescript
const { series } = await sdk.invoices.listSeries();
const propia = series.find((s) => s.documentType === 'STANDARD' && s.isAppDefault);
console.log(propia?.code, propia?.example); // APP APP-202600001

const factura = await sdk.invoices.create({ ...datosDeLaFactura, seriesCode: 'APP' });
console.log(factura.invoiceNumber, factura.seriesCode); // APP-202600001 APP
```

### Python

```python
lista = await sdk.invoices.list_series()
propia = next(s for s in lista.series if s.document_type == "STANDARD" and s.is_app_default)
print(propia.code, propia.example)  # APP APP-202600001

factura = await sdk.invoices.create(datos_de_la_factura.model_copy(update={"series_code": "APP"}))
print(factura.invoice_number, factura.series_code)  # APP-202600001 APP
```

La lista trae también, solo informativa (`selectable: false`), la serie de rectificativas: tus rectificativas van siempre a ella, aunque la original esté en una serie de tu aplicación. Nunca devuelve el siguiente número.

## Ventanilla única del IVA (Próximamente)

La ventanilla única del IVA está en «Próximamente»: todavía no está disponible para las organizaciones, y cualquier dato suyo responde `403 FEATURE_COMING_SOON`. Compruébalo con `isFeatureComingSoonError(error)` (`is_feature_coming_soon_error(exc)` en Python) y `getComingSoonFeature(error)` (`get_coming_soon_feature`), que devuelve `featureKey` y `featureLabel`. No es un bloqueo de plan: `isPlanError()` responde `false`.

Desde la 4.4.0 el SDK ya trae su contrato:

- `taxSystem: 'EU_OSS'` (`TaxSystem.EU_OSS`), con líneas `EU_VAT` que llevan `vatRateClass` (`vat_rate_class`) y `ossSupplyType` (`oss_supply_type`). UTILIA OS resuelve y congela el porcentaje del país del consumidor.
- `consumerResidence.evidenceItems` (`evidence_items`, con `OssLocationEvidenceItem` en Python): la prueba de la ubicación del consumidor.
- `getEuVatRates({ date, country })` (`get_eu_vat_rates(date=..., country=...)`, asíncrono y síncrono): los tipos vigentes de la Unión.
- En las respuestas, `taxSystem`, `consumerCountryCode` y `ossScheme` en cada factura, y `vatRateClass` y `ossSupplyType` en cada línea. En Python, leer una factura `EU_OSS` no falla.
- Los códigos `OSS_*` en `INVOICE_ERROR_CODES` (`InvoiceErrorCode`).

La ventanilla única del IVA está en «Próximamente»: hasta que esté disponible para tu organización, cualquier dato suyo responde `403 FEATURE_COMING_SOON`.

## Consultar facturas

`list` acepta filtros por `status`, `type` (desde la 4.3.0: `STANDARD`, `SIMPLIFIED`, `RECTIFYING` o `CREDIT_NOTE`), `seriesCode` (`series_code`, desde la 4.4.0), `page` y `limit`. Ojo con la forma de la respuesta: en JavaScript la lista viene en `data`; en Python, en `invoices`.

Estados: `DRAFT`, `ISSUED`, `SENT`, `PAID`, `PARTIALLY_PAID`, `OVERDUE`, `CANCELLED`, `DISPUTED`, `OVERDUE_UNCOLLECTABLE`, `REFUNDED`, `PARTIALLY_REFUNDED` y `RECTIFIED`.

### JavaScript

```typescript
const page = await sdk.invoices.list('user_01HXYZ', { status: 'PAID', limit: 50 });
for (const item of page.data) {
  console.log(`${item.invoiceNumber}: ${item.totalAmount} ${item.currency}`);
}

const stats = await sdk.invoices.getStats('user_01HXYZ');
console.log(`Pendiente: ${stats.totalPending} ${stats.currency}`);
```

### Python

```python
from utilia_sdk.models import ListInvoicesFilters

page = await sdk.invoices.list("user_01HXYZ", ListInvoicesFilters(status="PAID", limit=50))
for item in page.invoices:
    print(f"{item.invoice_number}: {item.total_amount} {item.currency}")

stats = await sdk.invoices.get_stats("user_01HXYZ")
print(f"Pendiente: {stats.total_pending} {stats.currency}")
```

## El PDF y el enlace público

`downloadPdf` descarga el PDF autenticado con tu clave: hazlo en tu servidor y entrégalo a tu usuario desde una ruta tuya que compruebe que puede verlo. El campo `pdfUrl` de la factura es una ruta protegida, no un enlace que puedas compartir.

Para que el usuario vea y pague la factura por su cuenta, emite un **enlace público** con `regeneratePublicLink`. El servidor solo guarda una huella del enlace, así que:

- La dirección te llega **una sola vez**, en la respuesta. Guárdala en tu base de datos.
- `getPublicLink` solo informa de vigencia, caducidad y acciones permitidas; su `publicUrl` llega siempre vacío.
- Emitir un enlace nuevo **revoca el anterior** de esa factura.

Los enlaces de la API externa permiten ver y pagar (`VIEW` y `PAY`).

### JavaScript

```typescript
import { writeFile } from 'node:fs/promises';

const blob = await sdk.invoices.downloadPdf(invoice.id, 'user_01HXYZ');
await writeFile('factura.pdf', new Uint8Array(await blob.arrayBuffer()));

const link = await sdk.invoices.regeneratePublicLink(invoice.id, { userId: 'user_01HXYZ' });
await guardarEnlace(invoice.id, link.publicUrl, link.expiresAt);
```

### Python

```python
from pathlib import Path

from utilia_sdk.models import RegenerateExternalInvoicePublicLinkInput

pdf = await sdk.invoices.download_pdf(invoice.id, "user_01HXYZ")
Path("factura.pdf").write_bytes(pdf)

link = await sdk.invoices.regenerate_public_link(
    invoice.id, RegenerateExternalInvoicePublicLinkInput(user_id="user_01HXYZ")
)
await guardar_enlace(invoice.id, link.public_url, link.expires_at)
```

## Enviar, cobrar y registrar pagos

**Enviar por correo.** `send` usa la plantilla, el remitente y la vía de envío de tu organización. Sin `to`, va al contacto de facturación principal del cliente o, si no hay, al correo del usuario. Solo alcanza a direcciones conocidas de esa factura (contactos de facturación del cliente, contactos del CRM marcados para recibir facturas, el correo del usuario y el destinatario de la emisión): cualquier otra en `to` o `cc` rechaza el envío entero con un 422 `INVOICE_EMAIL_RECIPIENT_NOT_ALLOWED`. Hay un cupo de 100 envíos por hora y aplicación; al superarlo, la respuesta es un 429 con la espera en `Retry-After`.

**Cobrar sin el usuario delante.** `charge` cobra contra una [autorización de cobro](https://os.utilia.ai/docs/developers/sdk/payment-authorizations) que el usuario firmó antes; sin ella responde 409 `PAYMENT_AUTHORIZATION_REQUIRED`. Usa siempre la misma `idempotencyKey` para reintentar el mismo cobro: una clave nueva es un cobro nuevo. El resultado puede ser `SUCCEEDED`, `PROCESSING`, `FAILED` o `REQUIRES_ACTION`: este último no es un error, significa que el banco pide que el usuario se autentique; llévalo a la `publicUrl` que trae la respuesta (es un enlace adicional y no revoca el que ya tuviera). Para cobrar con el usuario delante, usa [Cobros](https://os.utilia.ai/docs/developers/sdk/payments).

**Registrar un pago fuera de la pasarela.** `markPaid` (`mark_paid`) registra una transferencia, efectivo, cheque u otro medio (`TRANSFER`, `CASH`, `CHECK`, `CARD` u `OTHER`). Sin `amount` la marca como pagada entera; con él, registra un pago parcial.

### JavaScript

```typescript
await sdk.invoices.send(invoice.id, {
  userId: 'user_01HXYZ',
  message: 'Adjuntamos la factura de octubre.',
});

const cobro = await sdk.invoices.charge(invoice.id, {
  userId: 'user_01HXYZ',
  idempotencyKey: `cobro_${invoice.id}`,
});
if (cobro.status === 'REQUIRES_ACTION') llevarAlUsuario(cobro.publicUrl);

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

### Python

```python
from utilia_sdk.models import (
    ChargeExternalInvoiceInput,
    MarkExternalInvoicePaidInput,
    SendExternalInvoiceInput,
)

await sdk.invoices.send(
    invoice.id,
    SendExternalInvoiceInput(user_id="user_01HXYZ", message="Adjuntamos la factura de octubre."),
)

cobro = await sdk.invoices.charge(
    invoice.id,
    ChargeExternalInvoiceInput(user_id="user_01HXYZ", idempotency_key=f"cobro_{invoice.id}"),
)
if cobro.status == "REQUIRES_ACTION":
    llevar_al_usuario(cobro.public_url)

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

## Anular y devolver dinero

`cancel` anula una factura emitida. No se pueden anular borradores ni facturas ya anuladas. Hace un único intento: si falla por la red, consulta la factura antes de repetir.

Anular **no devuelve dinero**, y es solo para una factura que no debió emitirse. Para devolver lo cobrado con la pasarela de UTILIA OS está `refunds.create`: sin `amountCents` (`amount_cents`) devuelve todo lo cobrado; el motivo puede ser `duplicate`, `fraudulent` o `requested_by_customer`; y con `createRectifyingInvoice` (`create_rectifying_invoice`) pides en el mismo acto la factura rectificativa, con el código legal que decide UTILIA OS.

### JavaScript

```typescript
await sdk.invoices.cancel(invoice.id, 'user_01HXYZ', 'Factura duplicada por error');

await sdk.invoices.refunds.create(otraFactura.id, {
  userId: 'user_01HXYZ',
  reason: 'requested_by_customer',
  idempotencyKey: `devolucion_${otraFactura.id}`,
});
```

### Python

```python
from utilia_sdk.models import RefundExternalInvoiceInput

await sdk.invoices.cancel(invoice.id, "user_01HXYZ", reason="Factura duplicada por error")

await sdk.invoices.refunds.create(
    otra_factura.id,
    RefundExternalInvoiceInput(
        user_id="user_01HXYZ",
        reason="requested_by_customer",
        idempotency_key=f"devolucion_{otra_factura.id}",
    ),
)
```

## Rectificar una factura

Desde la 4.3.0, `rectifications.create` emite la rectificativa de una
factura de tu aplicación cuando devolviste el dinero con tu propia
pasarela (`cause: 'REFUND'`) o un contracargo deja la venta sin efecto
(`cause: 'CHARGEBACK'`). Tú no eliges el código legal: UTILIA OS emite
`R5` si la original es una simplificada sin destinatario identificado y
`R1` en los demás casos, siempre por diferencias y con importes
negativos.

- `amount` (con impuestos, 2 decimales como máximo) rectifica una parte; sin él, todo lo que quede.
- `refund` (`reference`, `refundedAt`, `method`) registra la devolución que ya hiciste en tu pasarela, sin mover dinero. Exige además el scope `crm:invoices:write` y que la factura conste cobrada fuera de la pasarela de UTILIA OS.
- `idempotencyKey` (`idempotency_key`) es **obligatoria**. Si `operationStatus` (`operation_status`) llega en `review_required`, repite la misma llamada con la misma clave para terminarla.
- La fecha de expedición es siempre la de hoy.
- La rectificativa va siempre a la serie de rectificativas (`R-`), también si la original está en una serie de tu aplicación.

`rectifications.list` devuelve las rectificativas de una factura,
`remainingRectifiableAmount` y, cuando es menor,
`rectifiableWithRefundAmount`. La rectificativa es una factura más: se
lee, se descarga y se envía con `get`, `downloadPdf` y `send` usando su
`id`.

### JavaScript

```typescript
const resultado = await sdk.invoices.rectifications.create(factura.id, {
  userId: 'user_01HXYZ',
  idempotencyKey: `devolucion_${factura.id}`,
  cause: 'REFUND',
  refund: { reference: 're_3QabcDEF' },
});

console.log(resultado.rectification.rectificationCode); // 'R5' o 'R1'
console.log(resultado.remainingRectifiableAmount);
```

### Python

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

resultado = await sdk.invoices.rectifications.create(
    factura.id,
    CreateExternalInvoiceRectificationInput(
        user_id="user_01HXYZ",
        idempotency_key=f"devolucion_{factura.id}",
        cause=ExternalInvoiceRectificationCause.REFUND,
        refund=ExternalRectificationRefundInput(reference="re_3QabcDEF"),
    ),
)

print(resultado.rectification.rectification_code, resultado.remaining_rectifiable_amount)
```

Los errores de negocio traen sus datos en `error.details` desde la
4.3.0: `missingFields`, `lineNumber` o `remainingRectifiableAmount`. Las
reglas completas, la advertencia sobre los contracargos y la tabla de
errores están en [Facturas rectificativas](https://os.utilia.ai/docs/developers/api/invoice-rectifications).

## Programar un cobro

`scheduledCharges` (`scheduled_charges`) cobra una factura en una fecha futura, contra una autorización de cobro vigente **de alcance `SCHEDULED_INVOICE`** (las de alcance `RECURRING` cubren suscripciones, no facturas sueltas; si no vale, responde 409 `PAYMENT_AUTHORIZATION_NOT_ACTIVE`). La fecha tiene que caer después de la ventana de cancelación de la organización.

- `schedule(facturaId, { userId, scheduledAt, paymentAuthorizationId })` lo programa.
- `list(facturaId, { userId })` lista los de una factura, con el estado de cada uno (figura siguiente).
- `cancel(cobroId, { userId })` lo cancela mientras siga abierta la ventana (`cancellableUntil`); después, 409 `SCHEDULED_CHARGE_NOT_CANCELLABLE`. Para cambiar la fecha, cancela y vuelve a programar.
- `retryNow(cobroId, { userId })` (`retry_now`) adelanta el reintento de un cobro fallido. El resultado llega por aviso a tu servidor. Hay un tope de cinco reintentos manuales por factura en 24 horas.

**Estados de un cobro programado**

1. **Programado** (`PENDING`). A la espera de su fecha.
   - **Cancelado** (`CANCELLED`, estado final). Se llega desde Programado cuando: cancel antes de cancellableUntil. No llega a cobrarse.
2. **En curso** (`RUNNING`). Se llega desde Programado cuando: Llega la fecha programada. Se está intentando el cobro.
   - **Requiere autenticación** (`AUTH_REQUIRED`). Se llega desde En curso cuando: El banco pide autenticación. El usuario tiene que confirmar el cobro.
   - **Reintento programado** (`RETRY_SCHEDULED`). Se llega desde En curso cuando: Fallo recuperable. Hay un reintento programado; retryNow lo adelanta.
   - **Fallido** (`FAILED_PERMANENTLY`, estado final). Se llega desde En curso cuando: Se agotan los reintentos o la tarjeta se rechaza sin remedio. No se volverá a intentar.
3. **Cobrado** (`COMPLETED`, estado final). Se llega desde En curso cuando: El cobro se completa. La factura queda cobrada.

Otras transiciones:

- Reintento programado → En curso: Llega la hora del reintento, o lo adelantas con retryNow.

_Cada estado con el valor de status que devuelve la API._

## Siguiente paso

- [Métodos de pago](https://os.utilia.ai/docs/developers/sdk/payment-methods): Guarda la tarjeta del usuario para poder cobrarle.
- [Suscripciones](https://os.utilia.ai/docs/developers/sdk/subscriptions): Cuotas que se facturan, envían y cobran solas cada periodo.
- [Guía de facturación paso a paso](https://os.utilia.ai/docs/developers/api/billing-guide): De la primera clave a la factura cobrada, con la API REST.
- [Avisos a tu servidor](https://os.utilia.ai/docs/developers/webhooks/events): Los eventos de facturas y cobros que recibirás.
