Errores
Todos los errores tienen la misma forma:
{
"error": {
"type": "invalid_request",
"code": "insufficient_balance",
"message": "No hay saldo suficiente para esta operación.",
"param": "amount",
"request_id": "req_9f2a4c8b7e1d3a5f"
}
}
Ramifica sobre code. Es parte del contrato y no cambia. El message está
escrito para que una persona lo entienda y puede cambiar de redacción sin aviso:
comparar cadenas de texto es construir una integración que se rompe cuando
alguien mejora un mensaje.
type agrupa por familia (invalid_request, authentication_error,
permission_error, not_found, conflict, rate_limit_error, api_error) y
sirve para el caso que no reconoces: te dice si tiene sentido reintentar.
:::note Un identificador mal escrito responde como uno que no existe
GET /v1/withdrawals/no-es-un-uuid devuelve el mismo 404 withdrawal_not_found que un identificador con la forma correcta que no está en
tu cuenta. No es un descuido: distinguirlos convertiría el endpoint en un
oráculo de identificadores válidos, y probando se podría averiguar qué
operaciones existen en la plataforma.
Consecuencia práctica: un 404 no te dice si te equivocaste al escribir o si el
recurso no es tuyo. Revisa el identificador antes de suponer que se borró.
:::
La regla de oro del reintento
| Código HTTP | ¿Reintentar? |
|---|---|
400, 403, 404, 422 | No. Reintentar da lo mismo. Arregla la petición |
401 | No, salvo que acabes de rotar la credencial |
409 | Sí, pero espera: hay otra petición igual en curso |
429 | Sí, después de los segundos que diga Retry-After |
5xx | Sí, con espera creciente y la misma Idempotency-Key |
:::danger Nunca reintentes un 5xx sin la misma clave de idempotencia
Un 500 no significa que no se ejecutó: puede haber fallado al responderte,
después de crear el cobro. Reintentar sin la clave crea un segundo cobro. Con
ella, te devolvemos el primero.
:::
Autenticación y permisos
| Código | HTTP | Qué pasó | Qué hacer |
|---|---|---|---|
invalid_api_key | 401 | Falta la cabecera, está mal formada, la credencial no existe, fue revocada, caducó, o la llamada viene de una IP no permitida | Comprueba Authorization: Bearer bk_…. Si acabas de rotar, despliega el secreto nuevo |
insufficient_scope | 403 | La credencial es válida pero no tiene el alcance que ese endpoint exige | El mensaje nombra el alcance que falta. Emite una credencial con él |
forbidden | 403 | La cuenta no puede hacer esa operación | Genérico. Si la verificación está pendiente el código es más específico — ver Antes de poder cobrar |
:::note Por qué el 401 es siempre el mismo Da igual si la credencial no existe, si fue revocada o si tu IP no está permitida: la respuesta es idéntica. Distinguirlos le confirmaría a quien prueba credenciales al azar cuáles existen. :::
Idempotencia
| Código | HTTP | Qué pasó | Qué hacer |
|---|---|---|---|
idempotency_key_required | 400 | Falta Idempotency-Key en una petición que crea o modifica | Añádela. Es obligatoria, no opcional |
idempotency_request_in_progress | 409 | Otra petición con esa misma clave se está ejecutando ahora | Espera y reintenta. No cambies la clave |
idempotency_key_reused | 422 | Esa clave ya se usó con un cuerpo distinto | Usa una clave nueva. Reutilizarla con otro contenido sería reemplazar en silencio |
idempotency_unavailable | 503 | No pudimos garantizar la idempotencia | Reintenta con la misma clave |
Una clave vive 24 horas. Después se trata como nueva.
Límites
| Código | HTTP | Qué hacer |
|---|---|---|
rate_limit_exceeded | 429 | Espera lo que diga RateLimit-Reset |
Las respuestas correctas traen RateLimit-Limit, RateLimit-Remaining y
RateLimit-Reset. Míralas antes de llegar al tope: es lo que te permite
autorregularte en vez de descubrirlo a golpes.
Antes de poder cobrar
Estos llegan con 403 y son los primeros que ve una integración nueva: la cuenta existe y la credencial es válida, pero todavía no puede mover dinero. No se arreglan desde la API — se resuelven en el panel del comercio.
| Código | Qué pasó | Qué hacer |
|---|---|---|
kyb_not_started | La empresa no ha empezado la verificación | Entrar al panel y completarla |
kyb_incomplete | Está a medias | El panel dice qué paso falta |
kyb_pending_review | Completa, esperando nuestra revisión | Esperar. No hay nada más que hacer |
kyc_not_started | La persona titular no ha empezado la suya | Panel |
kyc_incomplete | La de la persona titular está a medias | Panel |
email_not_verified | Correo de la cuenta sin verificar | Panel |
phone_not_verified | Teléfono de la cuenta sin verificar | Panel |
account_suspended | Cuenta suspendida | Escríbenos |
account_blocked | Cuenta bloqueada | Escríbenos |
user_not_found | La cuenta dueña de la credencial ya no existe | Emitir una credencial nueva |
:::tip Distínguelos de insufficient_scope
Los dos son 403 y significan cosas opuestas. insufficient_scope es tuyo:
emitiste la credencial sin ese alcance y lo arreglas en un minuto. Éstos son de
la cuenta: ningún alcance los levanta.
:::
Enlaces de pago
| Código | HTTP | Qué pasó | Qué hacer |
|---|---|---|---|
payment_link_not_found | 404 | No existe ese código en tu cuenta | Mismo error si no existe que si es de otra cuenta |
payment_link_not_cancellable | 409 | Ya está pagado, cancelado o caducado | Sólo un enlace active se puede cancelar |
payment_link_not_deletable | 409 | Todavía puede cobrar (active) o ya cobró (paid) | Cancélalo antes de borrarlo; uno pagado no se borra |
invalid_request | 400 | El cuerpo no pasa validación | El param dice qué campo |
payment_method_unavailable | 400 | Pediste un método en accepted_methods y no se pudo dejar listo en ese momento. No se creó el enlace | Reintenta (suele bastar), o créalo sin ese método. Ver Si pides métodos concretos |
Errores frecuentes al crear un enlace, y su causa real:
amountmandado como número → cadena decimal, siempre. Unfloatde JSON ya perdió precisión antes de llegarnos.- Un
accepted_methodsmal escrito → devuelve400, no se ignora. Un método ignorado en silencio crea un enlace con métodos distintos de los que pediste, y eso se descubre cobrando. - Ningún método viable para ese importe → los mínimos y máximos dependen de la ruta. → Pagos cruzados
Checkout
| Código | HTTP | Qué pasó |
|---|---|---|
checkout_not_found | 404 | El código no existe. Puede estar incompleto o mal copiado |
QR de mostrador
Los códigos son específicos a propósito: en una caja hay que saber por qué no se pudo cobrar sin llamar a nadie.
| Código | HTTP | Qué pasó | Qué hacer |
|---|---|---|---|
qr_not_found | 404 | La cuenta todavía no tiene QR | Se crea desde el panel |
qr_paused | 409 | El QR está pausado | POST /v1/qr/resume |
qr_revoked | 409 | El QR fue revocado | Hay que crear otro desde el panel |
qr_amount_not_positive | 400 | El importe es cero o negativo | Revisa lo que teclea la caja |
qr_amount_above_max_per_charge | 409 | Supera max_amount_per_charge | Sube el tope o parte el cobro |
qr_daily_limit_reached | 409 | Se agotó max_amount_per_day | Compara con amount_paid_today |
qr_too_many_pending_charges | 409 | Demasiados cargos sin pagar acumulados | Deja que caduquen o cancélalos |
qr_charge_unavailable | 409 | No se pudo generar el cargo por otro motivo | Reintenta; si persiste, escríbenos |
qr_daily_limit_reached y qr_amount_above_max_per_charge son tus propios
topes, no nuestros. Los pusiste tú con PATCH /v1/qr y los puedes cambiar
cuando quieras. → Cobro con QR
Webhooks
| Código | HTTP | Qué pasó |
|---|---|---|
webhook_url_invalid_url | 400 | La URL no se puede interpretar |
webhook_url_not_https | 400 | La URL no es https |
webhook_url_embedded_credentials | 400 | La URL lleva usuario y contraseña dentro |
webhook_url_private_host | 400 | El nombre resuelve a una dirección de red interna |
webhook_url_dns_failure | 400 | El nombre no resuelve |
webhook_endpoint_not_found | 404 | No existe ese destino en tu cuenta |
webhook_endpoints_limit_reached | 400 | Ya tienes 10, que es el tope por cuenta |
webhook_events_unknown | 400 | Algún evento no está en el catálogo |
Los tres primeros son la defensa contra que un destino apunte a una red privada: la URL la eliges tú y nuestro servidor es quien la llama. → Webhooks
Mover dinero: retiros y conversiones
Hasta hace poco todo rechazo de estas dos familias llegaba como
invalid_request y el motivo vivía sólo en el message. Ya no: cada uno trae
su código, y son pocos a propósito — están agrupados por lo que tienes que
hacer al recibirlos, que es para lo que sirve un código. El detalle concreto
(qué mínimo, en qué activo, cuánto llevas gastado) sigue en el message.
| Código | HTTP | Qué pasó | Qué hacer |
|---|---|---|---|
amount_below_minimum | 400 | El importe no llega al mínimo del riel o de la ruta | Súbelo. El message dice cuál es y en qué activo |
amount_above_maximum | 400 | Pasa del máximo por operación | Pártelo en varias |
invalid_amount | 400 | No tiene forma de importe (cero, negativo, texto) | Manda una cadena decimal |
amount_too_low | 400 | Tan pequeño que la comisión se lo come entero: no te quedaría nada | El message dice a partir de cuánto sí |
insufficient_balance | 400 | No hay saldo disponible | Es distinto de un límite: aquí el dinero no está |
daily_limit_exceeded | 400 | Se agotó tu cupo diario | Mañana vuelve a haber. No reintentes hoy |
monthly_limit_exceeded | 400 | Se agotó tu cupo mensual | Escríbenos si necesitas más |
Sólo en retiros
| Código | HTTP | Qué pasó | Qué hacer |
|---|---|---|---|
payout_destination_not_found | 404 | Ese destination_id no está en tu cuenta | Vuelve a listar GET /v1/payout_destinations |
destination_not_authorized | 400 | El destino existe pero no está verificado, o le falta el registro con el proveedor | Vuelve a darlo de alta |
network_unavailable | 400 | Esa red no está disponible ahora para ese activo | El message enumera las que sí |
invalid_address | 400 | La dirección no tiene la forma de esa red | Sólo se comprueba la forma: que sea la correcta es cosa tuya |
travel_rule_data_required | 400 | El importe supera el umbral de la Regla de Viaje (FATF R.16) | Manda beneficiary y repite |
provider_rejected | 400 | Lo rechazó el proveedor del riel, no nosotros | El message es suyo y puede cambiar sin aviso: ramifica por este código |
not_allowed_in_state | 400 | La operación no cabe en el estado actual (cancelar uno que ya salió) | Consulta el retiro para ver dónde está |
Sólo en conversiones
| Código | HTTP | Qué pasó | Qué hacer |
|---|---|---|---|
same_asset | 400 | from_asset y to_asset son el mismo | — |
pair_not_supported | 400 | Ese par no se convierte, ni con otro importe | No todos los activos tienen ruta entre sí |
quote_required | 400 | Falta quote_id | Una conversión no se ejecuta sin una cotización aceptada |
quote_expired | 400 | La cotización caducó o ya no está | Pide otra. Viven poco a propósito: el precio se mueve |
quote_mismatch | 400 | No es tuya, o el par y el importe no son los cotizados | Es lo que impide convertir algo distinto de lo que aceptaste |
slippage_exceeded | 400 | La tasa empeoró más de lo que permite max_slippage_bps | No se convirtió nada. Vuelve a cotizar |
:::tip Ramifica por code, nunca por message
Es la razón de que estos códigos existan. El message de provider_rejected
lo redacta el proveedor y ni siquiera lo controlamos nosotros.
:::
Registro y consultas
| Código | HTTP | Qué pasó |
|---|---|---|
invalid_cursor | 400 | El cursor no es uno nuestro. Usa el next_cursor tal cual |
transaction_not_found | 404 | No hay ese movimiento en tu cuenta |
request_log_not_found | 404 | No hay esa llamada registrada |
Cuando nada de esto explica lo que ves
| Código | HTTP | Qué hacer |
|---|---|---|
internal_error | 500 | Reintenta con la misma Idempotency-Key. Si persiste, escríbenos |
Guarda siempre el request_id. Va en toda respuesta —correcta o no— y en la
cabecera X-Request-Id. Es lo primero que te vamos a pedir, y con él localizamos
la llamada exacta en segundos.
Antes de abrir un ticket, mira tu propio registro:
curl "https://api.binkio.com/v1/request_logs?limit=25" \
-H "Authorization: Bearer bk_live_sec_…"
Si tu llamada no aparece ahí, no nos llegó — y el problema está entre tu servidor y nosotros, no en la API. Eso descarta la mitad de los casos sin esperar respuesta de nadie.