# Express (/developers/sdks/express)

`@fianto/express` turns the SDK's two route handlers into Express middleware: `checkout()` creates a
checkout session for the pay button, and `webhooks()` verifies Fianto's webhook requests. Both need
the request body exactly as it arrived, which decides where you mount them.

## Before you start [#before-you-start]

* An Express app. The peer dependency is `express` 4 or later.
* Your application's app id and secret, and its webhook signing secret (`whsec_…`) once it has a
  webhook URL. See [Authentication](/developers/authentication).
* Node.js 20.3 or later.

## Steps [#steps]

**Step 1.**

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

`@fianto/js` is for the pay button on your pages.

#### pnpm

```bash
pnpm add @fianto/express @fianto/sdk @fianto/js
```

#### npm

```bash
npm install @fianto/express @fianto/sdk @fianto/js
```

#### yarn

```bash
yarn add @fianto/express @fianto/sdk @fianto/js
```

#### bun

```bash
bun add @fianto/express @fianto/sdk @fianto/js
```

**Step 2.**

### Load your credentials [#load-your-credentials]

The SDK reads `FIANTO_APP_ID`, `FIANTO_APP_SECRET` and `FIANTO_WEBHOOK_SECRET` from the process
environment. Node does not read `.env` by itself: start the server with
`node --env-file=.env server.js` (Node.js 20.6 or later), or load the file with the `dotenv` package.

**Step 3.**

### Mount the Fianto routes before any body parser [#mount-the-fianto-routes-before-any-body-parser]

Register both routes before `express.json()`, `express.urlencoded()` or any other middleware that
reads the body. `createSession` gets the fetch `Request` and Express's own `{ req, res }`, so
`req.user` and anything else earlier middleware set is there. Read from `res`, never send through it:
the middleware sends its own response.

```ts
// server.ts
import express from 'express';
import { checkout, webhooks } 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, { req }) => {
      console.log('checkout from', req.ip);
      const body = (await request.json().catch(() => null)) as { item?: string } | null;
      const price = body?.item ? PRICES[body.item] : undefined;
      if (!price) 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),
  }),
);

app.post(
  '/webhooks/fianto',
  webhooks({
    secret: process.env.FIANTO_WEBHOOK_SECRET,
    onOrderPaid: async (event) => {
      // Record event.id in the same database transaction as the fulfilment.
      console.log('paid', event.data.order_id, event.id);
    },
    onVerificationError: (error) => console.warn('fianto webhook rejected:', error.reason),
  }),
);

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

> **express.raw does not rescue a route behind a global parser:**
>
> If `app.use(express.json())` runs before the Fianto route, the body is already read, and adding
> `express.raw(...)` to the route does not bring it back. Move the Fianto routes first, or scope the
> parser to paths that are not Fianto's, such as `app.use('/app', express.json())`.

When the body was already read, the middleware passes a `BodyAlreadyParsedError` to `next(error)`,
so your error handler (Express's default answers 500) receives it.

> **The payer pays the amount plus the service fee:**
>
> `amount` is what you receive. When the service fee is above zero, Fianto adds it on top and the
> payer pays it.

**Step 4.**

### Know the body cap [#know-the-body-cap]

`checkout()` reads at most 1 MiB of body. `webhooks()` reads at most its `maxBodyBytes`, 1 MiB by
default. A larger body is answered 413 `{"error":"payload_too_large"}` by the middleware itself,
before the webhook handler runs, so `onVerificationError` is not called for it.

**Step 5.**

### Set up for a proxy [#set-up-for-a-proxy]

When the browser does not send `Sec-Fetch-Site: same-origin`, the checkout route compares its
`Origin` with the request URL, which the middleware builds from `req.protocol` and the `Host` header.
Behind a proxy that ends TLS, Express sees `http://`, and the `Host` header may be an internal name,
so the route answers 403 `forbidden_origin`. Setting Express's `trust proxy` fixes the protocol;
`allowedOrigins` with your public origins fixes both:

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

const app = express();
app.set('trust proxy', 1); // match your own proxy setup

app.post(
  '/api/checkout',
  checkout({
    allowedOrigins: ['https://shop.example'],
    createSession: async () => ({
      mode: 'payment',
      order_id: `order_${crypto.randomUUID()}`,
      amount: '10.00',
      description: 'Coffee beans, 1 kg',
      success_url: 'https://shop.example/thank-you',
      cancel_url: 'https://shop.example/cart',
    }),
  }),
);
```

The proxy must pass the webhook body through unchanged: a proxy that decompresses or re-encodes it
breaks the signature.

**Step 6.**

### Serve the pay button [#serve-the-pay-button]

Your pages load `<fianto-button>` from the installed `@fianto/js`. See
[Browser](/developers/sdks/js) for the script and the events.

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

> With the server running on port 3000, send the webhook route a signed sample. It makes no API call.
>
> ```bash
> npx @fianto/cli trigger order.paid \
>   --forward-to http://localhost:3000/webhooks/fianto \
>   --secret "$FIANTO_WEBHOOK_SECRET"
> ```
>
> `$FIANTO_WEBHOOK_SECRET` must be exported in this shell; a `.env` file is not enough. The CLI prints
> `→ 200 order.paid (local sample)` and your `onOrderPaid` logs the sample order.

## Troubleshooting [#troubleshooting]

| You see | Why | Fix |
|---|---|---|
| 500 with `BodyAlreadyParsedError` | A body parser read the body before the Fianto route. | Mount the Fianto routes before any global parser, or scope the parser away from them. `express.raw` on the route does not help once the body is read. |
| 413 `{"error":"payload_too_large"}` | The body is over 1 MiB, or over the webhook route's `maxBodyBytes`. | Fianto's own requests are far smaller; check what is calling the route. |
| Checkout route answers 403 `forbidden_origin` behind a proxy | Express sees `http://` or an internal host, so the request URL's origin does not match the browser's. | Set `trust proxy`, or pass `allowedOrigins: ['https://your.shop']`. |
| Checkout route answers 500 `internal_error` | Fianto refused for a reason the route does not pass to the browser, such as wrong credentials. | Read the original error in `onError`. |
| Webhook route answers 400 `invalid_webhook` though mounted first | The secret is wrong, a proxy re-encoded the body, or your clock is more than 300 s off. | Check what `onVerificationError` logs. |
| A `GET` to the route answers 404 | Routes registered with `app.post` only see `POST`. | Nothing to fix: Fianto and the button always send `POST`. |

## See also [#see-also]

- [Quickstart](/get-started/quickstart): The whole one-time payment flow in Express.

- [Verify webhooks](/developers/webhooks/verify): Every webhook option, and verifyWebhook by hand.

- [Browser](/developers/sdks/js): openCheckout and the fianto-button element.

- [Examples](/developers/examples): A runnable Express server.