Começar
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:
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.
# 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":"[email protected]","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."; semtaxId→400 "A valid CPF or CNPJ is required."Os dois em português, os dois antes de qualquer cobrança acontecer.
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.
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:
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).
Descobrir que pagou
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
webhooks.
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.
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 [].
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
entregar o produto.
Testar tudo isso de ponta a ponta
Em sandbox você fecha o loop sozinho, sem chave de provedor:
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 testar no sandbox.
A página inteira, junta
// 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 reembolso.