# Errores, identificador de petición y reintentos

Cómo es un error de la API externa, qué significa cada errorCode, quién lo arregla, qué cabeceras leer y cuándo merece la pena reintentar.

Todos los errores de la API externa tienen la misma forma y casi todos traen un código estable, `errorCode`, para que tu programa decida sin leer el texto. Desde el sistema de pruebas, cada error de `/api/external/v1` dice además quién puede arreglarlo (`fixedBy`) y dónde está explicado (`docsUrl`): esa dirección lleva a su apartado de esta página.

Si es la primera vez que integras, o lo hace tu asistente de IA, empieza por [Integrar con un asistente de IA](https://os.utilia.ai/docs/developers/getting-started/ai-assistants): explica el ciclo completo de diagnosticar, comprobar y leer el error.

## La forma de un error

```json
{
  "statusCode": 403,
  "message": "A esta aplicación le falta el permiso de la API «Emitir facturas» (crm:invoices:issue) para esta operación. Lo concede un administrador de la organización en UTILIA OS, en la ficha de la aplicación, Configuración › Ajustes › Permisos.",
  "error": "FORBIDDEN",
  "errorCode": "INSUFFICIENT_SCOPE",
  "requiredScopes": ["crm:invoices:issue"],
  "fixedBy": "organization",
  "docsUrl": "https://os.utilia.ai/docs/developers/getting-started/errors-and-retries#insufficient_scope",
  "retryable": false,
  "timestamp": "2026-10-21T10:31:12.000Z",
  "requestId": "550e8400-e29b-41d4-a716-446655440000"
}
```

| Nombre | Tipo | Obligatorio | Descripción |
| - | - | - | - |
| `statusCode` | `number` | Sí | Código HTTP de la respuesta. |
| `message` | `string` | Sí | Explicación en castellano para tu registro o para una persona. Puede llevar datos del caso: no decidas por él. |
| `error` | `string` | Sí | Nombre del estado HTTP en mayúsculas (BAD_REQUEST, NOT_FOUND...). Para decidir está errorCode. |
| `errorCode` | `string` | No | Código estable del caso. Es lo que lee tu programa. Cada uno tiene su apartado en esta página. |
| `fixedBy` | `enum` | No | Quién puede arreglarlo: developer, organization o utilia. Solo en /api/external/v1. |
| `docsUrl` | `string` | No | Dirección pública de la explicación del código, con su ancla. Sin código, la del estado HTTP (#http-404). Solo en /api/external/v1. |
| `requestId` | `string` | No | Identificador de la petición, el mismo que la cabecera X-Request-Id. Cítalo al pedir ayuda. |
| `timestamp` | `string` | Sí | Fecha y hora de la respuesta (ISO 8601). |
| `requiredScopes` | `string[]` | No | Solo en el 403 INSUFFICIENT_SCOPE: los permisos de la API que le faltan a la aplicación. |
| `retryable` | `boolean` | No | En todo error de /api/external/v1. true: la misma petición puede funcionar si la repites pasado un rato (con la misma clave de idempotencia, si la lleva). false: repetirla no cambiará nada. Es el valor de cada código en este catálogo, salvo en los errores que deciden el suyo según el caso, como un PDF que todavía se está preparando. Manda sobre el estado HTTP. |
| `retryAfterSeconds` | `number` | No | Con retryable: true y en algunos 429, los segundos que conviene esperar antes de repetirla. |

Algunos errores añaden campos propios en la raíz, junto a `errorCode`: los de plan traen `moduleKey` o `capabilityKey` ([Errores de plan](https://os.utilia.ai/docs/developers/sdk/plan-errors)), los de facturas `missingFields`, `lineNumber` o `seriesCode`, y los de fusión `mergedIntoId`. Con los SDK se leen en `error.details`.

> **Nota: No pintes el objeto de error**
>
> El cuerpo del error es un objeto, no un texto. Si lo pasas tal cual a un aviso o a un componente de React, la pantalla se rompe con «Objects are not valid as a React child». Muestra `message`, decide con `errorCode` y guarda `requestId`.

## Quién lo arregla

`fixedBy` dice quién puede resolverlo, no de quién es la culpa. Es la primera pregunta que debe hacerse tu programa, y sobre todo tu asistente de IA: si no te toca a ti, cambiar el código no lo arregla.

**Los tres valores de fixedBy**

### developer (Quien programa)

Cambia la petición, la clave, el ritmo o el orden de las llamadas. Lo arreglas tú.

### organization (Un administrador de la organización)

Permisos de la aplicación, plan, facturación, persona responsable o dominios. Pídeselo; desde el código no se rodea.

### utilia (Nosotros)

Un fallo de nuestro lado o una función en «Próximamente». Si se repite, escríbenos con el requestId.

## Cabeceras que debes leer

| Nombre | Tipo | Obligatorio | Descripción |
| - | - | - | - |
| `X-Request-Id` | `string` | Sí | En todas las respuestas. Si tu petición trae uno (letras, números, guiones y guiones bajos, hasta 128 caracteres), se respeta; si no, se genera. |
| `X-Utilia-Quota-Limit` | `number` | No | Peticiones por hora de la clave de tu aplicación. En toda respuesta a partir de identificar la aplicación. |
| `X-Utilia-Quota-Remaining` | `number` | No | Las que te quedan en la ventana actual, contada ya esta petición. |
| `X-Utilia-Quota-Reset` | `number` | No | Cuándo se vacía el contador, en segundos UNIX. |
| `Retry-After` | `number` | No | En un 429: los segundos que debes esperar. En el cupo por hora es lo que de verdad le queda a la ventana; en el freno por minuto y en las pruebas, lo que queda de su minuto. |
| `X-RateLimit-Limit / -Remaining / -Reset` | `number` | No | Las del freno de ráfagas por minuto, no las del cupo por hora. Reset va en segundos relativos. En el 429 DIAGNOSTICS_RATE_LIMITED, las del cupo de esa prueba (6 o 5 al minuto), con Remaining a 0. |
| `X-Utilia-Ignored-Fields` | `string` | No | En una escritura que responde bien: los campos del cuerpo que se han descartado sin error, separados por coma y espacio (hasta 20, solo nombres, nunca valores). Casi siempre es un nombre mal escrito. |
| `X-Deprecation-Warning` | `string` | No | La llamada pasa sin un permiso que pronto será obligatorio. Nombra el permiso y la fecha. Nunca sale con un permiso crítico, que se exige siempre. |
| `X-Utilia-Api-Key-Expires` | `string` | No | Llamas con la clave anterior, durante una renovación: hasta cuándo vale. |

`X-Utilia-Ignored-Fields` es la más traicionera de olvidar: la respuesta es un `201` y el dato no se guarda. Si la ves, compara el nombre con la referencia de la ruta o pasa el cuerpo por [la comprobación de peticiones](https://os.utilia.ai/docs/developers/getting-started/check-request#comprobar-una-petición-sin-ejecutarla), que dice lo mismo antes de enviar nada. Los límites y sus cifras están en [Límites de uso y CORS](https://os.utilia.ai/docs/developers/getting-started/rate-limits-and-cors).

## Seguir una petición con X-Request-Id

Enviar tu propio identificador te permite seguir la misma petición en tus registros y en los nuestros: es lo primero que te pedirá soporte.

```http
X-Api-Key: utl_tu_clave
X-Request-Id: pedido-8812-intento-1
```

## Cuándo reintentar

Reintenta solo cuando el fallo puede desaparecer por sí solo. Todo error de `/api/external/v1` trae `retryable`, y manda él sobre el estado HTTP. Solo falta cuando la respuesta no es de la API: un fallo de red, un tiempo de espera agotado o una página de error de un intermediario.

**¿Repito la petición?**

¿El cuerpo del error trae retryable?

- true: **Repite la misma petición**. Espera retryAfterSeconds y mándala igual, con la misma idempotencyKey si la lleva.
- false: **No repitas**. Repetirla dará lo mismo. Lee errorCode y fixedBy.
- No lo trae: ¿Es un 429?
  - Sí: **Espera Retry-After** (`429`). Espera los segundos de la cabecera y reparte las llamadas en el tiempo.
  - No: ¿Es un 5xx o un fallo de red?
    - Sí: ¿Es una lectura o lleva idempotencyKey?
      - Sí: **Repite con espera creciente**. 1, 2, 4 y 8 segundos con algo de azar, cuatro intentos como mucho y siempre la misma idempotencyKey.
      - No: **Comprueba antes de repetir**. Una escritura sin clave, como POST /tickets, pudo hacerse. Búscala (por ejemplo, en los tickets del usuario) y repítela solo si no está.
    - No, es otro 4xx: **Corrige antes de repetir**. 400, 401, 403, 404, 409 y 422 no cambian por repetir: arregla la petición, la clave, el permiso o el plan.

_Una escritura sin clave de idempotencia pudo llegar a hacerse aunque no recibieras respuesta: compruébalo antes de repetirla._

Lo crítico, en una frase: nunca repitas una escritura con una clave de idempotencia nueva, y nunca repitas una que no lleva clave sin comprobar antes si llegó a hacerse. Un `5xx` o un corte de red no dicen si la operación se hizo: solo que no te llegó la respuesta. Los SDK, desde la 4.6.0, ya no repiten solos los `POST` sin clave de idempotencia y esperan lo que pide `Retry-After`; ver [Gestión de errores del SDK](https://os.utilia.ai/docs/developers/sdk/error-handling) y, para saber qué versión está publicada, [Versiones y compatibilidad](https://os.utilia.ai/docs/developers/sdk/versions).

### Repetir sin duplicar: idempotencyKey

En la API externa, la idempotencia es un **campo del cuerpo**, `idempotencyKey` (de 1 a 128 caracteres), y solo la admiten las operaciones que crean algo con dinero de por medio. No hay cabecera: `Idempotency-Key` no la lee ninguna ruta de `/api/external/v1`.

| Operación | `idempotencyKey` | Qué pasa al repetir con la misma |
| - | - | - |
| `POST /invoices` | Opcional | Devuelve la factura original con su `issuanceDeliveryStatus`. Vale 24 horas desde que se creó la factura; una emisión que no llegó a completarse no caduca, y repetirla consulta su estado sin reemitir ni reenviar nada. |
| `POST /invoices/:id/rectifications` | Obligatoria | Devuelve la misma rectificativa (`idempotent: true`) y reanuda lo que quedara pendiente. |
| `POST /invoices/:id/charge` | Opcional | Durante 24 horas, devuelve el cobro ya iniciado en lugar de volver a cobrar. |
| `POST /invoices/:id/refund` | Opcional | Durante 24 horas, devuelve el reembolso ya emitido. |
| `POST /subscriptions` | Opcional | Durante 24 horas, devuelve la suscripción creada. |
| `POST /invoices/:id/scheduled-charges` | Opcional | Durante 24 horas, devuelve el cobro programado ya creado. |

La clave identifica una operación junto con su cuerpo: la misma clave con otro cuerpo responde `409` (en facturas, rectificativas, cobros y reembolsos, [`BILLING_IDEMPOTENCY_CONFLICT`](#billing_idempotency_conflict)). Usa una por operación de tu lado (por ejemplo, el identificador de tu pedido) y guárdala antes de llamar, para repetir con la misma si no llega la respuesta. El resto de escrituras, como abrir un ticket o identificar a un usuario, no tienen clave de idempotencia.

> **Atención: Emitir una factura puede tardar varios segundos**
>
> Pon a tu cliente HTTP un tiempo de espera de al menos 30 segundos para `POST /external/v1/invoices`. Si aun así se corta, repite con la **misma** `idempotencyKey`: si la primera sigue en marcha, recibes la misma factura con `issuanceDeliveryStatus: processing`, nunca una segunda. Ver [Reintentar sin duplicar la factura](https://os.utilia.ai/docs/developers/api/invoices#reintentar-sin-duplicar-la-factura).

## Cómo leer el catálogo

Cada código tiene su apartado, con el ancla que usa `docsUrl` (el código en minúsculas, como `#insufficient_scope`). Debajo del nombre van tres datos: el estado HTTP, quién lo arregla (`fixedBy`) y si repetir la misma petición más tarde puede funcionar (el `retryable` que trae el código por defecto; los pocos errores que lo deciden según el caso lo explican en su apartado, y manda siempre el del cuerpo). Un código que no esté aquí se explica por [su estado HTTP](#estados-http-sin-código).

## Credencial y acceso

### API_KEY_REQUIRED

`401` · Lo arregla quien programa · No se reintenta

La petición no trae la clave de la aplicación en la cabecera `X-Api-Key`. Envíala en cada petición: `X-Api-Key: utl_...`.

### API_KEY_INVALID

`401` · Lo arregla quien programa · No se reintenta

La clave no existe, está desactivada o ha caducado. Comprueba que la copiaste entera y que es la vigente; si se renovó, usa la nueva. El mensaje es el mismo en los tres casos, a propósito.

### API_KEY_NOT_LINKED

`401` · Lo arregla quien programa · No se reintenta

La clave existe, pero no pertenece a ninguna aplicación externa: suele ser una clave de envío de leads (también empieza por `utl_`). Usa la clave de la aplicación, de su ficha en **Ajustes › Creadas por tu empresa**.

### API_KEY_WRONG_KIND

`401` · Lo arregla quien programa · No se reintenta

La credencial es de otro tipo: una clave de API de la organización (`utk_`) contra `/api/external/v1`, en `X-Api-Key` o en `Authorization`. La `utk_` va en `Authorization: Bearer` contra las rutas de la aplicación (`/api/...`); aquí va la clave de la aplicación externa (`utl_`) en `X-Api-Key`. También lo recibes al revés: una `utl_` en `Authorization: Bearer` contra una ruta de la aplicación. Cuál elegir está en [Integrar con un asistente de IA](https://os.utilia.ai/docs/developers/getting-started/ai-assistants#qué-credencial-usar).

### API_KEY_WRONG_HEADER

`401` · Lo arregla quien programa · No se reintenta

La clave de la aplicación ha llegado en `Authorization: Bearer`. Envíala en la cabecera `X-Api-Key`.

### API_KEY_IN_URL_NOT_ALLOWED

`401` · Lo arregla quien programa · No se reintenta

La clave llegó en la dirección, donde queda en registros, servidores intermedios e historial. Da igual el nombre del parámetro (`apiKey`, `api_key`, `x-api-key`...) y también cuenta una clave `utl_…` escrita como valor de otro parámetro. Envíala en `X-Api-Key`; para las conexiones de eventos en tiempo real, pide antes un pase de conexión. Los SDK lo hacen solos. La forma antigua, `?apiKey=` en una conexión de eventos en tiempo real, se admite todavía y también dejará de admitirse: ver [Eventos en tiempo real](https://os.utilia.ai/docs/developers/getting-started/authentication#eventos-en-tiempo-real-un-pase-en-lugar-de-la-clave).

Si la petición trae la clave en la dirección **y** en la cabecera `X-Api-Key`, de momento entra por la cabecera, pero el diagnóstico avisa con el hallazgo [`key-in-url`](https://os.utilia.ai/docs/developers/getting-started/testing#key-in-url). Arréglalo cuanto antes, porque dejará de admitirse, y pide que se renueve la clave: la que viajó en la dirección puede estar expuesta.

### STREAM_TOKEN_INVALID

`401` · Lo arregla quien programa · No se reintenta

El pase de conexión no es válido, ya se usó o ha caducado. Cada pase sirve una vez: pide uno nuevo para cada conexión.

### STREAM_TOKEN_UNAVAILABLE

`503` · Lo arreglamos nosotros · Se puede reintentar

Ahora mismo no se puede emitir el pase de conexión. Repítelo a los pocos segundos.

### STREAM_TOKEN_USER_REQUIRED

`400` · Lo arregla quien programa · No se reintenta

En una aplicación con widget, el pase de la conexión de eventos de tickets necesita el usuario (`userId`). Añádelo a la petición del pase.

### WIDGET_IDENTITY_REQUIRED

`403` · Lo arregla quien programa · No se reintenta

La aplicación exige verificar a la persona del widget y la petición trae un `userId` sin su firma en la cabecera `X-Utilia-User-Signature`. Calcula en tu servidor la firma del `userId` con el secreto del widget (HMAC-SHA256 en hexadecimal) y pásasela al widget en `userSignature`. Ver [Verificar la identidad en el widget](https://os.utilia.ai/docs/developers/widget/identity-verification).

### WIDGET_IDENTITY_INVALID

`403` · Lo arregla quien programa · No se reintenta

La firma de `X-Utilia-User-Signature` no corresponde a ese `userId`: se calculó para otro usuario, con un secreto que ya no vale o sobre un valor distinto del `userId` que manda el widget. Vuelve a calcularla en tu servidor con el secreto del widget vigente, sobre el `userId` exacto que recibe el widget.

### WIDGET_IDENTITY_SECRET_MISSING

`403` · Lo arregla la organización · No se reintenta

La aplicación exige verificar a la persona del widget y todavía no tiene secreto del widget, así que ninguna petición con `userId` puede pasar. Un administrador de la organización lo genera en la ficha de la aplicación y te lo pasa para firmar en tu servidor. Mientras tanto, la verificación se puede poner en el modo que solo registra los fallos.

### APP_DISABLED

`403` · Lo arregla la organización · No se reintenta

La aplicación está desactivada en UTILIA OS y todas sus llamadas se rechazan. La activa un administrador desde la ficha de la aplicación. También el diagnóstico (`GET /external/v1/diagnostics`) responde este 403: el hallazgo [`app-disabled`](https://os.utilia.ai/docs/developers/getting-started/testing#app-disabled) solo se ve en la pestaña **Pruebas** del panel y por MCP.

### APP_RESPONSIBLE_INACTIVE

`403` · Lo arregla la organización · No se reintenta

La aplicación no tiene una persona responsable activa: nunca la tuvo, o está dada de baja o bloqueada. Sin ella se rechaza toda llamada, también la del diagnóstico. Un administrador nombra otra en la ficha de la aplicación. En el panel y por MCP, el diagnóstico lo explica con [`responsible-missing`](https://os.utilia.ai/docs/developers/getting-started/testing#responsible-missing) o [`responsible-inactive`](https://os.utilia.ai/docs/developers/getting-started/testing#responsible-inactive).

### APP_RESPONSIBLE_PERMISSION_DENIED

`403` · Lo arregla la organización · No se reintenta

La persona responsable no tiene en UTILIA OS el permiso que exige esta operación sobre ese registro. Lo comprueban las rectificativas, los cambios de una suscripción y los cobros programados; el resto de la API se rige por los permisos de la API de la aplicación. Se arregla en los permisos de esa persona, no en los de la aplicación.

### ORIGIN_NOT_ALLOWED

`403` · Lo arregla la organización · No se reintenta

Dos casos con el mismo código: el origen del navegador no está entre los dominios autorizados de la aplicación, o la operación solo se admite de servidor a servidor (como emitir una rectificativa) y llegó desde un navegador. El mensaje dice cuál. En el primero, un administrador añade el dominio; en el segundo, llama desde tu servidor.

### CORS_ORIGIN_NOT_ALLOWED

`403` · Lo arregla la organización · No se reintenta

El navegador llama desde un origen que la plataforma no tiene autorizado. Ver [Límites de uso y CORS](https://os.utilia.ai/docs/developers/getting-started/rate-limits-and-cors).

### RATE_LIMIT_EXCEEDED

`429` · Lo arregla quien programa · Se puede reintentar

Se ha agotado un cupo por hora: el de la aplicación, o el propio de una ruta (contactos, leads, clientes, sincronizar, buscar por correo o por NIF, o enviar facturas por correo). El mensaje dice cuál. Si es el de la aplicación, `X-Utilia-Quota-Remaining` te avisa antes de llegar; si llega con `X-Utilia-Quota-Remaining` mayor que cero, es el de la ruta. Espera los segundos de `Retry-After` (o de `retryAfterSeconds`, si viene en el cuerpo) y reparte las llamadas. Cada cupo y su cifra están en [Los límites de un vistazo](https://os.utilia.ai/docs/developers/getting-started/rate-limits-and-cors#los-límites-de-un-vistazo).

### INSUFFICIENT_SCOPE

`403` · Lo arregla la organización · No se reintenta

A la aplicación le falta un permiso de la API para esta operación. `requiredScopes` dice cuál, y el mensaje lo nombra con su etiqueta. Lo concede un administrador en los permisos de la API de la aplicación; si no lo necesitas, deja de llamar a esa ruta. El catálogo de permisos está en [Permisos de la API](https://os.utilia.ai/docs/developers/api/scopes).

El 403 solo nombra el permiso cuando depende de lo que pides: la ruta, un `?include=` o un filtro. Cuando depende del dato, no hay 403: un contacto dado de baja o inactivo, leído sin `crm:contacts:read:opted-out`, responde [`404 CONTACT_NOT_FOUND`](#contact_not_found), igual que uno que no existe, para no confirmar que esa persona existe y se ha dado de baja.

Si UTILIA OS abre un periodo de transición para un permiso nuevo, la llamada sin él pasa hasta esa fecha con la cabecera `X-Deprecation-Warning`. Los permisos críticos, los de datos personales sensibles y los financieros (cobrar, devolver dinero, anular o rectificar facturas, entre otros), no tienen transición nunca: sin ellos, la llamada responde este 403 desde la primera vez.

### SCOPE_NOT_ALLOWED_FROM_BROWSER

`403` · Lo arregla quien programa · No se reintenta

La operación exige un permiso que no se usa con una clave que va en una página web. Una clave así la puede copiar cualquiera, así que desde un navegador solo valen los permisos del widget: tickets, sugerencias de IA de los tickets, archivos, `users.identify` y el envío de errores. Vale para cualquier aplicación, tenga o no dominios autorizados. `notAllowedScopes` dice qué permisos lo impiden. Llama a esa operación desde tu servidor, con la clave de una aplicación sin dominios autorizados. Una clave publicada (con dominios autorizados o usada desde un navegador) solo debe tener esos permisos: si tiene otros, retíralos o pasa esas operaciones a otra aplicación, porque la llamada puede responder este mismo 403 aunque no venga de un navegador. Ver [Claves dentro de una página web](https://os.utilia.ai/docs/developers/getting-started/rate-limits-and-cors#claves-dentro-de-una-página-web).

### SCOPE_NOT_ALLOWED_WITH_ALLOWED_ORIGINS

`400` · Lo arregla la organización · No se reintenta

No lo devuelve `/api/external/v1`: llega al configurar la aplicación en el panel o por MCP. Una aplicación cuya clave está publicada (tiene dominios autorizados o se ha usado desde un navegador) no puede recibir permisos fuera de los del widget, y a una que los tiene no se le pueden añadir dominios. `notAllowedScopes` dice cuáles. Crea otra aplicación, sin dominios autorizados, para usar esos permisos desde tu servidor; quitar permisos o dominios se admite siempre, pero quitar los dominios no basta para conceder después un permiso de servidor: la clave estuvo en una página y hay que renovarla sin transición. Ver [Claves dentro de una página web](https://os.utilia.ai/docs/developers/getting-started/rate-limits-and-cors#claves-dentro-de-una-página-web).

### AUTH_TOKEN_INVALID

`401` · Lo arregla quien programa · No se reintenta

No lo devuelve `/api/external/v1`: llega de las rutas de la aplicación (`/api/...`) cuando la cabecera `Authorization` no trae una sesión válida (falta, está mal formada o ha caducado). Si llamas con una clave de API de la organización, sus errores propios están en [su tabla](#claves-de-api-de-la-organización).

## Plan y cuenta de la organización

### MODULE_NOT_ENABLED

`403` · Lo arregla la organización · No se reintenta

El plan de la organización no incluye el aplicativo que usa esta operación. El cuerpo trae `moduleKey`. No se arregla reintentando ni pidiendo permisos: lo resuelve el plan.

### CAPABILITY_NOT_ENABLED

`403` · Lo arregla la organización · No se reintenta

El plan de la organización no incluye la función que usa esta operación. El cuerpo trae `capabilityKey`.

### FEATURE_COMING_SOON

`403` · Lo arreglamos nosotros · No se reintenta

La función está en «Próximamente»: todavía no está disponible. No es un bloqueo de plan ni de permisos. Cuando esté disponible, la misma petición funcionará sin cambios.

### TENANT_SUSPENDED

`403` · Lo arregla la organización · No se reintenta

La cuenta de la organización está suspendida. Las consultas siguen funcionando; lo que crea o cambia datos se rechaza.

### TENANT_CANCELLED

`403` · Lo arregla la organización · No se reintenta

La cuenta de la organización está dada de baja. Como en la suspensión, las consultas siguen funcionando y las escrituras no.

### TENANT_TERMINATION_READ_ONLY

`403` · Lo arregla la organización · No se reintenta

La organización está cerrando su cuenta y solo admite consultas y exportaciones.

## Datos de la petición

### VALIDATION_ERROR

`400` · Lo arregla quien programa · No se reintenta

Los datos no cumplen las reglas de la operación. El mensaje dice qué campo falla. Para encontrarlos todos de una vez, pasa el cuerpo por [la comprobación de peticiones](https://os.utilia.ai/docs/developers/getting-started/check-request#comprobar-una-petición-sin-ejecutarla): devuelve cada error con la ruta del campo.

Los campos que la operación no declara no dan este error: se descartan y la respuesta lo dice en `X-Utilia-Ignored-Fields`. Las rectificativas son la excepción y rechazan cualquier campo fuera del contrato. Ver [Dónde va cada parámetro](https://os.utilia.ai/docs/developers/getting-started/parameters).

Un campo obligatorio que no llega dice «Falta este campo obligatorio», nunca la regla de longitud o de formato que tenga después.

### INVALID_JSON

`400` · Lo arregla quien programa · No se reintenta

El cuerpo declara `Content-Type: application/json`, pero no se puede leer como JSON. El mensaje dice la línea y la columna donde falla y qué suele pasar ahí (comillas simples, una coma de más antes de cerrar, una llave sin cerrar); `details` trae `position`, `line` y `column` cuando se saben. Genera el cuerpo con el serializador de tu lenguaje (`JSON.stringify`, `json.dumps`) en vez de escribirlo a mano.

### JSON_CONTENT_TYPE_REQUIRED

`400` · Lo arregla quien programa · No se reintenta

La petición trae cuerpo, pero sin la cabecera `Content-Type: application/json`, así que el cuerpo no se ha leído y todos los campos aparecen como si faltaran. Pasa también con `curl -d` sin `-H "Content-Type: application/json"`, que envía el JSON como formulario. Añade la cabecera; los SDK la ponen solos.

### METHOD_NOT_ALLOWED

`405` · Lo arregla quien programa · No se reintenta

La dirección existe, pero no con ese método. La cabecera `Allow` y el campo `allowedMethods` dicen con cuáles; por ejemplo, `DELETE /external/v1/users` responde que existe con `GET`. Si la dirección no existe con ningún método, la respuesta es `404`.

### REQUEST_BODY_TOO_LARGE

`413` · Lo arregla quien programa · No se reintenta

El cuerpo pasa del tamaño máximo que se admite; `details.limitBytes` trae el tope. Los archivos no van dentro del JSON: súbelos por su ruta de subida y manda solo su identificador. «Comprobar una petición» tiene su propio tope, más pequeño.

## Usuarios externos y tickets

### USER_NOT_FOUND

`404` · Lo arregla quien programa · No se reintenta

El usuario externo no existe. Dalo de alta antes con `POST /external/v1/users/identify`. Lo devuelven las rutas de tickets e IA.

### EXTERNAL_USER_NOT_FOUND

`404` · Lo arregla quien programa · No se reintenta

Lo mismo que `USER_NOT_FOUND`, en las rutas de facturación y métodos de pago. Trátalos igual.

### TICKET_NOT_FOUND

`404` · Lo arregla quien programa · No se reintenta

El ticket no existe o no es de este usuario.

### TICKET_ACCESS_DENIED

`403` · Lo arregla quien programa · No se reintenta

El usuario no tiene acceso a este ticket.

### TICKET_CLOSED

`403` · Lo arregla quien programa · No se reintenta

El ticket está cerrado y no admite mensajes nuevos. Reábrelo si su estado lo permite.

### TICKET_CANNOT_REOPEN

`404` · Lo arregla quien programa · No se reintenta

El ticket no existe o su estado no permite reabrirlo.

## Archivos e IA

### FILE_MISSING

`400` · Lo arregla quien programa · No se reintenta

La petición no incluye ningún archivo.

### FILE_INVALID_MIME

`400` · Lo arregla quien programa · No se reintenta

Ese tipo de archivo no está permitido en la aplicación.

### FILE_TOO_LARGE

`400` o `413` · Lo arregla quien programa · No se reintenta

El archivo supera el tamaño máximo configurado en la aplicación.

### FILE_TOO_MANY

`400` · Lo arregla quien programa · No se reintenta

La subida trae más archivos de los que admite la operación.

### FILE_INVALID_ATTACHMENTS

`400` · Lo arregla quien programa · No se reintenta

Algún adjunto no es válido o no pertenece a la aplicación.

### FILE_QUOTA_EXCEEDED

`403` · Lo arregla la organización · No se reintenta

La aplicación ha agotado su espacio de almacenamiento.

### FILE_NOT_FOUND

`404` · Lo arregla quien programa · No se reintenta

El archivo no existe.

### AI_NO_INPUT

`400` · Lo arregla quien programa · No se reintenta

Hay que enviar texto, audio o imágenes.

### AI_AUDIO_TOO_LARGE

`400` · Lo arregla quien programa · No se reintenta

El audio es demasiado grande para la transcripción directa. Usa `POST /external/v1/tickets/ai-transcribe-async`.

### EXTERNAL_AI_DISABLED

`403` · Lo arregla la organización · No se reintenta

La organización tiene desactivada la IA para las aplicaciones externas.

### AI_BUDGET_EXHAUSTED

`403` · Lo arregla la organización · No se reintenta

Se ha agotado el saldo de IA disponible para esta operación.

## Facturación de la aplicación

Para descubrir estos errores sin emitir nada, valida la factura antes: [Probar la facturación sin emitir facturas](https://os.utilia.ai/docs/developers/api/invoice-testing).

### BILLING_NOT_ENABLED

`403` · Lo arregla la organización · No se reintenta

La facturación de la aplicación está desactivada. La activa un Super Administrador en **Dinero › Facturación** de la ficha de la aplicación, con un motivo. Mientras tanto, `POST /external/v1/invoices/validate` funciona y lo dice como bloqueo.

### BILLING_AMOUNT_EXCEEDS_LIMIT

`403` o `422` · Lo arregla la organización · No se reintenta

El importe supera el tope configurado para la facturación de la aplicación.

### BILLING_IDEMPOTENCY_CONFLICT

`409` · Lo arregla quien programa · No se reintenta

La clave de idempotencia ya se usó con otros datos. Una clave identifica una operación con su cuerpo: si cambias el cuerpo, es otra operación y necesita otra clave.

### BILLING_CURRENCY_NOT_SUPPORTED

`409` · Lo arregla quien programa · No se reintenta

Esa moneda no se admite en esta operación.

### INVOICE_NOT_FOUND

`404` · Lo arregla quien programa · No se reintenta

La factura no existe.

### INVOICE_NOT_ACCESSIBLE

`404` · Lo arregla quien programa · No se reintenta

La factura no existe o no es de esta aplicación. El mensaje es el mismo en los dos casos.

### INVOICE_STATE_INVALID

`409` · Lo arregla quien programa · No se reintenta

El estado de la factura no permite esta operación.

### INVOICE_ALREADY_CANCELLED

`409` · Lo arregla quien programa · No se reintenta

La factura ya está anulada.

### INVOICE_ALREADY_PAID

`409` · Lo arregla quien programa · No se reintenta

La factura ya está cobrada.

### INVOICE_PDF_NOT_AVAILABLE

`503` o `409` · Lo arreglamos nosotros · Se puede reintentar

El PDF de la factura todavía no está disponible. Con `503` y `retryable: true`, repite pasados `retryAfterSeconds` (15 si se está preparando, 120 si el almacén no responde). Con `409` y `retryable: false`, el PDF necesita que lo revise el equipo de la organización.

### INVOICE_PDF_PENDING

`409` · Lo arreglamos nosotros · Se puede reintentar

Al descargar el PDF de una factura emitida (`GET /external/v1/invoices/:id/pdf`): el PDF original todavía se está preparando. Repite la descarga en unos segundos.

### INVOICE_PDF_REVIEW_REQUIRED

`409` · Lo arregla la organización · No se reintenta

El PDF original de la factura no se puede verificar y el equipo de la organización tiene que revisarlo antes de descargarlo o enviarlo. Repetir no lo arregla.

### INVOICE_PDF_STORAGE_UNAVAILABLE

`503` · Lo arreglamos nosotros · Se puede reintentar

El almacén de documentos no ha respondido al pedir el PDF. Repite la descarga más tarde.

### INVOICE_EMAIL_NOT_SENT

`502` · Lo arreglamos nosotros · Se puede reintentar

La factura está emitida, pero el correo no ha salido. Con `retryable: true` el servidor de correo no responde; con `retryable: false`, el servidor de salida rechazó el envío. La factura no se vuelve a crear nunca: se reenvía.

### INVOICE_EMAIL_NO_RECIPIENT

`422` · Lo arregla quien programa · No se reintenta

No hay ninguna dirección a la que enviar la factura.

### INVOICE_EMAIL_RECIPIENT_NOT_ALLOWED

`422` · Lo arregla quien programa · No se reintenta

Un destinatario no es una dirección conocida de la factura.

### INVOICE_PUBLIC_LINK_NOT_AVAILABLE

`404` · Lo arregla quien programa · No se reintenta

La factura no tiene un enlace público disponible.

### INVOICE_RECIPIENT_REQUIRED

`400` · Lo arregla quien programa · No se reintenta

Una factura ordinaria necesita el bloque `recipient`. Para un consumidor sin datos, emite una simplificada.

### INVOICE_RECIPIENT_INCOMPLETE

`400` · Lo arregla quien programa · No se reintenta

Faltan datos del destinatario. `missingFields` dice cuáles.

### SIMPLIFIED_INVOICE_AMOUNT_EXCEEDED

`422` · Lo arregla quien programa · No se reintenta

Una factura simplificada no puede superar 400 € impuestos incluidos. Emite una ordinaria.

### SIMPLIFIED_RECIPIENT_INCOMPLETE

`400` · Lo arregla quien programa · No se reintenta

La simplificada trae el NIF del destinatario sin el resto de sus datos. `missingFields` dice cuáles.

### TAX_INCLUSIVE_PRICE_NOT_REPRESENTABLE

`422` · Lo arregla quien programa · No se reintenta

Con precios con impuestos incluidos, el importe de una línea no tiene base exacta al céntimo. `lineNumber` dice cuál (empieza en 1).

### TAX_INCLUSIVE_GLOBAL_DISCOUNT_UNSUPPORTED

`400` · Lo arregla quien programa · No se reintenta

Los precios con impuestos incluidos no admiten un descuento global.

### TAX_SYSTEM_RESIDENCE_MISMATCH

`400` · Lo arregla quien programa · No se reintenta

La residencia del destinatario no cuadra con el sistema fiscal de la factura.

### IGIC_SMALL_BUSINESS_TAXED_LINE

`400` · Lo arregla quien programa · No se reintenta

En el régimen del pequeño empresario del IGIC, las líneas no llevan cuota (tampoco `IGIC_0`). `lineNumber` dice cuál.

### IGIC_SMALL_BUSINESS_REQUIRES_IGIC

`400` · Lo arregla quien programa · No se reintenta

La exención del pequeño empresario solo vale en facturas con IGIC.

### IGIC_SMALL_BUSINESS_NOT_ACTIVE

`400` · Lo arregla la organización · No se reintenta

La fecha de la factura está fuera de los periodos del régimen del pequeño empresario que tiene configurados la organización.

### INVOICE_SERIES_NOT_AVAILABLE

`400` · Lo arregla quien programa · No se reintenta

La serie de `seriesCode` no existe o no es de esta aplicación. El mensaje es el mismo en los dos casos.

### INVOICE_SERIES_TYPE_MISMATCH

`400` · Lo arregla quien programa · No se reintenta

La serie es de otro tipo de documento (una de ordinarias para una simplificada, o al revés).

### INVOICE_SERIES_INACTIVE

`409` · Lo arregla la organización · No se reintenta

La serie de facturación está desactivada.

### INVOICE_ISSUE_DATE_OUT_OF_ORDER

`400` · Lo arregla quien programa · No se reintenta

`issueDate` es futura o anterior a la última factura de la serie. Trae `issueDate` y `today`, o `lastIssueDate` y `seriesCode`. No se numera nada.

### INVOICE_NUMBERING_UNAVAILABLE

`500` · Lo arreglamos nosotros · Se puede reintentar

La numeración no se pudo preparar y la emisión no llegó a empezar: no se reservó número ni se creó nada. Repetir la petición, con la misma clave de idempotencia, es seguro.

### INVOICE_ISSUANCE_REVIEW_REQUIRED

`500` · Lo arregla la organización · No se reintenta

La factura quedó emitida, pero su cierre fiscal no terminó y pasa a revisión del equipo de la organización. Repetir con la misma clave devuelve esa factura en revisión, nunca otra. No la emitas de nuevo con otra clave.

### RECTIFICATION_NOT_ALLOWED

`409` · Lo arregla quien programa · Según el cuerpo

La factura no admite una rectificación ahora; el mensaje dice por qué. Con `retryable: true` (la emisión o el PDF original siguen en curso) basta con repetirla pasados `retryAfterSeconds`. Ver [Cuándo se puede rectificar](https://os.utilia.ai/docs/developers/api/invoice-rectifications#cuándo-se-puede-rectificar).

### RECTIFICATION_AMOUNT_EXCEEDS_REMAINING

`409` · Lo arregla quien programa · No se reintenta

El importe supera lo que queda por rectificar. Trae `remainingRectifiableAmount`.

### RECTIFICATION_AMOUNT_NOT_REPRESENTABLE

`422` · Lo arregla quien programa · No se reintenta

El importe no se puede repartir al céntimo entre los tramos fiscales de la factura.

### RECTIFICATION_HISTORY_REVIEW_REQUIRED

`409` · Lo arregla la organización · No se reintenta

Las rectificaciones anteriores no cuadran y el equipo de la organización tiene que revisarlas antes de rectificar otra vez.

### RECTIFICATION_OPERATION_INCOMPLETE

`500` · Lo arreglamos nosotros · Según el cuerpo

La rectificación no terminó de registrarse por un fallo interno. Con `retryable: true`, repetirla con la **misma** clave de idempotencia la reanuda sin duplicar nada; con `retryable: false`, la operación necesita que la revise el equipo.

### REFUND_NOT_ALLOWED

`409` · Lo arregla quien programa · No se reintenta

Esta factura no admite un reembolso: no tiene cobros completados o el importe supera lo cobrado.

### REFUND_EXCEEDS_COLLECTED

`409` · Lo arregla quien programa · No se reintenta

La devolución supera lo cobrado fuera de la pasarela de UTILIA OS. Trae `refundableAmount`.

### REFUND_REFERENCE_ALREADY_USED

`409` · Lo arregla quien programa · No se reintenta

La referencia de la devolución ya figura en otro reembolso de la organización, también si lo registró a mano su equipo.

### CLIENT_NOT_ACCESSIBLE

`404` · Lo arregla quien programa · No se reintenta

El cliente no existe o no está al alcance de esta aplicación.

## Cobros, métodos de pago y suscripciones

### STRIPE_ACCOUNT_NOT_READY

`409` · Lo arregla la organización · No se reintenta

La cuenta de cobros de la organización todavía no está lista para cobrar.

### PAYMENT_METHOD_NOT_FOUND

`404` · Lo arregla quien programa · No se reintenta

El método de pago no existe.

### PAYMENT_METHOD_INACTIVE

`409` · Lo arregla quien programa · No se reintenta

El método de pago ya no está activo.

### PAYMENT_METHOD_OWNERSHIP

Lo arregla quien programa · No se reintenta

El método de pago es de otro usuario.

### PAYMENT_METHOD_ACTIVE_INTENT

`409` · Lo arregla quien programa · Se puede reintentar

El método de pago tiene un cobro en curso. Repite cuando termine.

### PAYMENT_AUTHORIZATION_REQUIRED

`409` · Lo arregla quien programa · No se reintenta

Para cobrar sin la persona delante hace falta su autorización de cobro. Pídesela con su consentimiento.

### AUTO_CHARGE_DISABLED_FOR_ORGANIZATION

`409` · Lo arregla la organización · No se reintenta

La organización tiene desactivado el cobro automático con tarjeta. No es lo mismo que la falta de autorización del usuario: recoger otro consentimiento no lo desbloquea.

### PAYMENT_AUTHORIZATION_NOT_FOUND

`404` · Lo arregla quien programa · No se reintenta

La autorización de cobro no existe, o es de otro usuario o de otra aplicación.

### PAYMENT_AUTHORIZATION_NOT_ACTIVE

`409` · Lo arregla quien programa · No se reintenta

La autorización ya no sirve para cobrar: fue revocada, se consumió o su tarjeta caducó.

### PAYMENT_AUTHORIZATION_CONSENT_MISMATCH

`422` · Lo arregla quien programa · No se reintenta

El texto o la versión del consentimiento no coinciden con el vigente. Vuelve a pedir el texto y muéstralo tal cual.

### SUBSCRIPTION_NOT_ACCESSIBLE

`404` · Lo arregla quien programa · No se reintenta

La suscripción no existe, no es de esta aplicación o no es de este usuario.

### SUBSCRIPTION_STATE_INVALID

`409` · Lo arregla quien programa · No se reintenta

El estado de la suscripción no permite esta operación: pausar una ya pausada, reanudar una cancelada o cambiar una terminada.

### SCHEDULED_CHARGE_NOT_ACCESSIBLE

`404` · Lo arregla quien programa · No se reintenta

El cobro programado no existe o no es de esta aplicación.

### SCHEDULED_CHARGE_NOT_CANCELLABLE

`409` · Lo arregla quien programa · No se reintenta

El cobro programado ya no se puede cancelar: su ventana se cerró o ya se está ejecutando.

### SCHEDULED_CHARGE_RETRY_TOO_SOON

`409` · Lo arregla quien programa · Se puede reintentar

Es pronto para reintentar este cobro: el motor admite 5 reintentos manuales cada 24 horas. Repetir en bucle no lo acelera.

### CHARGE_FAILED

`402` · Lo arregla quien programa · No se reintenta

La pasarela ha rechazado el cobro. El cuerpo trae `failureCode` y `failureMessage`.

### CHARGE_REQUIRES_ACTION

`409` · Lo arregla quien programa · No se reintenta

El banco exige que la persona confirme el cobro. No es un fallo de tu aplicación: lleva a tu usuario al enlace público de pago.

### OUTBOUND_DOMAIN_NOT_ALLOWED

`403` · Lo arregla la organización · No se reintenta

El destino no está en la lista de dominios salientes autorizados de la organización. Lo añade un administrador.

## CRM, chat y presupuestos

### CONTACT_NOT_FOUND

`404` · Lo arregla quien programa · No se reintenta

El contacto no existe, es de otra organización o la aplicación no puede leerlo: un contacto dado de baja o inactivo responde igual si la aplicación no tiene `crm:contacts:read:opted-out`. El cuerpo es el mismo en los tres casos. Si esperabas encontrarlo, pide ese permiso a un administrador.

### LEAD_NOT_FOUND

`404` · Lo arregla quien programa · No se reintenta

El lead no existe.

### CLIENT_NOT_FOUND

`404` · Lo arregla quien programa · No se reintenta

El cliente no existe.

### CONTACT_MERGED

`409` · Lo arregla quien programa · No se reintenta

El contacto se fusionó con otro y no se ha escrito nada. Usa el identificador que indica la respuesta.

### CLIENT_MERGED

`409` · Lo arregla quien programa · No se reintenta

El cliente se fusionó con otro y no se ha escrito nada. Usa `mergedIntoId`, en la raíz de la respuesta.

### LEAD_ALREADY_CONVERTED

`409` · Lo arregla quien programa · No se reintenta

El lead ya está convertido.

### LEAD_ALREADY_QUALIFIED

`409` · Lo arregla quien programa · No se reintenta

El lead ya está cualificado.

### LEAD_ALREADY_DISQUALIFIED

`409` · Lo arregla quien programa · No se reintenta

El lead ya está descartado.

### LEAD_ALREADY_LOST

`409` · Lo arregla quien programa · No se reintenta

El lead ya está marcado como perdido.

### LEAD_CONVERSION_REQUIRES_REASON

`422` · Lo arregla quien programa · No se reintenta

Para convertir este lead hace falta un motivo (`reason`, de 20 caracteres como mínimo). Queda en el registro de auditoría.

### LEAD_UPDATE_CONFLICT

`409` · Lo arregla quien programa · No se reintenta

El lead ha cambiado desde que lo leíste. Vuelve a leerlo y repite con su `updatedAt` nuevo en `expectedUpdatedAt`.

### CLIENT_UPDATE_CONFLICT

`409` · Lo arregla quien programa · No se reintenta

El cliente ha cambiado desde que lo leíste. Vuelve a leerlo antes de guardar.

### EXTERNAL_APP_CREATOR_MISSING

`422` · Lo arregla la organización · No se reintenta

La aplicación no tiene una persona creadora válida en UTILIA OS, y la operación (como convertir un lead) la necesita.

### CHANNEL_NOT_FOUND

`404` · Lo arregla quien programa · No se reintenta

El canal no existe.

### MESSAGE_NOT_FOUND

`404` · Lo arregla quien programa · No se reintenta

El mensaje no existe.

### APP_NOT_AUTHORIZED_IN_CHANNEL

`403` · Lo arregla la organización · No se reintenta

La aplicación tiene el permiso de la API, pero no está autorizada en ese canal. El permiso autoriza la acción; el canal se concede aparte.

### BUDGET_NOT_FOUND

`404` · Lo arregla quien programa · No se reintenta

El presupuesto no existe. El mismo 404 si es de otra organización.

### BUDGET_SIGNATURE_NOT_FOUND

`404` · Lo arregla quien programa · No se reintenta

La firma no existe en ese presupuesto.

### BUDGET_COMMENT_VISIBILITY_NOT_ALLOWED

`403` · Lo arregla quien programa · No se reintenta

Por la API solo se escriben comentarios visibles para el cliente (`CLIENT`). Las notas internas del equipo nunca.

### BUDGET_SIGNED_PDF_UNAVAILABLE

No es un error HTTP: llega en `reason.errorCode` de una verificación de firma con `status: UNVERIFIABLE` (respuesta `200`). El PDF firmado ya no existe. No indica que el documento se haya alterado.

### BUDGET_SIGNED_PDF_UNREADABLE

Como el anterior, pero el PDF existe y el almacén no lo ha devuelto. Puede ser pasajero: repite la verificación más tarde.

## Instalaciones del catálogo

Los devuelven las rutas de `/external/v1/app-installations`, con la clave de la aplicación que publica una ficha en el catálogo. Cómo se canjea el código está en [La instalación y el canje del código](https://os.utilia.ai/docs/developers/apps-platform/installation-and-exchange).

### EXCHANGE_FROM_BROWSER

`403` · Lo arregla quien programa · No se reintenta

El canje llegó con las cabeceras de un navegador (`Origin` o `Sec-Fetch-Site`). Las credenciales de una instalación solo se entregan a un servidor: haz el canje desde el tuyo, nunca desde la página a la que llega la persona.

### INSTALLATION_CODE_WRONG_APP

`403` · Lo arregla quien programa · No se reintenta

El código es de la ficha de otra aplicación de tu organización. Canjéalo con la clave de la aplicación que publicó esa ficha.

### INSTALLATION_CODE_INVALID

`404` · Lo arregla quien programa · No se reintenta

El código no existe para tu aplicación, o la instalación ya no se puede conectar porque está desinstalada o suspendida. No dice cuál de las dos cosas pasa. Comprueba que llegó entero; si sigue, pide a la empresa que vuelva a conectar desde Instaladas.

### INSTALLATION_CODE_USED

`409` · Lo arregla quien programa · No se reintenta

El código ya se canjeó y las credenciales no se entregan dos veces. Suele ser la misma dirección de instalación abierta dos veces: si ya guardaste las credenciales de esa `installation_id`, sigue adelante. Si las perdiste, pide a la empresa que vuelva a conectar.

### INSTALLATION_CODE_EXPIRED

`410` · Lo arregla la organización · No se reintenta

Pasaron los 30 minutos del código sin canjearlo, y la instalación está en **Falta conectar**. La empresa pulsa **Conectar con** tu aplicación en Instaladas y te llega un código nuevo.

### APP_INSTALLATION_NOT_FOUND

`404` · Lo arregla quien programa · No se reintenta

No hay ninguna instalación con ese identificador en la ficha de tu aplicación: no existe o es de otra aplicación. Usa el `installation_id` que recibiste en la dirección de instalación o en `X-Utilia-Installation-Id`.

## Registro de errores de la aplicación

### ERROR_STORAGE_UNAVAILABLE

`503` · Lo arreglamos nosotros · Se puede reintentar

El registro de errores no ha podido guardar el error que mandaste a `POST /errors/report`. Llega con `Retry-After: 30`: repítelo pasados esos segundos. En un lote (`POST /errors/batch`) no hay 503: el error que no se guardó trae `reason: storage_failed` y se repite solo ese.

Que un error no se registre por los límites del registro (`rate_limited`, `quota_exhausted`, `overloaded`) o por ser un 4xx esperado (`discarded_expected`) no es un error HTTP: la respuesta es `201` con `accepted: false` y el motivo. Ver [Enviar los errores de tu aplicación](https://os.utilia.ai/docs/developers/api/errors#respuesta).

## Pruebas y diagnóstico

Los devuelven las rutas de `/external/v1/diagnostics`. Cómo se usan está en [Rutas del diagnóstico](https://os.utilia.ai/docs/developers/getting-started/diagnostics).

### DIAGNOSTICS_RATE_LIMITED

`429` · Lo arregla quien programa · Se puede reintentar

Has lanzado demasiadas pruebas seguidas: 6 pruebas activas por minuto y aplicación (en el panel, por persona), o 5 avisos de prueba por minuto y aplicación (un cupo compartido con el panel). Espera los segundos de `Retry-After`, que también van en `retryAfterSeconds`. Las cabeceras `X-RateLimit-*` de esta respuesta son las de ese cupo, con `X-RateLimit-Remaining: 0`, no las del freno general por minuto.

### DIAGNOSTICS_UNAVAILABLE

`503` · Lo arreglamos nosotros · Se puede reintentar

Ahora mismo no se pueden lanzar pruebas: el contador de su cupo no responde, y la prueba activa y los avisos de prueba no se lanzan sin contarlos. No dice nada de tu integración. Repite en unos minutos.

### DIAGNOSTICS_NOT_AVAILABLE_FOR_PUBLISHABLE_KEY

`403` · Lo arregla la organización · No se reintenta

La clave de la aplicación está publicada en páginas web (tiene dominios autorizados o se ha usado desde un navegador) y cualquiera puede leerla. Con ella no se lanzan las pruebas que enseñan su tráfico, leen como la aplicación o envían avisos a su servidor: las llamadas rechazadas, la prueba activa, la muestra de un aviso y la comprobación del receptor. Lánzalas desde la pestaña **Pruebas** de la aplicación en el panel. El diagnóstico, `/me`, `check-request` y el contrato siguen respondiendo con esa clave. Ver [Claves publicadas en una web](https://os.utilia.ai/docs/developers/getting-started/diagnostics#claves-publicadas-en-una-web).

### WEBHOOK_NO_URL

`400` · Lo arregla la organización · No se reintenta

La aplicación no tiene dirección de avisos configurada.

### WEBHOOK_URL_INVALID

`400` · Lo arregla la organización · No se reintenta

La dirección de avisos no es válida o apunta a una red privada. Por seguridad, ni se guarda ni se entrega.

### WEBHOOK_SECRET_NOT_CONFIGURED

`409` · Lo arregla la organización · No se reintenta

La aplicación no tiene clave de firma de avisos, así que no se puede comprobar el receptor.

## Estados HTTP sin código

Si la respuesta no trae `errorCode`, o trae uno que no está en esta página, `docsUrl` apunta aquí, a su estado.

### HTTP 400

La petición no es válida. Lo arregla quien programa.

### HTTP 401

La petición no trae una credencial válida. Lo arregla quien programa.

### HTTP 403

La aplicación no tiene acceso a esta operación. Lo arregla la organización.

### HTTP 404

La dirección o el recurso no existen. Lo arregla quien programa. Una ruta que no es de la API pública responde igual que una que no existe: compruébala con [la comprobación de peticiones](https://os.utilia.ai/docs/developers/getting-started/check-request#comprobar-una-petición-sin-ejecutarla), que sugiere las parecidas.

### HTTP 405

La operación no admite ese método HTTP. Lo arregla quien programa.

### HTTP 408

La petición ha tardado demasiado en llegar. Se puede reintentar.

### HTTP 409

La operación choca con el estado actual del recurso. Lo arregla quien programa: vuelve a leerlo antes de repetir.

### HTTP 413

La petición es demasiado grande. Lo arregla quien programa.

### HTTP 415

El formato del cuerpo no se admite. Envía `Content-Type: application/json`.

### HTTP 422

Los datos tienen la forma correcta, pero no se pueden aplicar. Lo arregla quien programa.

### HTTP 429

Se ha superado un límite de peticiones. Sin código es un freno por minuto: el general, el de facturas y cobros o el de los errores del navegador. Espera lo que diga `Retry-After`. Se puede reintentar. Las cifras están en [Los límites de un vistazo](https://os.utilia.ai/docs/developers/getting-started/rate-limits-and-cors#los-límites-de-un-vistazo).

### HTTP 499

No es una respuesta que reciba tu programa: es como queda registrada, en las [llamadas rechazadas](https://os.utilia.ai/docs/developers/getting-started/diagnostics#llamadas-rechazadas) y en el panel, una llamada que tu programa cortó antes de recibir la respuesta. Casi siempre, su tiempo de espera es más corto de lo que tarda la ruta. Lo arregla quien programa: alarga la espera y, si era una escritura, comprueba si se hizo antes de repetirla.

### HTTP 500

Ha fallado algo de nuestro lado. Lo arreglamos nosotros y se puede reintentar. Si se repite, escríbenos con el `requestId`.

### HTTP 502

Un servicio del que dependemos no ha respondido bien. Se puede reintentar.

### HTTP 503

El servicio no está disponible en este momento. Se puede reintentar.

### HTTP 504

Un servicio del que dependemos ha tardado demasiado. Se puede reintentar.

## Otras familias de errores

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

La ventanilla única del IVA está en «Próximamente». Cualquier dato suyo responde `403 FEATURE_COMING_SOON` con `featureKey: "finance.oss"`.

### Gestión de aplicaciones desde el panel

No los devuelve la API externa, sino la API del panel con la que una persona gestiona las aplicaciones (`/api/integrations/apps/...`):

| errorCode | HTTP | Qué significa |
| - | - | - |
| `APP_SLUG_DUPLICATE` | 409 | Ya existe una aplicación con ese identificador. |
| `EXTERNAL_APP_SLUG_HELD_BY_DELETED` | 409 | El identificador lo conserva una aplicación eliminada. Restáurala o elimínala definitivamente para reutilizarlo. |
| `APP_NOT_FOUND` | 404 | La aplicación no existe, es de otra organización o, según la operación, está eliminada. |
| `EXTERNAL_APP_HAS_ACTIVE_BILLING` | 409 | No se puede eliminar: tiene suscripciones activas o en pausa o cobros programados pendientes. `details` dice cuántos. |
| `CONFIRMATION_MISMATCH` | 400 | Para eliminarla definitivamente, `confirmName` tiene que ser el nombre exacto de la aplicación. |
| `EXTERNAL_APP_PERMANENT_DELETE_BLOCKED` | 409 | Algo impide eliminarla definitivamente. `details.blockers` dice qué y cómo resolverlo. |
| `SCOPE_OUT_OF_PLAN` | 400 | El permiso que intentas conceder exige algo que el plan no incluye. |
| `DELIVERY_NOT_FOUND` | 404 | La entrega del aviso no existe. |
| `DELIVERY_ALREADY_SUCCESS` | 400 | No se puede reintentar una entrega que ya llegó bien. |

### Claves de API de la organización

No los devuelve la API externa, sino la API de la aplicación cuando la llamas con una [clave de API de la organización](https://os.utilia.ai/docs/developers/organization-api-keys/overview) (`Authorization: Bearer utk_...`). Qué hacer con cada uno está en [su tabla de errores](https://os.utilia.ai/docs/developers/organization-api-keys/first-request#errores-de-una-petición).

| errorCode | HTTP | Qué significa |
| - | - | - |
| `API_KEY_INVALID` | 401 | La clave no existe o está mal copiada. |
| `API_KEY_REVOKED` | 401 | La clave está revocada. |
| `API_KEY_EXPIRED` | 401 | La clave ha caducado. |
| `API_KEY_UNAVAILABLE` | 401 | No se ha podido comprobar la clave en ese momento. |
| `API_KEY_BROWSER_NOT_ALLOWED` | 403 | La petición llegó desde un navegador: estas claves solo valen desde un servidor. |
| `API_KEY_RESPONSIBLE_INACTIVE` | 403 | La persona responsable de la clave ya no está activa. |
| `API_KEY_ROUTE_NOT_AVAILABLE` | 403 | Esa ruta no admite claves. |
| `API_KEY_STREAMING_NOT_AVAILABLE` | 403 | Las conexiones de eventos en tiempo real no admiten claves. |
| `API_KEY_PERMISSION_NOT_GRANTED` | 403 | La clave no incluye el permiso; `permissions` dice cuáles faltan. |
| `API_KEY_RESPONSIBLE_PERMISSION_MISSING` | 403 | La persona responsable no tiene el permiso; `permissions` dice cuáles. |
| `API_KEY_ACTION_REQUIRES_SESSION` | 403 | La acción pide la verificación en dos pasos de una persona. |
| `RATE_LIMIT_EXCEEDED` | 429 | Cupo por hora de la clave agotado, o demasiadas claves no válidas desde tu IP. Llega con `Retry-After`. |

## Siguiente paso

- [Probar tu integración](https://os.utilia.ai/docs/developers/getting-started/testing-quickstart): Qué le falta a tu aplicación, sin escribir nada, en cinco pasos.
- [Dónde va cada parámetro](https://os.utilia.ai/docs/developers/getting-started/parameters): Los fallos de forma más habituales, antes de que aparezcan.
