# Quickstart (/get-started/quickstart)

You build three pieces: a checkout route that creates the session on your server, a button that
opens Fianto's checkout in a popup, and a webhook route that marks the order paid.

**The steps of a one-time checkout**

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 [#before-you-start]

* Your merchant account is approved.
* Your receiving wallet has a USDC token account. If it has none, send it any amount of USDC once.
* You created an application in the dashboard and copied its app id (`fian_app_…`) and secret
  (`fian_sk_live_…`). The secret is shown only once.
* You know the public `https` address your webhook route will have. The application's webhook
  signing secret (`whsec_…`) appears once the application has a webhook URL (step 6).
* Node.js 20.3 or later.

## Steps [#steps]

**Step 1.**

### Install the packages [#install-the-packages]

Pick the line for your framework.

#### pnpm

```bash
# Next.js
pnpm add @fianto/nextjs @fianto/react @fianto/sdk
# Express
pnpm add @fianto/express @fianto/js @fianto/sdk
# Hono
pnpm add @fianto/hono @fianto/js @fianto/sdk
```

#### npm

```bash
# Next.js
npm install @fianto/nextjs @fianto/react @fianto/sdk
# Express
npm install @fianto/express @fianto/js @fianto/sdk
# Hono
npm install @fianto/hono @fianto/js @fianto/sdk
```

#### yarn

```bash
# Next.js
yarn add @fianto/nextjs @fianto/react @fianto/sdk
# Express
yarn add @fianto/express @fianto/js @fianto/sdk
# Hono
yarn add @fianto/hono @fianto/js @fianto/sdk
```

#### bun

```bash
# Next.js
bun add @fianto/nextjs @fianto/react @fianto/sdk
# Express
bun add @fianto/express @fianto/js @fianto/sdk
# Hono
bun add @fianto/hono @fianto/js @fianto/sdk
```

**Step 2.**

### Set your credentials [#set-your-credentials]

Put the three values in your server's environment. The SDK reads them by these names.

```bash
FIANTO_APP_ID=fian_app_...
FIANTO_APP_SECRET=fian_sk_live_...
FIANTO_WEBHOOK_SECRET=whsec_...
```

Keep them on the server. The app secret must never reach a browser. You fill in
`FIANTO_WEBHOOK_SECRET` in step 6. `FIANTO_BASE_URL` is optional;
it defaults to `https://api.fianto.xyz`.

Next.js reads a `.env.local` file on its own. Express and Hono on Node do not read `.env`: load it
when you start the server, for example `node --env-file=.env server.js` (Node.js 20.6 or later),
or with the `dotenv` package.

**Step 3.**

### Add a checkout route [#add-a-checkout-route]

This route decides the price and creates the session. Never take a price from the request body.
The route only accepts same-origin `POST` requests, opens checkout in popup mode and answers
`{ id, url }`. If the order already has an open session with the same terms, it reissues that
session's link.

#### Next.js

```ts
// app/api/checkout/route.ts
import { Checkout } from '@fianto/nextjs';

// Prices live on your server.
const ITEMS: Record<string, { amount: string; description: string }> = {
  beans: { amount: '10.00', description: 'Coffee beans, 1 kg' },
};

export const POST = Checkout({
  createSession: async (request) => {
    const body = (await request.json().catch(() => null)) as { item?: string } | null;
    const item = body?.item ? ITEMS[body.item] : undefined;
    if (!item) return new Response('Unknown item', { status: 400 });

    return {
      mode: 'payment',
      order_id: `order_${crypto.randomUUID()}`, // use your own order's id
      amount: item.amount, // a decimal USDC string
      description: item.description, // required with amount
      success_url: 'https://shop.example/thank-you',
      cancel_url: 'https://shop.example/cart',
    };
  },
  onError: (error) => console.error(error),
});
```

#### Express

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

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

const app = express();

// Mount fianto routes BEFORE express.json(): they need the raw body.
app.post(
  '/api/checkout',
  checkout({
    createSession: async (request) => {
      const body = (await request.json().catch(() => null)) as { item?: string } | null;
      const item = body?.item ? ITEMS[body.item] : undefined;
      if (!item) return new Response('Unknown item', { status: 400 });

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

app.use(express.json()); // parsers for your other routes go after
app.listen(3000);
```

#### Hono

```ts
// src/index.ts
import { Hono } from 'hono';
import { checkout } from '@fianto/hono';

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

const app = new Hono();

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

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

export default app;
```

`export default app` only exports the app. On Node, start it with `@hono/node-server`:
`serve({ fetch: app.fetch, port: 3000 })`. Other runtimes choose their own port.

`success_url` and `cancel_url` must be `https`. Make the page at `success_url` say that the payment
is being confirmed. Reaching it does not prove the payer paid.

> **The amount is what you receive:**
>
> You receive the `amount` you set, here `'10.00'` USDC. When the platform's service fee is above zero,
> it is added on top, so the payer pays the amount plus the fee. The checkout page shows the fee on
> its own "Service fee" line.

**Step 4.**

### Add the pay button [#add-the-pay-button]

The button calls your checkout route and opens checkout in a popup. Serve the page that shows it
from the same origin as your `success_url`: the popup reports its result only to that origin.

#### Next.js

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

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

// Never tell the payer "nothing happened": 'closed' means you do not know.
const MESSAGES: Record<string, string> = {
  succeeded: 'Thanks! We are confirming your payment.',
  canceled: 'Checkout canceled.',
  expired: 'That checkout link expired. Please try again.',
  closed: 'We could not tell what happened. Check your order status before paying again.',
};

export function BuyButton() {
  const [message, setMessage] = useState<string | null>(null);
  return (
    <div>
      <FiantoButton
        session={() => fetchCheckoutSession('/api/checkout', { body: { item: 'beans' } })}
        label="buy"
        onResult={(result) => setMessage(MESSAGES[result.status] ?? null)}
        onError={(error) => console.error(error)}
      />
      {message ? <p role="status">{message}</p> : null}
    </div>
  );
}
```

#### Express

Load the button script: with a bundler, `import '@fianto/js/button'`; without one, serve the file
`dist/fianto-button.global.iife.js` from the installed `@fianto/js` package at exactly the path
your `<script>` tag uses (`/fianto-button.js` below).

```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 MESSAGES = {
    succeeded: 'Thanks! We are confirming your payment.',
    canceled: 'Checkout canceled.',
    expired: 'That checkout link expired. Please try again.',
    closed: 'We could not tell what happened. Check your order status before paying again.',
  };
  var button = document.querySelector('fianto-button');
  button.addEventListener('fianto:result', function (event) {
    document.getElementById('status').textContent = MESSAGES[event.detail.status] || '';
  });
  button.addEventListener('fianto:error', function (event) {
    console.error(event.detail.code, event.detail.message);
  });
</script>
```

The button posts its `data-*` attributes as the JSON body, so `data-item="beans"` arrives as
`{ "item": "beans" }`.

#### Hono

Load the button script: with a bundler, `import '@fianto/js/button'`; without one, serve the file
`dist/fianto-button.global.iife.js` from the installed `@fianto/js` package at exactly the path
your `<script>` tag uses (`/fianto-button.js` below).

```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 MESSAGES = {
    succeeded: 'Thanks! We are confirming your payment.',
    canceled: 'Checkout canceled.',
    expired: 'That checkout link expired. Please try again.',
    closed: 'We could not tell what happened. Check your order status before paying again.',
  };
  var button = document.querySelector('fianto-button');
  button.addEventListener('fianto:result', function (event) {
    document.getElementById('status').textContent = MESSAGES[event.detail.status] || '';
  });
  button.addEventListener('fianto:error', function (event) {
    console.error(event.detail.code, event.detail.message);
  });
</script>
```

The button posts its `data-*` attributes as the JSON body, so `data-item="beans"` arrives as
`{ "item": "beans" }`.

If your page sends a `Cross-Origin-Opener-Policy` header, set it to `same-origin-allow-popups`.

**Step 5.**

### Add the webhook route and fulfil the order [#add-the-webhook-route-and-fulfil-the-order]

Fianto sends `order.paid` when the payment is finalized. Fulfil the order there, not from the
button's result or the `success_url`. The route verifies the signature and answers Fianto's URL
verification itself.

#### Next.js

```ts
// app/api/webhooks/fianto/route.ts
import { Webhooks } from '@fianto/nextjs';

export const POST = Webhooks({
  secret: process.env.FIANTO_WEBHOOK_SECRET,
  onOrderPaid: async (event) => {
    // The same event can arrive more than once. Record event.id in the same
    // database transaction as the fulfilment, and skip ids you have already seen.
    console.log('paid', event.data.order_id, event.id);
  },
});
```

#### Express

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

const app = express();

// Before express.json(), like the checkout route.
app.post(
  '/webhooks/fianto',
  webhooks({
    secret: process.env.FIANTO_WEBHOOK_SECRET,
    onOrderPaid: async (event) => {
      // The same event can arrive more than once. Record event.id in the same
      // database transaction as the fulfilment, and skip ids you have already seen.
      console.log('paid', event.data.order_id, event.id);
    },
  }),
);

app.use(express.json());
app.listen(3000);
```

#### Hono

```ts
// src/index.ts
import { Hono } from 'hono';
import { webhooks } from '@fianto/hono';

const app = new Hono();

app.post(
  '/webhooks/fianto',
  webhooks({
    secret: process.env.FIANTO_WEBHOOK_SECRET,
    onOrderPaid: async (event) => {
      // The same event can arrive more than once. Record event.id in the same
      // database transaction as the fulfilment, and skip ids you have already seen.
      console.log('paid', event.data.order_id, event.id);
    },
  }),
);

export default app;
```

In Express and Hono, add this route to the same app as the checkout route.

**Step 6.**

### Register the webhook URL [#register-the-webhook-url]

1. In the dashboard, set your application's webhook URL to the route's public `https` address.
2. Open "Reveal signing secret" in the same settings, put the `whsec_…` value in
   `FIANTO_WEBHOOK_SECRET` and restart your server.
3. Fianto sends the URL a signed verification challenge, and your route answers it once it has the
   right secret. If the first try failed because the secret was missing, click "Verify again".

> **Verify the URL before you take payments:**
>
> Events published before your URL is first verified are stored but never delivered. You can still
> read them with `GET /v1/events`. (A URL that was verified and is later suspended holds its events
> instead, and gets them once it passes verification again.)

## Check it worked [#check-it-worked]

> With your server running locally, send it a signed sample `order.paid`. This makes no API call.
>
> ```bash
> npx @fianto/cli trigger order.paid \
>   --forward-to http://localhost:3000/api/webhooks/fianto \
>   --secret "$FIANTO_WEBHOOK_SECRET"
> ```
>
> `$FIANTO_WEBHOOK_SECRET` must be exported in this shell; a `.env` file is not enough. Export it,
> or paste the `whsec_…` value in its place.
>
> The CLI prints `→ 200 order.paid (local sample)` and your `onOrderPaid` logs the order. For
> Express and Hono, forward to `http://localhost:3000/webhooks/fianto`. If your server listens on
> another port, use that port.

## Troubleshooting [#troubleshooting]

| You see | Why | Fix |
|---|---|---|
| Express answers 500 `BodyAlreadyParsedError` | A body parser such as `express.json()` ran before the Fianto route. | Mount the Fianto routes before any global JSON parser, or scope the parser away from them, such as `app.use('/app', express.json())`. Adding `express.raw` to the route does not help once the body is read. |
| Webhook route answers 400 `invalid_webhook` | The secret is wrong, something re-encoded the body before the route, or your clock is more than 300 s off. | Use the application's `whsec_` secret, remove body-reading middleware or proxy rewrites, and sync your clock. |
| The button reports `closed` while the payer is still paying | Your page sends `Cross-Origin-Opener-Policy: same-origin`, which cuts the popup off. | Send `Cross-Origin-Opener-Policy: same-origin-allow-popups` on the page with the button. |
| The popup is blocked | Checkout was opened after an `await` or a `setTimeout`, not directly in the click. | Open checkout synchronously in the click handler. The button already does. |
| Creating the session fails with `url_insecure` | `success_url` or `cancel_url` is `http`. | Use `https` URLs. |
| No result reaches your page | The popup posts its result only to the `success_url` origin. | Serve the page with the button from the same origin as `success_url`. |
| Checkout route answers 403 `forbidden_origin` | Behind a proxy the route sees a different origin than the browser. | Pass `allowedOrigins`, or set Express `trust proxy`. |
| Checkout route answers 500 `internal_error` | Fianto refused for a reason the route does not pass to the browser: wrong credentials, an account not approved, or `merchant_token_account_missing`. | Read the error your `onError` logs. |

## See also [#see-also]

- [How money moves](/get-started/how-money-moves): What the payer signs, and confirmed vs finalized.

- [Subscriptions quickstart](/get-started/subscriptions-quickstart): Sell a recurring price.

- [Developers](/developers): The v1 API, webhooks, SDKs and CLI in depth.