> ## Documentation Index
> Fetch the complete documentation index at: https://docs.cucu.bo/llms.txt
> Use this file to discover all available pages before exploring further.

# Changelog

> Cambios de contrato de la API de facturacion, en orden cronologico inverso.

Cada entrada lista lo que cambia para un integrador y marca con **Requiere accion** lo que puede
romper una integracion existente si no se revisa.

## 2026-10-28 (9)

* **Configurar SIAT v3 valida el ambiente.** `PUT /api/v1/admin/onboarding/companies/{id}/siat-version` y la tool `configurarVersionSiat`
  con `v3` responden `422 SIAT_V3_NO_DISPONIBLE` al momento de configurar si el ambiente no habla v3 (antes la company quedaba en v3 y
  fallaba al emitir). `verificationUrl` ahora cae a la consulta oficial del SIN cuando la factura no guardó su QR. Sin **Requiere accion**.

## 2026-10-28 (8)

* **Nuevo `POST /api/v1/invoices/preview`** y tool MCP `previsualizarFactura`: el mismo body que emitir, pero sin emitir
  (no numera, no guarda, no toca el SIN). Devuelve montos v2 o v3 según tu company (neto, IVA, monto a pagar, diferencia contra el
  precio final pedido), el XML que se mandaría y validaciones contra el XSD oficial del SIN y las fórmulas. Ver
  [Previsualizar factura](https://docs.cucu.bo/api/preview-invoice.md). Sin **Requiere accion**.

## 2026-10-28 (7)

* **SIAT v3 para todos los sectores en sandbox, sin cambiar tus precios.** Si tu company pasa a v3, `priceUnit` sigue siendo el
  precio final que cobras (`priceBasis=GROSS`, default): cucu deriva el neto y el IVA con el redondeo del SIN y arma el XML como
  pide la norma. La respuesta (REST, MCP y webhooks) agrega `siatVersion`, `priceBasis`, `amountNet`, `amountIva`, `amountToPay`,
  `amountRequested` y `amountDifference`: cuando un precio final no se puede representar en v3 (ej. Bs 100,00 → 99,99) la diferencia
  viene explícita, nunca se cobra distinto en silencio. Quien prefiera mandar netos usa `priceBasis=NET` (por company o por
  request). Campos nuevos opcionales para v3: `incotermScope` (3, 20), `contractNumber` (20, 21), `productDescription`,
  `locality`, `originDepartment`, `originMunicipality` (21), `beneficiaryComplement` (13). El PDF/ticket muestra TOTAL PRECIO NETO
  e IVA (13 %) en v3. **Producción no cambia** (RND 102100000011 vigente).

## 2026-10-28 (6)

* **SIAT versión 3 (IVA transparente) en sandbox, por company.** El SIN publicó el Anexo Técnico Ley 1733 (RND 102600000034) para
  pruebas en el piloto; en producción sigue vigente la RND 102100000011 (v2) y **nada cambia**. En sandbox, soporte puede pasar
  una company a v3 (tool admin [`configurarVersionSiat`](https://docs.cucu.bo/mcp/herramientas-admin#configurarversionsiat) o
  `PUT /api/v1/admin/onboarding/companies/{id}/siat-version`), con lo que el XML lleva `subTotalNeto`, `totalPrecioNeto`, `iva`
  y `montoaPagar`. Cómo se interpreta `priceUnit` y qué sectores cubre: ver la entrada (7), que reemplaza a esta.
  Códigos nuevos en [errores](https://docs.cucu.bo/errors.md): `SIAT_V3_NO_DISPONIBLE`, `SECTOR_NOT_SUPPORTED_V3`. Sin **Requiere accion** hasta que el
  SIN fije la fecha de producción; lo anunciaremos aquí.

## 2026-10-28 (5)

* **Entrega por WhatsApp y Telegram, decidida por cada empresa.** Apagada por defecto. Se activa por el canal de soporte (tool admin [`configurarEntregaChat`](https://docs.cucu.bo/mcp/herramientas-admin#configurarentregachat) o `PUT /api/v1/admin/onboarding/tenants/{code}/entrega-chat`), eligiendo canales (`clientPhone` → WhatsApp, `clientTelegramUserId` → Telegram) y remitente: el número/bot de cucu o el propio de la empresa. Mientras no se active, enviar `clientPhone` no genera ningún mensaje.
* **`/f/{cuf}` se elimina** (`404`): cucu no tiene página de facturas. `verificationUrl` y `publicPageUrl` traen la consulta oficial del SIN (la URL del QR). **Requiere accion** si guardabas links `/f/`. Vigente en sandbox cuando el despliegue de `dev` termine; en producción, con la fecha de cierre de los links públicos.
* **`GET /api/v1/public/invoice/{cuf}` sin datos del comprador**: ya no trae `clientName` ni `clientDocument` (el CUF es derivable y la ruta no pide credencial). **Requiere accion** si los leías de ahí: usa `GET /api/v1/invoices/cuf/{cuf}` con tu API Key. Sandbox ahora; producción con el próximo despliegue.
* **Correo al comprador**: el botón "Verificar en Impuestos" lleva a la consulta del SIN (`verificationUrl`); si la factura no tiene QR, no hay botón.
* **Sandbox:** los links públicos `/api/v1/public/invoice/{cuf}/*` responden `410 PUBLIC_DOCUMENTS_GONE`. Prueba ahí la migración a [`documents`](https://docs.cucu.bo/api/get-invoice#descargar-documentos).

## 2026-10-28 (4)

### Documentos privados (fase 1)

**Requiere accion:** migrar de `pdfUrl` / links publicos a `documents` antes de la fecha que se anunciara. MCP `descargarDocumento` devuelve `download_url`.

* Nuevos `GET /api/v1/invoices/{id}/documents/{tipo}` y `GET /api/v1/invoices/cuf/{cuf}/documents/{tipo}` (`a4`, `ticket`, `ticketsiat`, `chititicket`, `xml`, `zip`), con API Key. Ver [Descargar documentos](https://docs.cucu.bo/api/get-invoice.md#descargar-documentos).
* Nuevo `GET|HEAD /api/v1/documents/t/{token}`: link temporal (300 s, 3 usos por defecto) para que agentes y apps descarguen el archivo.
* `InvoiceResponse` (REST y MCP) agrega `documents` y `verificationUrl`. `pdfUrl`, `ticketUrl`, `xmlUrl` y `publicPageUrl` quedan **deprecados** y se eliminaran al cierre de los links publicos. Los webhooks `invoice.*` agregan el bloque `documents`; `pdf_url` no cambia por ahora.
* MCP `descargarDocumento` devuelve `download_url`, `expires_at` y `max_uses` en lugar de `contentBase64` / `sizeBytes`, y suma los formatos `ticketsiat`, `chititicket` y `zip`.
* Links publicos: sin cambios por ahora, salvo `Cache-Control: public, no-cache` + `ETag` tambien en el XML (antes un año) y `409` en facturas sin documento. Al cerrarse responderan `410 PUBLIC_DOCUMENTS_GONE` y `/f/{cuf}` quedara solo para verificacion. Ver [Acceso publico](https://docs.cucu.bo/api/public-access.md#cierre-y-migración).
* Codigos nuevos en [errores](https://docs.cucu.bo/errors.md): `DOCUMENT_NOT_AVAILABLE`, `DOCUMENT_LINK_GONE`, `PUBLIC_DOCUMENTS_GONE`.

## 2026-10-28 (3)

* Se eliminaron `POST /api/v1/ai/normativas/load` y `POST /api/v1/ai/normativas/reload`. No estaban documentados y cualquier API Key podía dispararlos; la carga de normativa pasa a ser una operación de soporte (`indexarNormativa`, admin). `GET /api/v1/ai/normativas/search` y `/status` siguen igual.

## 2026-10-28

Segunda entrega del dia: endurecimiento de seguridad en MCP, webhooks, sincronizacion de catalogos y
logo del PDF.

### MCP: todas las herramientas exigen `X-API-Key`, utilidades incluidas

**Requiere accion** si algun cliente MCP llamaba a `buscarNormativa`, `verificarFormatoNit`,
`consultarDatosNit`, `erroresComunesSiat`, `buscarContingencias` o `calcularMontoLiteral` sin llave:
ahora se autentican como el resto de las 49 herramientas y, sin `X-API-Key` (o con una invalida), el
resultado de la herramienta es `Error: API Key requerido. Envie el header X-API-Key con su clave (sk_live_xxx o sk_test_xxx)`.
Cada llamada cuenta ademas para el limite de requests por minuto de la llave, el mismo que en REST.
`buscarContingencias` devuelve **solo** las contingencias del tenant autenticado. Ver
[Utilidades SIAT](https://docs.cucu.bo/mcp/herramientas-utilidades.md) y [Autenticacion MCP](https://docs.cucu.bo/mcp/advanced.md#autenticacion).

### Webhooks: politica de destinos en cada entrega, sin credenciales en la URL, `lastError` generico

**Requiere accion** si la URL de algun endpoint lleva credenciales embebidas
(`https://usuario:clave@host/...`): al registrar o actualizar se rechaza con `400`
`La URL del webhook no admite credenciales embebidas`. Pasa la autenticacion de tu receptor por la firma
`webhook-signature` o por un token en el path/query, y actualiza la URL con `PUT /api/v1/webhooks/endpoints/{id}`.

* La URL se valida con una unica politica al registrar/actualizar **y en cada entrega**: se resuelve el
  host, se valida cada IP y la conexion va a la IP validada. Un host que pasa a resolver a una direccion
  interna deja de recibir entregas (`lastError = "Destino del webhook no permitido"`).
* Rangos bloqueados ampliados: ademas de loopback y privadas, CGNAT (`100.64.0.0/10`), link-local y
  metadata (`169.254.0.0/16`), `0.0.0.0/8`, multicast, ULA (`fc00::/7`) e IPv4 embebida en IPv6
  (`::ffff:`, NAT64, 6to4) y Teredo. Lista completa en [Webhooks → Seguridad](https://docs.cucu.bo/api/webhooks.md#seguridad).
* `lastError` de una entrega ya no trae el detalle de la excepcion: toda falla de red es
  `No se pudo conectar con el endpoint o no respondió a tiempo`; un host que no resuelve o que apunta a una
  IP bloqueada es `Destino del webhook no permitido`; una respuesta no `2xx` es `HTTP <codigo>`.
  Si tu integracion parseaba `lastError` para distinguir timeout de conexion rechazada, ya no es posible.
* Las entregas no siguen redirecciones (`3xx` = `HTTP 3xx`, fallo) y no leen el cuerpo de tu respuesta.

### Sincronizacion de catalogos: POS de otro tenant responde `404`

**Requiere accion** si tu integracion esperaba `403`. En `POST /api/v1/siat/sync/all` y
`POST /api/v1/siat/sync/{catalog}`, un `posId` de otro tenant responde `404` `POS_NOT_FOUND`
(`Punto de venta no encontrado`), igual que uno inexistente. Antes era `403` `FORBIDDEN`. Ver
[Catalogos](https://docs.cucu.bo/api/catalogs.md#errores).

### Logo del PDF: solo el subido por storage

El PDF embebe unicamente el logo guardado por `POST /api/v1/storage/brand/logo` (bytes de la empresa o el
objeto bajo `brands/{tenantId}/`). Nunca se descarga desde una URL: si tu empresa tenia asociada una URL
externa como logo, el PDF sale sin logo hasta que subas el archivo. Ver [Storage](https://docs.cucu.bo/api/storage.md#subir-logo-de-empresa).

## 2026-10-28

### Emision mas rapida: la respuesta sale apenas el SIN acepta

`POST /api/v1/invoices` responde en cuanto el SIAT valida la factura (o queda en contingencia).
El PDF (A4 y ticket), la subida a storage, el email al cliente y la notificacion por chat pasan a
entregarse **despues**, por una cola propia con reintentos. Antes la respuesta esperaba a que todo
eso terminara (varios segundos).

* `pdfUrl`, `ticketUrl` y `xmlUrl` siguen siendo validas desde la respuesta. Si descargas el PDF
  antes de que la entrega lo haya guardado, se genera en el momento: la primera descarga tarda mas,
  nunca `404`. Si otro proceso ya lo esta generando: `503` `DOCUMENT_IN_PREPARATION` + `Retry-After`.
* El webhook `invoice.emitted` puede llegar **antes** que el email al cliente.
* Los PDF publicos ya no se cachean un ano: `Cache-Control: public, no-cache` + `ETag` por version
  (la misma URL sirve la vigente y la anulada). El XML sigue inmutable.
* La reversion de una anulacion reenvia la factura vigente por correo, con asunto
  `[ANULACIÓN REVERTIDA] ...`.

Revisa: si tu integracion asumia que al recibir el `201` (o el webhook) el cliente ya tenia el
correo, o que el PDF estaba listo al instante, ajustala. Ver [Crear factura](https://docs.cucu.bo/api/create-invoice.md#que-pasa-despues-de-la-respuesta).

### `idempotencyKey` en la factura; clave de una factura anulada → `422`

* La factura devuelve `data.idempotencyKey` (la `Idempotency-Key` con la que se emitio, `null` si no
  se envio) en la emision y en `GET /invoices/{id}`, `/cuf/{cuf}` y `/by-idempotency-key/{key}`.
* **Requiere accion.** Reusar la `Idempotency-Key` de una factura **anulada** responde `422` con
  `error.code = IDEMPOTENCY_KEY_CANCELLED`: no se devuelve la factura anulada como replay ni se emite
  otra con esa clave. Para facturar de nuevo, usa una clave nueva.

### `429` por credenciales invalidas repetidas desde una IP

**Requiere accion** si tu integracion rota llaves o reintenta un `401` en bucle. Mas de 20
credenciales invalidas por minuto desde una misma IP (API Keys en rutas protegidas, credenciales
admin, API Keys en `/mcp`) → esa IP recibe `429` con `Retry-After` presente la llave que presente,
hasta la proxima ventana. El trafico con una llave valida no lo activa, y una request sin credencial
no cuenta. Detalle en [Autenticacion](https://docs.cucu.bo/authentication.md#freno-a-credenciales-invalidas-429).

### Reenvio de factura por correo: destino validado y cupo por hora

**Requiere accion** si reenvias correos de forma automatica. `POST /api/v1/invoices/{id}/resend-email`:

* `?email=` tiene que ser **una sola** direccion valida; si no, `400`.
* Cupo de **5 reenvios por factura por hora** y **60 por tenant por hora**. Superado cualquiera:
  `429` `RATE_LIMITED` + `Retry-After`.

### Paginacion: tope de 100 por pagina

**Requiere accion** si pedias `size` mayor a 100. En `GET /invoices` y `GET /invoices/search`,
`size > 100` no es error: se sirven 100 y `data.size` informa el tamano efectivo. `size <= 0` → 20;
`page < 0` → 0. En MCP, `listarFacturas` sirve como maximo 50 (antes la documentacion REST decia
"max 50": el tope REST es 100).

### Los `500` devuelven una referencia, no el mensaje interno

`error.code = INTERNAL_ERROR`, `message = "Error interno del servidor"` y
`error.details = "ref=<uuid>"`. El detalle real queda solo en los logs de cucu bajo esa referencia:
mandala a soporte. Los `4xx` y los errores de dominio conservan su mensaje en `message`.

Ademas, la pagina de [Errores](https://docs.cucu.bo/errors.md) se corrigio para reflejar el sobre real: el objeto `error`
tiene `code` y, solo en `INTERNAL_ERROR`, `details`; **no** trae `field`, `siatCode` ni
`siatDescription`. El texto legible siempre va en `message`.

### Recursos de otro tenant responden `404`

**Requiere accion** si tu integracion esperaba `400` para "POS no encontrado". En `/api/v1/events/*`
y `/api/v1/massive/*`, un punto de venta, evento o lote inexistente **o de otro tenant** responde
`404` `NOT_FOUND` con el mismo mensaje. Antes era `400`. Lo mismo aplica a la empresa en la subida
de logo.

### Logo de empresa: PNG/JPEG/WebP verificados por contenido, 2 MiB

`POST /api/v1/storage/brand/logo`: el formato se detecta por los bytes del archivo (PNG, JPEG o
WebP), no por el `Content-Type`; el body va como `application/octet-stream`. Limite 2 MiB. Errores
nuevos: `400` `LOGO_INVALIDO`, `413` `LOGO_DEMASIADO_GRANDE`, `404` si la empresa no es tuya. La
clave en storage es `brands/{tenantId}/logo.{png|jpg|webp}`.

### Cabeceras de seguridad en las respuestas

Todas las respuestas traen `X-Content-Type-Options: nosniff`, `Referrer-Policy: no-referrer` y
`Strict-Transport-Security`. Las respuestas JSON traen ademas `X-Frame-Options: DENY` y una CSP
restrictiva; los documentos publicos y `/f/{cuf}` quedan fuera de esas dos para poder embeberse.
No requiere accion salvo que embebieras respuestas JSON de la API en un iframe.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.