Errores
Todo error de Liqfy es un objeto JSON con una estructura estable: programa tu manejador contra ella una sola vez y deja de adivinar. Toda respuesta, de éxito o de error, también incluye el encabezado X-Request-Id (generado cuando no envías uno, devuelto tal cual cuando lo envías).
#Estructura
{
"error": {
"type": "invalid_request_error",
"code": "invalid_request",
"message": "Validation failed",
"details": [
{ "param": "amount", "message": "must be a positive integer" }
]
},
"request_id": "req_01J..."
}| Campo | Siempre presente | Descripción |
|---|---|---|
error.type | sí | Categoría estable: una de invalid_request_error, authentication_error, permission_error, not_found_error, conflict_error, rate_limit_error, api_error. |
error.code | sí | Código de máquina estable en snake_case. Es sobre esto que tu manejador debe ramificar, no sobre message. |
error.message | sí | Explicación legible por humanos. Seguro de registrar en logs; no es un contrato para automatización. |
error.param | cuando falla exactamente un campo | Identifica ese único campo con error. Si fallan varios campos, está ausente y solo error.details los enumera. |
error.details | en fallos de validación | { param, message }[] estructurado para errores de validación de varios campos. |
request_id | sí | Cítalo al contactar con soporte: rastrea cada log, span y mensaje de Kafka. También se devuelve en el encabezado de respuesta X-Request-Id. |
Alias de compatibilidad: el cuerpo de la respuesta también incluye algunos campos planos de nivel superior —
statusCode,message,requestIdy (cuando están presentes)code/fieldErrors— junto al objetoerror, para clientes (incluido el panel) que los leen directamente. Lee el objeto anidadoerror; no se garantiza que los alias planos sobrevivan a una futura versión mayor.
#Mapa de códigos de estado
#Creación de cobro con resultado desconocido
409 idempotency_outcome_unknown significa que la creación original pudo haber llegado al proveedor de pagos, pero su resultado aún no está confirmado. Reintenta con la misma Idempotency-Key y el mismo cuerpo de solicitud, usando backoff. No generes una nueva clave para ese cobro: podría crear otro pago.
La operación original se conserva incluso después de cinco minutos o 24 horas. Un reintento recupera su resultado guardado cuando está disponible; de lo contrario, devuelve este error sin emitir otra creación. La ausencia de un resultado en la consulta no prueba que el cobro nunca se haya creado. Contacta con soporte indicando el request_id si el resultado sigue siendo desconocido. Las respuestas completadas permanecen disponibles durante al menos 24 horas.
#Estados HTTP
| HTTP | error.type | Acción recomendada para el cliente |
|---|---|---|
| 200 | — | — |
| 201 | — | Guarda el id devuelto y continúa. |
| 400 | invalid_request_error | Corrige el cuerpo/los encabezados y reintenta. No reintentes a ciegas. |
| 401 | authentication_error | Verifica que el encabezado Authorization: Bearer lleve una clave correcta y activa. |
| 403 | permission_error | Bloqueado por el alcance (scope), la protección antifraude/de velocidad, un intento de reembolso con clave de API (marcador de mensaje REFUND_ADMIN_ONLY) o la lista de IP permitidas. Revisa la solicitud o contacta con soporte. |
| 404 | not_found_error | Comprueba el id que proporcionaste. |
| 409 | conflict_error | Misma Idempotency-Key usada con un cuerpo distinto (idempotency_key_reused), un primer intento aún en curso (idempotency_key_in_flight) o un estado no cancelable (payment_not_cancellable). |
| 422 | invalid_request_error | Violación de regla de negocio (sigue teniendo forma de solicitud): lee message, corrige la entrada, no reintentes. |
| 429 | rate_limit_error | Reduce el ritmo. Reintenta con retardo exponencial + jitter; respeta Retry-After cuando esté presente. |
| 5xx | api_error | Es seguro reintentar con la misma Idempotency-Key: la idempotencia te protege de duplicados. |
#Errores comunes
#400 Bad Request
La validación falló: error.code es invalid_request y error.details enumera todos los campos con error (error.param solo está presente cuando falla exactamente un campo).
{
"error": {
"type": "invalid_request_error",
"code": "invalid_request",
"message": "Validation failed",
"details": [
{ "param": "amount", "message": "must be a positive integer" },
{ "param": "payment_method", "message": "Invalid enum value" }
]
},
"request_id": "req_01J..."
}También se devuelve 400 cuando falta el encabezado Idempotency-Key en una escritura financiera. En POST /v1/charges y POST /v1/pix/charges el encabezado es el único canal. En POST /payments se acepta el campo del cuerpo idempotencyKey como alias: si se envían ambos deben coincidir (una discrepancia es un 400) y el valor que cuenta es el del encabezado. POST /payments/{id}/refund es exclusivo del panel de administración, por lo que nunca acepta ninguna de las dos formas desde una clave de API.
Acción Corrige la solicitud y reintenta. Nunca entres en bucle.
#401 Unauthorized
{
"error": {
"type": "authentication_error",
"code": "unauthorized",
"message": "Invalid API key"
},
"request_id": "req_01J..."
}Posibles causas:
- Falta el encabezado: asegúrate de que
Authorization: Bearer lq_live_...esté configurado. - La clave fue rotada: revisa el panel.
- Espacios en blanco: las claves deben coincidir exactamente, sin espacios iniciales ni finales.
Acción Verifica la clave. Si es correcta, puede que la clave se haya desactivado: contacta con soporte.
#403 Forbidden
La protección antifraude/de velocidad bloquea un cobro antes de que llegue al proveedor — error.code es fraud.*:
{
"error": {
"type": "permission_error",
"code": "fraud.customer_velocity_exceeded",
"message": "Too many payment attempts for this customer in a short window"
},
"request_id": "req_01J..."
}Un segundo código fraud.* cubre el límite de velocidad propio de la cuenta (mensaje: "Too many payment attempts from this account in a short window").
Acción Revisa al cliente / reduce la frecuencia de los reintentos. Solo los cobros vigentes cuentan para el límite: los cobros expirados o cancelados (p. ej., un QR de Pix que caducó sin pagarse) se ignoran, así que reintentarlos es seguro. Si el bloqueo parece incorrecto, escribe a suporte@liqfy.com.br indicando el request_id.
#Reembolsos — marcador de mensaje REFUND_ADMIN_ONLY
POST /payments/{id}/refund rechaza a todo llamante con clave de API con 403, sea cual sea el cuerpo: los reembolsos se emiten únicamente desde el panel de administración de Liqfy. Esto no es un valor de code: error.code es el "forbidden" simple y el token sobrevive solo como prefijo del mensaje, así que ramifica según el código para el rechazo y compara el prefijo solo como marcador (p. ej., message.startsWith("REFUND_ADMIN_ONLY")):
{
"error": {
"type": "permission_error",
"code": "forbidden",
"message": "REFUND_ADMIN_ONLY: reembolsos devem ser solicitados pelo painel/admin, ..."
},
"request_id": "req_01J..."
}Consulta Referencia de la API — Reembolsos.
#Lista de IP permitidas
Cuando la cuenta tiene configurada una lista de IP permitidas, una solicitud desde una dirección fuera de ella se rechaza con 403 — mismo sobre permission_error, error.code "forbidden". La comprobación es una protección por cuenta en las rutas principales de la cuenta (billeteras, retiros, transacciones, casos MED, api-keys y rutas similares); las rutas de movimiento de dinero de los servicios de pagos y de webhooks (/v1/charges, /v1/pix/charges, /v1/payments, /v1/webhooks) no están cubiertas por ella. Añade a la lista todas las direcciones de salida que usen tus integraciones (incluidos los hosts de CI y de failover), o la solicitud nunca llegará a la comprobación de la clave. Detalles y cómo actualizar la lista: Claves de API.
#account_suspended
La cuenta está suspendida o bloqueada: las solicitudes con clave de API a cobros, pagos, retiros, billetera y transacciones se rechazan hasta que soporte levante el bloqueo. La clave en sí sigue siendo válida: rotarla no ayuda.
{
"error": {
"type": "permission_error",
"code": "account_suspended",
"message": "Conta suspensa: a API não aceita requisições desta conta. Fale com o suporte."
},
"request_id": "req_01J..."
}Acción No reintentes. Escribe a suporte@liqfy.com.br indicando el request_id.
#404 Not Found
{
"error": {
"type": "not_found_error",
"code": "not_found",
"message": "Charge ch_a1b2c3d4-0000-0000-0000-000000000009 not found"
},
"request_id": "req_01J..."
}Acción Verifica el id. Si acabas de crear el recurso, espera 1–2 segundos y reintenta.
#409 Conflict
{
"error": {
"type": "conflict_error",
"code": "idempotency_key_reused",
"message": "Idempotency key 'ORD-7821' already used with a different request body"
},
"request_id": "req_01J..."
}Repetiste una Idempotency-Key con un cuerpo distinto del original. Liqfy se niega a sobrescribir en silencio.
Otros dos códigos conflict_error llegan con 409:
error.code | Cuándo | Acción |
|---|---|---|
idempotency_key_reused | Misma clave, cuerpo distinto. | Usa una clave nueva: la causa habitual es reutilizar un id de pedido después de que el cliente cambió el carrito. |
idempotency_key_in_flight | Una primera solicitud con esta clave todavía se está procesando. | Espera a que termine (o reintenta tras unos segundos): no cambies de clave. |
payment_not_cancellable | POST /payments/{id}/cancel sobre un cobro PROCESSING. | Espera a que el cobro se liquide y, en su lugar, reembolsa desde el panel de administración. |
#Rechazos por reglas de negocio
Los fallos de forma de la solicitud (campos ausentes/no válidos) son 400, error.type: "invalid_request_error". Algunos fallos de configuración posteriores — por ejemplo, ningún proveedor configurado para el payment_method y el currency solicitados — se transmiten desde el orquestador interno con su propio estado y un código de máquina UPPER_SNAKE_CASE, con mayor frecuencia 422:
{
"error": {
"type": "invalid_request_error",
"code": "NO_PROVIDER_CONFIGURED",
"message": "No payment provider is configured for this payment method and currency."
},
"request_id": "req_01J..."
}Códigos de esta familia del lado de los cobros: PROVIDER_UNAVAILABLE, PROVIDER_ERROR, PAYMENT_REJECTED, NO_PROVIDER_CONFIGURED, UNKNOWN_METHOD, MOCK_PROVIDER_IN_PRODUCTION, PAYER_IDENTITY_REQUIRED (cuando expected_payer_tax_id bloquea el cobro y el documento del pagador no coincide).
Códigos del lado de pagos/retiros: WALLET_NOT_FOUND, WALLET_NOT_SPENDABLE, AMOUNT_BELOW_MINIMUM, AMOUNT_ABOVE_MAXIMUM, DAILY_PAYOUT_LIMIT_EXCEEDED (422), INSUFFICIENT_BALANCE, WITHDRAWAL_NOT_CANCELLABLE. Ten en cuenta la excepción en la ruta de retiros: INSUFFICIENT_BALANCE llega como 400, no como 422: ramifica según error.code, no según el estado.
Acción Lee error.code y message. No reintentes: corrige la entrada o contacta con soporte si la cuenta debería tener un proveedor configurado.
#429 Too Many Requests
{
"error": {
"type": "rate_limit_error",
"code": "rate_limited",
"message": "Rate limit exceeded"
},
"request_id": "req_01J..."
}Encabezados de esta respuesta (del plugin de rate limiting de Kong, con alcance por clave de API; consulta Primeros pasos §4 para los umbrales actuales):
Retry-After: 12
X-RateLimit-Limit-Minute: 5000
X-RateLimit-Remaining-Minute: 0Acción Reduce el ritmo: respeta Retry-After si está presente; de lo contrario, usa backoff exponencial con jitter.
async function withRetry(fn, attempts = 5) {
for (let i = 0; i < attempts; i++) {
try { return await fn(); }
catch (err) {
if (err.status !== 429 && err.status < 500) throw err;
const base = Math.min(1000 * 2 ** i, 30_000);
const jitter = Math.random() * 0.3 * base;
await new Promise(r => setTimeout(r, base + jitter));
}
}
throw new Error('exhausted retries');
}#5xx — del lado del servidor
{
"error": {
"type": "api_error",
"code": "service_unavailable",
"message": "Upstream unavailable"
},
"request_id": "req_01J..."
}Es seguro reintentar. Usa la misma Idempotency-Key para que nunca se cree un cobro duplicado si el procesamiento realmente tuvo éxito pero la respuesta se perdió.
#Errores de entrega de webhooks (servidor → tu endpoint)
Cuando Liqfy no puede alcanzar tu endpoint de webhook, el fallo aparece en GET /v1/webhooks/deliveries:
{
"id": "b2c3d4e5-f6a7-4890-9abc-def012345678",
"status": "FAILED",
"attempts": 3,
"maxAttempts": 15,
"lastStatusCode": 502,
"lastError": "HTTP 502",
"lastResponseBody": "Bad Gateway",
"nextRetryAt": "2026-07-23T16:08:00.000Z"
}lastStatusCode / lastError | Qué significa |
|---|---|
2xx | Entregado. No se reintentará. |
4xx | Tu endpoint rechazó el payload. Liqfy sigue reintentando hasta maxAttempts: corrige y repite si es necesario. |
5xx | Tu endpoint está caído. Liqfy reintenta con backoff exponencial. |
429 / Retry-After | Liqfy respeta tu Retry-After y reprograma el envío. |
Connection timeout / ETIMEDOUT | Tu endpoint tardó >30 s en responder. Responde siempre 200 OK rápido y encola el procesamiento de forma asíncrona. |
getaddrinfo ENOTFOUND | Tu dominio no resuelve. Actualiza la URL registrada. |
self signed certificate | Error de TLS. Liqfy requiere certificados públicos válidos. |
Tras maxAttempts (por defecto 15), la entrega se envía a la DLQ y puede repetirse manualmente. Consulta Webhooks para la semántica completa de reintentos/DLQ.
#Errores de gestión de webhooks
Códigos en las rutas de administración de /v1/webhooks (permission_error / conflict_error / rate_limit_error según el código):
error.code | Cuándo |
|---|---|
WEBHOOK_ENDPOINT_DUPLICATE | La URL ya está registrada en esta cuenta (409). |
WEBHOOK_ENDPOINT_LIMIT | La cuenta alcanzó su límite de endpoints activos. |
WEBHOOK_HTTPS_REQUIRED | Una URL que no es https:// fuera de sandbox/staging. |
WEBHOOK_OWN_DOMAIN | La URL apunta a un dominio Liqfy. |
WEBHOOK_TEST_RATE_LIMIT | Demasiadas llamadas a POST /endpoints/{id}/test en una hora (429). |
WEBHOOK_ROLE_REQUIRED | El rol del llamante no puede gestionar endpoints (access.canManage: false). |
IMPERSONATION_VIEW_ONLY | La sesión está en modo de suplantación y solo puede leer. |
TENANT_NOT_ACTIVE | La cuenta no está activa. |
#Decodificación de error.details
error.details es un arreglo plano de { param, message } que se puede renderizar tal cual:
{error.details.map(({ param, message }) => (
<li key={param}>{param}: {message}</li>
))}El alias fieldErrors es la misma información reestructurada como { field: reasons[] }:
{
"fieldErrors": {
"amount": ["must be a positive integer"]
}
}#Cuándo escribir a soporte
Contacta con suporte@liqfy.com.br (o abre un ticket) si:
- Ves un 5xx que persiste más de 5 minutos
- Una entrega de webhook está atascada en
FAILEDdespués demaxAttempts - Llega un
403 Forbiddende forma inesperada - La misma
Idempotency-Keydevuelve cobros distintos en llamadas distintas (es un bug: nunca debería ocurrir)
Cita siempre el request_id: nos permite rastrear cada log, span y mensaje de Kafka en una sola consulta.
#Límite de lectura por entidad
HTTP 429, error.type: rate_limit_error, error.code: entity_rate_limited. Espera los segundos indicados por Retry-After antes de volver a leer esta entidad. Consulta Polling y límites.