Skip to main content
The other pages cover what you do: catalog, charging, deliverable. This one covers what happens on the buyer’s side — and it is where integrations get stuck, because it is the part with intermediate states. The whole thread is this, and it holds on both selling paths:
Those two polling steps are the part nobody guesses. One at a time.

Path A: you have the invoice (checkout())

You have had invoiceId from the start, so this is the short path:
Store the invoiceId → your user pair before showing the screen. It is how you will know whose purchase it was when the payment comes back. The link is the recommendation for anyone who does not want to build a screen. If you want to control the experience and still use a link — or to test the flow over HTTP — it is three steps, and the invoice only exists at the third.
Three things that cost time if you do not know them:
  • The invoice does not exist before step 3. The session has no invoiceId — it appears in the charge response. Do not look for it earlier.
  • The charge is on the session, not on the invoice. /sessions/{id}/charge, not /invoices/{id}/charge. The second one exists and serves path A.
  • No email → 400 "E-mail is required."; no taxId → 400 "A valid CPF or CNPJ is required." Both in Portuguese, both before any charge happens.
These routes do not require an Idempotency-Key. The rule “every non-GET method requires the key” applies to the authenticated API. The public /pay/* routes — the ones the buyer’s browser calls — accept requests without it.
The taxId 422 arrives at the charge, not at checkout. checkout() accepts a customer without a tax id and creates a finalised and numbered invoice. The 422 customer_tax_id_required only shows up at pay.charge, and that invoice can no longer be paid — it stays open forever in your reporting.The validation lives at the charge because the party requiring the document is the method’s provider, and the method is only chosen there. Collect taxId before creating the invoice; if you already created one without it, clean up:
The same holds for a customer with no name and no email: one of the two is required (422 customer_name_required).

Finding out they paid

Always pass timeoutMs. The default is 600000 — ten minutes — so on an unpaid invoice, which is the normal case, your handler hangs instead of answering. In production the payment.confirmed webhook is the right path; in sandbox registering one answers 503, so it is polling — details in webhooks.
Do not read the invoice only once. Confirmation arrives through the provider’s webhook, a moment after you trigger the payment. A single read returns open and you show “waiting” to somebody who already paid. Measured: the invoice turns paid in under 1s sometimes, and in about 3s other times — the variance is the provider’s network, not yours.

Delivering — and the second poll

Here is the mistake you can make with the whole doc open in front of you: the grant does not exist the instant the invoice turns paid. Delivery runs after the payment confirms, so the first read returns [].
An empty list is a 200, never a 404 — deliberately, precisely so this is safe to poll in a loop. The rest of the deliverable is in delivering the product.

Testing all of this end to end

In sandbox you close the loop yourself, with no provider key:
Check the field, never the shape of pixPayload — in production the field does not come and the payload is EMV. That is what separates a test button from a production bug. It is spelled out in testing in the sandbox.

The whole page, together

download coming back undefined is not an error: it is a product with no deliverable, or delivery that has not run yet. In both cases, showing “your access arrives by email in a moment” beats an empty screen — and the email does go out, as long as the address is real.

If the sale is reversed

Refunding is not only giving money back: with a digital product the buyer already has the file, and the link is still in their inbox. What decides what happens to the access is the amount of the refund — full switches it off, partial does not. It is in refunds.