No Fianto fee during the beta. Any fee comes with at least 30 days notice. See pricing
DevelopersGuides8 min read

Add a Pay with USDC button to your website.

You need three pieces for a Pay with USDC button: a server route that creates a Fianto checkout session with your app secret, a button from @fianto/js or @fianto/react that opens that session in a popup, and a webhook route that marks the order paid when order.paid arrives. The browser never sees a key, and nothing the browser reports counts as proof of payment.

On this page
What do you need before adding a USDC checkout button?Should the button open a popup or redirect the page?How to create the checkout session on your serverHow to put the button on the pageWhat does each button result mean?How to mark the order paid from the webhookHow to test a USDC button without a test modeSourcesQuestions
TL;DR
  • Fianto has no publishable key: the button calls your own route, which creates the session server-side with Basic auth and answers { id, url }.
  • The SDK route helpers always create popup sessions; if the browser blocks the popup, the button falls back to a full-page redirect by default.
  • Mark the order paid only on a verified order.paid webhook or a server-side read showing PAID, never on the button result "succeeded".

What do you need before adding a USDC checkout button?

Before the button can take money, you need an approved merchant account and four things in your server environment:

  • An application's app id (fian_app_) and secret (fian_sk_live_), from the Developers page of the dashboard. Until your account is approved, the API answers 401.
  • Its webhook signing secret (whsec_), shown in the dashboard once the application has a webhook URL.
  • A receiving wallet that already holds a USDC token account; otherwise session creation fails with merchant_token_account_missing.
  • Node.js 20.3 or later for the SDK packages, which are at version 0.3.0 on npm as of 15 October 2026.

There is no publishable or browser key, so the amount is always decided on your server and a visitor cannot edit the price in the page to pay less. Fianto takes USDC on Solana only, and the merchant pays no fee and no network fee. If you would rather not write code at all, compare this with a dashboard-made link in payment link or checkout API.

Use the popup for a button on your own site, because the customer never leaves your page and your code learns how checkout ended. A checkout session has a ui_mode of redirect (the API default) or popup. The SDK checkout routes always create popup sessions.

PopupRedirect
How it opensA 480 x 720 window over your pageThe whole tab goes to the checkout page
After a successful paymentThe popup posts its result to your page and closesThe page stays on its receipt, "Payment received", with a "Continue to" button, named after your shop, that opens your success_url
What your page learnssucceeded, canceled, expired or closedNothing: Fianto adds no query parameters to your URLs
What it needsYour page on the same origin as success_url, and a popup opened straight from the clickNothing extra

Browsers allow a popup only right after a user action. MDN's entry on transient activation says a popup "must be triggered from a UI element's event handler." The Fianto button opens the window first and only then waits for your route, so it passes. If the popup is blocked anyway, the default fallback: 'redirect' sends the whole page to checkout instead.

How to create the checkout session on your server

The server route receives the click, decides the price from your own data, and asks Fianto for a session. With Express, checkout() from @fianto/express does the API call and answers the button with { id, url }. Install @fianto/express, @fianto/sdk and @fianto/js, and mount the route before any body parser:

server.ts
import express from 'express';
import { checkout } from '@fianto/express';

const PRICES: Record<string, { amount: string; description: string }> = {
  beans: { amount: '10.00', description: 'Coffee beans, 1 kg' },
};

const app = express();

app.post(
  '/api/checkout',
  checkout({
    createSession: async (request) => {
      const body = (await request.json().catch(() => null)) as { item?: string } | null;
      const price = body?.item ? PRICES[body.item] : undefined;
      if (price === undefined) return new Response('Unknown item', { status: 400 });

      return {
        mode: 'payment',
        order_id: `order_${crypto.randomUUID()}`, // use your own order's id
        amount: price.amount,
        description: price.description,
        success_url: 'https://shop.example/thank-you',
        cancel_url: 'https://shop.example/cart',
      };
    },
    onError: (error) => console.error('fianto checkout failed', error),
  }),
);

// The webhook route from the last step goes here too, before any body parser.
app.use(express.json()); // parsers for your other routes go after

The SDK reads FIANTO_APP_ID and FIANTO_APP_SECRET from the environment. The amount is a decimal string from 0.01 to 1,000,000 USDC with at most 6 decimals, and it is what you receive. If the same order_id already has an open session with the same terms, the route reissues that session's link and returns it; with different terms it answers 409 order_session_mismatch, so give a changed cart a fresh order id. The Next.js and Hono packages offer the same route as Checkout() and checkout().

How to put the button on the page

Plain HTML: the fianto-button element

For a page without a build step, serve dist/fianto-button.global.iife.js from the installed @fianto/js on your own site and drop in the element. On click it posts its data-* attributes as JSON to your route, so data-item="beans" arrives as { "item": "beans" }.

buy.html
<script src="/fianto-button.js"></script>

<fianto-button session-endpoint="/api/checkout" data-item="beans" label="buy"></fianto-button>
<p id="status" role="status"></p>

<script>
  var button = document.querySelector('fianto-button');
  button.addEventListener('fianto:result', function (event) {
    document.getElementById('status').textContent = event.detail.status;
  });
  button.addEventListener('fianto:error', function (event) {
    console.error(event.detail.code, event.detail.message);
  });
</script>

React: FiantoButton

app/buy-button.tsx
'use client';

import { FiantoButton, fetchCheckoutSession } from '@fianto/react';

export function BuyButton() {
  return (
    <FiantoButton
      session={() => fetchCheckoutSession('/api/checkout', { body: { item: 'beans' } })}
      label="pay"
      onResult={(result) => console.log(result.status)}
      onError={(error) => console.error(error)}
    />
  );
}

Both take a label (plain, pay, buy, checkout, subscribe or donate), a theme and a shape. If you need your own design, useCheckout from @fianto/react gives you open, status and result for any element. Every option is in the browser SDK docs.

What does each button result mean?

Each button result describes what the browser saw, not what your wallet received:

ResultMeaningWhat to show
succeededSolana confirmed the customer's transaction on the checkout page"Thanks, we are confirming your payment"
canceledThe customer cancelled on the checkout pageA way back to the cart
expiredThe session ran out of time, 30 minutes by defaultAsk them to check the order before paying again
closedUnknown: no result reached your page, and a payment may still be confirmingAsk them to check the order before paying again

Even succeeded is reported at the confirmed stage, before Fianto treats the payment as final. Your order should change only on the server, in the next step. Why the two moments differ is covered in how a merchant knows a crypto payment arrived.

The button, the hosted page and the receipt are built for you. You write the two routes.

See the developer tools

How to mark the order paid from the webhook

Mark the order paid when a verified order.paid webhook arrives, which Fianto sends once the payment is finalized on Solana. verifyWebhook checks the Standard Webhooks signature over the raw body; your route also answers the one-off endpoint.verification challenge that proves you own the URL:

server.ts
import { isWebhookVerificationError, verifyWebhook } from '@fianto/sdk/webhooks';

// Before any global express.json(): express.raw keeps req.body as the untouched bytes.
app.post('/webhooks/fianto', express.raw({ type: '*/*' }), async (req, res, next) => {
  let event;
  try {
    event = await verifyWebhook(req.body, req.headers, { secret: process.env.FIANTO_WEBHOOK_SECRET });
  } catch (error) {
    if (isWebhookVerificationError(error)) {
      res.status(400).json({ error: 'invalid_webhook' });
      return;
    }
    next(error);
    return;
  }

  switch (event.type) {
    case 'endpoint.verification':
      res.status(200).json({ challenge: event.data.challenge });
      return;
    case 'order.paid':
      // Your own code: mark event.data.order_id paid, once per event.id.
      await markOrderPaid(event.data.order_id, event.id);
      break;
    default:
      break; // answer 2xx for types you do not handle
  }
  res.status(200).json({ received: true });
});

Delivery is at least once and in no fixed order, so record event.id with the fulfilment and skip ids you have seen. Any 2xx within 10 seconds counts as delivered; otherwise Fianto tries up to 8 times over about 28 hours. If you would rather check on demand, fianto.orders.retrieveByOrderId(orderId) from @fianto/sdk reads the order by your own id; fulfil only when its status is PAID. Granting and revoking access from these events is laid out in webhooks for access.

How to test a USDC button without a test mode

Fianto has no test mode or sandbox, so you test the button in two halves. The full routine is in testing a USDC integration when there is no test mode. The webhook half runs locally with signed samples from the CLI, which makes no API call:

terminal
npx @fianto/cli trigger order.paid \
  --forward-to http://localhost:3000/webhooks/fianto \
  --secret "$FIANTO_WEBHOOK_SECRET"

The checkout half needs a real session on Solana mainnet. Create one for 0.01 USDC, the lowest price allowed, and pay it from a second wallet of your own. The test costs that wallet the Solana network fee, under $0.01, plus the price, which lands in your receiving wallet; there is no Fianto fee during the beta. After the beta the customer pays about 0.1 to 0.5% on top, and you still pay nothing. If you also want to sell without code, a USDC payment link gives you the same checkout from the dashboard.

Sources
Questions

Questions about a Pay with USDC button.

Can I add a crypto checkout button without a backend?

Not with Fianto. The session must be created with your app secret, which never goes to a browser, so you need at least one small server route; without any server, use a payment link from the dashboard instead.

Which wallets can pay through the button?

Any Solana wallet that follows the Wallet Standard and can sign a transaction without sending it. A wallet that can only sign and send in one step is refused with wallet_unsupported.

Is the success_url visit proof that the customer paid?

No. Reaching success_url or a popup result of succeeded only means the browser saw success. Fulfil from a verified order.paid webhook or a server-side read of the order.

Can the button take other tokens than USDC?

No. Fianto takes USDC on Solana only.

What does the button cost the merchant?

Nothing. The merchant never pays Fianto or a Solana network fee; during the beta there is no Fianto fee, and after it the customer pays about 0.1 to 0.5% on top.

Read next.

Payments8 min read

What a hosted USDC checkout does that a wallet address doesn't

Posting your wallet address gets you paid, but it tells you nothing else: which order the USDC was for, whether the amount was right, or when the transfer became final. A hosted USDC checkout wraps each payment in a session that carries your order id, writes the exact amount into the transaction the customer signs, expires on a timer, and tells your server only once Solana has finalized the payment. For a tip or a one-off payment from someone you know, an address is fine; for orders you fulfil automatically, use a session.

Product7 min read

Payment link or checkout API? Choosing how to take USDC

Use a payment link when one fixed price is the whole offer and you do not want to run a server; use the checkout API when the sale starts inside your own site or app and you need your order IDs, your metadata or an amount decided per order. On Fianto both end on the same hosted checkout, with the same fees and the same wallet-to-wallet payment. What differs is who creates the checkout session, which application owns the sale, and what your code can read afterwards.

Payments7 min read

How a merchant knows a crypto payment arrived: references, memos and order matching

To know a crypto payment really arrived, you need three answers: which transfer belongs to which order, whether it moved the right token and amount into your wallet, and whether the network has made it final. On Solana the first is solved with a reference key added to the transfer, the second by reading the transfer itself, and the third by waiting for the finalized commitment level. Fianto does all three and sends your server a signed order.paid webhook only once the payment is finalized.

Two routes and a button.

Create the session on the server, fulfil on order.paid.

New posts are announced on X and Discord.