# Hono (/developers/sdks/hono)

`@fianto/hono` gives you `checkout()` and `webhooks()` as Hono handlers. They pass Hono's untouched
`c.req.raw` request to the SDK's handlers, so the same code is designed to run on Node, Cloudflare
Workers, Bun and Deno. The difference between runtimes is where your secrets come from.

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

* A Hono app. The peer dependency is `hono` 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).
* On Node: Node.js 20.3 or later and `@hono/node-server` to start the app.

## 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/hono @fianto/sdk @fianto/js
```

#### npm

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

#### yarn

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

#### bun

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

**Step 2.**

### Add both routes on Node [#add-both-routes-on-node]

On Node, the handlers read `FIANTO_APP_ID`, `FIANTO_APP_SECRET` and `FIANTO_WEBHOOK_SECRET` from
`process.env`. Load your `.env` when you start the server, for example with
`node --env-file=.env` (Node.js 20.6 or later) or the `dotenv` package.

```ts
// src/index.ts
import { serve } from '@hono/node-server';
import { Hono } from 'hono';
import { checkout, webhooks } from '@fianto/hono';

const app = new Hono();

app.post(
  '/api/checkout',
  checkout({
    createSession: async (request, c) => {
      console.log('checkout at', c.req.path); // c is Hono's Context
      return {
        mode: 'payment',
        order_id: `order_${crypto.randomUUID()}`, // use your own order's id
        amount: '10.00', // decided on your server, never taken from the request
        description: 'Coffee beans, 1 kg',
        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),
  }),
);

serve({ fetch: app.fetch, port: 3000 }); // @hono/node-server starts the server on Node
```

Install `@hono/node-server` next to `hono` for this. No `app.use(...)` before these routes may read
or replace the request body.

> **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 3.**

### On Cloudflare Workers, pass secrets from `c.env` [#on-cloudflare-workers-pass-secrets-from-cenv]

Workers has no `process.env`. Store the three values below as Worker secrets or variables, and build the
handlers inside the route, where `c.env` exists. Do not create them at module level: the secrets are
not there yet.

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

// Type the bindings so c.env.X resolves.
interface Bindings {
  FIANTO_APP_ID: string;
  FIANTO_APP_SECRET: string;
  FIANTO_WEBHOOK_SECRET: string;
}

const app = new Hono<{ Bindings: Bindings }>();

app.post('/api/checkout', (c) =>
  checkout({
    fianto: new Fianto({ appId: c.env.FIANTO_APP_ID, appSecret: c.env.FIANTO_APP_SECRET }),
    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',
    }),
    onError: (error) => console.error('fianto checkout failed', error),
  })(c),
);

app.post('/webhooks/fianto', (c) =>
  webhooks({
    secret: c.env.FIANTO_WEBHOOK_SECRET,
    onOrderPaid: async (event) => {
      console.log('paid', event.data.order_id, event.id);
    },
  })(c),
);

export default app;
```

For a self-hosted or local Fianto backend, also pass `baseUrl` to `new Fianto(...)`.

> **Designed for Workers, tested on Node:**
>
> The handlers use only `fetch`, `Request`, `Response` and Web Crypto, so they need no Node
> polyfills. CI tests the SDK on Node.js 22 and 24 and smoke-tests the built `@fianto/sdk` on Node.js
> 20.3.0; Workers, Bun and Deno are not tested yet.

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

> With the app running locally 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. The CLI prints
> `→ 200 order.paid (local sample)` and your `onOrderPaid` logs the sample order. If your dev server
> listens on another port, use that port.

## Troubleshooting [#troubleshooting]

| You see | Why | Fix |
|---|---|---|
| Webhook route answers 400 `invalid_webhook`, `reason` `invalid_secret` | No secret reached the handler: on Workers `process.env` does not exist, so the default finds nothing. | Pass `secret: c.env.FIANTO_WEBHOOK_SECRET`, built inside the route. |
| Checkout route answers 500 and `onError` logs `Missing appId: pass it to new Fianto({ appId }) or set FIANTO_APP_ID.` | The default client reads `process.env`, which Workers lacks. | Pass `fianto: new Fianto({ appId: c.env.FIANTO_APP_ID, appSecret: c.env.FIANTO_APP_SECRET })`. |
| Webhook route answers 400 `invalid_webhook` on Node | The secret is wrong, earlier middleware read the body, or your clock is more than 300 s off. | Check what `onVerificationError` logs, and keep body-reading middleware off this route. |
| Checkout route answers 403 `forbidden_origin` | The request did not carry `Sec-Fetch-Site: same-origin`, and its `Origin` is not in `allowedOrigins`. | Call it from your own pages, or pass `allowedOrigins`. |
| Nothing listens after `node src/index.js` | `export default app` starts no server on Node. | Start it with `@hono/node-server`'s `serve`. |
| 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 Hono.

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

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

- [@fianto/sdk](/developers/sdks/sdk): Client and handler options in full.