Skip to content

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:

IntentTypical use
crmB2B SaaS / CRM
prepaid-ai-chatChat/LLM with per-meter credits
one-timePack / ebook / single charge
usage-saasPay-as-you-go metered

Commands

CommandWhat forState today
infi claim create --ref cli --jsonProvisions a claimable tenant + keyok
infi sync infi.company.tsApplies the desired stateok
infi sync infi.company.ts --planDry-run (diff)ok
infi pullBackend → infi.company.tsok
infi doctor --jsonSetup health (checks + hints)ok
infi go-live --jsonGuidance for claim → account → KYC → sk_live_ok
infi bootstrap --intent …Claim + company file + sync + doctorok

Plan grants

Each product may declare grants[]:

  • on: "cycle" — credits when the period opens or renews (subscription/prepaid)
  • on: "payment" — credits on payment.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.