Códigos de Error
Todos los códigos de error que devuelve la API, agrupados por tipo, con qué hacer en cada caso.
ᚱ
"Cuando las fuerzas del caos interfieren, los dioses envían mensajes claros a los mortales. Aprende los códigos sagrados que revelan la naturaleza de cada obstáculo en el camino."Los Guardianes de Bifrost
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
ᚱ
"La sabiduría de los ancianos enseña que seguir los caminos correctos garantiza un viaje seguro y próspero por los nueve reinos digitales."Mímir, Guardián de la Sabiduría Ancestral
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