DevelopersAccept payments
Checkout sessions
Create a one-time USDC checkout session from your server, send the payer to it, and reissue or cancel it.
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.
Scroll sideways to see the whole diagram →
Show as text
- 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.
- Send the payer to that `url`: Fianto's hosted checkout page.
- The payer's wallet signs the transaction. The wallet only signs; it does not send.
- The checkout page hands the signed transaction to Fianto.
- Fianto broadcasts it to Solana, and rebroadcasts it when needed.
- When the transaction is confirmed, Fianto tells the checkout page, which shows the payer success. The order is not `PAID` yet.
- 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.amountwithdescription: a decimal USDC string from0.01to1000000, with at most 6 decimals, such as"12.50".descriptionis required withamount(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:
| Field | Rules |
|---|---|
customer_email, customer_reference | Your own details about the payer |
ui_mode | redirect (the default) or popup |
expires_at | From 5 minutes to 24 hours from now. Default: 30 minutes |
metadata | Up to 20 keys; key up to 40 characters, value up to 500 |
line_items | Display 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 tourl. After a success, checkout moves the payer tosuccess_urlafter about 3 seconds. A cancel goes tocancel_url. An expired session stays on the checkout page. Fianto adds no query parameters to either URL. - Popup (
ui_mode: popup): openurlin a popup with@fianto/jsor@fianto/react. When the payer succeeds, cancels or the session expires, the popup posts{ type: 'fianto.checkout', session_id, status }to the origin of yoursuccess_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
| Status | Meaning | What you can do |
|---|---|---|
OPEN | Waiting for the payer. The link works until expires_at. | Reissue the link, or cancel. |
COMPLETED | The payment finalized. The order is PAID. | Fulfil the order (from order.paid). |
EXPIRED | Nobody paid before expires_at. | Create a new session with the same order_id to try again. |
CANCELED | You 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 see | Why | Fix |
|---|---|---|
400 amount_or_price_required | You sent neither price_id nor amount, both, or amount without description. | Send price_id alone, or amount with description. |
400 amount_out_of_range | The 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_insecure | success_url or cancel_url is http. | Use https URLs. |
400 expires_at_out_of_range | expires_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_mismatch | The 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_paid | This order_id is already paid. | Use a new order_id to charge again. |
422 merchant_token_account_missing | Your receiving wallet has no USDC token account. | Send the wallet any amount of USDC once. |
url is null | The 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_reissuable | The 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_progress | A 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_cancelable | The session is no longer open. | Read the session to see its status. |
The checkout page refuses with build_limit_reached or too_close_to_expiry | A 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_unavailable | Fianto 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