# Errores

El formato de los errores, el request_id y qué hacer con cada código.

## Formato

Todo error responde con un código HTTP distinto de 2xx y este cuerpo:

```json
{
  "error": {
    "code": "INSUFFICIENT_SCOPE",
    "message": "Esta API key no tiene el permiso \"sales:read\". Crea una key con ese permiso en cuenta.posdata.so → Desarrolladores.",
    "required_scope": "sales:read",
    "request_id": "req_6vJx0Qm2cPp1bK9a"
  }
}
```

| Campo        | Qué es                                                                                                                                                      |
| ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `code`       | El código estable para tu lógica. **Ramifica por `code`, nunca por `message`**. Los códigos no cambian de nombre ni se reutilizan.                          |
| `message`    | Explicación en español para una persona, con el siguiente paso. Puede cambiar de redacción.                                                                 |
| `param`      | El campo que causó el error, cuando aplica (por ejemplo `sku`, `warehouse_id` o `Idempotency-Key`). En errores de validación puede ser una ruta con puntos. |
| `request_id` | El identificador de la petición. También llega en el header `Request-Id` de **todas** las respuestas.                                                       |

Algunos errores traen campos adicionales: `required_scope` (en `INSUFFICIENT_SCOPE`), `feature` (en `PLAN_REQUIRED`), `retry_after` (en `RATE_LIMITED`), `devices` (en `DEVICES_OUTDATED`) y `available` (en `INSUFFICIENT_STOCK`). Tu código debe tolerar campos nuevos.

Guarda el `request_id` en tus logs. Si escribes a [soporte](https://developers.posdata.so/soporte/), inclúyelo: con él encontramos la petición exacta. Los errores nunca exponen detalles internos (trazas, SQL ni mensajes de otros servicios).

## Códigos

### Autenticación (401)

| Código                      | HTTP | Cuándo pasa                                                         | Qué hacer                                                                 |
| --------------------------- | ---- | ------------------------------------------------------------------- | ------------------------------------------------------------------------- |
| `UNAUTHENTICATED`           | 401  | Falta el header `Authorization` o no tiene la forma `Bearer <key>`. | Envía `Authorization: Bearer pdk_…`.                                      |
| `INVALID_API_KEY`           | 401  | La key no empieza por `pdk_live_`/`pdk_test_` o no existe.          | Revisa que la copiaste completa. Si la perdiste, crea otra.               |
| `API_KEY_REVOKED`           | 401  | La key fue revocada.                                                | Crea una nueva en cuenta.posdata.so → Desarrolladores.                    |
| `API_KEY_EXPIRED`           | 401  | La key llegó a su fecha de vencimiento.                             | Crea una nueva y actualiza tu integración.                                |
| `API_KEY_WRONG_ENVIRONMENT` | 401  | Usas una key de pruebas en producción o al revés.                   | `pdk_live_` va con `api.posdata.so`; `pdk_test_` con `apidev.posdata.so`. |
| `ACCOUNT_INACTIVE`          | 401  | La cuenta dueña de la key está inactiva.                            | Escribe a soporte.                                                        |

### Autorización (403)

| Código                         | HTTP | Cuándo pasa                                                                                              | Qué hacer                                                                                           |
| ------------------------------ | ---- | -------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| `PLAN_REQUIRED`                | 403  | La cuenta no tiene el plan Pro (con o sin facturación electrónica) activo. Trae `feature: "public_api"`. | Las keys siguen guardadas y vuelven a funcionar cuando la cuenta tenga Pro. No reintentes en bucle. |
| `INSUFFICIENT_SCOPE`           | 403  | La key no tiene el permiso del endpoint. Trae `required_scope`.                                          | Crea una key con ese permiso.                                                                       |
| `BROWSER_REQUESTS_NOT_ALLOWED` | 403  | La petición trae el header `Origin` (viene de un navegador).                                             | Llama a la API desde tu servidor. Nunca pongas la key en un front-end.                              |

### La petición (400, 404, 413, 415)

| Código              | HTTP | Cuándo pasa                                                                                                                                                                                                  | Qué hacer                                                                |
| ------------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------ |
| `VALIDATION_ERROR`  | 400  | Un campo o parámetro no es válido, falta uno obligatorio, o enviaste uno que no existe (los filtros y campos desconocidos se rechazan, no se ignoran). También con **415** si el cuerpo no es JSON en UTF-8. | Lee `message` y `param`, corrige y reintenta.                            |
| `INVALID_JSON`      | 400  | El cuerpo no es JSON válido.                                                                                                                                                                                 | Revisa la serialización y envía `Content-Type: application/json`.        |
| `PAYLOAD_TOO_LARGE` | 413  | El cuerpo supera 256 KB.                                                                                                                                                                                     | Envía menos datos por petición.                                          |
| `INVALID_CURSOR`    | 400  | `starting_after` no es un cursor válido.                                                                                                                                                                     | Usa el `next_cursor` de la respuesta anterior tal cual, sin modificarlo. |
| `NOT_FOUND`         | 404  | El recurso no existe en **esta** cuenta (o el id no tiene formato de uuid).                                                                                                                                  | Verifica el id. Un id de otra cuenta responde igual que uno inventado.   |
| `ROUTE_NOT_FOUND`   | 404  | La ruta o el método no existen.                                                                                                                                                                              | Revisa la [referencia](https://developers.posdata.so/referencia/).       |

### Idempotencia

| Código                     | HTTP | Cuándo pasa                                                                                                   | Qué hacer                                                                                                                                              |
| -------------------------- | ---- | ------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `IDEMPOTENCY_KEY_REQUIRED` | 400  | Un `POST` sin header `Idempotency-Key`.                                                                       | Envía un valor único por operación (un UUID).                                                                                                          |
| `IDEMPOTENCY_KEY_REUSED`   | 422  | Usaste el mismo `Idempotency-Key` con una petición distinta.                                                  | Usa un valor nuevo para cada operación distinta.                                                                                                       |
| `IDEMPOTENCY_IN_PROGRESS`  | 409  | La primera petición con ese `Idempotency-Key` todavía se está procesando, o se interrumpió antes de terminar. | Espera unos segundos y reintenta con el **mismo** valor. Si sigue igual, verifica con un `GET` si el recurso se creó **antes** de usar un valor nuevo. |

Detalle en [Idempotencia](https://developers.posdata.so/idempotencia/).

### Reglas de negocio (409, 422)

| Código                        | HTTP | Cuándo pasa                                                                                                                                                                                                                                                                                                                                                                                   | Qué hacer                                                                                                                                                                                                                                                                                                                                     |
| ----------------------------- | ---- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `CONFLICT`                    | 409  | Ya existe un registro con esos datos únicos.                                                                                                                                                                                                                                                                                                                                                  | Busca el registro existente en vez de crearlo otra vez.                                                                                                                                                                                                                                                                                       |
| `SKU_ALREADY_EXISTS`          | 409  | Al editar una variante (`PATCH /products/{id}/variants/{variantId}`), otra variante de la cuenta ya usa ese `sku`. Trae `param: "sku"`.                                                                                                                                                                                                                                                       | Usa un SKU distinto o busca la variante que ya lo tiene.                                                                                                                                                                                                                                                                                      |
| `NO_WAREHOUSE`                | 409  | Un movimiento o un conteo de stock (o un producto con `initial_stock`) sin `warehouse_id`, y la cuenta no tiene una bodega predeterminada: o todavía no hay bodegas sincronizadas desde las cajas, o hay varias y ninguna es la predeterminada.                                                                                                                                               | Indica `warehouse_id` (consúltalas con `GET /warehouses`) o abre Posdata en una caja con conexión para que sincronice sus bodegas.                                                                                                                                                                                                            |
| `DEVICES_OUTDATED`            | 409  | Al editar un producto o una variante (`PATCH /products/{id}` o `PATCH /products/{id}/variants/{variantId}`), algún dispositivo del negocio todavía no tiene la versión de Posdata que acepta ediciones sin stock. **No se modificó nada.** Trae `devices`: la lista de esos dispositivos, cada uno con `name`, `kind`, `platform`, `app_version` y `last_seen_at`.                            | Abre esos dispositivos con internet para que se actualicen solos (tardan un minuto en reportarse) y reintenta, o revoca los que ya no uses en cuenta.posdata.so → Dispositivos. Crear productos y mover stock sí funciona mientras tanto. Mira [por qué](https://developers.posdata.so/sincronizacion/#las-cajas-deben-estar-actualizadas).   |
| `INSUFFICIENT_STOCK`          | 409  | Una salida (`POST /inventory/movements` con `type: "out"`) pide más de lo que la API puede comprometer en esa bodega: el stock menos el margen de la API. **No se registró nada.** Trae `param: "quantity"` y `available`: lo que sí puedes sacar (puede ser `0`).                                                                                                                            | No reintentes la misma petición en bucle: saca como máximo `available` o marca el producto como agotado. El margen lo ajusta el negocio en cuenta.posdata.so → Inventario → Configuración. Mira [Salidas atómicas](https://developers.posdata.so/stock/#salidas-at%C3%B3micas).                                                               |
| `STOCK_MODE_LEGACY`           | 409  | La cuenta todavía no lleva el stock en la nube: alguna de sus cajas principales aún no tiene la versión de Posdata que lo soporta. Lo responden `GET /inventory/levels`, `GET /inventory/kardex`, `GET /inventory/valuation`, `POST /inventory/counts` y los movimientos de variantes (`POST /inventory/movements` con `variant_id` o de un producto con variantes). **No se registró nada.** | Pídele al negocio que abra sus cajas con internet para que se actualicen solas (y que revoque en cuenta.posdata.so → Dispositivos las que ya no use); el paso ocurre solo. Mientras tanto, los movimientos de productos sin variantes sí funcionan. Mira [los dos modos](https://developers.posdata.so/stock/#dos-modos-las-cajas-o-la-nube). |
| `COMPOSITE_STOCK_UNSUPPORTED` | 422  | Intentaste mover o contar stock de un producto que se arma con receta.                                                                                                                                                                                                                                                                                                                        | Mueve o cuenta el stock de sus insumos.                                                                                                                                                                                                                                                                                                       |
| `VARIANT_STOCK_UNSUPPORTED`   | 422  | **Obsoleto: ya no se emite.** Lo respondían los movimientos de productos con variantes antes de que las variantes movieran su propio stock. Se conserva en el catálogo hasta una nueva versión de la API.                                                                                                                                                                                     | Nada. Si tu integración ramificaba por este código, trata `STOCK_MODE_LEGACY` de la misma forma.                                                                                                                                                                                                                                              |

### Límites y servidor

| Código           | HTTP | Cuándo pasa                                                                                                                    | Qué hacer                                                                      |
| ---------------- | ---- | ------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------ |
| `RATE_LIMITED`   | 429  | Superaste un [límite de uso](https://developers.posdata.so/limites/). Trae `retry_after` (segundos) y el header `Retry-After`. | Espera lo que indica `Retry-After` y reintenta.                                |
| `INTERNAL_ERROR` | 500  | Falló algo de nuestro lado.                                                                                                    | Reintenta con espera exponencial. Si persiste, escríbenos con el `request_id`. |

## Qué reintentar

- **Sí**: `429` (después de `Retry-After`), `409 IDEMPOTENCY_IN_PROGRESS`, `500` y errores de red o tiempos de espera. Si es un `POST`, reintenta con el **mismo** `Idempotency-Key`.
- **No, sin cambiar algo antes**: el resto de `4xx`. Repetir la misma petición dará el mismo error. Por ejemplo, `409 INSUFFICIENT_STOCK` solo cambia cuando entra stock y `409 STOCK_MODE_LEGACY`, cuando la cuenta pasa al stock en la nube.

---

Fuente: https://developers.posdata.so/errores/ · Índice para agentes: https://developers.posdata.so/llms.txt
