Skip to Content

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 sobre message en su lugar: code no desambigua entre, digamos, invalid_input y payment_method_not_supported (ambos 400).
  • message: el nombre centinela en lower-snake-case, derivado de la constante interna errors.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

HTTPValores típicos de codeQué significa
400INVALID_INPUT, MISSING_REQUIRED, INVALID_FORMAT, INVALID_LENGTH, INVALID_VALUE, INVALID_USER_STATUS, INVALID_USER_ROLE, PAYMENT_METHOD_NOT_SUPPORTED, AMOUNT_BELOW_MINIMUMPetición incorrecta — mira details
401INVALID_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.
402INSUFFICIENT_CREDITEl saldo prepagado del comerciante se agotó: recarga antes de reintentar
403FORBIDDEN, IP_BLOCKEDLa clave es válida pero le falta el scope/permiso de IP para esta llamada
404NOT_FOUND, RECORD_NOT_FOUND, USER_NOT_FOUND, SESSION_NOT_FOUNDEl recurso no existe (o no existe para este comerciante)
409ALREADY_EXISTS, USER_ALREADY_EXISTS, SESSION_ALREADY_EXISTSReplay de idempotencia con un body diferente, o la máquina de estados rechazó la transición
429TOO_MANY_REQUESTS, TOO_MANY_ATTEMPTSLímite de tasa alcanzado; retrocede y reintenta
500INTERNAL_SERVER_ERROR, DATABASE_CONNECTION_ERROR, DATABASE_QUERY_ERROR, DATABASE_TRANSACTION_ERROR, REDIS_CONNECTION_ERROR, REDIS_OPERATION_ERROR, EXTERNAL_SERVICE_ERROREs 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.)
503SERVICE_UNAVAILABLEUna 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 rechazada
  • INVALID_TOKEN: token JWT/sesión no parseable o manipulado
  • TOKEN_EXPIRED: JWT pasado de exp
  • INVALID_OTP: el OTP no coincide
  • OTP_EXPIRED: OTP emitido fuera de la ventana de tolerancia
  • SESSION_EXPIRED: sesión del dashboard envejecida
  • INVALID_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ón
  • IP_BLOCKED: la IP está en la lista de abuso

No encontrado (404)

  • NOT_FOUND: genérico
  • RECORD_NOT_FOUND: fila ausente para el ID dado
  • USER_NOT_FOUND: búsqueda de usuario fallida
  • SESSION_NOT_FOUND: id de sesión del dashboard no reconocido

Conflicto (409)

  • ALREADY_EXISTS: genérico
  • USER_ALREADY_EXISTS: signup chocó con una restricción única
  • SESSION_ALREADY_EXISTS: inserción de sesión duplicada

Validación (400)

  • INVALID_INPUT: genérico; revisa details
  • MISSING_REQUIRED: un campo obligatorio estaba ausente
  • INVALID_FORMAT: el valor no coincidió con el formato esperado (p. ej. UUID, URL, email)
  • INVALID_LENGTH: el valor es demasiado corto o demasiado largo
  • INVALID_VALUE: valor fuera del enum/rango permitido
  • INVALID_USER_STATUS: el usuario está en un estado que prohíbe la acción
  • INVALID_USER_ROLE: al rol le falta permiso para la acción
  • PAYMENT_METHOD_NOT_SUPPORTED: la combinación proveedor/activo no está habilitada para el comerciante
  • AMOUNT_BELOW_MINIMUM: importe de la orden por debajo del piso por red o por entorno. El details.floor_usd de la respuesta lleva el piso configurado (USD) para que puedas mostrarlo directamente; el texto message tambié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 gateway
  • TOO_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 DB
  • DATABASE_QUERY_ERROR: el plan de query falló en tiempo de ejecución
  • DATABASE_TRANSACTION_ERROR: commit/rollback falló
  • REDIS_CONNECTION_ERROR: no se pudo alcanzar Redis
  • REDIS_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 respuesta Date + 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)

SuperficieLí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.