API Reference
Complete catalogue of every public Liqfy v1 endpoint. All paths are relative to the production base URL — https://api.liqfy.com.br/v1. (A dedicated sandbox host is not yet available.)
Authenticate every request with your API key in the Authorization: Bearer <key> header — Authorization: Bearer <LIQFY_API_KEY> (lq_test_*/lq_live_*). The legacy apikey: <key> header is still accepted for compatibility. All bodies are JSON. All amounts are integers in the smallest currency unit.
The API is organised around a few resources:
- Charges (
/v1/charges) — create and read Pix charges:object: "charge",ch_…ids, headerIdempotency-Key,pixblock. This is where every integration starts. - Wallets (
/v1/wallets) — your account balance. - Payment operations (
/v1/payments) — cancellation, stats, receipts and other operations on a charge, addressed by its raw id (thech_prefix stripped). Refunds are issued from the admin panel, not from this API. - Webhooks (
/v1/webhooks) — register endpoints and inspect deliveries.
#Charges
/v1/charges.
Public contract from API conventions §3. Every response is built by an allowlist serializer — no internal id, PSP name, cost or raw provider payload can appear here.
#Create charge
POST /v1/charges — Pix-first shortcut: POST /v1/pix/charges fixes payment_method: "pix" in the body (overriding any value you send) and returns the exact same Charge.
Headers
| Header | Required | Notes |
|---|---|---|
Authorization | yes | Bearer + your API key (lq_test_* / lq_live_*). The legacy apikey header is still accepted. |
Idempotency-Key | yes | Any non-blank string, scoped by (merchant, key). A financial write — missing it is a 400. |
Body
| Field | Type | Required | Description |
|---|---|---|---|
amount | integer | yes | Smallest currency unit, > 0. |
currency | string | no | 3-letter ISO 4217 code, upper-cased by the API. Defaults to "BRL". Pix charges must use BRL. |
payment_method | string | yes | "pix" — the only value accepted today. |
description | string | no | Up to 500 chars; folded into metadata.description on read. |
expires_in | integer | no | Pix validity in seconds, 60–604800 (7 days). Outside that range the API answers 400 — the value is never capped. When the provider returns its own pix.expires_at, that value is authoritative. |
customer.name | string | no | |
customer.document | string | no | CPF or CNPJ. |
customer.email | string | no | Must be a valid email address. |
customer.phone | string | no | |
expected_payer_tax_id | string | no | Locks the charge to a payer: checksum-valid CPF (11) or CNPJ (14) digits (normalized to digits only). If you also send customer.document, the two must match; if you set it, customer.name becomes required. |
metadata | object | no | Free-form; reserved/underscore-prefixed keys are stripped. |
Example
curl -X POST https://api.liqfy.com.br/v1/pix/charges \
-H "Authorization: Bearer $LIQFY_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"amount": 1000,
"currency": "BRL",
"description": "Pedido #12345",
"customer": { "name": "Maria Silva", "document": "12345678901" }
}'Response 201 Created
{
"id": "ch_a1b2c3d4-0000-0000-0000-000000000009",
"object": "charge",
"amount": 1000,
"currency": "BRL",
"status": "pending",
"payment_method": "pix",
"customer": { "name": "Maria Silva", "document": "12345678901" },
"pix": {
"br_code": "000201BRCODEPIX",
"qr_code_url": "https://qr.example/img.png",
"expires_at": "2026-07-23T15:00:00.000Z"
},
"checkout_url": "https://checkout.liqfy.com.br/a1b2c3d4-0000-0000-0000-000000000009",
"settlement": {},
"metadata": { "order_id": "12345" },
"created_at": "2026-07-23T14:30:00.000Z"
}pix.txid (when the provider bound one) and settlement.end_to_end_id (only once paid and settled) are contextual Pix data — see PIX. charge.id is never equal to charge.pix.txid.
Replaying the same Idempotency-Key with the same body returns the original 201 and sets the response header Idempotency-Replayed: true.
checkout_url is Liqfy's hosted checkout for the charge — redirect the payer there instead of rendering your own Pix screen. It comes back on both create and GET /v1/charges/{id}, and can't be derived from charge.id (the checkout path drops the ch_ prefix).
Public status vocabulary: pending, processing, paid, failed, expired, cancelled, refunded, disputed.
#Get charge
GET /v1/charges/{id}
Accepts either the ch_…-prefixed id or the raw internal id. Scoped to the caller's account — a charge from another account 404s.
Response 200 OK — same Charge shape as creation.
When a MED case is opened on the charge, metadata.med.caseId carries the public id of that MED case (med_…, the same id as GET /v1/med/cases/{id}), so you can link the charge to the case. The provider's own case id is never exposed.
#Get charge by idempotency key
GET /v1/charges/by-idempotency-key/{key}
Looks up the charge you created with a given Idempotency-Key — useful to recover a charge after a timeout without replaying the create. Scoped to the caller's account. The key is trimmed; an empty key or one longer than 255 chars answers 400.
Response 200 OK — same Charge shape as creation.
#List charges
GET /v1/charges
GET /v1/charges lists the account's charges with cursor pagination ({ object: "list", data, has_more, next_cursor }) — ordering is createdAt desc, stable, no page/offset on this route.
GET /v1/charges?limit=25&starting_after=ch_01J…&status=paid| Param | Type | Default | Description |
|---|---|---|---|
limit | integer | 25 | 1–100. |
starting_after | string | — | A charge id (ch_…) of the same resource — cursor, not an offset. Up to 140 chars. |
status | enum | — | Public status vocabulary (pending, paid, …). |
created_after | ISO 8601 | — | Inclusive lower bound on created_at. |
created_before | ISO 8601 | — | Inclusive upper bound on created_at. |
customer_id | string | — | Filters by payer document. Up to 140 chars. |
idempotency_key | string | — | Filters by the Idempotency-Key used on creation (1–255 chars, trimmed). |
{
"object": "list",
"data": [],
"has_more": false,
"next_cursor": null
}page/offset are never accepted on this cursor list — only starting_after. When has_more is true, pass next_cursor as starting_after to fetch the next page.
For a paged, filterable view of the same transactions, see GET /v1/payments.
#Wallets
/v1/wallets. Your account's balances. Read-only, scoped to your API key — the write routes under /v1/wallets/* are dashboard-only and answer 403 to an API key.
#Get balance
GET /v1/wallets/balance
The currency query param is optional. Without it (the recommended form) the response carries every currency on the account in balances[], plus the primary currency's fields (BRL by default) at the top:
curl "https://api.liqfy.com.br/v1/wallets/balance" \
-H "Authorization: Bearer $LIQFY_API_KEY"Response 200 OK
{
"available": 880,
"pending": 0,
"total": 880,
"retained": 0,
"currency": "BRL",
"next_release_at": null,
"next_release_amount": null,
"balances": [
{ "currency": "BRL", "available": 880, "held": 0, "pending": 0, "total": 880 }
],
"primary": { "currency": "BRL", "available": 880, "held": 0, "pending": 0, "total": 880 }
}| Field | Description |
|---|---|
available | Spendable/payoutable balance of the primary currency, smallest unit (centavos for BRL). 880 = R$ 8,80. Signed — it goes negative when the account is in debt. |
held | Amount held (retained mid-flight) in balances[]/primary entries. Not present at the top level. |
pending | Settled but still held — not spendable yet (primary currency). Includes in-flight refund holds, so it can be greater than retained. |
total | available + held + pending of the primary currency. |
retained | Total under retention (PENDING holds) of the primary currency. Does not include refund holds. |
currency | Primary currency (ISO 4217). Defaults to BRL. |
next_release_at | ISO 8601 string of when the next pending tranche releases, or null when nothing is scheduled. |
next_release_amount | Amount of that next release (smallest currency unit), or null. |
balances[] | One entry per currency: { currency, available, held, pending, total }. |
primary | The primary currency (same fields as a balances[] entry). |
The top-level fields (available/pending/total/currency) are the primary currency — kept for backward compatibility. For multi-currency accounts, iterate over balances[].
How the numbers are computed (they are not interchangeable):
balances[]andprimarycount only activeOPERATIONALwallets, summed per currency:available = Σ(balance − heldBalance),held = Σ heldBalance,pending = Σ pendingBalance,total = available + held + pending.- This form never returns
404: with no wallet at all you get zeros andbalances: []. - A single payout draws from one wallet, and the flat
?currency=shape below reads the default wallet only — so for a currency with several wallets,/balance,?currency=and the payout check can show three differentavailablenumbers.
#Single currency (legacy shape)
GET /v1/wallets/balance?currency=BRL
Pass currency to get the flat single-currency shape:
curl "https://api.liqfy.com.br/v1/wallets/balance?currency=BRL" \
-H "Authorization: Bearer $LIQFY_API_KEY"{
"available": 880,
"held": 0,
"pending": 0,
"currency": "BRL",
"next_release_at": null,
"next_release_amount": null
}currency is case-insensitive. This shape reads the account's default wallet for that currency only (not a sum), and an unknown currency returns zeros instead of an error. There is no total/retained here.
Auth & errors. Always send the API key. A missing or invalid key →
401(never404). Withoutcurrencythis route never returns404; with?currency=, a404not_foundmeans the account has no wallet at all (not a routing problem). A wrong path (/v1/wallets/balance, plural, with the/v1prefix) or an intermediary that doesn't forward/v1/wallets/*never reaches Liqfy and surfaces as its own error.
#List wallets
GET /v1/wallets
Every wallet on the account — any walletType, active and inactive, several per currency possible — same auth. A bare JSON array, no pagination.
curl https://api.liqfy.com.br/v1/wallets \
-H "Authorization: Bearer $LIQFY_API_KEY"Response 200 OK
[
{
"id": "a1b2c3d4-e5f6-4789-9abc-def012345678",
"currency": "BRL",
"label": "Operating wallet",
"walletType": "OPERATIONAL",
"isDefault": true,
"isActive": true,
"balance": 880,
"pendingBalance": 0,
"createdAt": "2026-04-25T15:42:11.000Z"
}
]balance/pendingBalance are in the smallest currency unit. They are not the same amounts GET /v1/wallets/balance surfaces as available/pending: balance already includes the held amount (available = balance − heldBalance on an active OPERATIONAL wallet), pendingBalance matches pending only in the single-wallet case, and this list carries no heldBalance — so available is not computable from it.
#Get wallet
GET /v1/wallets/{id}
One wallet by id (UUID). Any type, active or not, scoped to the caller's account.
curl https://api.liqfy.com.br/v1/wallets/a1b2c3d4-e5f6-4789-9abc-def012345678 \
-H "Authorization: Bearer $LIQFY_API_KEY"Response 200 OK
{
"id": "a1b2c3d4-e5f6-4789-9abc-def012345678",
"currency": "BRL",
"walletType": "OPERATIONAL",
"label": "Operating wallet",
"isDefault": true,
"balance": 880,
"pendingBalance": 0,
"heldBalance": 0,
"isActive": true,
"createdAt": "2026-04-25T15:42:11.000Z",
"updatedAt": "2026-07-23T14:31:00.000Z"
}An unknown id, or one from another account, answers 404 not_found.
#Payment operations
/v1/payments. These routes operate on the same charge you created via /v1/charges, addressed by its raw id (the ch_ prefix stripped). They cover operations the /v1/charges surface doesn't expose yet — cancellation, stats, receipts, reconciliation — plus create/read/list in the transaction wire format. To create a charge, prefer Charges.
Refunds are not part of the merchant API: POST /payments/{id}/refund answers 403 to every API-key request (error.code "forbidden", message prefixed with the REFUND_ADMIN_ONLY marker). Refunds are issued from the Liqfy admin panel — see Refunds below.
Internal admin routes under /payments (for example GET /payments/{id}/provider-status) are not part of the merchant API: called with an API key they answer 404, as if the route did not exist.
#Create payment
POST /payments
Headers
| Header | Required | Notes |
|---|---|---|
Authorization | yes | Bearer + your API key. |
Idempotency-Key | yes | Preferred channel. The body field idempotencyKey is accepted as an alias — if both are sent they must match (divergent values → 400); sending neither → 400. |
Body
| Field | Type | Required | Description |
|---|---|---|---|
amount | integer | yes | Smallest currency unit, > 0. |
currency | string (3–8) | no | Defaults to "BRL". Examples: BRL, EUR, USD, USDT. |
paymentMethods | string[] | unless checkout | One or more of the 11 methods: PIX, CRYPTO, CREDIT_CARD, BOLETO, MBWAY, MULTIBANCO, IDEAL, BANCONTACT, SEPA, BIZUM, SWAP_NOW. With checkout it is optional and narrows the template's methods. |
checkout | string | no | Hosted-checkout template slug (1–80 chars). An unknown slug → 404; if the template service is unreachable → 503. |
customerId | string | no | Your internal user id. Forwarded with the request only — not persisted, not echoed in responses or merchant webhooks. |
customerName | string | no | |
customerDocument | string | no | CPF, CNPJ, NIF, or other tax id. |
customerEmail | string | no | Must be a valid email address. |
customerPhone | string | no | |
customer | object | no | Nested { name?, document?, email?, phone? }. Flattened into the customer* fields; the flat fields win on conflict. |
expectedPayerDocument | string | no | Digits-only payer CPF/CNPJ lock. |
items | object[] | no | { name, quantity, unitPrice, totalPrice } — all integers > 0 except name. |
metadata | object | no | Free-form. returnUrl/cancelUrl keys inside it must be http(s) URLs, but the first-class returnUrl/cancelUrl fields below are the return URLs. |
webhookUrl | string | no | Per-transaction webhook URL override. Must be a valid URL. |
country | string | no | 2–3 chars. |
riskTier | enum | no | LOW, MEDIUM or HIGH. |
environment | enum | no | SANDBOX, STAGING or PRODUCTION. When omitted, the platform derives it. The key alone decides where the charge actually goes. |
returnUrl | string | no | Where the payer returns after paying: http(s) URL without whitespace or fragment. |
cancelUrl | string | no | Where the payer goes on cancel: same URL rule. Requires returnUrl. |
expiresIn | integer | no | Pix validity in seconds, 60–604800. |
idempotencyKey | string | yes | Body alias for the Idempotency-Key header. Unique per intended request; replays return the original. |
Response 201 Created
{
"id": "a1b2c3d4-e5f6-4789-9abc-def012345678",
"status": "WAITING_PAYMENT",
"amount": 24900,
"currency": "BRL",
"paymentMethods": ["PIX"],
"customerName": "Maria Silva",
"customerDocument": "12345678909",
"customerEmail": "maria@example.com",
"expectedPayerDocument": "12345678909",
"actualPayerDocument": null,
"payerValidationStatus": "PENDING",
"metadata": { "orderId": "ORD-7821" },
"checkoutUrl": "https://checkout.liqfy.com.br/c/a1b2c3d4-e5f6-4789-9abc-def012345678",
"createdAt": "2026-04-25T15:42:11.000Z"
}Method-specific fields (pixQrCode, mbEntity, boletoLine, cardRedirectUrl, …) are already in the creation response when the method produces them synchronously — Pix BR Code/QR, Multibanco entity/reference, boleto and card redirects all come back on 201. Only artefacts that genuinely arrive later need a follow-up GET /payments/{id}.
checkoutUrl is present on create and get, and absent on list rows and on the cancel response. The payer-validation group is expectedPayerDocument (what you locked), actualPayerDocument (what the provider reports) and payerValidationStatus (NOT_REQUIRED, PENDING, MATCHED, MISMATCH, UNVERIFIED).
#Get payment
GET /payments/{id}
Response 200 OK
{
"id": "a1b2c3d4-e5f6-4789-9abc-def012345678",
"status": "PAID",
"amount": 24900,
"currency": "BRL",
"paymentMethods": ["PIX"],
"paidWith": "PIX",
"pixQrCode": "data:image/png;base64,iVBOR...",
"pixCopyPaste": "00020126580014br.gov.bcb.pix...",
"pixExpiresAt": "2026-04-25T16:12:11.000Z",
"cardRedirectUrl": null,
"mbEntity": null,
"mbReference": null,
"mbExpiresAt": null,
"boletoBarcode": null,
"boletoLine": null,
"boletoPdfUrl": null,
"boletoExpiresAt": null,
"cryptoAddress": null,
"cryptoAmount": null,
"cryptoNetwork": null,
"cryptoCurrency": null,
"platformFee": 200,
"expectedPayerDocument": "12345678909",
"actualPayerDocument": "12345678909",
"payerValidationStatus": "MATCHED",
"metadata": { "orderId": "ORD-7821" },
"checkoutUrl": "https://checkout.liqfy.com.br/c/a1b2c3d4-e5f6-4789-9abc-def012345678",
"createdAt": "2026-04-25T15:42:11.000Z",
"paidAt": "2026-04-25T15:43:08.000Z"
}Fields are null when not applicable to the chosen paymentMethods.
No /v1/payments response (create, get, list, cancel) says which acquirer served the charge: provider, providerAccountId, providerTransactionId, providerFeeSource, cardClientSecret and tenantId are not returned (removed on 2026-10-12), and neither is the acquirer's cost (providerFee, netAmount). A card or bank redirect always comes as the checkout's /c/{id} link.
#List payments
GET /payments
Query parameters
| Param | Type | Default | Description |
|---|---|---|---|
page | int | 1 | |
limit | int | 20 | Max 100. |
status | enum | — | See Status enum below. |
currency | string | — | Filter by currency. |
startDate | ISO 8601 | — | Inclusive lower bound on createdAt. |
endDate | ISO 8601 | — | Inclusive upper bound on createdAt. |
source | enum | — | API, PANEL, MANUAL, or INDEFINIDO (rows with no source). |
sortBy | string | createdAt | A transaction column name (createdAt, amount, status, paidAt, updatedAt, …). An unknown name is a 500, not a 400 — stick to real columns. |
sortOrder | enum | desc | asc or desc. |
Response 200 OK
{
"data": [ { "id": "a1b2c3d4-e5f6-4789-9abc-def012345678", "...": "..." } ],
"total": 142,
"page": 1,
"limit": 20
}List rows do not carry checkoutUrl. This is a paged envelope (total/page/limit) — cursor pagination lives on GET /v1/charges.
#Stats
GET /payments/stats?days=7
| Param | Type | Default | Description |
|---|---|---|---|
days | int | 7 | Business days in the timeZone, today included. |
from/to | ISO 8601 | — | Explicit window; overrides days when present. |
timeZone | string | America/Sao_Paulo | IANA time-zone name. An invalid name → 400. |
days=7 = today plus the six previous calendar days. Totals (totalTransactions, paidTransactions, successRate, volumeByCurrency, methodBreakdown) cover the whole window, whatever its length. dailyVolume has one point per calendar day and at most 366 points: for a longer window it holds the 366 most recent days. hourlyVolume is present only when the window is a single day — one point per business hour, keyed like dailyVolume but with the hour appended ("YYYY-MM-DDTHH"). methodBreakdownByCurrency has one flat row per (method, currency) pair. Sandbox/test charges are excluded. Always scoped to your account — a request whose account cannot be resolved answers 401.
Response 200 OK
{
"totalTransactions": 142,
"paidTransactions": 119,
"todayTransactions": 8,
"successRate": 84,
"volumeByCurrency": [
{ "currency": "BRL", "volume": 1245000, "count": 95 },
{ "currency": "EUR", "volume": 89400, "count": 24 }
],
"todayVolumeByCurrency": { "BRL": 24900, "EUR": 8990 },
"dailyVolume": [
{ "date": "2026-04-19", "currencies": { "BRL": 89000 } },
{ "date": "2026-04-20", "currencies": { "BRL": 124500, "EUR": 4500 } }
],
"hourlyVolume": [
{ "date": "2026-04-20T09", "currencies": { "BRL": 24900 } }
],
"methodBreakdown": [
{ "method": "PIX", "volume": 980000, "count": 78 },
{ "method": "CREDIT_CARD", "volume": 265000, "count": 34 },
{ "method": "MULTIBANCO", "volume": 89400, "count": 7 }
],
"methodBreakdownByCurrency": [
{ "method": "PIX", "currency": "BRL", "volume": 980000, "count": 78 },
{ "method": "CREDIT_CARD", "currency": "BRL", "volume": 265000, "count": 34 },
{ "method": "MULTIBANCO", "currency": "EUR", "volume": 89400, "count": 7 }
],
"statusCounts": { "PAID": 119, "REFUSED": 10 },
"avgPaySeconds": 42
}#Metrics
GET /payments/metrics?days=7¤cy=BRL
Conversion-funnel and volume metrics for your merchant account. days defaults to 7 (same window rule as Stats, no 366-point cap); currency is optional (filters to a single currency); timeZone works like in Stats. Scoped to your API key — without a resolvable account it answers 401, never platform-wide numbers.
Response 200 OK
{
"days": 7,
"currency": "BRL",
"overall": { "created": 142, "paid": 119, "conversionRate": 83.8 },
"byMethod": [
{ "method": "PIX", "created": 90, "paid": 78, "conversionRate": 86.7 }
],
"refunds": { "count": 2, "volumeByCurrency": { "BRL": 4500 } },
"chargebacks": { "count": 0 }
}#Net volume
GET /payments/net-volume
Net settled volume (amount − platformFee, PAID only) for a window.
| Param | Type | Default | Description |
|---|---|---|---|
from/to | ISO 8601 | — | Window bounds. |
timeZone | string | America/Sao_Paulo | IANA time-zone name. An invalid name → 400. |
Response 200 OK
{
"from": "2026-04-19T00:00:00.000Z",
"to": "2026-04-25T23:59:59.999Z",
"byCurrency": {
"BRL": { "netVolume": 1220500, "txCount": 95, "grossVolume": 1245000, "feeTotal": 24500 }
}
}#Conversion
GET /payments/conversion
Rows of { currency, status, count, volume } for the funnel by status — charges created since from, in the status you read, test charges excluded.
| Param | Type | Default | Description |
|---|---|---|---|
from | ISO 8601 | — | Inclusive lower bound. An invalid value → 400. |
Response 200 OK
[
{ "currency": "BRL", "status": "PAID", "count": 95, "volume": 1245000 },
{ "currency": "BRL", "status": "REFUSED", "count": 4, "volume": 12000 }
]#Refunds (admin-panel-only)
POST /payments/{id}/refund
Not callable with an API key. Every merchant request is refused with 403, whatever the body is: error.code is "forbidden" and the message starts with the REFUND_ADMIN_ONLY marker (a message prefix, not a code — see Errors). Refunds (full or partial, PIX or card) are requested from the Liqfy admin panel, which places the wallet hold first and then runs the refund. The status of a refund can be followed by the merchant on GET /payments/{id}/refunds and on the charge.refunded webhook.
The body documented for the internal caller — idempotencyKey, amount, reason, strategy (devolution/cashout), passFeeToTenant, destinationKey, refundId — is not a merchant contract; do not build against it.
#List refunds
GET /payments/{id}/refunds
Returns every refund issued against a transaction (newest first). The acquirer's refund id is not returned.
[
{
"id": "b2c3d4e5-f6a7-4890-9abc-def012345678",
"transactionId": "a1b2c3d4-e5f6-4789-9abc-def012345678",
"amount": 24900,
"currency": "BRL",
"reason": null,
"failureReason": null,
"status": "REFUNDED",
"origin": "MERCHANT",
"idempotencyKey": "order-7821",
"refundedAt": "2026-04-25T15:50:00.000Z",
"createdAt": "2026-04-25T15:50:00.000Z",
"updatedAt": "2026-04-25T16:12:00.000Z"
}
]status is one of PENDING, IN_PROGRESS, PENDING_RECONCILIATION, REFUNDED, FAILED; origin is MERCHANT, AUTOMATIC_PAYER_RESTRICTION or REVIEW_REJECTION. reason is only filled for merchant-originated refunds — for refunds opened by the platform (AUTOMATIC_PAYER_RESTRICTION, REVIEW_REJECTION) it is always null. failureReason is always null for the merchant: a failed refund is signalled by status: "FAILED".
#Cancel payment
POST /payments/{id}/cancel
Cancels an in-flight charge — valid only while WAITING_PAYMENT or PENDING. PROCESSING is refused with 409 payment_not_cancellable, and any other status with 400. Settled (PAID/APPROVED) charges must be refunded, not cancelled. Returns the updated transaction in the same shape as GET /payments/{id} (no checkoutUrl) and publishes the charge.failed event, with status: "CANCELLED" and previousStatus.
#Resend webhook
POST /payments/{id}/resend-webhook
Re-emits the transaction's current status as a fresh webhook delivery — useful when your endpoint was down.
{ "resent": true, "status": "PAID" }#Reconcile
POST /payments/{id}/reconcile
Asks the platform to re-check the transaction with the provider and settle any disagreement.
Response 200 OK
{ "reconciled": true, "status": "PAID" }An already-terminal transaction answers { "reconciled": false, "status": "…" }. Internal reasons and provider statuses are not part of the merchant response — only reconciled and status are returned.
#Confirm sandbox payment
POST /payments/{id}/confirm-sandbox
The merchant's self-test lever for webhook end-to-end: on a transaction created with a test key, this turns the simulated charge PAID and fires the normal webhooks. It never credits a wallet. See Sandbox.
Response 200 OK
{ "confirmed": true, "status": "PAID" }An already-terminal charge answers { "confirmed": false, "status": "…", "reason": "ALREADY_TERMINAL" }. A charge that is not a sandbox/test transaction is refused with 400.
#Receipt
GET /payments/{id}/receipt
Liqfy's own document (PDF) for a transaction with a final outcome — never the acquirer's document, and it never names the provider. In-flight transactions answer 400.
{ "contentType": "application/pdf", "base64": "JVBERi0xLjcK..." }A document exists only for these outcomes, and its title follows the outcome:
| Status | Document title |
|---|---|
PAID / APPROVED | Comprovante de Pagamento PIX — the only payment receipt; carries the BACEN E2E id when known. |
EXPIRED | Cobrança Expirada |
REFUSED | Cobrança Não Aprovada |
REFUNDED | Comprovante de Devolução |
CANCELLED | Cobrança Cancelada |
CHARGEBACK | Pagamento em Devolução |
Every non-paid document states that it is not proof of payment and tells the end customer what happened and what to do next. Payer documents are masked.
#Webhooks
#Register endpoint
POST /webhooks/endpoints
{
"url": "https://merchant.example.com/hooks/liqfy",
"events": ["charge.paid", "charge.failed", "charge.expired"]
}Event names (charge-related): charge.created, charge.paid, charge.failed, charge.expired, charge.refunded, payout.created, payout.approved, payout.paid, payout.failed, plus med_case.created and med_case.updated. These are delivered in the versioned evt_ envelope and signed X-Liqfy-Signature: t=<unix>,v1=<hex> — see Webhooks. Subscribe to these. (med.resolved belongs to the same family but travels in the older { event, data } envelope with the sha256= signature.)
Only canonical charge and payout event names are accepted. Retired names return HTTP 400 LEGACY_WEBHOOK_EVENT with the canonical replacement. Existing subscriptions are migrated and deduplicated.
Response 201 Created
{
"id": "e5f6a7b8-c9d0-4123-9ef0-123456789012",
"url": "https://merchant.example.com/hooks/liqfy",
"events": ["charge.paid", "charge.failed", "charge.expired"],
"secret": "b8f3a9c1d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1",
"status": "ACTIVE"
}secret is shown once — store it immediately. Outside sandbox/staging the URL must be https:// and may not point at a Liqfy domain; a duplicate URL answers 409, and the account has a limited number of active endpoints.
#List endpoints
GET /webhooks/endpoints
{
"data": [
{
"id": "a1b2c3d4-e5f6-4789-9abc-def012345678",
"url": "https://...",
"events": ["payment.status_changed"],
"status": "ACTIVE",
"createdAt": "...",
"updatedAt": "..."
}
],
"access": { "canManage": true },
"httpsOnly": true
}Ids are UUIDs. access.canManage tells whether the caller may write endpoints; httpsOnly is whether the platform currently requires https:// URLs.
secret is never returned by this endpoint.
#Test endpoint
POST /webhooks/endpoints/{id}/test
Sends one synchronous canonical charge.paid sample with the versioned envelope and timestamped signature. The hourly test limit returns 429 WEBHOOK_TEST_RATE_LIMIT when exceeded.
{
"ok": true,
"statusCode": 200,
"durationMs": 143,
"errorKind": null,
"error": null,
"responseSnippet": "{\"received\":true}",
"eventType": "charge.paid",
"url": "https://merchant.example.com/hooks/liqfy",
"sentHeaders": { "X-Liqfy-Event-Type": "charge.paid" },
"sentPayload": { "id": "evt_...", "object": "event", "...": "..." }
}errorKind is timeout, connection or http when ok is false.
#Rotate secret
POST /webhooks/endpoints/{id}/rotate-secret
{ "id": "e5f6a7b8-c9d0-4123-9ef0-123456789012" }Response 200 OK
{ "id": "e5f6a7b8-c9d0-4123-9ef0-123456789012", "secret": "<64-char hex>" }The new secret is shown once. Rotation is an instantaneous server-side swap — there is no overlap window. Every webhook signed after this call uses the new secret, so make your verifier accept both the old and new secret across your deploy, then drop the old one (see webhooks.md).
#Update endpoint
PATCH /webhooks/endpoints/{id}
Update URL, subscribed events, or status (ACTIVE/INACTIVE). Send only the fields to change.
{ "url": "https://merchant.example.com/hooks/v2", "status": "ACTIVE" }Response 200 OK — { id, url, events, status }. Secret never returned.
#Delete endpoint
DELETE /webhooks/endpoints/{id}
Permanently removes the endpoint. Pending deliveries to it are retargeted to the account's other active endpoints; only when none remain are they cancelled. Delivery history is kept.
Response 200 OK — { "deleted": true }.
#Event catalog
GET /webhooks/event-catalog
Returns the full, current list of subscribable public event names with descriptions. This route is behind the same API-key auth as the rest of /v1/webhooks. Charge/payout-relevant excerpt:
[
{ "event": "charge.created", "description": "Cobrança criada (Pix/cartão gerado, aguardando pagamento)." },
{ "event": "charge.paid", "description": "Cobrança paga e confirmada (Pix/cartão liquidado)." },
{ "event": "charge.failed", "description": "Cobrança falhou ou foi cancelada." },
{ "event": "charge.expired", "description": "Cobrança expirou sem pagamento." },
{ "event": "charge.refunded","description": "Cobrança reembolsada." },
{ "event": "payout.created", "description": "Saque solicitado." },
{ "event": "payout.approved", "description": "Payout approved." },
{ "event": "payout.paid", "description": "Saque liquidado com sucesso." },
{ "event": "payout.failed", "description": "Saque falhou ou foi rejeitado." }
]#List deliveries
GET /webhooks/deliveries
| Param | Description |
|---|---|
status | PENDING, PROCESSING, DELIVERED, FAILED, CANCELLED |
eventType | Filter by event name (charge.paid, charge.refunded, …). |
endpointId | Filter by registered endpoint. |
limit | Default 25, max 100. |
offset | Default 0. |
{
"data": [
{
"id": "b2c3d4e5-f6a7-4890-9abc-def012345678",
"endpointId": "a1b2c3d4-e5f6-4789-9abc-def012345678",
"eventType": "charge.paid",
"status": "DELIVERED",
"attempts": 1,
"maxAttempts": 15,
"lastStatusCode": 200,
"lastError": null,
"nextRetryAt": null,
"deliveredAt": "2026-04-25T15:43:09.000Z",
"createdAt": "2026-04-25T15:43:08.000Z",
"updatedAt": "2026-04-25T15:43:09.000Z",
"payload": { "type": "charge.paid", "object": "event", "data": { "object": { "id": "ch_example", "object": "charge", "status": "paid" } } }
}
],
"total": 142,
"limit": 25,
"offset": 0
}Rows carry payload and lastError, not the response body — that lives on the delivery detail below.
#Get delivery
GET /webhooks/deliveries/{id}
The full delivery, including the response body and the attempt log.
{
"id": "b2c3d4e5-f6a7-4890-9abc-def012345678",
"endpointId": "a1b2c3d4-e5f6-4789-9abc-def012345678",
"eventType": "charge.paid",
"status": "FAILED",
"attempts": 3,
"maxAttempts": 15,
"lastStatusCode": 502,
"lastError": "HTTP 502",
"lastResponseBody": "Bad Gateway",
"logs": [
{
"id": "c3d4e5f6-a7b8-4901-9abc-def012345678",
"deliveryId": "b2c3d4e5-f6a7-4890-9abc-def012345678",
"attempt": 1,
"statusCode": 502,
"error": "HTTP 502",
"responseBody": "Bad Gateway",
"duration": 312,
"url": "https://merchant.example.com/hooks/liqfy",
"createdAt": "2026-04-25T15:43:09.000Z"
}
],
"endpoint": { "url": "https://merchant.example.com/hooks/liqfy", "status": "ACTIVE" }
}#Replay delivery
POST /webhooks/deliveries/{id}/replay
Queues one delivery again (resets attempts and schedule). A DELIVERED delivery is refused with 400, and one PROCESSING with 400 — wait and retry.
{ "id": "b2c3d4e5-f6a7-4890-9abc-def012345678", "status": "PENDING", "attempts": 0, "nextRetryAt": "2026-04-25T16:08:00.000Z" }#Replay deliveries in bulk
POST /webhooks/deliveries/replay-bulk
{ "status": "FAILED", "endpointId": "a1b2c3d4-e5f6-4789-9abc-def012345678", "limit": 50 }| Param | Description |
|---|---|
status | Target status. Default FAILED + CANCELLED. Unknown value → 400. |
endpointId | Restrict to one endpoint. |
limit | Default 50, max 500. |
Response 200 OK — { "requeued": 12 }.
#Pull events
GET /webhooks/events
Reconciliation view of the events, keyset-paginated: { data, nextCursor } (note the camelCase nextCursor). Rows are { eventType, payload, status, lastStatusCode, deliveredAt, createdAt } — no per-row id.
| Param | Description |
|---|---|
since/until | ISO 8601 bounds on createdAt. |
status | Delivery status filter. |
eventType | Event name filter. |
limit | Default 50, max 100. |
cursor | Opaque cursor from nextCursor. |
#Delivery stats
GET /webhooks/stats
{
"total": 1402,
"PENDING": 3,
"PROCESSING": 1,
"DELIVERED": 1380,
"FAILED": 12,
"CANCELLED": 6
}#Status enum
The table below is the detailed status enum returned by the /v1/payments transaction routes (and eventType/payload.data.status on their webhook deliveries). POST/GET /v1/charges never emit these values; they emit the smaller public vocabulary (pending, processing, paid, failed, expired, cancelled, refunded, disputed — see Getting Started §5), which the enum below maps onto.
| Transaction status | Description | Public charge.status |
|---|---|---|
PENDING | Internal — being created. Rarely surfaced. | pending |
WAITING_PAYMENT | Awaiting customer action. | pending |
PROCESSING | Card / 3DS in flight. | processing |
PAID | Settled — non-card methods. | paid |
APPROVED | Settled — card methods. | paid |
REFUSED | Acquirer or issuer declined. | failed |
CANCELLED | Cancelled before completion. | cancelled |
EXPIRED | Time window elapsed. | expired |
REFUNDED | Fully refunded. | refunded |
CHARGEBACK | Issuer raised a chargeback (cards). | disputed |
DISPUTE | Dispute open: cardholder dispute (cards), or a Pix MED case on a paid Pix charge. A won or withdrawn MED returns the charge to PAID. | disputed |
charge.paid represents a paid charge. charge.failed and charge.expired distinguish failure and expiry. A Pix charge moving to DISPUTE because of a MED does not emit a charge.* webhook — the MED is followed through the MED case (see metadata.med.caseId on Get charge).
#Webhook payload schema
#Canonical envelope (charge.* / payout.*)
{
"id": "evt_5f3a9b2c1d4e5f6a7b8c9d0e1f2a3b4c",
"object": "event",
"api_version": "2026-07-23",
"type": "charge.paid",
"created_at": "2026-07-23T14:31:00.000Z",
"data": {
"object": {
"id": "ch_a1b2c3d4-0000-0000-0000-000000000009",
"object": "charge",
"amount": 24900,
"currency": "BRL",
"status": "paid",
"payment_method": "pix",
"settlement": {}
}
}
}| Field | Always present | Description |
|---|---|---|
id | yes | evt_… — stable per business event, identical across retries. Dedup on this. |
object | yes | Always "event". |
api_version | yes | Contract version, e.g. 2026-07-23. |
type | yes | charge.created | charge.paid | charge.failed | charge.expired | charge.refunded | payout.created | payout.approved | payout.paid | payout.failed | med_case.created | med_case.updated. payout.approved = payout approved, awaiting settlement (processing). |
data.object | yes | The public object for the event — same serializer and field names as the REST surface for charge/payout, with the event-specific reductions described in Webhooks. |
#Webhook headers
| Header | Description |
|---|---|
X-Liqfy-Signature | charge.*/payout.* events: t=<unix>,v1=<hex> HMAC of "<t>.<rawBody>". KYC, account lifecycle and med.resolved events: sha256=<hex> HMAC of the raw body. |
X-Liqfy-Delivery-Id | Unique per delivery — identical across every retry of that delivery; use for transport-level dedup. |
X-Liqfy-Event-Type | Mirrors type / event. |
See webhooks.md for verification samples.
#HTTP status codes
| Code | Used for |
|---|---|
200 | Successful read |
201 | Resource created |
204 | No content (e.g. logout) |
400 | Validation error, or a financial write missing Idempotency-Key (or with divergent header/body keys on /v1/payments) |
401 | Missing / invalid API key |
403 | Blocked by the fraud / velocity guard — fraud.tenant_velocity_exceeded (too many charge attempts from the account in a short window) or fraud.customer_velocity_exceeded (too many for the same customer document or email; expired and cancelled charges don't count) —, an API-key refund attempt (message marker REFUND_ADMIN_ONLY, not a code), account_suspended, or the IP allowlist (account routes only — wallets, withdrawals, transactions, MED, api-keys; not payments/charges/webhooks) |
404 | Resource not found |
409 | Conflict — idempotency_key_reused, idempotency_key_in_flight, payment_not_cancellable |
422 | Charge creation rejected by provider routing/configuration. The 4xx status and a stable code are passed through: NO_PROVIDER_CONFIGURED, UNKNOWN_METHOD, PAYMENT_REJECTED, MOCK_PROVIDER_IN_PRODUCTION, PAYER_IDENTITY_REQUIRED, PAYER_NAME_REQUIRED |
429 | Rate limited |
500 | Server error — safe to retry (idempotency protects) |
503 | Upstream acquirer unavailable (PROVIDER_UNAVAILABLE) — retry with backoff |
Charge-creation error messages are Liqfy's own neutral texts — they never carry the provider's name, its internal codes or account configuration. An unexpected upstream response is reported as PROVIDER_ERROR. Quote the request_id to support for the detail.
See Errors for the full canonical error envelope (error.type/error.code/error.message, request_id, X-Request-Id).