Docs / Patterns / Express
Express.
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.
// routes/checkout.js
import express from "express";
export const checkout = express.Router();
checkout.post("/api/checkout", express.json(), async (req, res) => {
const r = 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: req.body.amount_usd,
customer_email: req.body.customer_email,
metadata: req.body.metadata,
return_url: `${req.headers.origin}/orders/success`,
cancel_url: `${req.headers.origin}/cart`,
}),
});
if (!r.ok) return res.status(502).json({ error: "checkout_failed" });
const invoice = await r.json();
res.redirect(303, invoice.checkout_url);
});
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.
// routes/webhooks.js
// IMPORTANT: mount express.raw() before express.json() for this path so the body
// bytes match what was signed.
import express from "express";
import { createHmac, timingSafeEqual } from "node:crypto";
export const webhooks = express.Router();
webhooks.post(
"/webhooks/settle",
express.raw({ type: "application/json" }),
(req, res) => {
const signature = req.header("settle-signature");
if (!signature) return res.status(400).send("missing signature");
const expected = createHmac("sha256", process.env.SETTLE_WEBHOOK_SECRET)
.update(req.body)
.digest("hex");
const a = Buffer.from(signature, "hex");
const b = Buffer.from(expected, "hex");
if (a.length !== b.length || !timingSafeEqual(a, b)) {
return res.status(401).send("bad signature");
}
const event = JSON.parse(req.body.toString("utf8"));
if (event.type === "invoice.paid") {
// Mark order paid.
}
res.json({ received: true });
}
);
Notes.
Critical: mount express.raw() on the webhook path BEFORE any global express.json() middleware. If you let express.json() consume the body first, the bytes you HMAC will not match what Settle signed and every webhook will 401.