DevelopersTools
Hono
Add Fianto's checkout and webhook handlers to a Hono app on Node or Cloudflare Workers, where secrets come from c.env.
@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
hono4 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-serverto start the app.
Steps
Install the packages
@fianto/js is for the pay button on your pages.
pnpm add @fianto/hono @fianto/sdk @fianto/jsAdd 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 NodeInstall @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 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
Was this page helpful? Tell us