<!-- Source: https://docs.infraio.xyz/es/api-reference/errors -->
<!-- Last updated: 2026-10-04 -->

# Errores

Cada respuesta 4xx/5xx lleva el mismo envelope JSON:

```json
{
  "code": 400,
  "message": "invalid_input",
  "details": [
    { "field": "items[0].unit_price", "message": "must be a positive decimal string" }
  ]
}
```

- `code`: el **status HTTP numérico** (`400`, `401`, `404`, …). Útil para manejo genérico de la capa HTTP, pero para la lógica de ramificación ramifica sobre `message` en su lugar: `code` no desambigua entre, digamos, `invalid_input` y `payment_method_not_supported` (ambos 400).
- `message`: el nombre centinela en **lower-snake-case** (p. ej. `INVALID_INPUT` pasa a `"invalid_input"`). Estable a través de releases: ramifica sobre esto.
- `details`: se rellena en errores de validación. Array de objetos `{ field, message }` para que el cliente pueda fijar errores a inputs. Se omite en otro caso.

> **Warning:**
>
> Los cuerpos de error no incluyen un trace ID ni un timestamp. Para reportar un problema, envía a soporte la cabecera de respuesta `Date`, las cabeceras `X-RateLimit-*`, tu merchant ID, el endpoint y la hora aproximada de la petición.

> **Note:**
>
> **Los fallos de autenticación usan una forma diferente.** El envelope
> de arriba es lo que devuelve la API en la mayoría de los errores. Las
> peticiones rechazadas antes de procesarse (una `X-Signature` faltante
> o inválida, un `X-Client-ID` desconocido, o un `X-Timestamp` obsoleto
> en una llamada `/b2b/v1/*`) regresan como
> `{ "error": "...", "message": "..." }`, donde `error` es un slug
> grueso (`unauthorized` / `bad_request` / `service_unavailable`) y
> `message` lleva los detalles. No hay `code` numérico ni `details`.
> Ramifica primero por status HTTP, luego lee `message`. Ejemplo (401):
>
> ```json
> { "error": "unauthorized", "message": "invalid signature" }
> ```

## Status HTTP → códigos típicos

| HTTP | Valores típicos de `code` | Qué significa |
| --- | --- | --- |
| **400** | `INVALID_INPUT`, `MISSING_REQUIRED`, `INVALID_FORMAT`, `INVALID_LENGTH`, `INVALID_VALUE`, `PAYMENT_METHOD_NOT_SUPPORTED`, `AMOUNT_BELOW_MINIMUM` | Petición incorrecta — mira `details` |
| **401** | `INVALID_CREDENTIALS`, `INVALID_TOKEN`, `TOKEN_EXPIRED`, `INVALID_SIGNATURE` (B2B); `SESSION_EXPIRED` (solo dashboard) | Falló la auth: clave incorrecta, timestamp expirado, firma equivocada. Los códigos de inicio de sesión del dashboard no son relevantes para las integraciones B2B. |
| **402** | `INSUFFICIENT_CREDIT` | El saldo prepagado del comerciante se agotó: recarga antes de reintentar |
| **403** | `FORBIDDEN`, `IP_BLOCKED` | La clave es válida pero le falta el scope/permiso de IP para esta llamada |
| **404** | `NOT_FOUND`, `RECORD_NOT_FOUND` | El recurso no existe (o no existe para este comerciante) |
| **409** | `ALREADY_EXISTS` | Replay de idempotencia con un body diferente, o la máquina de estados rechazó la transición |
| **429** | `TOO_MANY_REQUESTS`, `TOO_MANY_ATTEMPTS` | Límite de tasa alcanzado; retrocede y reintenta |
| **500** | `INTERNAL_SERVER_ERROR`, `EXTERNAL_SERVICE_ERROR` | Es culpa nuestra; es seguro reintentar con backoff. |
| **503** | `SERVICE_UNAVAILABLE` | Una dependencia aguas abajo está caída. Reintenta con backoff |

## Referencia completa de códigos

Los valores de `code` que puedes ver:

### Autenticación (401)
- `INVALID_CREDENTIALS`: combinación de clave API rechazada
- `INVALID_TOKEN`: token no parseable o manipulado
- `TOKEN_EXPIRED`: token expirado
- `SESSION_EXPIRED`: sesión del dashboard expirada
- `INVALID_SIGNATURE`: discrepancia de firma HMAC en llamadas B2B / webhook

### Autorización (403)
- `FORBIDDEN`: autenticado, pero no tienes permiso para realizar esta acción
- `IP_BLOCKED`: las peticiones desde esta dirección IP están bloqueadas

### No encontrado (404)
- `NOT_FOUND`: genérico
- `RECORD_NOT_FOUND`: fila ausente para el ID dado

### Conflicto (409)
- `ALREADY_EXISTS`: genérico

### Validación (400)
- `INVALID_INPUT`: genérico; revisa `details`
- `MISSING_REQUIRED`: un campo obligatorio estaba ausente
- `INVALID_FORMAT`: el valor no coincidió con el formato esperado (p. ej. UUID, URL, email)
- `INVALID_LENGTH`: el valor es demasiado corto o demasiado largo
- `INVALID_VALUE`: valor fuera del enum/rango permitido
- `PAYMENT_METHOD_NOT_SUPPORTED`: la combinación proveedor/activo no está habilitada para el comerciante
- `AMOUNT_BELOW_MINIMUM`: importe de la orden por debajo del piso por red o por entorno. El `details.floor_usd` de la respuesta lleva el piso configurado (USD) para que puedas mostrarlo directamente; el texto `message` también lo indica.
- `INSUFFICIENT_BALANCE`: la wallet del comprador no tiene suficiente del activo de pago para cubrir la transferencia.
- `INSUFFICIENT_GAS`: a la wallet del comprador le falta token nativo para pagar la comisión de red (gas) de la transferencia.

### Pago / facturación (402)
- `INSUFFICIENT_CREDIT`: el saldo prepagado del comerciante no puede cubrir la comisión de red (gas) y de la plataforma. Recarga vía dashboard, luego reintenta

### Límite de tasa (429)
- `TOO_MANY_REQUESTS`: límite de tasa por IP excedido
- `TOO_MANY_ATTEMPTS`: intentos fallidos repetidos sobre el mismo recurso (p. ej. OTP) activaron un throttle

### Errores del servidor (500)
- `EXTERNAL_SERVICE_ERROR`: falló un proveedor de terceros
- `INTERNAL_SERVER_ERROR`: error inesperado; contacta con soporte con la cabecera de respuesta `Date` y las cabeceras `X-RateLimit-*`

### Disponibilidad (503)
- `SERVICE_UNAVAILABLE`: el servicio no está disponible temporalmente; reintenta con backoff

## Errores de validación (400)

Cuando el problema es un request body malformado, `details` es un array para que puedas mapear errores de vuelta a los campos:

```json
{
  "code": 400,
  "message": "invalid_input",
  "details": [
    { "field": "items[0].unit_price", "message": "must be a positive decimal string" },
    { "field": "success_url", "message": "must be a valid https URL" }
  ]
}
```

## Errores de auth (401)

`INVALID_SIGNATURE` tiene tres causas comunes:

- Clave secreta incorrecta (revisa de nuevo la variable de entorno)
- Desviación de timestamp > 5 min (sincroniza NTP)
- Cadena canónica incorrecta (lo más frecuente: olvidaste los separadores `\n`, o firmaste un body parseado-y-luego-reserializado que difiere de lo que enviaste)

Consulta [Autenticación](https://docs.infraio.xyz/es/api-reference/authentication) para el algoritmo de firma exacto.

## Límites de tasa (429)

| Superficie | Límite |
| --- | --- |
| Todas las rutas de la API (incl. `/b2b/v1/*`) | Por dirección IP: **500 req/min**, bucket compartido. No por comerciante. |
| Checkout público (`/checkout/*`) | Sub-bucket más estricto: **20 req/min por IP** |

`X-RateLimit-Limit` y `X-RateLimit-Remaining` se incluyen en cada respuesta con límite de tasa, no solo en las exitosas. Trátalas como el presupuesto en vivo para tu IP.

Las respuestas con límite de tasa incluyen una cabecera `Retry-After` (segundos hasta que se reinicia la ventana): respétala. Como fallback, retrocede con jitter: 1s base + exponencial hasta 30s.

> **Note:**
>
> Los límites de tasa pueden cambiar. Si los alcanzas con tráfico legítimo (p. ej., reconciliando un rango histórico grande), contacta con soporte.

## Crédito insuficiente (402)

`INSUFFICIENT_CREDIT` (HTTP **402 Payment Required**) significa que tu saldo prepagado no puede cubrir la comisión de red (gas) y de la plataforma para la operación que intentaste realizar: típicamente liquidar un pago cripto o una acción on-chain con comisión de red patrocinada. Recarga desde el dashboard del comerciante (**Billing → Add credit**), luego reintenta la operación. Las operaciones ya en curso continúan automáticamente una vez que recargas el saldo.

## Errores del servidor (5xx)

Un 500 significa que no pudimos procesar la petición. Reintenta con backoff: tu `idempotency_key` garantiza que no cargarás doble si la petición original tuvo éxito parcial.

Si los reintentos no se recuperan en un minuto, muestra al comprador un genérico "pago temporalmente no disponible" y contacta con soporte con el endpoint que falla, tu merchant ID, los valores de la cabecera de respuesta `Date` y `X-RateLimit-*`, y la hora aproximada de la petición: eso es suficiente para que encontremos la petición.

## Errores de entrega de webhook

Las entregas de webhook son un canal de fallo separado: no aparecen como errores de API porque tu servidor no es el que llama. Cuando una entrega devuelve un no-2xx (o agota el tiempo), se reintenta con backoff exponencial a 0s, 1min, 5min, 15min, 1h, 6h (seis intentos en total, el mismo calendario que [Webhooks → Resumen](https://docs.infraio.xyz/es/webhooks/overview)). El historial completo por entrega aparece bajo **Developers → Webhooks → [endpoint] → Delivery log** en el dashboard. Tras el sexto intento la entrega se marca como "Failed" en el dashboard y puedes reenviarla manualmente una vez que tu servidor esté saludable.
