> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://developer.crehana.com/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://developer.crehana.com/_mcp/server.

# Errores

**Programá contra el status HTTP.** Es el único dato que viene siempre, que
significa lo mismo en todos los endpoints y que no va a cambiar sin anunciarse
en el [changelog](/changelog).

El cuerpo sirve para mostrarle algo a una persona y para depurar, pero su forma
no es única: hoy conviven varias, según el endpoint y según qué falló. Si tu
integración ramifica lógica leyendo el cuerpo, se va a romper al cambiar de
endpoint.

## Qué significa cada status

| Status | Cuándo aparece                                                   | Qué hacer                                       |
| ------ | ---------------------------------------------------------------- | ----------------------------------------------- |
| `400`  | El request es inválido para la lógica de negocio                 | Corregir los datos. Reintentar igual no sirve.  |
| `401`  | Faltan credenciales, o no son válidas                            | Revisar que vayan los dos headers               |
| `403`  | Credenciales válidas, sin permiso sobre ese recurso              | Revisar el alcance de tus credenciales          |
| `404`  | El recurso no existe, o no pertenece a tu organización           | Verificar el identificador                      |
| `409`  | Conflicto: el recurso ya existe o está en un estado incompatible | Consultar el estado actual antes de reintentar  |
| `422`  | El cuerpo no cumple el esquema, o la operación no afectó a nadie | Ver el detalle                                  |
| `500`  | Error inesperado del lado nuestro                                | Reintentar con backoff; si persiste, reportarlo |

> **Warning**
>
> El `422` tiene dos orígenes distintos. Uno es la validación de esquema. El otro
> es de negocio: los endpoints de asignación masiva devuelven `422` cuando la
> llamada **no terminó asignando a ningún usuario**, aunque el cuerpo fuera
> válido. Si tu integración asigna contenido, tratá ese caso aparte.

## Las formas del cuerpo

Estas son las tres que vas a encontrar en la práctica. Las tres traen el detalle
bajo `detail`, y ahí se termina el parecido.

### Una lista de mensajes

La más común. Es lo que devuelven los `400`, `401`, `403` y `404` de la mayoría
de los endpoints:

```json
{
  "detail": {
    "errors": ["Centralized organization with slug acme does not exist"]
  }
}
```

### La validación de esquema

Cuando un parámetro o un campo del cuerpo no cumple el tipo o el rango. `detail`
acá es una **lista**, no un objeto, y cada elemento dice qué campo falló:

```json
{
  "detail": [
    {
      "loc": ["query", "limit"],
      "msg": "ensure this value is less than or equal to 50",
      "type": "value_error.number.not_le"
    }
  ]
}
```

`loc` es el camino al campo: `["query", "limit"]` es un parámetro de query,
`["body", "email"]` un campo del cuerpo. Es la forma más útil para mostrarle al
usuario qué corregir.

### Un texto suelto

En los `500`, `detail` puede venir como un string, o como un objeto con una sola
clave:

```json
{ "detail": "The request could not be processed." }
```

No trae información accionable a propósito. Si te llega uno, reintentá con
backoff y si persiste [reportalo](/documentacion/recursos/soporte) con la hora y
la ruta.

## Cómo leer las tres con el mismo código

Alcanza con normalizar `detail` a una lista de textos antes de tocarlo:

**`Python`**

```python title="Python"
def error_messages(payload: dict) -> list[str]:
    """Devuelve los mensajes de error de cualquier respuesta de la API."""
    detail = payload.get("detail")

    if isinstance(detail, str):
        return [detail]
    if isinstance(detail, list):
        return [item.get("msg", str(item)) for item in detail]
    if isinstance(detail, dict):
        if "errors" in detail:
            return list(detail["errors"])
        return [str(value) for value in detail.values()]

    # Algunas operaciones devuelven el detalle en `message`.
    return [payload["message"]] if "message" in payload else []
```

**`JavaScript`**

```javascript title="JavaScript"
function errorMessages(payload) {
  const { detail } = payload;

  if (typeof detail === "string") return [detail];
  if (Array.isArray(detail)) return detail.map((item) => item.msg ?? String(item));
  if (detail && typeof detail === "object") {
    return detail.errors ? [...detail.errors] : Object.values(detail).map(String);
  }

  return payload.message ? [payload.message] : [];
}
```

> **Note**
>
> Los textos que salen de acá son para un log o para mostrarle a una persona.
> **No los compares en tu código**: no son estables y pueden cambiar sin aviso. La
> decisión de qué hacer sale del status HTTP.

## Reintentos

Reintentá solo `500` y errores de red, con backoff exponencial. Los `4xx`
describen un problema del request: reintentarlos sin cambiar nada da el mismo
resultado.

Las operaciones de escritura no son idempotentes salvo que se indique lo
contrario, así que antes de reintentar una que pudo haber llegado, consultá el
estado actual del recurso.

> **Note**
>
> Que hoy convivan varias formas de cuerpo es deuda, no diseño. Si en algún
> momento se unifican, se anuncia en el [changelog](/changelog) como cualquier
> otro cambio del contrato. Es la razón de fondo para ramificar por status y no
> por cuerpo: el código que ya hace eso no se toca el día que cambie.