Docs / Patterns / Hono
Hono.
Server route that creates an invoice and redirects to hosted checkout, and a webhook handler that verifies the signature and reacts to invoice.paid. Both files are paste-and-go.
Install.
Env vars.
Set these on your server. SETTLE_API_KEY is your sk_test_… or sk_live_… key from the dashboard. SETTLE_WEBHOOK_SECRET is returned by register_webhook when you set up the endpoint.
SETTLE_API_KEYSETTLE_WEBHOOK_SECRET
Server route.
Receives a POST from your frontend with the order amount and customer email, creates an invoice via the REST API, and 303-redirects the customer to the hosted checkout URL.
// src/checkout.ts
import { Hono } from "hono";
export const checkout = new Hono();
checkout.post("/api/checkout", async (c) => {
const body = await c.req.json();
const res = await fetch("https://api.settle.xxx/v1/invoices", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.SETTLE_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
amount_usd: body.amount_usd,
customer_email: body.customer_email,
metadata: body.metadata,
return_url: `${new URL(c.req.url).origin}/orders/success`,
cancel_url: `${new URL(c.req.url).origin}/cart`,
}),
});
if (!res.ok) return c.json({ error: "checkout_failed" }, 502);
const invoice = await res.json();
return c.redirect(invoice.checkout_url, 303);
});
Webhook handler.
Verifies the HMAC-SHA256 signature on the raw request body, then handles invoice.paid. Read Webhooks for the event list and retry semantics.
// src/webhooks.ts
import { Hono } from "hono";
import { createHmac, timingSafeEqual } from "node:crypto";
export const webhooks = new Hono();
webhooks.post("/webhooks/settle", async (c) => {
const signature = c.req.header("settle-signature");
if (!signature) return c.text("missing signature", 400);
const raw = await c.req.text();
const expected = createHmac("sha256", process.env.SETTLE_WEBHOOK_SECRET!)
.update(raw)
.digest("hex");
const a = Buffer.from(signature, "hex");
const b = Buffer.from(expected, "hex");
if (a.length !== b.length || !timingSafeEqual(a, b)) {
return c.text("bad signature", 401);
}
const event = JSON.parse(raw);
if (event.type === "invoice.paid") {
// Mark order paid.
}
return c.json({ received: true });
});
Notes.
Mount both routers on your Hono app: app.route('/', checkout); app.route('/', webhooks). Hono gives you raw body via c.req.text() — use that for signature verification, not c.req.json().