Skip to content

Get started

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:

SourceWhen
productId from provisioningComes in the JSON from POST /public/v1/claimables — 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

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

No published version, 422

Skipping step 3 gives you this:

{"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/[email protected] 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.

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:

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

const { invoice, url } = await infi.checkout({
  productId: product.id,
  customer: { externalId: yourUserId, email: "[email protected]" },
  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).

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/[email protected] on, checkout() forwards it:

const { invoice, url } = await infi.checkout({
  slug: "your-tenant",
  productId: product.id,
  customer: { externalId: yourUserId, email: "[email protected]", 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_).

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.

Next step