Errores
Cada respuesta 4xx/5xx lleva el mismo envelope 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 sobremessageen su lugar:codeno desambigua entre, digamos,invalid_inputypayment_method_not_supported(ambos 400).message: el nombre centinela en lower-snake-case (p. ej.INVALID_INPUTpasa 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.
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.
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):
{ "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 rechazadaINVALID_TOKEN: token no parseable o manipuladoTOKEN_EXPIRED: token expiradoSESSION_EXPIRED: sesión del dashboard expiradaINVALID_SIGNATURE: discrepancia de firma HMAC en llamadas B2B / webhook
Autorización (403)
FORBIDDEN: autenticado, pero no tienes permiso para realizar esta acciónIP_BLOCKED: las peticiones desde esta dirección IP están bloqueadas
No encontrado (404)
NOT_FOUND: genéricoRECORD_NOT_FOUND: fila ausente para el ID dado
Conflicto (409)
ALREADY_EXISTS: genérico
Validación (400)
INVALID_INPUT: genérico; revisadetailsMISSING_REQUIRED: un campo obligatorio estaba ausenteINVALID_FORMAT: el valor no coincidió con el formato esperado (p. ej. UUID, URL, email)INVALID_LENGTH: el valor es demasiado corto o demasiado largoINVALID_VALUE: valor fuera del enum/rango permitidoPAYMENT_METHOD_NOT_SUPPORTED: la combinación proveedor/activo no está habilitada para el comercianteAMOUNT_BELOW_MINIMUM: importe de la orden por debajo del piso por red o por entorno. Eldetails.floor_usdde la respuesta lleva el piso configurado (USD) para que puedas mostrarlo directamente; el textomessagetambié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 excedidoTOO_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 tercerosINTERNAL_SERVER_ERROR: error inesperado; contacta con soporte con la cabecera de respuestaDatey las cabecerasX-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:
{
"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 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.
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). 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.