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

# 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](https://beinfi.com/en/link-de-pagamento) and
[SDK](https://beinfi.com/en/sdk). Use the CLI if you prefer a catalog in git to a catalog in
the dashboard.

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

## The file

```ts theme={null}
// 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.

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

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

```json theme={null}
{ "balances": [ { "meter": "tokens", "balance": "50000", "total": "50000" } ] }
```

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

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

<Info>
  **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:

  ```ts theme={null}
  { 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`.
</Info>

## 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](https://beinfi.com/en/webhooks).

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