Começar
Sua primeira venda
Do zero à fatura paga: qual caminho escolher, o que guardar, e o que fazer no clique duplo.
Existem três formas de cobrar por um produto, e elas resolvem o mesmo problema com trabalhos muito diferentes do seu lado. Escolha primeiro, implemente depois.
Qual caminho é o seu
| O que você constrói | Quando usar | |
|---|---|---|
| Link de pagamento | nada | Venda pontual, cobrança por WhatsApp, primeira venda antes de existir app |
checkout() | sua página de "obrigado" | Você tem app e quer o comprador dentro dele, mas não quer montar tela de pagamento |
invoices.createForProduct() | a tela de pagamento inteira | Você quer controle total do visual e do fluxo |
Os três terminam na mesma fatura e no mesmo webhook. A diferença é quanto da experiência é sua.
Na dúvida, comece pelo link
É o único que não exige nada do seu app. Você troca depois — o produto e o catálogo são os mesmos.
O caminho do meio, ponta a ponta
Assumindo que você já tem um produto publicado (veja catálogo) e uma chave:
// 1. cria cliente + fatura, e devolve a URL hospedada
const { invoice, url } = await infi.checkout({
slug: "seu-tenant",
productId,
customer: {
externalId: seuUserId, // o id do SEU usuário
email: "[email protected]",
taxId: "52998224725", // Pix e boleto exigem CPF/CNPJ
},
successUrl: "https://seu-app.com/obrigado",
});
// 2. GUARDE ISSO. É o passo que ninguém documenta e todo mundo esquece.
await db.pedidos.insert({ userId: seuUserId, invoiceId: invoice.id, status: "aberta" });Você precisa mapear `invoiceId` → seu usuário
Nós não sabemos quem é o seu usuário — você passa um externalId e nós
devolvemos um invoiceId. Quando o pagamento confirmar, o webhook traz o
invoiceId, não o seu usuário. Se você não guardou o par, recebeu dinheiro e
não sabe de quem.
É a decisão de arquitetura mais importante desta página e ela cabe em uma linha de tabela no seu banco.
Cobrar, e mostrar o Pix na sua tela
const pay = await infi.pay.charge({ slug, invoiceId: invoice.id, method: "pix" });
pay.pixPayload; // copia-e-cola EMV — renderize como QR
pay.pixQrImage; // PNG em base64, já pronto: use se vier
pay.pixExpiresAt; // quando o QR morreHoje só Pix tem artefato pra sua tela
boleto e card retornam apenas invoiceUrl — a página hospedada do
provedor. Não existe campo de linha digitável nem de código de barras na resposta,
e clientSecret/publishableKey (cartão confirmado no navegador) só aparecem
onde o cartão está habilitado no tenant.
Como o pagador não deve ir pro site do provedor, o caminho hoje para boleto e
cartão é o link de pagamento ou o
url que o checkout() devolve — os dois são checkout nosso, com a sua marca de
merchant. Verifique cardEnabled na leitura pública do link antes de oferecer
cartão.
Saber que pagou
Não confie no retorno do charge: ele volta pending. Pagamento é assíncrono.
// produção: webhook assinado
// sandbox: polling, porque registrar webhook responde 503
const pago = await infi.pay.waitForPaid({ slug, invoiceId: invoice.id, timeoutMs: 15000 });Quando confirmar, use o invoiceId do evento pra achar o pedido que você guardou
no passo 2. Detalhes em webhooks.
O comprador clicou duas vezes em "Comprar"
Todo método que não é GET na API autenticada exige Idempotency-Key (as
rotas públicas de /pay/*, que o navegador do comprador chama, não exigem). Isso
não é burocracia: é o
que impede que dois cliques virem duas faturas.
// a MESMA chave para a MESMA intenção de compra
const chave = `pedido-${seuUserId}-${productId}-${new Date().toISOString().slice(0,10)}`;
const { invoiceId } = await infi.checkout({ slug, productId, customer, idempotencyKey: chave });
await infi.pay.charge({ slug, invoiceId, method: "pix", idempotencyKey: `${chave}-pix` });A partir de @beinfi/[email protected] os dois aceitam idempotencyKey (e desde a
0.10.4 o checkout() devolve invoiceId já tipado como string, sem precisar
de !). Os métodos de
recurso (products.create, invoices.create, coupons.create, …) já recebiam a
chave como último argumento.
- Mesma chave, mesmo corpo → você recebe a resposta original de volta. Uma fatura só.
- Mesma chave, corpo diferente →
409 idempotency_key_reused. É proteção: quer dizer que você reusou a chave pra outra coisa. - Sem chave →
400 idempotency_key_required.
O SDK gera uma automaticamente quando você não passa — o que protege contra retry de rede, não contra clique duplo, porque cada chamada nova ganha chave nova. Para o clique duplo, a chave tem que vir de algo estável na sua intenção, como no exemplo acima.
O mais simples é não deixar clicar duas vezes
Desabilite o botão no primeiro clique e trate a Idempotency-Key como a rede de
segurança, não como a primeira linha de defesa.
Antes de vender: o nome que o comprador vê
Seu tenant nasce com um nome de placeholder. Se você não trocar, o checkout e o link de pagamento dizem literalmente "New app" — e ninguém compra de uma loja chamada New app.
await infi.account.update({ name: "Cafeteria Orvalho" });
// opcional, citado no mandato de pagamento:
await infi.account.update({ termsUrl: "https://cafeteriaorvalho.com/termos" });Vale na hora, sem republicar produto e sem gerar link novo — o mesmo link passa a
mostrar o nome novo. infi.account.get() lê de volta. (A partir do
@beinfi/[email protected]; antes disso é PATCH /account/tenant.)
Vendeu. Agora entrega
Se o que você vende é um arquivo ou um acesso, não monte isso à mão: anexe o entregável ao produto e a Infi manda o link pessoal pro comprador quando o pagamento confirma — e te devolve o mesmo link pra você mostrar na sua página de obrigado. Está em entregar o produto.
O lado do comprador — descobrir que pagou, entregar, e os dois polling que ninguém adivinha — está em a página de obrigado.