Skip to content
LIQFYdocs
PTENENEN
Go to dashboard

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, header Idempotency-Key, pix block. 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 (the ch_ 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

HeaderRequiredNotes
AuthorizationyesBearer + your API key (lq_test_* / lq_live_*). The legacy apikey header is still accepted.
Idempotency-KeyyesAny non-blank string, scoped by (merchant, key). A financial write — missing it is a 400.

Body

FieldTypeRequiredDescription
amountintegeryesSmallest currency unit, > 0.
currencystringno3-letter ISO 4217 code, upper-cased by the API. Defaults to "BRL". Pix charges must use BRL.
payment_methodstringyes"pix" — the only value accepted today.
descriptionstringnoUp to 500 chars; folded into metadata.description on read.
expires_inintegernoPix 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.namestringno
customer.documentstringnoCPF or CNPJ.
customer.emailstringnoMust be a valid email address.
customer.phonestringno
expected_payer_tax_idstringnoLocks 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.
metadataobjectnoFree-form; reserved/underscore-prefixed keys are stripped.

Example

bash
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

json
{
  "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.

http
GET /v1/charges?limit=25&starting_after=ch_01J…&status=paid
ParamTypeDefaultDescription
limitinteger251–100.
starting_afterstring—A charge id (ch_…) of the same resource — cursor, not an offset. Up to 140 chars.
statusenum—Public status vocabulary (pending, paid, …).
created_afterISO 8601—Inclusive lower bound on created_at.
created_beforeISO 8601—Inclusive upper bound on created_at.
customer_idstring—Filters by payer document. Up to 140 chars.
idempotency_keystring—Filters by the Idempotency-Key used on creation (1–255 chars, trimmed).
json
{
  "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:

bash
curl "https://api.liqfy.com.br/v1/wallets/balance" \
  -H "Authorization: Bearer $LIQFY_API_KEY"

Response 200 OK

json
{
  "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 }
}
FieldDescription
availableSpendable/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.
heldAmount held (retained mid-flight) in balances[]/primary entries. Not present at the top level.
pendingSettled but still held — not spendable yet (primary currency). Includes in-flight refund holds, so it can be greater than retained.
totalavailable + held + pending of the primary currency.
retainedTotal under retention (PENDING holds) of the primary currency. Does not include refund holds.
currencyPrimary currency (ISO 4217). Defaults to BRL.
next_release_atISO 8601 string of when the next pending tranche releases, or null when nothing is scheduled.
next_release_amountAmount of that next release (smallest currency unit), or null.
balances[]One entry per currency: { currency, available, held, pending, total }.
primaryThe 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[] and primary count only active OPERATIONAL wallets, 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 and balances: [].
  • 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 different available numbers.

#Single currency (legacy shape)

GET /v1/wallets/balance?currency=BRL

Pass currency to get the flat single-currency shape:

bash
curl "https://api.liqfy.com.br/v1/wallets/balance?currency=BRL" \
  -H "Authorization: Bearer $LIQFY_API_KEY"
json
{
  "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 (never 404). Without currency this route never returns 404; with ?currency=, a 404 not_found means the account has no wallet at all (not a routing problem). A wrong path (/v1/wallets/balance, plural, with the /v1 prefix) 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.

bash
curl https://api.liqfy.com.br/v1/wallets \
  -H "Authorization: Bearer $LIQFY_API_KEY"

Response 200 OK

json
[
  {
    "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.

bash
curl https://api.liqfy.com.br/v1/wallets/a1b2c3d4-e5f6-4789-9abc-def012345678 \
  -H "Authorization: Bearer $LIQFY_API_KEY"

Response 200 OK

json
{
  "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

HeaderRequiredNotes
AuthorizationyesBearer + your API key.
Idempotency-KeyyesPreferred channel. The body field idempotencyKey is accepted as an alias — if both are sent they must match (divergent values → 400); sending neither → 400.

Body

FieldTypeRequiredDescription
amountintegeryesSmallest currency unit, > 0.
currencystring (3–8)noDefaults to "BRL". Examples: BRL, EUR, USD, USDT.
paymentMethodsstring[]unless checkoutOne 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.
checkoutstringnoHosted-checkout template slug (1–80 chars). An unknown slug → 404; if the template service is unreachable → 503.
customerIdstringnoYour internal user id. Forwarded with the request only — not persisted, not echoed in responses or merchant webhooks.
customerNamestringno
customerDocumentstringnoCPF, CNPJ, NIF, or other tax id.
customerEmailstringnoMust be a valid email address.
customerPhonestringno
customerobjectnoNested { name?, document?, email?, phone? }. Flattened into the customer* fields; the flat fields win on conflict.
expectedPayerDocumentstringnoDigits-only payer CPF/CNPJ lock.
itemsobject[]no{ name, quantity, unitPrice, totalPrice } — all integers > 0 except name.
metadataobjectnoFree-form. returnUrl/cancelUrl keys inside it must be http(s) URLs, but the first-class returnUrl/cancelUrl fields below are the return URLs.
webhookUrlstringnoPer-transaction webhook URL override. Must be a valid URL.
countrystringno2–3 chars.
riskTierenumnoLOW, MEDIUM or HIGH.
environmentenumnoSANDBOX, STAGING or PRODUCTION. When omitted, the platform derives it. The key alone decides where the charge actually goes.
returnUrlstringnoWhere the payer returns after paying: http(s) URL without whitespace or fragment.
cancelUrlstringnoWhere the payer goes on cancel: same URL rule. Requires returnUrl.
expiresInintegernoPix validity in seconds, 60–604800.
idempotencyKeystringyesBody alias for the Idempotency-Key header. Unique per intended request; replays return the original.

Response 201 Created

json
{
  "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

json
{
  "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

ParamTypeDefaultDescription
pageint1
limitint20Max 100.
statusenum—See Status enum below.
currencystring—Filter by currency.
startDateISO 8601—Inclusive lower bound on createdAt.
endDateISO 8601—Inclusive upper bound on createdAt.
sourceenum—API, PANEL, MANUAL, or INDEFINIDO (rows with no source).
sortBystringcreatedAtA transaction column name (createdAt, amount, status, paidAt, updatedAt, …). An unknown name is a 500, not a 400 — stick to real columns.
sortOrderenumdescasc or desc.

Response 200 OK

json
{
  "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

ParamTypeDefaultDescription
daysint7Business days in the timeZone, today included.
from/toISO 8601—Explicit window; overrides days when present.
timeZonestringAmerica/Sao_PauloIANA 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

json
{
  "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&currency=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

json
{
  "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.

ParamTypeDefaultDescription
from/toISO 8601—Window bounds.
timeZonestringAmerica/Sao_PauloIANA time-zone name. An invalid name → 400.

Response 200 OK

json
{
  "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.

ParamTypeDefaultDescription
fromISO 8601—Inclusive lower bound. An invalid value → 400.

Response 200 OK

json
[
  { "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.

json
[
  {
    "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.

json
{ "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

json
{ "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

json
{ "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.

json
{ "contentType": "application/pdf", "base64": "JVBERi0xLjcK..." }

A document exists only for these outcomes, and its title follows the outcome:

StatusDocument title
PAID / APPROVEDComprovante de Pagamento PIX — the only payment receipt; carries the BACEN E2E id when known.
EXPIREDCobrança Expirada
REFUSEDCobrança Não Aprovada
REFUNDEDComprovante de Devolução
CANCELLEDCobrança Cancelada
CHARGEBACKPagamento 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

json
{
  "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

json
{
  "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

json
{
  "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.

json
{
  "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

json
{ "id": "e5f6a7b8-c9d0-4123-9ef0-123456789012" }

Response 200 OK

json
{ "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.

json
{ "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:

json
[
  { "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

ParamDescription
statusPENDING, PROCESSING, DELIVERED, FAILED, CANCELLED
eventTypeFilter by event name (charge.paid, charge.refunded, …).
endpointIdFilter by registered endpoint.
limitDefault 25, max 100.
offsetDefault 0.
json
{
  "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.

json
{
  "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.

json
{ "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

json
{ "status": "FAILED", "endpointId": "a1b2c3d4-e5f6-4789-9abc-def012345678", "limit": 50 }
ParamDescription
statusTarget status. Default FAILED + CANCELLED. Unknown value → 400.
endpointIdRestrict to one endpoint.
limitDefault 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.

ParamDescription
since/untilISO 8601 bounds on createdAt.
statusDelivery status filter.
eventTypeEvent name filter.
limitDefault 50, max 100.
cursorOpaque cursor from nextCursor.

#Delivery stats

GET /webhooks/stats

json
{
  "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 statusDescriptionPublic charge.status
PENDINGInternal — being created. Rarely surfaced.pending
WAITING_PAYMENTAwaiting customer action.pending
PROCESSINGCard / 3DS in flight.processing
PAIDSettled — non-card methods.paid
APPROVEDSettled — card methods.paid
REFUSEDAcquirer or issuer declined.failed
CANCELLEDCancelled before completion.cancelled
EXPIREDTime window elapsed.expired
REFUNDEDFully refunded.refunded
CHARGEBACKIssuer raised a chargeback (cards).disputed
DISPUTEDispute 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.*)

json
{
  "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": {}
    }
  }
}
FieldAlways presentDescription
idyesevt_… — stable per business event, identical across retries. Dedup on this.
objectyesAlways "event".
api_versionyesContract version, e.g. 2026-07-23.
typeyescharge.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.objectyesThe 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

HeaderDescription
X-Liqfy-Signaturecharge.*/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-IdUnique per delivery — identical across every retry of that delivery; use for transport-level dedup.
X-Liqfy-Event-TypeMirrors type / event.

See webhooks.md for verification samples.

#HTTP status codes

CodeUsed for
200Successful read
201Resource created
204No content (e.g. logout)
400Validation error, or a financial write missing Idempotency-Key (or with divergent header/body keys on /v1/payments)
401Missing / invalid API key
403Blocked 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)
404Resource not found
409Conflict — idempotency_key_reused, idempotency_key_in_flight, payment_not_cancellable
422Charge 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
429Rate limited
500Server error — safe to retry (idempotency protects)
503Upstream 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).