DevelopersTools

Hono

Add Fianto's checkout and webhook handlers to a Hono app on Node or Cloudflare Workers, where secrets come from c.env.

2 min read

@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

  • 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.
  • On Node: Node.js 20.3 or later and @hono/node-server to start the app.

Steps

Install the packages

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

pnpm add @fianto/hono @fianto/sdk @fianto/js

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.

// 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.

On Cloudflare Workers, pass secrets from c.env

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.

// 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

With the app running locally on port 3000, send the webhook route a signed sample. It makes no API call.

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

You seeWhyFix
Webhook route answers 400 invalid_webhook, reason invalid_secretNo 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 NodeThe 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_originThe 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.jsexport default app starts no server on Node.Start it with @hono/node-server's serve.
A GET to the route answers 404Routes registered with app.post only see POST.Nothing to fix: Fianto and the button always send POST.

See also

Was this page helpful? Tell us

On this page