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, derivado de la constante internaerrors.go(p. ej.INVALID_INPUT → "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.
Actualmente no devolvemos trace_id ni timestamp en el body. Borradores anteriores de esta página prometían ambos: eso era aspiracional. Si necesitas correlacionar un log del servidor con una petición, captura la cabecera de respuesta Date y las cabeceras de rate-limit del lado del gateway (X-RateLimit-*) y cítalas en un ticket de soporte.
Los rechazos en el borde del gateway usan una forma diferente. El envelope
de arriba es lo que emiten los servicios backend. Las peticiones rechazadas
en el gateway antes de llegar a un servicio (una X-Signature faltante/
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. Así que un verificador debe ramificar primero por
status HTTP, luego leer message, y solo tratar code/details como
presentes una vez que la petición superó el gateway. Body de ejemplo del
gateway (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, INVALID_USER_STATUS, INVALID_USER_ROLE, PAYMENT_METHOD_NOT_SUPPORTED, AMOUNT_BELOW_MINIMUM | Petición incorrecta — mira details |
| 401 | INVALID_CREDENTIALS, INVALID_TOKEN, TOKEN_EXPIRED, INVALID_SIGNATURE (B2B); INVALID_OTP, OTP_EXPIRED, SESSION_EXPIRED (solo dashboard / JWT) | Falló la auth: clave incorrecta, timestamp expirado, firma equivocada. Los códigos OTP/SESSION_EXPIRED solo aparecen en rutas dashboard-JWT (/payment/v1/*); las integraciones puras B2B no los verán. |
| 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, USER_NOT_FOUND, SESSION_NOT_FOUND | El recurso no existe (o no existe para este comerciante) |
| 409 | ALREADY_EXISTS, USER_ALREADY_EXISTS, SESSION_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, DATABASE_CONNECTION_ERROR, DATABASE_QUERY_ERROR, DATABASE_TRANSACTION_ERROR, REDIS_CONNECTION_ERROR, REDIS_OPERATION_ERROR, EXTERNAL_SERVICE_ERROR | Es culpa nuestra; es seguro reintentar con backoff. (Los códigos específicos de proveedor como TWILIO_SERVICE_ERROR / SENDGRID_SERVICE_ERROR existen internamente pero solo aparecen en flujos de notificación del lado dashboard, no en endpoints B2B.) |
| 503 | SERVICE_UNAVAILABLE | Una dependencia aguas abajo está caída. Reintenta con backoff |
Referencia completa de códigos
El conjunto completo de valores de code que puedes ver (coincide con payment-service/pkg/errors/errors.go):
Auth y sesiones (401)
INVALID_CREDENTIALS: combinación username/password o clave API rechazadaINVALID_TOKEN: token JWT/sesión no parseable o manipuladoTOKEN_EXPIRED: JWT pasado deexpINVALID_OTP: el OTP no coincideOTP_EXPIRED: OTP emitido fuera de la ventana de toleranciaSESSION_EXPIRED: sesión del dashboard envejecidaINVALID_SIGNATURE: discrepancia de firma HMAC en llamadas B2B / webhook
Autorización (403)
FORBIDDEN: autenticado pero el rol/scope/límite del comerciante bloquea la acciónIP_BLOCKED: la IP está en la lista de abuso
No encontrado (404)
NOT_FOUND: genéricoRECORD_NOT_FOUND: fila ausente para el ID dadoUSER_NOT_FOUND: búsqueda de usuario fallidaSESSION_NOT_FOUND: id de sesión del dashboard no reconocido
Conflicto (409)
ALREADY_EXISTS: genéricoUSER_ALREADY_EXISTS: signup chocó con una restricción únicaSESSION_ALREADY_EXISTS: inserción de sesión duplicada
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 permitidoINVALID_USER_STATUS: el usuario está en un estado que prohíbe la acciónINVALID_USER_ROLE: al rol le falta permiso para la acciónPAYMENT_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 gas nativo para difundir la transferencia.
Pago / facturación (402)
INSUFFICIENT_CREDIT: el saldo prepagado del comerciante no puede cubrir la tarifa de gas / plataforma. Recarga vía dashboard, luego reintenta
Límite de tasa (429)
TOO_MANY_REQUESTS: límite de tasa de IP del gatewayTOO_MANY_ATTEMPTS: intentos fallidos repetidos sobre el mismo recurso (p. ej. OTP) activaron un throttle
Almacenamiento / infraestructura (500)
DATABASE_CONNECTION_ERROR: no se pudo alcanzar la DBDATABASE_QUERY_ERROR: el plan de query falló en tiempo de ejecuciónDATABASE_TRANSACTION_ERROR: commit/rollback fallóREDIS_CONNECTION_ERROR: no se pudo alcanzar RedisREDIS_OPERATION_ERROR: comando Redis fallóEXTERNAL_SERVICE_ERROR: fallo genérico de terceros (proveedor no clasificado abajo)TWILIO_SERVICE_ERROR: llamada Twilio SMS / Verify fallóSENDGRID_SERVICE_ERROR: envío de correo SendGrid fallóINTERNAL_SERVER_ERROR: caso no capturado por las categorías anteriores; captura la cabecera de respuestaDate+X-RateLimit-*y contacta
Disponibilidad (503)
SERVICE_UNAVAILABLE: una dependencia crítica aguas abajo reporta no saludable; backoff + reintento
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 cubre tres modos de fallo distintos: la única manera de distinguirlos es probando cada posibilidad por descarte:
- 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 |
|---|---|
Cada ruta del gateway (incl. /b2b/v1/*) | Por IP en el gateway: 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 emiten en cada respuesta limitada por el gateway (no solo en éxitos). 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 están sujetos a cambio. Si estás chocando con ellos legítimamente (p. ej., reconciliando un rango histórico grande), contacta: los endpoints en bulk están en el roadmap.
Crédito insuficiente (402)
INSUFFICIENT_CREDIT (HTTP 402 Payment Required) significa que el saldo prepagado del comerciante no puede cubrir la próxima tarifa de gas-y-plataforma para la operación que intentaste realizar: típicamente liquidar un pago cripto o ejecutar una acción on-chain con gas patrocinado. Recarga desde el dashboard del comerciante (Billing → Add credit), luego reintenta la operación; el trabajo en vuelo espera y retoma automáticamente una vez que el saldo se libera.
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 pivotemos a la petición relevante en nuestros logs.
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 encola para reintento con backoff exponencial a 0s, 1min, 5min, 15min, 1h, 6h (seis intentos en total: 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 manda a dead-letter: el dashboard muestra una etiqueta “Failed” que puedes reenviar manualmente una vez que tu servidor esté saludable.