DevelopersAccept payments

Checkout sessions

Create a one-time USDC checkout session from your server, send the payer to it, and reissue or cancel it.

4 min read

A checkout session is one payment link for one of your orders. Your server creates it with POST /v1/checkout-sessions, the payer pays on Fianto's hosted checkout page, and Fianto tells your server when the payment is final. This guide covers one-time payments (mode: payment); for recurring prices see Subscriptions.

The steps of a one-time checkout
The steps of a one-time checkout, from your server to Solana and backYour serverFianto APICheckout pagePayer's walletSolanacreate session1send payer to url2payer signs3signed transaction4broadcast5confirmedpayer sees success6finalizedorder PAIDwebhooks7

Scroll sideways to see the whole diagram →

Show as text
  1. From your server, create a session with `POST /v1/checkout-sessions`. The first create returns its `url`; creating again for the same open order returns `url: null`, so reissue the link instead.
  2. Send the payer to that `url`: Fianto's hosted checkout page.
  3. The payer's wallet signs the transaction. The wallet only signs; it does not send.
  4. The checkout page hands the signed transaction to Fianto.
  5. Fianto broadcasts it to Solana, and rebroadcasts it when needed.
  6. When the transaction is confirmed, Fianto tells the checkout page, which shows the payer success. The order is not `PAID` yet.
  7. When it is finalized, the order becomes `PAID`, the payment `SUCCEEDED`, and Fianto sends `order.paid` and `checkout.session.completed` to your webhook endpoint — only at finalized.

Before you start

  • Your merchant account is approved.
  • You have an application's app id and secret. See Authentication.
  • Your receiving wallet has a USDC token account. If it has none, send it any amount of USDC once, or creating a session fails with merchant_token_account_missing.
  • You have a webhook route for order.paid. See Webhooks.

Steps

Decide what the payer pays

Send exactly one of these:

  • price_id: an active one-time price on an active product, created in the dashboard. The price sets the amount.
  • amount with description: a decimal USDC string from 0.01 to 1000000, with at most 6 decimals, such as "12.50". description is required with amount (up to 1,000 characters).

line_items (up to 50, each with a name, a quantity and an amount) are shown to the payer only. Fianto never adds them up or checks them against the amount, so they do not set what the payer pays.

The service fee is added on top

When the platform's service fee is above zero, it is added on top of your amount and paid by the payer, in the same transaction, to Fianto's treasury. The fee is rounded down in the payer's favour and fixed on the session when you create it.

Create the session

mode, order_id (your own id, up to 128 characters), success_url and cancel_url are required. The URLs must be https, up to 2,048 characters. Unknown fields are refused with 400 validation_failed.

curl https://api.fianto.xyz/v1/checkout-sessions \
  --user "$FIANTO_APP_ID:$FIANTO_APP_SECRET" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: checkout-order_1001-1" \
  -d '{
    "mode": "payment",
    "order_id": "order_1001",
    "amount": "12.50",
    "description": "Coffee beans, 1 kg",
    "success_url": "https://shop.example/thank-you",
    "cancel_url": "https://shop.example/cart"
  }'

Optional fields:

FieldRules
customer_email, customer_referenceYour own details about the payer
ui_moderedirect (the default) or popup
expires_atFrom 5 minutes to 24 hours from now. Default: 30 minutes
metadataUp to 20 keys; key up to 40 characters, value up to 500
line_itemsDisplay only, see the previous step

The url comes back only once

The response carries url only when you create a session or reissue its link. Every other read returns url: null. Creating again with the same order_id while its session is still open returns that same session with url: null. To get a working link for it, reissue the link (step 5).

Send the payer to checkout

  • Redirect (ui_mode: redirect): send the browser to url. After a success, checkout moves the payer to success_url after about 3 seconds. A cancel goes to cancel_url. An expired session stays on the checkout page. Fianto adds no query parameters to either URL.
  • Popup (ui_mode: popup): open url in a popup with @fianto/js or @fianto/react. When the payer succeeds, cancels or the session expires, the popup posts { type: 'fianto.checkout', session_id, status } to the origin of your success_url, then closes. Serve the page that opens checkout from that origin.

The SDK's checkout route helpers always create popup sessions. See the quickstart.

Fulfil the order from Fianto, not from the browser

The checkout page shows the payer a success message once the payment is confirmed. Fianto marks the order PAID and the payment SUCCEEDED, and sends order.paid and checkout.session.completed, only once it is finalized.

success_url is not proof of payment

Reaching success_url, or a popup reporting succeeded, only means the payer's browser believes checkout finished. Fulfil the order from the order.paid webhook, or from a server-side read that shows the order PAID.

Reissue or cancel the session when you need to

Reissue the link with POST /v1/checkout-sessions/{id}/link (reissueLink in the SDK). It returns a new url and the old link stops working. Fianto refuses when the session is not open or has less than 120 seconds left (session_not_reissuable), when a payment is in progress or the last transaction prepared for the payer could still land (payment_in_progress), or when Fianto could not check Solana in time (checkout_unavailable, with Retry-After).

Cancel with POST /v1/checkout-sessions/{id}/cancel. Only an open session with no payment in progress can be cancelled. Fianto sends checkout.session.canceled; there is no order event, and the order stays PENDING. The payer can also cancel on the checkout page, with the same result.

import { Fianto } from '@fianto/sdk';

const fianto = new Fianto();

export async function newLink(sessionId: string) {
  const session = await fianto.checkoutSessions.reissueLink(sessionId);
  return session.url; // the old link no longer works
}

export async function cancelCheckout(sessionId: string) {
  await fianto.checkoutSessions.cancel(sessionId);
}

Both are POSTs, so the curl form needs an Idempotency-Key too.

Session statuses

StatusMeaningWhat you can do
OPENWaiting for the payer. The link works until expires_at.Reissue the link, or cancel.
COMPLETEDThe payment finalized. The order is PAID.Fulfil the order (from order.paid).
EXPIREDNobody paid before expires_at.Create a new session with the same order_id to try again.
CANCELEDYou or the payer cancelled it. The order stays PENDING.Create a new session with the same order_id to try again.

Fianto checks for expired sessions every 30 seconds, and a payment in flight keeps a session from expiring. When that check expires a session, Fianto sends checkout.session.expired and order.expired. When a new create for the same order_id replaces a lapsed session, that session still sends checkout.session.expired, but there is no order.expired: the order is reopened for the new session.

EXPIRED and CANCELED are not final for the order. If a payment for that session still finalizes, and you have not opened a newer session for the order, the order becomes PAID and Fianto sends order.paid (without checkout.session.completed). Keep your webhook route listening.

Check it worked

Read the session back from your server:

curl https://api.fianto.xyz/v1/checkout-sessions/SESSION_ID \
  --user "$FIANTO_APP_ID:$FIANTO_APP_SECRET"

Right after creating it, status is OPEN and url is null. After a payer pays and the payment finalizes, status is COMPLETED, order.status is PAID, and your webhook route received order.paid.

Troubleshooting

You seeWhyFix
400 amount_or_price_requiredYou sent neither price_id nor amount, both, or amount without description.Send price_id alone, or amount with description.
400 amount_out_of_rangeThe amount is below 0.01 or above 1,000,000 USDC.Send an amount in range, as a decimal string.
400 validation_failed with field: "amount"amount is not a decimal string with at most 6 decimal places (for example a JSON number, or "10.1234567").Send the amount as a string such as "10.5".
400 url_insecuresuccess_url or cancel_url is http.Use https URLs.
400 expires_at_out_of_rangeexpires_at is less than 5 minutes or more than 24 hours away.Pick a time in that window, or leave it out for 30 minutes.
price_not_found, price_archived, product_archived or price_type_mismatchThe price_id does not name an active one-time price on an active product of yours.Copy the price id from the dashboard, and check it is one-time and not archived.
409 order_already_paidThis order_id is already paid.Use a new order_id to charge again.
422 merchant_token_account_missingYour receiving wallet has no USDC token account.Send the wallet any amount of USDC once.
url is nullThe order already has an open session, or you read the session instead of creating it.Reissue the link with POST /v1/checkout-sessions/{id}/link.
409 session_not_reissuableThe session is not open, or has less than 120 seconds left.If it is still open, cancel it first. Then create a new session with the same order_id.
409 payment_in_progressA payment for this session is being processed, or (on a reissue) the last transaction prepared for the payer could still land.Wait for it to finish; do not create a second payment for the order.
409 session_not_cancelableThe session is no longer open.Read the session to see its status.
The checkout page refuses with build_limit_reached or too_close_to_expiryA session allows 10 successful builds and 30 build attempts, and no build in its last 120 seconds.Cancel the session, then create a new one with the same order_id.
409 checkout_unavailableFianto could not reach or check Solana in time (for example while checking your receiving wallet, or before a reissue).Retry after Retry-After (5 seconds). The SDK retries it for you.

See also

Was this page helpful? Tell us

On this page