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

# Entregar o produto

> Anexe o arquivo ou o link ao produto e a Infi entrega sozinha quando o pagamento confirma.

Vender um produto digital tem duas metades. A primeira — cobrar — está em
<a href="/primeira-venda">primeira venda</a>. Esta é a segunda:
**o comprador pagou, agora ele precisa receber.**

<Info>
  **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.)
</Info>

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.

<Tip>
  **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`.
</Tip>

## Só em produto de compra única

O entregável existe apenas em produto `type: "item"` com
`pricingModel: "one_time"`. Qualquer outra combinação:

```json theme={null}
// PUT /metering/products/{id}/deliverable  -> 422
{ "error_code": "deliverable_not_allowed",
  "message": "Deliverables are only allowed on one_time item products." }
```

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.

### Um link (o caminho mais curto)

Serve pra Notion, Drive, um vídeo no Vimeo, sua própria área de membros:

```ts theme={null}
await infi.products.deliverable.save(productId, {
  kind: "link",
  url: "https://seusite.com/area-de-membros/guia",
});
```

### Um arquivo

Três passos, porque os bytes vão direto do seu processo pro storage sem passar
pela nossa API:

```ts theme={null}
import { readFile } from "node:fs/promises";

const bytes = await readFile("./guia-do-cafe.pdf");

// 1. peça a URL assinada
const { uploadUrl, objectKey } = await infi.products.deliverable.presign(productId, {
  fileName: "guia-do-cafe.pdf",
  contentType: "application/pdf",
  sizeBytes: bytes.byteLength,
});

// 2. suba os bytes pra ela (PUT, sem header de auth nosso)
await fetch(uploadUrl, {
  method: "PUT",
  headers: { "Content-Type": "application/pdf" },
  body: bytes,
});

// 3. registre o objeto no produto
await infi.products.deliverable.save(productId, { kind: "file", objectKey });
```

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.

<Info>
  **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.
</Info>

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

```ts theme={null}
const grants = await infi.invoices.deliverable(invoiceId);
// [{ paymentId, token, downloadUrl, emailSentAt, createdAt }]

if (grants.length > 0) {
  mostrarBotao(grants[0].downloadUrl);   // sua página de obrigado
}
```

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.

<Warning>
  **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.
</Warning>

## O link de download

```
GET /pay/{slug}/download/{token}
```

Público, sem header de auth — o token é a credencial. Responde `302`:

| entregável | pra onde redireciona                                         |
| ---------- | ------------------------------------------------------------ |
| `link`     | a URL que você salvou                                        |
| `file`     | uma URL assinada e **nova** do storage, válida por 5 minutos |

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.

<Warning>
  **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.
</Warning>

## Trocar e remover

```ts theme={null}
await infi.products.deliverable.get(productId);      // o que está anexado hoje
await infi.products.deliverable.save(productId, {…}); // substitui
await infi.products.deliverable.delete(productId);    // remove (idempotente)
```

`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

```ts theme={null}
import { Infi } from "@beinfi/sdk";
const infi = new Infi({ secretKey: process.env.INFI_SECRET_KEY! });

// uma vez, ao cadastrar o produto
await infi.products.deliverable.save(productId, {
  kind: "link",
  url: "https://seusite.com/guia.pdf",
});

// a cada venda
const { invoiceId } = await infi.checkout({
  slug,
  productId,
  customer: { externalId: "u_1", email: "comprador@x.com", taxId: "52998224725" },
  idempotencyKey: `venda:${pedidoId}`,
});

const pago = await infi.pay.waitForPaid({ slug, invoiceId, timeoutMs: 15000 });
if (pago) {
  // A entrega roda DEPOIS do pagamento confirmar, então o grant aparece um
  // instante depois de `paid`. Uma leitura só devolve [] e você mostra uma
  // página de obrigado sem download. Faça o loop.
  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 { download: grants[0]?.downloadUrl };
}
```

O `taxId` não é opcional pra Pix, e o `idempotencyKey` evita cobrar duas vezes
no clique duplo — os dois estão explicados em
<a href="/primeira-venda">primeira venda</a>.

## 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
<a href="/reembolso">reembolso</a>.

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