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:
| Source | When |
|---|---|
productId from provisioning | Comes 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.