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

# Catalog

> Product → published version → link: the chain that exists before you sell anything.

Before charging for something, that something has to exist in the catalog **and
be published**. Three calls. Without them, `links.create` answers `422`.

## Where `productId` comes from

Every charging page asks for a `productId`. It comes from one of three places:

| Source                              | When                                                                                                                                             |
| ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `productId` from provisioning       | Comes in the JSON from [`POST /public/v1/claimables`](https://beinfi.com/en/inicio-rapido) — it is the seed product, and it is **not** published |
| `await infi.products.list()`        | You already created the product                                                                                                                  |
| `await infi.products.create({...})` | Creating it now — the response carries `.id`                                                                                                     |

## The chain

```ts theme={null}
// 1. product
const product = await infi.products.create({
  key: "pricing-guide",           // the tenant's natural key (idempotent upsert)
  name: "Pricing guide",
  type: "item",                   // "item" (default) or "agent"
  pricingModel: "one_time",       // subscription | one_time | usage | prepaid
  currency: "BRL",
  basePrice: "49.90",
});

// 2. v1 already exists as a draft — take it
const [draft] = await infi.products.versions.list(product.id);

// 3. publish it (the step nobody guesses)
await infi.products.versions.publish(product.id, draft.id);

// 4. now it works
const link = await infi.links.create(product.id, { slug: "your-tenant" });
```

`products.create` already returns version 1 as a `draft` — you do not create a
version by hand, you list it and publish it.

<Warning>
  **No published version, 422.** Skipping step 3 gives you this:

  ```json theme={null}
  {"error_code":"validation_failed","message":"One or more fields are invalid.",
   "errors":[{"field":"productId",
     "description":"product has no published version; publish it before creating a payment link"}]}
  ```

  Since `@beinfi/sdk@0.10.0` that detail reaches you: `InfiError.errors[]` carries
  `{ field, description }`. On an earlier SDK only the generic "One or more fields
  are invalid" surfaces — and a missing publish is the first suspect.
</Warning>

## Price: you probably do not need `prices.add`

For a one-off product at a fixed amount, the product's `basePrice` **is** the
price — the invoice and the link already carry it. `products.prices.add` exists
for a **per-meter rate** (per token, per request), not for a flat price.

A meter only enters when you charge for usage:

```ts theme={null}
await infi.products.meters.create(product.id, {
  name: "tokens",            // the key you send in track()
  displayName: "Tokens",
  unit: "token",             // token | request | unit
  aggregation: "sum",
  valueProperty: "value",    // required unless aggregation: "count"
});
```

## Selling from your own page: `checkout()`

If you do not want to send a link and would rather have a "Buy" button in your
app, it is one call. It creates the invoice and returns the hosted URL where the
person pays (pix, boleto, card):

```ts theme={null}
const { invoice, url } = await infi.checkout({
  productId: product.id,
  customer: { externalId: yourUserId, email: "customer@company.com" },
  slug: "your-tenant",
  successUrl: "https://your-app.com/thanks",
});
// redirect the person to `url`
```

The amount comes from the product's published price — pass `amount` only to
override it. The person is enrolled in the product along the way, so you get an
invoice tied to the product (rather than a loose charge).

<Warning>
  **Pix and boleto require the payer's tax id.** Without a document, the charge stops at `422 customer_tax_id_required`. Pass
  `taxId` with the customer — from `@beinfi/sdk@0.10.1` on, `checkout()` forwards
  it:

  ```ts theme={null}
  const { invoice, url } = await infi.checkout({
    slug: "your-tenant",
    productId: product.id,
    customer: { externalId: yourUserId, email: "customer@company.com", taxId: "52998224725" },
  });
  ```

  Use the `url` that comes back — do not assemble the address by hand. It already
  points at the right host for your mode (`app-sandbox` with `sk_test_`, `app`
  with `sk_live_`).
</Warning>

<Info>
  **Calling it over curl.** Every `POST`/`PUT`/`DELETE` on the authenticated API requires an
  `Idempotency-Key` header — without it you get `400 idempotency_key_required`.
  The SDK generates one per call; over curl you send your own.
</Info>

## Next step

<CardGroup cols={2}>
  <Card title="Payment link" href="https://beinfi.com/en/link-de-pagamento">
    With a published version, the link is one line.
  </Card>

  <Card title="Deliver the product" href="https://beinfi.com/en/entrega-do-produto">
    Attach the file or the link; delivery goes out on its own when payment confirms.
  </Card>

  <Card title="Test in the sandbox" href="https://beinfi.com/en/testar-no-sandbox">
    Pay the test invoice and watch the status turn `paid`.
  </Card>

  <Card title="Know that they paid" href="https://beinfi.com/en/webhooks">
    Webhook in production, polling in sandbox.
  </Card>
</CardGroup>
