Errors
Every Liqfy error is a JSON object with a stable shape — code your handler against it once and stop guessing. Every response, success or error, also carries the X-Request-Id header (generated when you don't send one, echoed verbatim when you do).
#Shape
{
"error": {
"type": "invalid_request_error",
"code": "invalid_amount",
"message": "amount deve ser um inteiro positivo em centavos.",
"param": "amount",
"details": []
},
"request_id": "req_01J..."
}| Field | Always present | Description |
|---|---|---|
error.type | yes | Stable category — one of invalid_request_error, authentication_error, permission_error, not_found_error, conflict_error, rate_limit_error, api_error. |
error.code | yes | Stable, snake_case machine code. This is what your handler should switch on, not message. |
error.message | yes | Human-readable explanation. Safe to log; not a contract for automation. |
error.param | when field-scoped | Identifies the single failing field, when unambiguous. |
error.details | on validation failures | Structured { param, message }[] for multi-field validation errors. |
request_id | yes | Quote this when contacting support — it traces every log, span and Kafka message. Also echoed on the X-Request-Id response header. |
Compatibility aliases: the response body also carries a few flat top-level fields —
statusCode,message,requestId, and (when present)code/fieldErrors— alongside theerrorobject, for clients (including the dashboard) that read them directly. Read the nestederrorobject; the flat aliases are not guaranteed to survive a future major version.
#Status code map
| HTTP | error.type | Recommended client action |
|---|---|---|
| 200 | — | — |
| 201 | — | Store the returned id and continue. |
| 400 | invalid_request_error | Fix the body/headers and retry. Don't retry blindly. |
| 401 | authentication_error | Verify the apikey header carries a correct, active key. |
| 403 | permission_error | Blocked by scope or the anti-fraud/velocity guard. Review the customer or contact support. |
| 404 | not_found_error | Check the id you supplied. |
| 409 | conflict_error | Same Idempotency-Key used with a different body — use a fresh key. |
| 422 | invalid_request_error | Business-rule violation (still request-shaped) — read message, fix input, don't retry. |
| 429 | rate_limit_error | Back off. Retry with exponential delay + jitter; honour Retry-After when present. |
| 5xx | api_error | Safe to retry with the same Idempotency-Key — idempotency protects you from duplicates. |
#Common errors
#400 Bad Request
Validation failed — error.code is invalid_request unless a more specific code applies (e.g. invalid_amount); error.details lists every failing field.
{
"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..."
}Also 400 when the Idempotency-Key header is missing from a financial write (POST /v1/charges, POST /v1/pix/charges). The /v1/payments routes (including refund) read only the body field idempotencyKey, not this header — a 400 there means that body field is missing or invalid.
Action Fix the request and retry. Never loop.
#401 Unauthorized
{
"error": {
"type": "authentication_error",
"code": "unauthorized",
"message": "Invalid API key"
},
"request_id": "req_01J..."
}Possible causes:
- Header missing — make sure
apikey: lq_live_...is set. - Key was rotated — check the dashboard.
- Whitespace — keys are exact-match, no leading/trailing spaces.
Action Verify the key. If it's correct, the key may have been disabled — contact support.
#403 Forbidden
The fraud/velocity guard blocks a charge before it reaches the provider — error.code is always 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..."
}A second fraud.* code covers the account's own velocity limit (message: "Too many payment attempts from this account in a short window").
Action Review the customer / slow down retries. If the block looks wrong, email support@liqfy.com.br with the 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..."
}Action Verify the id. If you just created the resource, wait 1–2 seconds and retry.
#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..."
}You replayed an Idempotency-Key with a body that differs from the original. Liqfy refuses to silently overwrite.
Action Use a fresh Idempotency-Key. Common cause: re-using an order id after the customer changed the cart.
#Business-rule rejections
Request-shape failures (missing/invalid fields) are 400, error.type: "invalid_request_error". Some downstream configuration failures — for example no PSP configured for the requested payment_method — are passed through from the internal orchestrator with its own status, most commonly 422 Unprocessable Entity:
{
"error": {
"type": "invalid_request_error",
"code": "unprocessable_entity",
"message": "no provider for PIX"
},
"request_id": "req_01J..."
}Action Read message. Don't retry — fix the input or contact support if the account should have a provider configured.
#429 Too Many Requests
{
"error": {
"type": "rate_limit_error",
"code": "rate_limited",
"message": "Rate limit exceeded"
},
"request_id": "req_01J..."
}Headers on this response (from Kong's rate-limiting plugin, scoped per API key — see Getting Started §4 for the current thresholds):
Retry-After: 12
X-RateLimit-Limit-Minute: 5000
X-RateLimit-Remaining-Minute: 0Action Back off — honour Retry-After if present, otherwise exponential backoff with 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 — server-side
{
"error": {
"type": "api_error",
"code": "service_unavailable",
"message": "Upstream unavailable"
},
"request_id": "req_01J..."
}Safe to retry. Use the same Idempotency-Key so a duplicate charge never gets created if processing actually succeeded but the response was lost.
#Webhook delivery errors (server → your endpoint)
When Liqfy can't reach your webhook endpoint, the failure shows up in GET /v1/webhooks/deliveries:
{
"id": "wd_...",
"status": "FAILED",
"attempts": 3,
"maxAttempts": 15,
"lastStatusCode": 502,
"lastError": "HTTP 502",
"lastResponseBody": "Bad Gateway",
"nextRetryAt": "2026-07-23T16:08:00.000Z"
}lastStatusCode / lastError | What it means |
|---|---|
2xx | Delivered. Won't retry. |
4xx | Your endpoint rejected the payload. Liqfy still retries up to maxAttempts — fix and replay if needed. |
5xx | Your endpoint is down. Liqfy retries with exponential backoff. |
429 / Retry-After | Liqfy honours your Retry-After and reschedules. |
Connection timeout / ETIMEDOUT | Your endpoint took >30s to respond. Always 200 OK fast — queue async. |
getaddrinfo ENOTFOUND | Your domain doesn't resolve. Update the registered URL. |
self signed certificate | TLS error. Liqfy requires valid public certs. |
After maxAttempts (default 15), the delivery is sent to DLQ and can be replayed manually. See Webhooks for full retry/DLQ semantics.
#Decoding error.details
error.details is a flat array of { param, message } — renderable as-is:
{error.details.map(({ param, message }) => (
<li key={param}>{param}: {message}</li>
))}The fieldErrors alias is the same information reshaped as { field: reasons[] }:
{
"fieldErrors": {
"amount": ["must be a positive integer"]
}
}#When to email support
Contact integrations@liqfy.com.br (or open a ticket) if:
- You see a 5xx that persists more than 5 minutes
- A webhook delivery is stuck in
FAILEDaftermaxAttempts - A
403 Forbiddenarrives unexpectedly - The same
Idempotency-Keyreturns different charges on different calls (this is a bug — should never happen)
Always quote the request_id — it lets us trace every log, span, and Kafka message in one query.