---
title: "Create a checkout from a product (public)"
method: POST
path: "/api/products/{id}/checkout"
tags: ["public"]
---

# Create a checkout from a product (public)

`POST /api/products/{id}/checkout`

## Request

- Base URL: `https://api.depixapp.com`
- URL: `https://api.depixapp.com/api/products/{id}/checkout`
- Auth: none required

Public (unauthenticated) checkout creation from a live, active product — the flow behind the "buy" button of product pages AND of charge pages (/c/{id}). For charges the amount is computed per visit (base + late fees for the current FIFO cycle — see ChargeState) and the settled cycle is stamped into metadata.charge_cycle. Optional body: payer metadata and payer_tax_number.

**Errors on this operation are payer-scoped.** The caller is an anonymous third party paying a merchant, so an error is only allowed to keep its `response.errorMessage` and its `error.details` when it describes the payer, the payer's input, or a fact about the thing being paid — which is every code documented below. Refusals that belong to the MERCHANT'S account (receive limits, onboarding gates, account configuration) answer a fixed neutral message ("Não foi possível gerar o pagamento. Fale com o vendedor.", or "…agora. Tente novamente em alguns minutos." for a 5xx) with `error.details` OMITTED. `error.code` is always the real one. Do not build a payer flow that reads `error.details` from a code not listed here: it is withheld by design, and the same call authenticated as the merchant (POST /api/checkouts) returns the full numbers.

## Path parameters

- `id` string, required

## Request body

- object: Optional payer-provided fields merged into the checkout.
  - `metadata` object: Extra metadata merged over the product's metadata (max 4KB combined).
  - `payment_method` 'pix' | 'depix': Which rail settles the checkout. `pix` (default): the payer pays a PIX QR and the provider delivers DePix to the merchant. `depix`: the payer sends DePix wallet-to-wallet on Liquid to an address dedicated to this merchant, and settlement is observed on-chain — no PIX QR exists on this rail.
  - `expected_discount_pct` integer: The DePix discount percentage the page showed the payer, 0–90. Send it on the depix rail to make a stale page fail loudly: if the merchant's current discount differs, the request is rejected with 409 `discount_changed` carrying the fresh values instead of charging a different price.
  - `payer_tax_number` string: CPF/CNPJ of the payer. Required on the pix rail — omitting it returns 400 `tax_number_required`. Ignored on the depix rail.

## Response `201`

Checkout created.

- PublicProductCheckoutResponse: Checkout created from a public product page. Exactly one of `pix` / `depix` is present, matching `payment_method`.
  - `id` string, required: Checkout id (chk_*).
  - `status` union, required: Checkout lifecycle status — the enum below is the complete, closed set. `completed` is the terminal SUCCESS state. Terminal states (safe to stop polling): completed, expired, cancelled.
    - 'pending': Awaiting payment; the PIX QR is active until expires_at.
    - 'processing': PIX paid; settlement in progress.
    - 'approved': Payment approved by the provider; DePix on the way to the merchant.
    - 'completed': TERMINAL — success: DePix delivered to the merchant.
    - 'expired': TERMINAL — QR expired unpaid.
    - 'cancelled': TERMINAL — cancelled upstream by the provider (refund/error).
  - `amount` integer, required: Charge amount in cents — the product price or, for charges (kind='charge'), the day's computed total (see `charge`).
  - `description` string, nullable, required: Product description carried onto the checkout.
  - `expires_at` string, required: Expiry timestamp (RFC 3339 UTC).
  - `is_live` boolean, required: Always true — only live products accept public checkouts.
  - `payment_url` string, required: Hosted checkout page URL.
  - `payment_method` 'pix' | 'depix': Which rail settles the checkout. `pix` (default): the payer pays a PIX QR and the provider delivers DePix to the merchant. `depix`: the payer sends DePix wallet-to-wallet on Liquid to an address dedicated to this merchant, and settlement is observed on-chain — no PIX QR exists on this rail.
  - `pix` object: PIX payment payload — present on the pix rail only.
    - `qr_code` string, required: PIX copy-and-paste (BR Code) payload.
  - `depix` DepixPayment: Payment instructions for the depix rail — present only on checkouts created with payment_method=depix, and only while the checkout can still be paid. The payer must send EXACTLY `amount_cents` of the DePix asset to `address`: attribution is by exact amount, so any other value stays unattributed and cannot be credited automatically. An on-chain payment is irreversible.
    - `address` string, required: Confidential Liquid address (lq1…) dedicated to this merchant's direct DePix receipts. In sandbox it is a non-payable placeholder — never send funds to it.
    - `amount_cents` integer, required: EXACT amount to send, in cents. It is the face amount minus the merchant's discount and minus a per-checkout cents adjustment (at most 99 cents, always DOWNWARDS) that makes the value unique among the merchant's open checkouts — that uniqueness is how an incoming payment is matched to this checkout.
    - `amount` string, required: The same amount as the decimal string a wallet signs (e.g. "89.97"). Display and transmit this value verbatim; never round it.
    - `asset_id` string, required: Liquid asset id of DePix. Sending any other asset to this address loses the funds — they cannot be returned.
    - `uri` string, nullable, required: BIP21-style Liquid payment URI carrying address, amount, asset and this checkout's id (`liquidnetwork:<address>?amount=<amount>&assetid=<asset_id>&depixid=<checkout_id>`) — hand it to a wallet instead of asking the payer to type anything. Any BIP21 wallet ignores `depixid` (it is deliberately not `req-`-prefixed, which would make a wallet reject the whole URI); the DePix App uses it to re-read this checkout and confirm the address and status before offering to pay, so a URI written by someone else is refused rather than paid. The URI never carries a store name: a name in the URI is a name in the payer's clipboard, which anyone can write. Null in sandbox, which has no payable destination, and null on a checkout that is no longer payable (see the `depix` field on CheckoutDetail): the rest of the object is then a settlement record, not an instruction.
    - `discount_pct` integer, required: Merchant discount applied on this rail, 0–90 (0 when the merchant offers none).
    - `original_amount_cents` integer, required: Face amount in cents before discount and uniqueness adjustment — the same value as the checkout's `amount`.
    - `detected` boolean, required: True once a matching payment has been SEEN on the network but not yet confirmed. Informational only: the status stays `pending` and no webhook fires until the first confirmation.
  - `charge` ChargeState: Derived payable state of a charge (kind='charge' product). Settlement is FIFO over the due-date series: the current cycle is selected by the number of cycles that EVER settled (a checkout that reached approved or completed — a later reversal does not rewind the series; re-billing a reversed cycle is the merchant's decision, not the API's) PLUS un-expired pending QRs, since a minted QR reserves the cycle it was priced for and two open QRs must never bill the same competência; the total is base + one-time fine + linear pro-rata interest (base × monthlyBps/10000 × daysLate/30), computed on the América/São_Paulo calendar (the due date itself is not late; lateness starts the next day) and clamped at the R$ 6.000 per-transaction cap.
    - `settled` boolean, required: True when a one-time charge is fully paid (every other field is then absent).
    - `cycle_due_date` string: Due date (YYYY-MM-DD) of the current — oldest unsettled — cycle.
    - `days_late` integer: Days past the due date on the São Paulo calendar (0 = not late).
    - `base_cents` integer: Configured base amount in cents.
    - `fine_cents` integer: One-time late fine in cents (0 when not late).
    - `interest_cents` integer: Accrued pro-rata interest in cents (0 when not late).
    - `total_today_cents` integer: What a QR created now charges: base + fine + interest, capped at 600000.
    - `capped` boolean: True when base + encargos exceeded the per-transaction cap and the total was clamped.
    - `status` 'late' | 'due_today' | 'upcoming': Cycle status.
    - `open_past_due_cycles` integer: Cycles already due and still unpaid (>1 means competências accumulated).
    - `in_flight` boolean: True when a processing checkout exists (Pix paid, settling) — present on list rows only.

## Other responses

- `400`: Invalid metadata, missing payer tax number, or a depix checkout requested from a merchant that does not accept the direct DePix rail.
- `404`: Product not found — includes rows owned by another account (ownership is never disclosed).
- `409`: Charge already paid (one-time), a payment is settling, an un-expired QR already holds the only remaining cycle, or the depix rail cannot issue this charge right now (`depix_busy` — fall back to PIX; `discount_changed` — the merchant's discount moved under a stale page).
- `429`: Rate limited, or too many PAID transactions for the same payer CPF/CNPJ (`payer_velocity_limit`: max 2 paid per sliding 30-minute window, deposits + checkouts combined; unpaid/expired QRs don't count). Retry after `retry_after` seconds (also in the Retry-After header).
- `503`: Merchant temporarily unavailable to receive payments, or platform maintenance.

---

[API](https://skmtc.dev/depixapp/apis/depix-api.md) · [All operations](https://skmtc.dev/depixapp/apis/depix-api/llms.txt) · [OpenAPI document](https://skmtc.dev/depixapp/apis/depix-api/revisions/04094536dd0f?raw)
