# Códigos de Error

> Versión en Markdown de la documentación pública de la API v1 de Bifrost Crypto, para agentes de IA y generadores de código. El mismo contenido de las páginas HTML, generado de la misma fuente, sin navegación ni estilos.
>
> Versión HTML: https://bifrostcrypto.com/es/docs/error-codes
> Archivos relacionados: [ai.md](https://bifrostcrypto.com/ai/es.md) · [Postman](https://bifrostcrypto.com/postman-collection.json) · [OpenAPI](https://bifrostcrypto.com/openapi.json) · [llms.txt](https://bifrostcrypto.com/llms.txt)
> Otros idiomas: [Português](https://bifrostcrypto.com/ai/pt-BR/error-codes.md) · [English](https://bifrostcrypto.com/ai/en/error-codes.md) · [Français](https://bifrostcrypto.com/ai/fr/error-codes.md)
> API v1 · último cambio de contrato el 2026-10-10

Todos los códigos de error que devuelve la API, agrupados por tipo, con qué hacer en cada caso.

## Formato del error

Todo error de /v1 tiene la misma forma, con el estado HTTP de las tablas siguientes. Compare error.code, que es snake_case estable. error.message es texto libre en inglés para sus logs. error.details solo aparece cuando hay datos útiles, como campos inválidos, límites o scopes.

```json
{
  "error": {
    "code": "insufficient_total_balance",
    "message": "Insufficient balance to cover all payouts",
    "details": {
      "crypto_currency": "USDT",
      "available": "150.00000000",
      "required": "420.50000000"
    }
  }
}
```

En un lote de POST /v1/payout, cada ítem rechazado trae el mismo objeto en results[].error, junto a su index y reference_id. La respuesta es 200 cuando todos los ítems tuvieron éxito, 207 cuando algunos tuvieron éxito y otros fallaron, y 400 cuando ninguno tuvo éxito:

```json
{
  "message": "payment_processing_completed",
  "summary": {
    "total_requested": 3,
    "successful": 1,
    "failed": 2
  },
  "results": [
    {
      "index": 0,
      "reference_id": "order_1001",
      "internal_reference": "BIFROST_REF_123456",
      "status": "created"
    },
    {
      "index": 1,
      "reference_id": "order_1002",
      "error": {
        "code": "invalid_parameters",
        "message": "Invalid parameters provided",
        "details": {
          "fields": {
            "wallet": "Invalid wallet format"
          }
        }
      }
    },
    {
      "index": 2,
      "reference_id": "order_1003",
      "error": {
        "code": "daily_payout_limit_exceeded",
        "message": "Daily payout limit for this API key exceeded"
      }
    }
  ]
}
```

- Los mensajes de validación están siempre en error.details.fields, indexados por nombre de campo, tanto en el nivel superior como dentro de un ítem del lote.
- Un ítem de payout duplicado cuenta como fallo: trae error.code duplicate_payment_reference más duplicate, original_status y original_created_at en el propio ítem. Un lote compuesto solo de duplicados responde 400.
- Decida por error.code, nunca por error.message: los mensajes pueden reescribirse, los códigos no cambian.
- 4xx significa que la solicitud debe cambiar antes de reenviarse. 5xx significa que el fallo fue de nuestro lado y la misma solicitud puede repetirse; en POST /v1/payout, el reference_id sigue protegiéndole contra un retiro duplicado.
- Una ruta /v1 desconocida devuelve 404 route_not_found en JSON. Un cliente que pide text/html en Accept recibe una página HTML.

## Errores de Autenticación y Acceso

| Código HTTP | Error | Descripción |
| --- | --- | --- |
| `401` | `api_key_missing` | No se envió el encabezado X-Bifrost-Invoke. |
| `401` | `api_key_invalid` | API Key desconocida, revocada o expirada. También se devuelve mientras la cuenta está suspendida. |
| `403` | `api_access_denied` | El acceso a la API está desactivado en esta cuenta. Se verifica en cada solicitud. |
| `403` | `email_not_verified` | El e-mail de la cuenta aún no ha sido verificado. |
| `403` | `ip_not_authorized` | La IP de origen no está en la whitelist de la clave. |
| `400` | `invalid_ip` | No se pudo determinar la IP del cliente. |
| `401` | `signature_headers_missing` | La clave exige firma y falta X-Bifrost-Signature, X-Bifrost-Timestamp o X-Bifrost-Nonce. |
| `401` | `signature_timestamp_invalid` | X-Bifrost-Timestamp no está compuesto solo de dígitos (segundos Unix). |
| `401` | `signature_timestamp_out_of_range` | El timestamp difiere de la hora del servidor en más de 300 segundos. |
| `401` | `signature_nonce_length_invalid` | El nonce tiene menos de 16 o más de 80 caracteres. |
| `401` | `signature_nonce_charset_invalid` | El nonce tiene caracteres fuera de A-Z, a-z, 0-9, guion bajo y guion. |
| `401` | `signature_invalid` | La firma no coincide con la cadena canónica. |
| `401` | `signature_replayed` | Este nonce ya fue usado por esta clave. |
| `401` | `signature_secret_unavailable` | La clave exige firma pero no tiene un secreto utilizable. Contacte al soporte. |
| `403` | `scope_denied` | La clave no tiene el alcance que exige este endpoint. details.required_scopes lista los que sirven y details.granted_scopes los que tiene la clave. |
| `429` | `rate_limit_exceeded` | La cuota de la clave en la ventana actual se agotó. Retry-After y details.retry_after_seconds indican cuándo reintentar. |
| `429` | `too_many_failed_authentications` | Esta IP está bloqueada temporalmente tras fallos de autenticación repetidos. Llega con Retry-After. |
| `401` | `user_not_authenticated` | No se pudo asociar la solicitud a su clave de API. Reintente; si se repite, contacte al soporte. |

## Errores de la Solicitud

| Código HTTP | Error | Descripción |
| --- | --- | --- |
| `404` | `route_not_found` | Ninguna ruta /v1 corresponde a la ruta y al método. Se responde antes de la autenticación. |
| `400` | `json_parse_exception` | El cuerpo de la solicitud no es JSON válido. Cualquier ruta /v1. |
| `400` | `json_parse_failed` | POST /v1/payout: el cuerpo se decodificó como null o false. |
| `400` | `invalid_parameters` | Parámetros inválidos o ausentes. details.fields asocia cada campo a su mensaje (POST /v1/payout con todos los ítems inválidos, POST /v1/payin, direcciones fijas, filtros de extractos). |
| `400` | `invalid_payment_list` | POST /v1/payout: el cuerpo está vacío o no es un objeto de payout ni una lista de payouts. |
| `400` | `max_payouts_exceeded` | POST /v1/payout: más de 50 payouts en una solicitud. |
| `409` | `idempotency_key_in_progress` | Una solicitud con esta Idempotency-Key aún está en curso. Espere y reintente con la misma clave. |
| `400` | `crypto_amount_required` | POST /v1/payin: process_by es crypto_amount y falta crypto_amount. |
| `400` | `fiat_amount_required` | POST /v1/payin: process_by es fiat_amount y falta fiat_amount. |
| `400` | `fiat_currency_required` | POST /v1/payin: process_by es fiat_amount y falta fiat_currency. |
| `400` | `invalid_page_number` | list-payout / list-payin: page menor que 1. |
| `400` | `invalid_per_page_limit` | list-payout / list-payin: per_page fuera de 1 a 100. |
| `400` | `invalid_status_filter` | list-payout / list-payin: valor de status desconocido. |
| `400` | `invalid_network_filter` | list-payout / list-payin: red desconocida. |
| `400` | `invalid_date_from` | list-payout / list-payin: date_from no es una fecha. |
| `400` | `invalid_date_to` | list-payout / list-payin: date_to no es una fecha. |
| `400` | `invalid_reference_id` | check-payout / check-payin: referencia de menos de 10 o más de 756 caracteres. Extractos: reference_id de más de 255. |
| `400` | `invalid_pagination` | Extractos: page o per_page fuera de rango. |
| `400` | `invalid_format` | Extractos: date_from o date_to no es una fecha (details.field). |
| `400` | `invalid_date_range` | Extractos: date_from es posterior a date_to. |
| `400` | `date_range_too_wide` | Extractos: el rango es más amplio de lo permitido. details.max_days indica el tope. |
| `400` | `invalid_amount` | Extractos: amount_from o amount_to inválido, o amount_from mayor que amount_to. |
| `400` | `missing_parameters` | get-price: falta currency1 o currency2. |
| `400` | `invalid_currency` | get-price: currency1 o currency2 no es un código de moneda válido. |
| `400` | `invalid_webhook_url` | PUT /v1/fixed-wallets/config: webhook_url apunta a un destino no permitido. |

## Errores de Negocio

| Código HTTP | Error | Descripción |
| --- | --- | --- |
| `400` | `insufficient_total_balance` | POST /v1/payout: los ítems de una moneda suman más que el saldo disponible. No se debita nada. details trae crypto_currency, available y required. |
| `404` | `user_not_found` | POST /v1/payout: la configuración de la cuenta está incompleta. Contacte al soporte. |
| `404` | `record_not_found` | El payout o payin consultado por check-payout / check-payin no existe en esta cuenta. También se devuelve cuando falta un dato de la cuenta que la solicitud necesita. |
| `400` | `duplicate_reference_id` | POST /v1/payin: ya existe un payin con este reference_id en la cuenta. |
| `400` | `invalid_network_or_deposits_disabled` | POST /v1/payin: red desconocida, o los depósitos están desactivados en ella. |
| `400` | `unsupported_token_or_deposits_disabled` | POST /v1/payin: token no soportado, o los depósitos están desactivados para él. |
| `400` | `token_network_mismatch` | POST /v1/payin: el token no está disponible en esta red. |
| `400` | `memo_required` | POST /v1/payin: este token exige memo. |
| `400` | `amount_out_of_limits` | POST /v1/payin: el monto en cripto está fuera de los límites del token. details trae min_allowed y max_allowed. |
| `400` | `value_below_minimum` | POST /v1/payin con fiat_amount: el monto convertido está por debajo del mínimo. |
| `400` | `value_above_maximum` | POST /v1/payin con fiat_amount: el monto convertido está por encima del máximo. |
| `400` | `token_not_supported_or_inactive` | POST /v1/payin con fiat_amount: el token no es soportado o está inactivo para conversión. |
| `400` | `crypto_amount_does_not_correspond_to_fiat_amount_provided` | POST /v1/payin: se enviaron crypto_amount y fiat_amount y no coinciden con la cotización actual. |
| `400` | `invalid_parameters_for_process_by` | POST /v1/payin: los montos enviados no encajan con el process_by elegido. |
| `404` | `price_not_found` | get-price: no hay cotización para este par de monedas. |
| `403` | `feature_disabled` | Las direcciones fijas no están habilitadas para la cuenta (config, actualización de config y creación). |
| `422` | `type_not_supported` | POST /v1/fixed-wallets: este type no está disponible para direcciones fijas. |
| `403` | `limit_not_allowed` | POST /v1/fixed-wallets: el límite de la cuenta para este type es cero. |
| `409` | `limit_reached` | POST /v1/fixed-wallets: la cuenta ya tiene el número máximo de direcciones fijas. |
| `409` | `no_wallet_available` | POST /v1/fixed-wallets: no hay ninguna dirección de ese tipo disponible ahora. Reintente más tarde. |

## Errores por Ítem del Lote de Payout (results[].error)

| Código HTTP | Error | Descripción |
| --- | --- | --- |
| `207/400` | `invalid_parameters` | El ítem no pasó la validación. error.details.fields asocia cada campo a su mensaje. |
| `207/400` | `missing_required_fields` | Falta un campo obligatorio del ítem. |
| `207/400` | `duplicate_reference_id_in_request` | El mismo reference_id aparece antes en el mismo lote. |
| `207/400` | `duplicate_payment_reference` | El reference_id ya se usó en esta cuenta. El ítem trae duplicate, original_status, original_created_at y el internal_reference original; no se hace una segunda transferencia. |
| `207/400` | `invalid_crypto_amount` | process_by es crypto_amount y crypto_amount falta o es inválido. |
| `207/400` | `invalid_fiat_amount` | process_by es fiat_amount y fiat_amount falta o es inválido. |
| `207/400` | `fiat_currency_required` | process_by es fiat_amount y falta fiat_currency. |
| `207/400` | `fiat_currency_not_accepted` | La fiat_currency no es aceptada. |
| `207/400` | `memo_required` | Este token exige memo. |
| `207/400` | `invalid_network_or_withdrawals_disabled` | Red desconocida, o los retiros están desactivados en ella. |
| `207/400` | `unsupported_token_or_withdrawals_disabled` | Token no soportado, o los retiros están desactivados para él. |
| `207/400` | `token_network_mismatch` | El token no está disponible en esta red. |
| `207/400` | `token_not_active` | El token no está activo para retiros o para conversión. |
| `207/400` | `invalid_format` | La dirección de la wallet no es válida para la red. |
| `207/400` | `limit_exceeded` | El monto está fuera del mínimo y del máximo del token (también cuando el monto fiat convertido queda fuera). |
| `207/400` | `amount_does_not_cover_fees` | Las comisiones consumen todo el monto, y el destinatario no recibiría nada. |
| `207/400` | `crypto_amount_does_not_correspond_to_fiat_amount_provided` | crypto_amount y fiat_amount no coinciden con la cotización actual. |
| `207/400` | `price_unavailable` | Sin cotización para convertir el monto, o la clave tiene tope diario y el token no tiene cotización en USD para contabilizar el retiro. |
| `207/400` | `insufficient_balance` | El saldo disponible no cubre este ítem. |
| `207/400` | `daily_payout_limit_exceeded` | El ítem superaría el tope diario configurado en la clave. |
| `207/400` | `daily_payout_limit_unavailable` | La clave tiene tope diario pero no se pudo registrar el consumo. Reenvíe el ítem. |
| `207/400` | `error_fetching_active_tokens · error_fetching_active_currencies` | No se pudo cargar el catálogo de tokens o monedas. Reenvíe el ítem. |
| `207/400` | `user_not_found · record_not_found` | Falta un dato de la cuenta que el retiro necesita. |
| `207/400` | `internal_error · database_error · transaction_failed · network_unavailable · duplicate_record` | El registro del retiro falló de nuestro lado y se revirtió. No se debitó nada por este ítem; verifíquelo con check-payout o list-payout antes de reenviar. |

## Errores de Sistema

| Código HTTP | Error | Descripción |
| --- | --- | --- |
| `500` | `internal_error` | Fallo inesperado. En POST /v1/payout, un fallo a mitad del lote también devuelve summary (con not_processed) y results en el nivel superior, para que reenvíe solo lo que falta. |
| `500` | `database_error` | Extractos: la consulta falló. |
| `500` | `error_fetching_active_currencies` | POST /v1/payout y POST /v1/payin: no se pudo cargar el catálogo de tokens o fiats. No se procesó nada. |
| `500` | `token_type_not_found · receipt_creation_failed · database_transaction_failed` | POST /v1/payin: el payin no pudo crearse de nuestro lado. |
| `502` | `external_processor_error` | POST /v1/payin y POST /v1/fixed-wallets: la dirección de depósito no pudo crearse ahora. No se creó nada; reintente más tarde. |
| `503` | `no_processor_configured · no_wallet_available` | POST /v1/payin: no hay procesador ni dirección de depósito disponible para este token y red ahora. Reintente más tarde. |
| `503` | `price_not_available_for_conversion · price_not_available_for_validation` | POST /v1/payin: no hay cotización disponible para convertir o verificar el monto. Reintente más tarde. |
| `503` | `service_unavailable` | POST /v1/payin y endpoints de summary: una dependencia no está disponible temporalmente. |
| `500` | `config_update_failed` | PUT /v1/fixed-wallets/config: la configuración no pudo guardarse. |
| `500` | `assign_failed` | POST /v1/fixed-wallets: la dirección no pudo asignarse. |

## Mejores Prácticas

### Seguridad

**Hacer**

- Almacena tu API Key en variables de entorno
- Configura IP Whitelist
- Verifique el X-Bifrost-Signature de cada webhook
- Usa HTTPS en todas las solicitudes
- Implementa timeouts en las solicitudes

**No Hacer**

- Compartir API Key públicamente
- Hacer commit de API Keys en Git
- Usar la misma API Key en múltiples entornos

### Rendimiento

- Los datos de extracto y de resumen pueden tener hasta 5 minutos de retraso; no los consulte con más frecuencia
- Implementa paginación adecuada
- Evita solicitudes excesivas (respeta el rate limit)
- Procesa webhooks de forma asíncrona

### Idempotencia

- Use un reference_id único y significativo: es el cerrojo contra el pago duplicado, y volver a presentarlo devuelve el pago original en vez de crear otro
- Envíe Idempotency-Key en los lotes: si se cae la conexión, repetir con la misma clave devuelve la respuesta original en vez de reprocesar
- POST /v1/payin no tiene Idempotency-Key: tras un timeout reenvíe con el mismo reference_id y, si la respuesta es duplicate_reference_id, obtenga el payin con GET /v1/list-payin?reference_id=…
- Trate 207 y 500 leyendo results: ambos traen lo que ya se creó, para que reenvíe solo lo que faltó
- Gestione los reintentos de webhook deduplicando por data.internal_reference junto con event, para no procesar dos veces el mismo aviso
