PaymentLink
POST /v1/payment_links — create a charge
Scope: payment_links:write. Idempotency: required.
| field | type | required | constraint |
|---|---|---|---|
amount | integer | yes | smallest unit of the currency; > 0 |
currency | string | yes | BRL | USD |
reference | string | no | ≤ 255 chars; your reconciliation key |
payment_methods | array<string> | no | subset of ["pix","card"]; default ["pix"] |
max_installments | integer | no | 1–21; default 12; only if card is enabled |
fee_handling | string | no | absorb | pass_through; default absorb |
expires_in | integer | no | seconds until expiry; >= 0 (0 = no expiry) |
campaign | boolean | no | default false; true keeps the link open after each payment and quotes FX per session |
description | string | no | ≤ 500; product name shown at checkout |
product_ref | string | no | ≤ 255; product SKU/reference (shown) |
details | string | no | ≤ 500; long description (shown) |
product_images | array<string> | no | ≤ 5; product images |
collect_fields | array<string> | no | subset of ["phone","address"]; if omitted, inherits the tenant config; collects these fields from the payer at checkout |
metadata | object | no | ≤ 20 keys; see Metadata |
amount + currency are the only denomination you provide. target_currency, charge_currency, and merchant_payout_currency are derived by the platform and returned in the response — never input. FX coverage — the margin on the exchange rate that protects against the variation between the quote and settlement — applies whenever there is conversion, regardless of the link's denomination currency.
campaign=false creates a single-charge link: after the first captured/settled payment, further attempts are refused. campaign=true creates a campaign link: it accepts multiple payments, each session locks the displayed rate for 900 seconds, and different payments may use different rates.
Response 201 — PaymentLink object:
{
"id": "pl_9f8c...", "mode": "live",
"status": "active", "checkout_url": "https://loja.pgz.link/pay/pl_9f8c...",
"amount": 250000, "currency": "BRL", "reference": "pedido-8842",
"target_currency": "BRL", "charge_currency": "BRL", "merchant_payout_currency": "USD",
"payment_methods": ["pix"], "max_installments": 12, "fee_handling": "absorb",
"expires_at": 1790000000, "description": "Rolex Submariner", "product_ref": "126610LN",
"details": null, "product_images": [], "collect_fields": [], "metadata": {}, "created": 1789990000
}Errors: unknown_parameter (400), invalid_request (400), missing_idempotency_key (400), invalid_idempotency_key (400), idempotency_key_reuse (409), invalid_api_key (401), insufficient_scope (403), merchant_net_not_positive (400 — see Link protection).
Quote — the only external price surface
The price of a charge is obtained exclusively through this endpoint. No fee, percentage, or exchange rate is published in this contract: you ask the endpoint at the moment.
POST /v1/quotes — quote
Scope: quotes:write. Idempotency: required.
| field | type | required | constraint |
|---|---|---|---|
amount | integer | yes | smallest unit; > 0 |
currency | string | yes | BRL | USD |
payment_method | string | no | pix | ted | card; if omitted, returns all scenarios |
installments | integer | no | 1–21; default 12 |
fee_handling | string | no | absorb | pass_through; if omitted, uses the pricing rule default |
The legacy shape { "payment_intent_id": "pi_..." } with payment_method? and installments? is also accepted.
Response 201 — Quote object when payment_method is provided:
{
"id": "qt_9f8c...",
"tenant_id": "three-hands",
"mode": "live",
"payment_intent_id": null,
"payment_method": "card",
"installments": 5,
"payer_total": 5000000,
"payer_currency": "BRL",
"receiver_net": 843375,
"receiver_currency": "USD",
"fx": {
"rate": 5.195993,
"base": "USD",
"quote": "BRL",
"locked_at": 1789990000,
"valid_until": 1789990900,
"rate_history_id": 123
},
"breakdown": {
"fee_handling": "absorb",
"total_fee": { "amount": 118905, "currency": "USD" },
"service_fee": { "final_cents": 118905, "final_currency": "USD", "percent_amount_cents": 118905, "fixed_cents": 0, "raw_total_cents": 118905, "min_applied": false, "max_applied": false },
"installment_surcharge": { "amount": 0, "currency": "USD" },
"tax": { "iof_amount_cents": 0, "iof_applicable": false, "iof_enabled": false, "iof_currency": "USD" }
},
"fx_rate_history_id": 123,
"rule_id": "cg_three-hands_5x",
"expires_at": 1789991800,
"signature": "sha256...",
"created_at": 1789990000
}The Quote is valid for 1800 seconds to be accepted. That negotiation validity is different from the internal FX validity (fx.valid_until) and from the 900-second checkout session. The numeric values above are illustrative of shape, not of price.
When payment_method is omitted, the response contains scenarios.pix, scenarios.ted, and scenarios.card[] with card from 1x up to the tenant's limit. Each scenario carries quote_id, method, installments, payer total, merchant net, public FX, expires_at, and signature.
Errors: unknown_parameter (400), invalid_request (400), payment_method_unsupported (400), quote_fx_required (400), idempotency, auth.
POST /v1/quotes/{id}/accept — accept a quote
Scope: payment_intents:write.
| field | type | required | constraint |
|---|---|---|---|
name | string | yes | title shown at checkout; 1–500 chars |
description | string | no | long description shown at checkout; ≤ 500 chars |
media_urls | array<string> | no | up to 5 Pagooz-hosted URLs |
expires_in | integer | no | seconds until expiry; 0 = no expiry |
campaign | boolean | no | default false; true does not store the quote's FX on the link and quotes FX per session |
reference | string | no | ≤ 255 chars; your reconciliation key |
metadata | object | no | free metadata |
payer_country | string | no | ISO 3166-1 alpha-2 |
Creates an internal PaymentIntent and a PaymentLink. The intent uses the values persisted on the quote: amount, currency, legal_entity_id, settlement_term, fee_strategy, and fx_rate_history_id. A single link (campaign=false) uses the quote's fx_rate_history_id, applies the 1% ratchet at checkout, and expires in 24h when expires_in is omitted. A campaign link (campaign=true) does not store the quote's FX on the link; each session quotes and locks the displayed rate for 900 seconds. An expired quote returns 400 quote_expired; a quote from another tenant returns 404 quote_not_found; an already-consumed quote returns 400 quote_consumed.
Response 201: payment link.
{
"id": "pl_9f8c...",
"url": "https://loja.pgz.link/pay/pl_9f8c...",
"checkout_url": "https://loja.pgz.link/pay/pl_9f8c...",
"amount": 5000000,
"currency": "BRL",
"payment_method": "card",
"installments": 5,
"payer_total": 5000000,
"payer_currency": "BRL",
"receiver_net": 843375,
"receiver_currency": "USD",
"fx_rate_history_id": 123,
"payment_intent_id": "pi_9f8c...",
"expires_at": 1790000000,
"reference": "pedido-8842"
}PaymentIntent
POST /v1/payment_intents — create a legacy intent
Scope: payment_intents:write.
| field | type | required | constraint |
|---|---|---|---|
legal_entity_id | string | no | default: the tenant's single active entity |
amount | integer | yes | smallest unit; > 0 |
currency | string | yes | BRL | USD |
payer_country | string | no | ISO 3166-1 alpha-2 |
settlement_term | string | no | default: tenants.settlement_window_days as D+N |
fee_strategy | object | no | legacy |
metadata | object | no | free metadata |
reference | string | no | ≤ 255 chars |
This endpoint remains for compatibility. The canonical flow is POST /v1/quotes followed by POST /v1/quotes/{id}/accept. quote_id is not accepted on this endpoint and returns 400 unknown_parameter.
Payment — reconciliation object
The Payment is the source of truth for reconciliation. Its values are the same ones produced at charge time, at the same precision (hundredth of a basis point), read from the same persisted source. No layer recomputes the price to display or notify. A difference between what the payer saw at checkout and what you receive here is a critical bug, not rounding.
Every Payment object carries the fields below; conditional fields follow the rule of their own row (they may be omitted or null exactly when the row indicates):
| field | meaning for reconciliation |
|---|---|
id | payment identifier (pay_...) |
link_id | the PaymentLink that originated it, or null |
status | persisted payment state |
reference | your free key, set at link creation, returned here and on every webhook |
created_at | when the payment was started (unix) |
updated_at | last update (unix; may be null) |
amount + currency | legacy alias of the charged gross |
gross_amount + gross_currency | gross charged to the payer |
fee_amount + fee_currency | total fee persisted when available |
net_amount + net_currency | merchant net |
fee_handling | absorb | pass_through — who paid the service fee on this payment (effective value); independent of breakdown/fees[] |
fx | { rate, base, quote, locked_at, valid_until, rate_history_id } — locked rate with coverage. Present only when there is currency conversion. With no conversion (e.g., BRL→BRL) the fx field is omitted — we never fabricate an FX object, rate: 1.0, an empty object, or null just to satisfy the shape. It does not expose spot, coverage, or internal source |
payment_method | pix | ted | card |
installments | integer when applicable, otherwise null |
failure_code | present when status = failed — Pagooz code for the reason (see Decline codes) |
Decline codes (payment.failed)
When a payment fails, decline_code is always a Pagooz semantic code. The original reason from any upstream system is never passed through: it is mapped internally to one of these codes, and anything without a known mapping becomes payment_declined.
decline_code | meaning |
|---|---|
insufficient_funds | insufficient balance/limit |
card_declined | declined by the issuer |
expired_card | expired card |
invalid_card | invalid card data |
card_not_supported | brand/type not supported |
authentication_failed | authentication (3-D Secure) not completed |
pix_expired | QR expired without payment |
pix_rejected | PIX rejected by the payer's bank |
payment_declined | generic decline (reason without mapping) |
GET /v1/payments/{id}
Scope: payments:read. Response 200: Payment object. Errors: payment_not_found (404).
GET /v1/payments — period close
Scope: payments:read. It is the period reconciliation endpoint.
Query:
| field | type | required | description |
|---|---|---|---|
from | string | yes | start date in America/Sao_Paulo, YYYY-MM-DD, inclusive at 00:00:00 |
to | string | yes | end date in America/Sao_Paulo, YYYY-MM-DD, inclusive at 23:59:59 |
limit | integer | no | default 20, maximum 100 |
starting_after | string | no | cursor: id of the last payment received on the previous page |
The boundaries follow the Brazilian banking calendar used in the cockpit: a payment at 2026-08-20T22:00:00-03:00 belongs to day 2026-08-20, even though it is 2026-08-21 in UTC.
Ordering: created_at DESC, id DESC. There is no status filter: the integrator receives all payments in the period and filters locally if desired.
Response 200: { data: Payment[], has_more, next_cursor }.
fee_handling and the fee acknowledgment flow
fee_handling defines who pays the service fee:
absorb— the payer pays the product amount; the fee reduces the merchant net. It is the default.pass_through— the payer pays product + fee; the merchant receives the full amount.
In pass_through, the checkout page requires the payer's explicit acknowledgment of the total before generating the charge (the platform validates that the acknowledged total matches, to the cent, the locked total). The acknowledgment is recorded as the internal event payment.fees_acknowledged. In absorb there is no acknowledgment. You do not handle this flow through the API — it belongs to the hosted checkout; you only receive the effective fee_handling on the Payment.
Link rules
- Currencies:
BRLandUSD. PIX and card charges settle in BRL to the payer; conversion and coverage are derived. - Expiry:
expires_inin seconds;0= no expiry. After expiring, the link accepts no payment and emitslink.expired. - Installments:
1–21(card only). The product default of a link is12(ADR-0006). - Methods:
pixand/orcard. - Economic protection (
absorb): if, at the given amount, the merchant net would be≤ 0after fees, creation fails withmerchant_net_not_positive(400). Usepass_throughor increase the amount.