Pular para o conteúdo

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.

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."; sem taxId → 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.