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

# SDK

> Cliente @beinfi/sdk: inscrição, medição de uso, meter e créditos.

Referência dos principais blocos do `@beinfi/sdk` (`0.10.0`). Se você só quer
receber sem construir nada, comece por
<a href="/link-de-pagamento">link de pagamento</a>.

## Cliente

```ts theme={null}
import { Infi } from "@beinfi/sdk";

const infi = new Infi({ secretKey: process.env.INFI_SECRET_KEY! });
// atalho: new Infi(process.env.INFI_SECRET_KEY!)
```

O prefixo da chave decide o modo, e o modo decide os dois hosts: `sk_test_` →
`api-sandbox.beinfi.com` + `app-sandbox.beinfi.com`; `sk_live_` → `api.beinfi.com`

* `app.beinfi.com`. Pra apontar pra outro lugar (local, self-host, teste), passe
  `apiUrl` e/ou `appUrl` no construtor — não existe variável de ambiente pra isso.

## Link de pagamento

Cobrança sem checkout do seu lado. Precisa de produto com **versão publicada** —
veja <a href="/catalogo">catálogo</a>:

```ts theme={null}
const link = await infi.links.create(productId, { slug: "seu-tenant" });
link.url; // https://app-sandbox.beinfi.com/pay/seu-tenant/links/plink_… (sandbox)

await infi.links.list(productId, { slug: "seu-tenant" });
await infi.links.revoke(productId, link.id);
```

## Identifique o pagador

A Infi **não faz o login do seu usuário final** — traga o seu auth (Clerk, Supabase,
NextAuth, o seu) e inscreva o id que você já tem no produto:

```ts theme={null}
const enrollment = await infi.products.enroll(productId, {
  externalId: seuUserId,          // o id do SEU auth (idempotente nele)
  email: "cliente@empresa.com",
  taxId: "52998224725",           // CPF/CNPJ: exigido por Pix e boleto
});

enrollment.id; // ← o id que toda chamada de cobrança referencia
```

`products.enroll` é a chamada recomendada: ela devolve a **inscrição** (enrollment)
do cliente naquele produto. Existe também `infi.customers.create(input)`, que cria
um cliente no nível do tenant e recebe **só** o input — passar `productId` pra ela
responde `400`.

<Warning>
  **Use `.id` — não `.customerId`.** A resposta do `enroll` traz **dois** ids diferentes, e o campo chamado
  `customerId` **não** é o que as chamadas de cobrança querem:

  ```ts theme={null}
  const e = await infi.products.enroll(productId, { externalId: seuUserId });
  e.id;          // 7b4e3712-…  ← é este que você guarda e passa adiante
  e.customerId;  // f7701d03-…  ← o cliente no nível do tenant, outro id
  ```

  O parâmetro se chama `customerId` mas espera `e.id`. Passar `e.customerId`
  responde `422 (customerId: unknown customer)`:

  ```ts theme={null}
  await infi.track({ customerId: e.id,         productId, meter: "tokens", value: "1" }); // ok
  await infi.track({ customerId: e.customerId, productId, meter: "tokens", value: "1" }); // 422
  ```

  Guarde `e.id` associado ao seu usuário. É a única coisa que você precisa
  persistir do nosso lado.
</Warning>

## Wallet: enroll + saldo numa chamada

Se o seu produto é pré-pago (o cliente compra crédito antes de usar),
`wallet.forCustomer` faz a inscrição, aplica o saldo inicial e devolve tudo que
você precisa depois:

```ts theme={null}
const wallet = await infi.wallet.forCustomer(seuUserId, {
  productKey: "ai-chat",
  starterCredits: "2000", // opcional — prefira grants[] no company file
});
// wallet.enrollmentId, wallet.productId, wallet.defaultMeter, wallet.balance()
```

<Warning>
  **O saldo ainda não é isolado por meter.** A API é por meter (`wallet.balance("tokens")`, `wallet.debit("exports", …)`), mas
  hoje existe **um saldo só por inscrição**: pedir o saldo de dois meters diferentes
  devolve o mesmo número (verificado com `@beinfi/sdk@0.10.0`). O meter vai na
  referência do lançamento, então o histórico separa — o saldo, não. Não conte com
  isolamento por meter ainda.
</Warning>

Grants do plano (`on: cycle | payment`) vivem em `infi.company.ts` — veja
<a href="/company-as-code">company as code</a>.

## Medição de uso

```ts theme={null}
await infi.track({
  customerId: enrollment.id,
  productId,                // obrigatório quando o tenant tem mais de um produto
  meter: "tokens",
  value: "1200",
});

await infi.trackBatch([
  { customerId: enrollment.id, productId, meter: "tokens", value: "1200" },
  { customerId: enrollment.id, productId, meter: "api_call", value: "1" },
]);
```

<Tip>
  **Fora do caminho crítico.** `track` registra uso sem segurar request nem run de agente. Ele responde
  `{ eventId, accepted, duplicate }` — `duplicate: true` é dedupe, não erro.
</Tip>

## LLM medido

`meter` checa saldo, roda a chamada e registra uso. Sem saldo →
`InsufficientCreditError` (402) **antes** de gastar.

```ts theme={null}
import { InsufficientCreditError } from "@beinfi/sdk";

try {
  const res = await infi.meter(
    { customerId: enrollment.id, productId, meter: "tokens" },
    () => openai.chat.completions.create({ model: "gpt-4o", messages }),
  );
} catch (err) {
  if (err instanceof InsufficientCreditError) {
    // 402 / upsell — err.balance é o saldo atual
  }
}
```

Streaming (baixa explícita no `onFinish`, porque o total de tokens só existe no
fim):

```ts theme={null}
await infi.meter(
  { customerId: enrollment.id, productId, meter: "tokens", mode: "streaming" },
  () =>
    streamText({
      onFinish: ({ usage }) =>
        infi.customers.credits.consume(enrollment.id, {
          amount: String(usage.totalTokens ?? 0),
        }),
    }),
);
```

Estado agregado (saldo + assinaturas + uso do período):

```ts theme={null}
const state = await infi.customers.state(enrollment.id);
// { customer, credit: { balance, total }, subscriptions, usage }

const report = await infi.usage.get({ customerId: enrollment.id });
// { from, to, meters: [{ meter, unit, totalValue, eventCount }] }
```

## Erros

```ts theme={null}
try {
  await infi.links.create(productId, { slug });
} catch (err) {
  err.status;   // 422
  err.code;     // "validation_failed"
  err.errors;   // [{ field: "productId", description: "product has no published version…" }]
}
```

`InfiError.errors[]` é onde vive o motivo real de um `422` — é o campo que responde
"por que essa escrita foi recusada". `err.fix` traz remediação quando a API manda
uma.

## React

```tsx theme={null}
import { UsagePanel } from "@beinfi/sdk/react";

const state = await infi.customers.state(enrollment.id); // server only
<UsagePanel state={state} creditLabel="créditos" />;
```

<CardGroup cols={2}>
  <Card title="Saber que pagou" href="/webhooks">
    Webhook assinado em produção, polling em sandbox.
  </Card>

  <Card title="Entregar o produto" href="/entrega-do-produto">
    Anexe arquivo ou link e a Infi entrega quando o pagamento confirma.
  </Card>
</CardGroup>
