Docs / Patterns / Next.js

Next.js.

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.

$ bun add @settle/sdk

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_KEY
  • SETTLE_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.

app/api/checkout/route.ts
// app/api/checkout/route.ts
// POST { amount_usd, customer_email?, metadata? } -> 303 redirect to Settle hosted checkout.
import { NextResponse } from "next/server";

export async function POST(req: Request) {
  const body = await 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: `${req.headers.get("origin")}/orders/success`,
      cancel_url: `${req.headers.get("origin")}/cart`,
    }),
  });

  if (!res.ok) {
    return NextResponse.json({ error: "checkout_failed" }, { status: 502 });
  }

  const invoice = await res.json();
  return NextResponse.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.

app/api/webhooks/settle/route.ts
// app/api/webhooks/settle/route.ts
import { NextResponse } from "next/server";
import { createHmac, timingSafeEqual } from "node:crypto";

export async function POST(req: Request) {
  const signature = req.headers.get("settle-signature");
  if (!signature) return new NextResponse("missing signature", { status: 400 });

  const raw = await req.text();
  const expected = createHmac("sha256", process.env.SETTLE_WEBHOOK_SECRET!)
    .update(raw)
    .digest("hex");

  const sigBuf = Buffer.from(signature, "hex");
  const expBuf = Buffer.from(expected, "hex");
  if (sigBuf.length !== expBuf.length || !timingSafeEqual(sigBuf, expBuf)) {
    return new NextResponse("bad signature", { status: 401 });
  }

  const event = JSON.parse(raw);
  if (event.type === "invoice.paid") {
    // Mark order paid. event.data.invoice has id, amount_usd, payment_tx, metadata.
    console.log("paid", event.data.invoice.id);
  }
  return NextResponse.json({ received: true });
}

Notes.

Place the route at app/api/checkout/route.ts and the webhook at app/api/webhooks/settle/route.ts. Use the test key (sk_test_…) until you flip the merchant to live. The webhook handler must read the raw request body before parsing — Next's App Router gives you that via req.text().

Was this helpful?