Skip to main content
Vender um produto digital tem duas metades. A primeira — cobrar — está em primeira venda. Esta é a segunda: o comprador pagou, agora ele precisa receber.
Requer @beinfi/sdk@0.10.4. invoices.deliverable(), o invoiceId que o checkout() devolve e o presign com tipos estreitados entraram na 0.10.4. Em versão anterior, as rotas HTTP já existem — chame direto. (A 0.10.3 foi publicada com bundle velho e não tem nada disso; não use.)
Você anexa um entregável ao produto. Quando um pagamento confirma, a Infi cria um link pessoal pra aquele comprador, manda por e-mail, e deixa o mesmo link disponível pra você servir da sua própria página de obrigado.
Não depende de webhook. A entrega é interna: roda quando o payment.confirmed sai da nossa fila, não quando o seu webhook responde. Ou seja, funciona em sandbox mesmo com o registro de webhook devolvendo 503.

Só em produto de compra única

O entregável existe apenas em produto type: "item" com pricingModel: "one_time". Qualquer outra combinação:
Assinatura não tem entregável — o que o assinante recebe é acesso, e isso é subscription, não download.

Anexar

Duas formas. Um produto tem um entregável: salvar de novo substitui o anterior. Serve pra Notion, Drive, um vídeo no Vimeo, sua própria área de membros:

Um arquivo

Três passos, porque os bytes vão direto do seu processo pro storage sem passar pela nossa API:
O passo 3 confere que o objeto existe de verdade antes de salvar — se você inverter a ordem, ele recusa com objectKey: "uploaded object was not found; upload before saving". E ele preenche sizeBytes/contentType sozinho se você omitir: subir um PDF sem declarar nada devolve contentType: "application/pdf" e o tamanho real. A uploadUrl vale 15 minutos. Ela é só pra escrever aquele objeto, então dá pra mandar direto do navegador do seu admin sem passar a sua sk_ pra frente.
Se o ambiente não tiver storage, presign responde 503. 503 storage_unconfigured quer dizer que aquele ambiente não tem storage de objeto configurado — não que você errou a chamada. Nesse caso use kind: "link": o resto do fluxo (grant, e-mail, download) é idêntico nos dois casos.

O que acontece quando o pagamento confirma

Nessa ordem, e sem você fazer nada:
  1. A Infi resolve o comprador e o entregável daquela fatura.
  2. Cria um grant: um token único pra aquele pagamento.
  3. Manda o e-mail, assunto Seu acesso / Your download is ready, com o link.
Se o mesmo evento for reprocessado, ele reencontra o grant e não manda um segundo e-mail — a garantia é no banco (UNIQUE (payment_id, deliverable_id)), não na sorte. Produto sem entregável não é erro: a entrega simplesmente não faz nada.

Servir você mesmo (recomendado)

Não dependa da caixa de entrada do comprador. Você tem o link:
Enquanto a fatura não foi paga, isso devolve 200 { "grants": [] } — lista vazia, nunca 404. Isso é de propósito: “ainda não entreguei” é um estado real que você fica consultando, e um 404 não daria pra distinguir de id errado. Então é seguro colocar num loop de polling, ao lado do pay.waitForPaid. Três motivos pra preferir esse caminho ao e-mail:
  • O e-mail pode simplesmente não sair, e o emailSentAt é como você sabe. Ele fica nulo quando o envio não aconteceu — e o caso que mais pega gente testando é endereço que não existe de verdade. Medido: @example.com (domínio reservado, nenhum provedor entrega) → emailSentAt nulo pra sempre; endereço real → preenchido em ~8s. O grant nasce nos dois casos, então a venda está entregável mesmo quando o e-mail não foi.
  • Comprador sem e-mail nenhum também gera grant. Sem essa chamada, a venda fica entregue pela metade e você não sabe.
  • O e-mail pode cair em spam. A sua página de obrigado, não.
Essa rota exige a chave secreta. invoices.deliverable roda com sk_, escopo billing:read — nunca com uma chave publicável. O token é uma credencial: quem tem ele baixa o produto, sem mais nenhuma prova de compra. Por isso ele também não aparece na resposta pública da fatura: id de fatura viaja em URL, histórico de navegador e ticket de suporte, e o download não pode viajar junto. Busque no seu servidor e entregue pro navegador que você acabou de cobrar.
Público, sem header de auth — o token é a credencial. Responde 302: Token desconhecido responde 404. No SDK, se você já tem o token, infi.pay.downloadUrl(slug, token) monta essa URL. A URL assinada de download é curta de propósito (5 min) porque ela é gerada a cada clique: quem compartilhar o link assinado compartilha algo que expira. O que não expira é o token — veja abaixo.
O link não expira e não tem limite de uso. Hoje o grant não tem validade nem contador: quem tiver o token baixa quantas vezes quiser, pra sempre. Ele é pessoal por ser secreto, não por ser verificado. Trate como senha — não jogue em log, não coloque em URL que você compartilha. Se o seu produto exige controle de acesso de verdade, use kind: "link" apontando pra uma área que você mesmo autentica.

Trocar e remover

delete também apaga o arquivo do storage. Grants já emitidos deixam de resolver — o comprador de ontem perde o acesso, então troque com save em vez de deletar quando a ideia é publicar uma versão nova.

Fluxo completo

O taxId não é opcional pra Pix, e o idempotencyKey evita cobrar duas vezes no clique duplo — os dois estão explicados em primeira venda.

E se você estornar?

O grant é uma capacidade sem prazo: quem tem o token baixa. Um estorno total desliga ele — o link passa a responder 410 e o grant volta com revokedAt — e um estorno parcial não. A regra inteira, incluindo como sobrescrever, está em reembolso. Isso vale só pro arquivo. Se o seu produto dá acesso a outra coisa (área de membros, cargo no Discord, chave de API), quem corta é você: assine payment.refunded e leia accessRevoked.