Skip to content
LIQFYdocs
PTENENEN
Go to dashboard

Card Payments

Accept Visa, Mastercard, American Express, Elo, Hipercard, and other major schemes through a Liqfy-hosted payment page — no card data ever touches your servers, 3D Secure handled automatically.

#Flow at a glance

text
1. POST /v1/payments  ──────▶  Liqfy returns the payment: id, status WAITING_PAYMENT, cardRedirectUrl + checkoutUrl
2. Redirect (or open in iframe) the customer to cardRedirectUrl
3. Customer enters card details + completes 3DS challenge on Liqfy's secure page
4. Customer is bounced back to your returnUrl (success or failure)
5. Webhook charge.paid (or charge.failed) confirms the outcome — trust the webhook, not the redirect

Why a redirect? It keeps cardholder data off your infrastructure entirely. Your application — backend or frontend — never sees a PAN, CVV, or expiry. Liqfy's processor handles 3DS, tokenisation, and fraud screening on our side.

#1. Create the payment

Endpoint POST /v1/payments

bash
curl -X POST https://api.liqfy.com.br/v1/payments \
  -H "Authorization: Bearer $LIQFY_API_KEY" \
  -H "Idempotency-Key: ORD-9821" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 8990,
    "currency": "EUR",
    "paymentMethods": ["CREDIT_CARD"],
    "customerName": "João Costa",
    "customerEmail": "joao@example.com",
    "customerPhone": "+351912345678",
    "returnUrl": "https://shop.example.com/orders/9821/return",
    "cancelUrl": "https://shop.example.com/cart",
    "metadata": { "orderId": "ORD-9821" }
  }'

Idempotency. POST /v1/payments accepts the Idempotency-Key header (preferred) or the body field idempotencyKey as a fallback when the header is absent; send at least one. If you send both, they must match — divergent values are 400, and neither at all is 400 on this route. Any non-blank string (surrounding whitespace is trimmed), scoped per merchant ((merchant, key)): replaying the same key and body returns the stored response, the same key with a different body is 409. The Idempotency-Replayed header is not emitted on this route — do not build retry logic on it.

FieldNotes
amountInteger > 0, smallest unit (cents). 8990 = € 89,90.
currency3–8 chars, default "BRL" (BRL, EUR, USD; additional currencies on request).
paymentMethods["CREDIT_CARD"] here. The API accepts PIX, CRYPTO, CREDIT_CARD, BOLETO, MBWAY, MULTIBANCO, IDEAL, BANCONTACT, SEPA, BIZUM, SWAP_NOW. At least one item. Required unless checkout is sent (else 400 — paymentMethods is required unless a checkout template is provided).
checkoutOptional checkout template slug (1–80). With a template you may omit paymentMethods: the template plus the charge currency decide the methods, filtered to what is actually routable, and they become the charge's paymentMethods. Unknown slug → 404; template service unreachable → 503.
customerIdOptional string.
customerName / customerDocument / customerEmail / customerPhoneFlat customer fields. customerEmail must be an e-mail address; customerPhone is free-form (E.164 recommended). customerEmail is strongly recommended — receipts, fraud signals, chargeback dispute proof.
customerAlternative nested { name?, document?, email?, phone? } (email must be an e-mail address); flattened into the fields above (flat fields win).
expectedPayerDocumentDigits-only expected payer CPF/CNPJ; becomes the payer lock where the provider applies it. A locked charge also needs the payer name (customerName), else PAYER_NAME_REQUIRED.
returnUrlWhere to send the customer after they finish (success or failure). http(s) URL without fragment. Canonical field.
cancelUrlWhere to send the customer on abort. Same URL rule; requires returnUrl (else 400 — cancelUrl requires returnUrl).
metadataFree-form key/value. metadata.returnUrl / metadata.cancelUrl, if present, must be http(s) URLs. metadata.returnUrl still drives the hosted-checkout redirect, but the canonical field is the top-level returnUrl.
itemsOptional { name, quantity, unitPrice, totalPrice }[]; name non-empty, the numbers integers > 0.
webhookUrlOptional per-transaction webhook URL override; must be a valid URL.
idempotencyKeyBody fallback for the Idempotency-Key header (see above).
countryOptional 2–3 chars; provider-account routing input.
riskTierOptional LOW, MEDIUM or HIGH; provider-account routing input.
environmentOptional SANDBOX, STAGING, PRODUCTION — lets you explicitly target your sandbox account. When omitted it is derived from the server environment.
expiresInOptional Pix QR time-to-live in seconds, 60–604800; forwarded to the provider as the requested expiry (a provider-returned expiry wins).

Response 201 Created — the create response already carries the method artefacts (here cardRedirectUrl) and checkoutUrl; you do not need a follow-up GET to see them.

json
{
  "id": "b2c3d4e5-f6a7-4890-9bcd-ef0123456789",
  "status": "WAITING_PAYMENT",
  "amount": 8990,
  "currency": "EUR",
  "paymentMethods": ["CREDIT_CARD"],
  "customerName": "João Costa",
  "customerEmail": "joao@example.com",
  "customerPhone": "+351912345678",
  "cardRedirectUrl": "https://checkout.liqfy.com.br/c/b2c3d4e5-f6a7-4890-9bcd-ef0123456789",
  "checkoutUrl": "https://checkout.liqfy.com.br/b2c3d4e5-f6a7-4890-9bcd-ef0123456789",
  "createdAt": "2026-04-25T15:50:11.000Z"
}

checkoutUrl is present on create and retrieve only — list rows and the cancel response omit it.

#Errors on create

HTTPcodeWhen
400— (flattened validation errors)Invalid body; idempotency key missing or header/body divergent; cancelUrl without returnUrl; paymentMethods missing without checkout.
401—The request does not identify your account (it must arrive authenticated through the API gateway).
403fraud.tenant_velocity_exceededToo many payment attempts from your account in a short window (default: 200 per 60 s).
403fraud.customer_velocity_exceededToo many payment attempts for the same customerDocument or customerEmail in a short window (default: 10 per 300 s). Expired and cancelled charges do not count; refused ones do. Charges with neither field are not subject to this rule.
404—Unknown checkout slug.
4xxNO_PROVIDER_CONFIGUREDNo payment provider is configured for this payment method and currency.
4xxUNKNOWN_METHODPayment method not supported.
4xxMOCK_PROVIDER_IN_PRODUCTIONThe method is not available for live charges.
4xxPAYER_IDENTITY_REQUIREDThe charge requires the payer's full identity (name, email, phone and tax ID).
4xxPAYER_NAME_REQUIREDexpectedPayerDocument sent without the payer name.
4xxPAYMENT_REJECTEDThe provider rejected the charge (also the fallback code for any other provider-side rejection).
503PROVIDER_UNAVAILABLEThe payment provider is temporarily unavailable — try again later (same Idempotency-Key).
5xxPROVIDER_ERRORThe provider returned an unexpected response.

Provider-side 4xx errors keep the HTTP status the routing layer returned. Error messages are neutral by design — they never name the provider or its internal codes; quote the request id to support if you need the detail.

With the official SDK (same neutral package for every brand — configure it with https://api.liqfy.com.br):

nodepythonphpdotnet
client.payments.create(input, { idempotencyKey })client.payments.create(input, idempotency_key)$client->payments->create($input, $idempotencyKey)client.Payments.CreateAsync(input, idempotencyKey)
client.payments.retrieve(id)client.payments.retrieve(id)$client->payments->retrieve($id)client.Payments.RetrieveAsync(id)
client.payments.list(query)client.payments.list(**query)$client->payments->list($query)client.Payments.ListAsync(query)

payments.list (GET /v1/payments) returns the page envelope { data, total, page, limit }. Query parameters: page (default 1), limit (default 20, max 100), status, currency, startDate / endDate (ISO-8601 date-time), source (API, PANEL, MANUAL, INDEFINIDO), sortBy, sortOrder (asc / desc).

#2. Redirect the customer

cardRedirectUrl comes in the create response (and on every GET /v1/payments/{id}), so you can redirect straight away:

bash
curl https://api.liqfy.com.br/v1/payments/b2c3d4e5-f6a7-4890-9bcd-ef0123456789 \
  -H "Authorization: Bearer $LIQFY_API_KEY"
json
{
  "id": "b2c3d4e5-f6a7-4890-9bcd-ef0123456789",
  "status": "WAITING_PAYMENT",
  "amount": 8990,
  "currency": "EUR",
  "cardRedirectUrl": "https://checkout.liqfy.com.br/c/b2c3d4e5-f6a7-4890-9bcd-ef0123456789",
  "createdAt": "2026-04-25T15:50:11.000Z"
}

Send the customer to cardRedirectUrl:

html
<a href="{{ cardRedirectUrl }}">Pay with card</a>

Or do a server-side 303 See Other:

js
res.redirect(303, transaction.cardRedirectUrl);

After the customer completes the flow (or aborts), they bounce back to the returnUrl you supplied (the hosted checkout reads the same redirect from metadata.returnUrl) — with ?transactionId=...&status=... appended. Do not trust those query parameters. They're informational only — wait for the webhook before fulfilling.

#3. Confirm via webhook

json
{
  "id": "evt_00000000000000000000000000000001",
  "object": "event",
  "api_version": "2026-07-23",
  "type": "charge.paid",
  "created_at": "2026-04-25T15:52:34.000Z",
  "data": {
    "object": {
      "id": "ch_b2c3d4e5-f6a7-4890-9bcd-ef0123456789",
      "object": "charge",
      "status": "paid",
      "amount": 8990,
      "currency": "EUR",
      "payment_method": "card",
      "settlement": {}
    }
  }
}

The public charge status is paid for successful card payments. The public status vocabulary is pending, processing, paid, failed, expired, cancelled, refunded, disputed.

Declines arrive as charge.failed; expiry prefers charge.expired. Read data.object.status for the public outcome. The event does not expose issuer decline reasons.

#Lifecycle

text
WAITING_PAYMENT  ──▶  PROCESSING  ──▶  APPROVED    ✓ fulfill order
                                  ──▶  REFUSED     issuer declined
                                  ──▶  CANCELLED   customer abandoned
                                  ──▶  EXPIRED     redirect link aged out (24h)

After settlement (D+1 to D+30 depending on scheme and your contract), you may also see:

text
APPROVED  ──▶  CHARGEBACK   issuer raised a chargeback
APPROVED  ──▶  DISPUTE      cardholder opened a dispute
APPROVED  ──▶  REFUNDED     you (or Liqfy) issued a refund

Each one fires a webhook so you can sync the order state.

#Other operations on the payment

EndpointWhat it does
POST /v1/payments/{id}/cancelCancels the payment; returns it in the same shape as GET /v1/payments/{id} (without checkoutUrl).
POST /v1/payments/{id}/resend-webhookAsks Liqfy to resend the payment's webhook.
POST /v1/payments/{id}/reconcileRe-checks the payment's status with the provider; returns { reconciled, status }.
GET /v1/payments/{id}/refundsRefund history of the payment. failureReason is always null and reason is only filled for refunds you requested yourself.
POST /v1/payments/{id}/confirm-sandboxSandbox only — confirms a simulated charge so the completion webhook fires. Refused for any non-sandbox charge; never credits a wallet.
POST /v1/payments/{id}/refundNot available to API-key callers: returns 403 REFUND_ADMIN_ONLY (see FAQ).

#Test cards

In sandbox, the redirect page accepts these deterministic numbers (any future expiry, any CVV):

NumberOutcome
4111 1111 1111 1111Always APPROVED
4000 0000 0000 0002Always REFUSED — generic decline
4000 0000 0000 0069Always REFUSED — expired card
4000 0000 0000 9995Always REFUSED — insufficient funds
4000 0000 0000 3220Triggers 3DS challenge then APPROVED

Sandbox charges run on the simulator: they never credit your wallet and are kept out of volume, revenue and metrics figures (they still appear in lists and in the charge detail). Use POST /v1/payments/{id}/confirm-sandbox to fire the completion webhook end-to-end with a test key.

See sandbox for the full list.

#FAQ

Q: Can I keep the customer on my domain? A: Phase 2 we'll ship an embeddable widget (iframe + tokeniser) that keeps your branding while still keeping cardholder data off your infrastructure. For now: hosted redirect.

Q: Do you support saved cards? A: Yes, but it requires your side to hold its own card-data-security compliance attestation. Email suporte@liqfy.com.br to enable the Vault API.

Q: Which 3DS version is used? A: 3DS 2.x with frictionless flow when the issuer permits. Strong Customer Authentication (SCA) is mandatory for EUR — this is enforced server-side.

Q: Refunds? A: Refunds are requested from the admin panel today, full or partial — the API does not expose a merchant-usable refund endpoint: POST /v1/payments/{id}/refund with an API key returns 403 REFUND_ADMIN_ONLY. You can still read the refund history with GET /v1/payments/{id}/refunds.

Q: My customer was charged but I never got a webhook. A: Check GET /v1/webhooks/deliveries — likely we tried and your endpoint returned a non-2xx. We retry up to 15 times. After that, use POST /v1/payments/{id}/resend-webhook or contact support to replay.

#Next

→ MB WAY · Multibanco · API Reference