Integração
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
link de pagamento.
Cliente
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), passeapiUrle/ouappUrlno 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 catálogo:
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:
const enrollment = await infi.products.enroll(productId, {
externalId: seuUserId, // o id do SEU auth (idempotente nele)
email: "[email protected]",
taxId: "52998224725", // CPF/CNPJ: exigido por Pix e boleto
});
enrollment.id; // ← o id que toda chamada de cobrança referenciaproducts.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.
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:
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 idO parâmetro se chama customerId mas espera e.id. Passar e.customerId
responde 422 (customerId: unknown customer):
await infi.track({ customerId: e.id, productId, meter: "tokens", value: "1" }); // ok
await infi.track({ customerId: e.customerId, productId, meter: "tokens", value: "1" }); // 422Guarde e.id associado ao seu usuário. É a única coisa que você precisa
persistir do nosso lado.
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:
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()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/[email protected]). 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.
Grants do plano (on: cycle | payment) vivem em infi.company.ts — veja
company as code.
Medição de uso
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" },
]);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.
LLM medido
meter checa saldo, roda a chamada e registra uso. Sem saldo →
InsufficientCreditError (402) antes de gastar.
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):
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):
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
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
import { UsagePanel } from "@beinfi/sdk/react";
const state = await infi.customers.state(enrollment.id); // server only
<UsagePanel state={state} creditLabel="créditos" />;