> ## 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.

# A página de obrigado

> O lado do comprador: da tela de pagamento até ele receber, nos dois caminhos de venda.

As outras páginas cobrem o que **você** faz: catálogo, cobrança, entregável. Esta
cobre o que acontece **do lado do comprador** — e é onde as integrações travam,
porque é a parte com estados intermediários.

O fio inteiro é este, e vale nos dois caminhos de venda:

```
tela de pagamento → ele paga → você descobre que pagou → você entrega
                                 ↑ polling            ↑ polling de novo
```

Os dois `polling` são a parte que ninguém adivinha. Vamos por partes.

## Caminho A: você tem a fatura (`checkout()`)

Você já tem `invoiceId` desde o começo, então é o caminho curto:

```ts theme={null}
const { invoiceId } = await infi.checkout({
  slug, productId,
  customer: { externalId: "u_1", email, taxId },
  idempotencyKey: `venda:${pedidoId}`,
});
const pay = await infi.pay.charge({ slug, invoiceId, method: "pix" });
// renderize pay.pixPayload como QR (ou use pay.pixQrImage)
```

Guarde o par `invoiceId → seu usuário` **antes** de mostrar a tela. É como você
vai saber de quem era a compra quando o pagamento voltar.

## Caminho B: você mandou um link de pagamento

O link é a recomendação pra quem não quer montar tela. Se você quiser controlar a
experiência mesmo usando link — ou testar o fluxo por HTTP — são três passos, e a
fatura só existe no terceiro.

```bash theme={null}
# 1. o que mostrar na tela (público, sem auth)
curl "$API/pay/$SLUG/links/$TOKEN"
# -> { "merchant": {...}, "product": "Guia", "testMode": true, "cardEnabled": false }

# 2. abrir a sessão — email e taxId são OBRIGATÓRIOS
curl -X POST "$API/pay/$SLUG/links/$TOKEN/sessions" -H 'Content-Type: application/json' \
  -d '{"email":"comprador@dominio.com","name":"Ana","taxId":"52998224725"}'
# -> 201 { "sessionId": "fa90ea6d-…", "product": {...}, "status": "…", "expiresAt": "…" }

# 3. cobrar — ESTE passo materializa a fatura
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": "…", … }
```

Três coisas que custam tempo se você não souber:

* **A fatura não existe antes do passo 3.** A sessão não tem `invoiceId` — ele
  aparece na resposta do charge. Não procure antes.
* **O charge é na sessão, não na fatura.** `/sessions/{id}/charge`, não
  `/invoices/{id}/charge`. A segunda existe e serve pro caminho A.
* **Sem `email` → `400 "E-mail is required."`; sem `taxId` →
  `400 "A valid CPF or CNPJ is required."`** Os dois em português, os dois antes
  de qualquer cobrança acontecer.

<Info>
  **Essas rotas não exigem `Idempotency-Key`.** A regra "todo método que não é GET exige a chave" vale pra API autenticada. As
  rotas públicas de `/pay/*` — as que o navegador do comprador chama — aceitam sem.
</Info>

<Warning>
  **O `422` do `taxId` chega no charge, não no checkout.** `checkout()` aceita um cliente sem CPF/CNPJ e cria uma fatura **finalizada e
  numerada**. O `422 customer_tax_id_required` só aparece no `pay.charge`, e aquela
  fatura não pode mais ser paga — ela fica `open` pra sempre no seu relatório.

  A validação vive no charge porque quem exige o documento é o provedor do método, e
  o método só é escolhido ali. Colete `taxId` antes de criar a fatura; se já criou
  uma sem, limpe:

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

  O mesmo vale pra cliente sem nome **e** sem e-mail: um dos dois é obrigatório
  (`422 customer_name_required`).
</Warning>

## Descobrir que pagou

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

**Sempre passe `timeoutMs`.** O default é 600000 — dez minutos — então numa fatura
não paga, que é o caso normal, o seu handler trava em vez de responder.

Em produção o webhook `payment.confirmed` é o caminho certo; em sandbox o
registro responde `503`, então é polling — detalhes em
<a href="/webhooks">webhooks</a>.

<Warning>
  **Não leia a fatura uma vez só.** A confirmação chega pelo webhook do provedor, alguns instantes depois de você
  disparar o pagamento. Uma leitura única devolve `open` e você mostra "aguardando"
  pra alguém que já pagou. Medido: a fatura vira `paid` em menos de 1s às vezes, e
  em \~3s outras — a variação é a rede do provedor, não a sua.
</Warning>

## Entregar — e o segundo polling

Aqui está o erro que dá pra cometer com a doc toda certa na mão: **o grant não
existe no mesmo instante que a fatura vira `paid`.** A entrega roda depois do
pagamento confirmar, então a primeira leitura devolve `[]`.

```ts theme={null}
if (pago) {
  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]) mostrarBotao(grants[0].downloadUrl);
}
```

Lista vazia é `200`, nunca `404` — de propósito, justamente pra isso ser
consultável num loop. O resto do entregável está em
<a href="/entrega-do-produto">entregar o produto</a>.

## Testar tudo isso de ponta a ponta

Em sandbox você fecha o loop sozinho, sem chave de provedor:

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

Cheque **o campo**, nunca o formato do `pixPayload` — em produção o campo não vem
e o payload é EMV. É o que separa um botão de teste de um bug em produção. Está
detalhado em <a href="/testar-no-sandbox">testar no sandbox</a>.

## A página inteira, junta

```ts theme={null}
// GET /obrigado?invoice=...
const invoiceId = req.query.invoice;
const pago = await infi.pay.waitForPaid({ slug, invoiceId, intervalMs: 700, timeoutMs: 15000 });
if (!pago) return render("aguardando", { invoiceId });   // deixe ele recarregar

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("obrigado", {
  download: grants[0]?.downloadUrl,        // pode ser undefined: produto sem entregável
  pedido: await meuBanco.porFatura(invoiceId),
});
```

`download` vindo `undefined` não é erro: é produto sem entregável, ou entrega que
ainda não rodou. Nos dois casos, mostrar "seu acesso chega por e-mail em
instantes" é melhor do que uma tela vazia — e o e-mail realmente sai, desde que o
endereço exista de verdade.

## Se a venda voltar atrás

Estornar não é só devolver dinheiro: num produto digital o comprador já tem o
arquivo, e o link continua no e-mail dele. Quem decide o que acontece com o
acesso é o **valor** do estorno — total desliga, parcial não. Está em
<a href="/reembolso">reembolso</a>.
