> ## Documentation Index
> Fetch the complete documentation index at: https://docs.beinfi.com/llms.txt
> Use this file to discover all available pages before exploring further.

# The thank-you page

> The buyer's side: from the payment screen to them receiving it, on both selling paths.

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:

```
payment screen → they pay → you find out they paid → you deliver
                              ↑ polling            ↑ polling again
```

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:

```ts theme={null}
const { invoiceId } = await infi.checkout({
  slug, productId,
  customer: { externalId: "u_1", email, taxId },
  idempotencyKey: `sale:${orderId}`,
});
const pay = await infi.pay.charge({ slug, invoiceId, method: "pix" });
// render pay.pixPayload as a QR (or use pay.pixQrImage)
```

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.

## Path B: you sent a payment link

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.

```bash theme={null}
# 1. what to show on the screen (public, no auth)
curl "$API/pay/$SLUG/links/$TOKEN"
# -> { "merchant": {...}, "product": "Guide", "testMode": true, "cardEnabled": false }

# 2. open the session — email and taxId are REQUIRED
curl -X POST "$API/pay/$SLUG/links/$TOKEN/sessions" -H 'Content-Type: application/json' \
  -d '{"email":"buyer@domain.com","name":"Ana","taxId":"52998224725"}'
# -> 201 { "sessionId": "fa90ea6d-…", "product": {...}, "status": "…", "expiresAt": "…" }

# 3. charge — THIS step materialises the invoice
curl -X POST "$API/pay/$SLUG/links/$TOKEN/sessions/$SESSION_ID/charge" \
  -H 'Content-Type: application/json' -d '{"method":"pix"}'
# -> 201 { "invoiceId": "2ae1b610-…", "pixPayload": "…", "sandboxConfirmUrl": "…", … }
```

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.

<Info>
  **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.
</Info>

<Warning>
  **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:

  ```ts theme={null}
  await infi.invoices.void(invoiceId);   // -> status "void"
  ```

  The same holds for a customer with no name **and** no email: one of the two is
  required (`422 customer_name_required`).
</Warning>

## Finding out they paid

```ts theme={null}
const paid = await infi.pay.waitForPaid({ slug, invoiceId, intervalMs: 700, timeoutMs: 15000 });
```

**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](https://beinfi.com/en/webhooks).

<Warning>
  **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.
</Warning>

## 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 `[]`.

```ts theme={null}
if (paid) {
  let grants = [];
  for (let i = 0; i < 10 && grants.length === 0; i++) {
    grants = await infi.invoices.deliverable(invoiceId);
    if (grants.length === 0) await new Promise((r) => setTimeout(r, 500));
  }
  if (grants[0]) showButton(grants[0].downloadUrl);
}
```

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](https://beinfi.com/en/entrega-do-produto).

## Testing all of this end to end

In sandbox you close the loop yourself, with no provider key:

```ts theme={null}
if (pay.sandboxConfirmUrl) {
  await fetch(pay.sandboxConfirmUrl, { method: "POST" });
}
```

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](https://beinfi.com/en/testar-no-sandbox).

## The whole page, together

```ts theme={null}
// GET /thanks?invoice=...
const invoiceId = req.query.invoice;
const paid = await infi.pay.waitForPaid({ slug, invoiceId, intervalMs: 700, timeoutMs: 15000 });
if (!paid) return render("waiting", { invoiceId });   // let them reload

let grants = [];
for (let i = 0; i < 10 && grants.length === 0; i++) {
  grants = await infi.invoices.deliverable(invoiceId);
  if (grants.length === 0) await new Promise((r) => setTimeout(r, 500));
}

return render("thanks", {
  download: grants[0]?.downloadUrl,        // may be undefined: product with no deliverable
  order: await myDb.byInvoice(invoiceId),
});
```

`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](https://beinfi.com/en/reembolso).
