Skip to main content
There are three ways to charge for a product, and they solve the same problem with very different amounts of work on your side. Choose first, implement after.

Which path is yours

All three end at the same invoice and the same webhook. The difference is how much of the experience is yours.
In doubt, start with the link. It is the only one that asks nothing of your app. You switch later — the product and the catalog are the same.

The middle path, end to end

Assuming you already have a published product (see catalog) and a key:
You have to map invoiceId → your user. We do not know who your user is — you pass an externalId and we return an invoiceId. When the payment confirms, the webhook carries the invoiceId, not your user. If you did not store the pair, you received money and do not know whose it is.It is the most important architectural decision on this page and it fits in one column of one table in your database.

Charging, and showing the pix on your own screen

Today only pix has an artifact for your screen. boleto and card return only invoiceUrl — the provider’s hosted page. There is no digitable line or barcode field in the response, and clientSecret/publishableKey (card confirmed in the browser) only appear where card is enabled on the tenant.Since the payer should not go to the provider’s site, today’s path for boleto and card is the payment link or the url that checkout() returns — both are our checkout, carrying your merchant name. Check cardEnabled on the link’s public read before offering card.

Knowing that they paid

Do not trust what charge returns: it comes back pending. Payment is asynchronous.
When it confirms, use the event’s invoiceId to find the order you stored in step 2. Details in webhooks.

The buyer clicked “Buy” twice

Every non-GET method on the authenticated API requires an Idempotency-Key (the public /pay/* routes, which the buyer’s browser calls, do not). This is not bureaucracy: it is what keeps two clicks from becoming two invoices.
From @beinfi/sdk@0.10.2 on, both accept idempotencyKey (and since 0.10.4 checkout() returns invoiceId already typed as string, with no ! needed). The resource methods (products.create, invoices.create, coupons.create, …) already took the key as their last argument.
  • Same key, same body → you get the original response back. One invoice.
  • Same key, different body → 409 idempotency_key_reused. That is protection: it means you reused the key for something else.
  • No key → 400 idempotency_key_required.
The SDK generates one when you do not pass it — which protects against a network retry, not against a double click, because each new call gets a new key. For the double click, the key has to come from something stable in your intent, as above.
The simplest fix is not letting them click twice. Disable the button on the first click and treat the Idempotency-Key as the safety net, not as the first line of defence.

Before selling: the name the buyer sees

Your tenant is born with a placeholder name. If you do not change it, the checkout and the payment link literally say “New app” — and nobody buys from a store called New app.
It takes effect immediately, with no republishing and no new link — the same link starts showing the new name. infi.account.get() reads it back. (From @beinfi/sdk@0.10.7; before that it is PATCH /account/tenant.)

You sold. Now deliver

If what you sell is a file or an access, do not build that by hand: attach the deliverable to the product and Infi sends the buyer their personal link when the payment confirms — and returns the same link to you, to show on your thanks page. It is in delivering the product. The buyer’s side — finding out they paid, delivery, and the two polls nobody guesses — is in the thank-you page.