Get started
Company as code
Declare tenant, products, apps and webhooks in infi.company.ts — sync like Terraform.
Company as code configures your catalog from a file: a TypeScript file versioned in git, applied with the CLI (plan/apply), instead of clicking through the dashboard for every change.
This is a setup tool, not part of your app — whoever builds against Infi at runtime uses payment link and SDK. Use the CLI if you prefer a catalog in git to a catalog in the dashboard.
The CLI infers the host from the key
From 0.2.0 on it resolves the host from the key prefix (sk_test_ → sandbox,
sk_live_ → production), so INFI_API_URL is only for deliberately pointing
somewhere else. Before 0.2.0 it was required in sandbox.
The file
// infi.company.ts
import { defineCompany } from "@beinfi/sdk";
export default defineCompany.fromIntent("prepaid-ai-chat");
// or hand-authored:
export default defineCompany({
products: [
{
key: "ai-chat",
name: "AI Chat",
pricingModel: "prepaid",
billingCycle: "monthly",
basePrice: "19.90",
meters: [{ key: "tokens", unit: "token", aggregation: "sum" }],
// Plan grants — credit that meter's balance on the customer's enrollment
grants: [{ meter: "tokens", amount: "50000", on: "cycle" }],
},
],
webhooks: [{ url: "https://your-app.com/api/webhooks/infi", events: ["payment.confirmed"] }],
});defineBilling / infi.billing.ts still work as aliases.
The file is loaded as ESM
The CLI imports the .ts directly. If the project's package.json has no
"type": "module", the load fails with "Cannot use import statement outside a
module".
Intents
Shortcuts that generate a sensible company file. They live in the CLI and in the
file — the public provisioning endpoint does not accept intent:
| Intent | Typical use |
|---|---|
crm | B2B SaaS / CRM |
prepaid-ai-chat | Chat/LLM with per-meter credits |
one-time | Pack / ebook / single charge |
usage-saas | Pay-as-you-go metered |
Commands
| Command | What for | State today |
|---|---|---|
infi claim create --ref cli --json | Provisions a claimable tenant + key | ok |
infi sync infi.company.ts | Applies the desired state | ok |
infi sync infi.company.ts --plan | Dry-run (diff) | ok |
infi pull | Backend → infi.company.ts | ok |
infi doctor --json | Setup health (checks + hints) | ok |
infi go-live --json | Guidance for claim → account → KYC → sk_live_ | ok |
infi bootstrap --intent … | Claim + company file + sync + doctor | ok |
Plan grants
Each product may declare grants[]:
on: "cycle"— credits when the period opens or renews (subscription/prepaid)on: "payment"— credits onpayment.confirmed(one-time packs)
A grant's meter is real: each meter has its own wallet. A grant on
tokens credits the tokens wallet, and
GET /metering/customers/{id}/wallet returns each balance:
{ "balances": [ { "meter": "tokens", "balance": "50000", "total": "50000" } ] }`/credit` is legacy and answers a different question
GET /metering/customers/{id}/credit reads only the legacy credits pool.
On an enrollment with 50,000 in tokens it answers 0 — that is not an empty
balance, it is the wrong wallet. Use /wallet?meter=…. The SDK does this on its
own since 0.11.1: infi.meter({ meter: "tokens" }) gates against that meter's
wallet.
`creditsPerCycle` still exists (but is legacy)
It is still in the types and still honoured: the rule is grants[{ on: "cycle" }]
first, and creditsPerCycle as a fallback. So an old file does not break — but
write grants[] in new code, which is the only way to credit a specific meter or
to credit on: "payment".
A `prepaid` version needs a price to publish
Publishing a prepaid version requires a positive basePrice or a meter
price published on it. With neither, publish answers 422 and the product stays
in draft — chargeable by nobody.
Which means a free tier (no monthly fee) works as long as the meter has a price, which is what rates the wallet's consumption:
{ key: "studio", type: "agent", pricingModel: "prepaid", billingCycle: "monthly",
meters: [{ key: "tokens", unit: "token", aggregation: "sum", valueProperty: "value" }],
grants: [{ meter: "tokens", amount: "50000", on: "cycle" }],
prices: [{ meter: "tokens", model: "per_unit", unitAmount: "0.00004", currency: "BRL" }] }Every price is a meter rate: a fixed amount is not a price, it is the
version's basePrice.
Webhooks in the file
The company file's webhooks[] is applied by sync — and in sandbox that hits
503 secret_store_unavailable, because a test tenant has no secret store. It is
not a syntax error in your file. See webhooks.
Agents: run infi doctor --json, and on any failure read InfiError.errors[] —
that is where { field, description } says what the API refused. fix.command /
hint only appear on some error codes.