Skip to content

Next.js · public alpha

Sell from a server action.

One server action creates the order and sends the buyer to a hosted checkout. Flint computes the totals and tax, takes the payment, emails the receipt, keeps the order history, and refunds by line item. Your app keeps its pages and its own data, and your Prisma schema never gets an Order model.

3.79% + 35¢ on cards · no monthly fee · free sandbox · no credit card

app/checkout/actions.ts
"use server";

import { Client } from "@flintpay/node";
import { redirect } from "next/navigation";

const flint = new Client({
  baseUrl: "https://api.withflintpay.com",
  apiKey: process.env.FLINT_API_KEY!,
});

export async function buy() {
  const order = await flint.orders.create({
    line_items: [
      { name: "Cold brew", quantity: "2",
        unit_price_money: { amount: "650", currency: "USD" } },
      { name: "Croissant", quantity: "1",
        unit_price_money: { amount: "495", currency: "USD" } },
    ],
  });

  const launch = await flint.checkoutSessions.create({
    order_id: order.order_id,
    redirects: {
      success_redirect_url: "https://example.com/thanks",
      cancel_redirect_url: "https://example.com/menu",
    },
  });

  redirect(launch.checkout_session.url!);
}

The whole purchase path. Line items go in, a checkout URL comes out, and the buyer is redirected. No total is sent, because Flint computes it.

Where redirect() lands: the same two cold brews and a croissant, tax added, wallets and cards ready. You wrote none of this screen.

two calls to a paid order · zero money tables in your schema · 574 operations when you need more

01The repo

On a payments API, your repo grows a store.

A payments API takes the payment and records an amount. What was sold, the tax on it, the refund, and the customer's history turn into files in your app, and a second copy of every sale that you keep in sync. On Flint they are fields on the order, and those files never exist.

Your repo on a payments API

a copy of every sale, in your databaseyours to keep in sync
  1. Orders

    Order, OrderItem, and Refund models

    prisma/schema.prisma, plus every migration after it

  2. Totals

    Subtotal, discount, and tax math

    lib/cart.ts, and a test for every rounding case

  3. Payment state

    A webhook that copies the sale into your tables

    app/api/stripe-webhook/route.ts: checkout.session.completed to rows

  4. Refunds

    Partial refund proration, tax share included

    lib/refunds.ts

  5. Order history

    An orders page, and the auth in front of it

    app/account/orders/page.tsx

  6. Back office

    An admin area to look up and refund sales

    app/admin/**

6 things to build, test, and migrate · 1 database to reconcile with the processor

The same store on Flint

ord_1kmn0aExampleGET /v1/orders/{order_id}
  1. Orders

    order.line_itemsGET /v1/orders/{order_id}
  2. Totals

    pricing_amountsPOST /v1/orders
  3. Payment state

    payment_statusorder.paid
  4. Refunds

    line_item_allocationsPOST /v1/refunds
  5. Order history

    customer_idGET /v1/orders
  6. Back office

    the dashboardapp.withflintpay.com

2 files in your repo · 0 money tables · 1 record to read

It counts double when an agent writes the code. Asked for checkout, it will scaffold the whole left column, and every rounding bug in it is yours to find. On Flint it writes two SDK calls and a webhook route. The sell online with AI page covers building a store with an agent end to end.

Already on Stripe Checkout? The post on what Next.js and Stripe tutorials leave out walks the async payments, concurrent webhook handlers, and client-supplied prices that production adds.

02Day one

The pages you were going to build are already live.

A checkout, an emailed receipt, a signed-in buyer account, and a back office come with the API key. None of them is a route in your app, so none of them is on your roadmap.

The buyer account at account.withflintpay.com. Flint signs the buyer in; you add your brand or your own domain when you want to.

A hosted checkout under your business name that takes cards, Apple Pay, Google Pay, ACH debit, and Affirm.

POST /v1/checkout-sessions

A receipt emailed to the buyer the moment the order is paid, built from the order's own line items.

order.paid

A buyer account with order history, subscriptions, and saved cards. It replaces app/account/orders and the auth in front of it.

account.withflintpay.com

A dashboard with 20 sections, including orders, refunds, disputes, payouts, and analytics. It replaces app/admin.

app.withflintpay.com

Payouts to your bank, each one tied back to the payments inside it.

GET /v1/payouts
app.withflintpay.com/payments
Flint dashboard payments list: amounts, statuses, payment ids, Visa and Mastercard methods and customers for a day of coffee shop orders
The dashboard's payments view. Orders, refunds, disputes, payouts, and analytics sit in the same sidebar the day you get your key.

03The boundary

Nothing about money ships to the browser.

Anything in a client component goes to every visitor, the math and the key with it. Next.js already draws the line between server and client, and Flint sits entirely on the server side of it.

The component a prompt writes
"use client";
// what a prompt generates when nothing pushes back

const KEY = process.env.NEXT_PUBLIC_PAY_KEY;   // in every browser now

export function Checkout({ items }) {
  const subtotal = items.reduce((sum, item) => sum + item.price * item.qty, 0);
  const total = subtotal * 1.08;               // tax, hopefully

  async function handlePay() {
    await fetch("https://api.example-payments.com/charges", {
      method: "POST",
      headers: { Authorization: `Bearer ${KEY}` },
      body: JSON.stringify({ amount: Math.round(total * 100) }),
    });
  }

  return <button onClick={handlePay}>Pay ${total.toFixed(2)}</button>;
}

It passes review. The key in a NEXT_PUBLIC_ variable is in every browser that loads the page, and a total computed in a component is a total anyone can edit before it is charged.

Every Flint API key is a server credential. There is no publishable key, so there is nothing for a client component to leak.

server-only keys

Your server sends line items and Flint computes subtotal, tax, and total. No arithmetic runs where a visitor can change it.

pricing_amounts

The buyer's browser receives one thing: the hosted checkout URL. Card fields render on Flint's page, never in your components.

checkout_session.url

The buyer pays the balance on the order record, whatever the page displayed.

settlement_amounts.outstanding_money

04The integration

Four steps from create-next-app to a paid order.

All of it runs on a free sandbox key. Nothing is host-specific: server actions, route handlers, and two env vars run the same on Vercel, in a container, or on a laptop.

  1. Put the key in a server-only env var

    .env.local

    Leave off the NEXT_PUBLIC_ prefix and Next.js keeps both values out of every client bundle.

    .env.local
    # .env.local
    # no NEXT_PUBLIC_ prefix, so Next keeps both out of every client bundle
    FLINT_API_KEY=your-sandbox-key
    FLINT_WEBHOOK_SECRET=your-endpoint-secret
  2. Create the order and the session on the server

    POST /v1/orders · POST /v1/checkout-sessions

    A form calls a server action. A client component calls a route handler. Either way your server builds the order from line items it controls and gets back one URL.

    app/checkout/actions.ts
    "use server";
    
    import { Client } from "@flintpay/node";
    import { redirect } from "next/navigation";
    
    const flint = new Client({
      baseUrl: "https://api.withflintpay.com",
      apiKey: process.env.FLINT_API_KEY!,
    });
    
    export async function buy() {
      const order = await flint.orders.create({
        line_items: [
          { name: "Cold brew", quantity: "2",
            unit_price_money: { amount: "650", currency: "USD" } },
          { name: "Croissant", quantity: "1",
            unit_price_money: { amount: "495", currency: "USD" } },
        ],
      });
    
      const launch = await flint.checkoutSessions.create({
        order_id: order.order_id,
        redirects: {
          success_redirect_url: "https://example.com/thanks",
          cancel_redirect_url: "https://example.com/menu",
        },
      });
    
      redirect(launch.checkout_session.url!);
    }

    redirect() sends the buyer straight to checkout from the same function that created the order.

    app/menu/page.tsx
    import { buy } from "../checkout/actions";
    
    export default function Menu() {
      return (
        <form action={buy}>
          <button type="submit">Order for pickup</button>
        </form>
      );
    }

    The entire frontend of the purchase: a form whose action is the server action. There is no payment form to build and no SDK in the client bundle.

    The order is built from line items your server controls, not from a price the browser sent.

    POST /v1/orders

    The session wraps that order, so the buyer pays exactly what the record says.

    POST /v1/checkout-sessions

    Pass an idempotency key and a double submit cannot create two orders.

    Idempotency-Key
  3. Add the webhook route

    app/api/flint/route.ts

    The success redirect is a hint; the webhook is the confirmation. A buyer can close the tab before the redirect, and anyone can open the success URL directly. The route verifies the delivery and fulfills. It does not rebuild the order, because the order already exists.

    app/api/flint/route.ts
    import { Client } from "@flintpay/node";
    
    const flint = new Client({
      baseUrl: "https://api.withflintpay.com",
      apiKey: process.env.FLINT_API_KEY!,
    });
    
    export async function POST(request: Request) {
      // route handlers do not pre-parse the body, so these are
      // the exact bytes the signature signs
      const rawBody = new Uint8Array(await request.arrayBuffer());
    
      // throws on a bad signature or a stale timestamp
      const { event, known } = flint.verifyWebhook(
        rawBody,
        Object.fromEntries(request.headers),
        [process.env.FLINT_WEBHOOK_SECRET!],
      );
    
      if (known && event.event_type === "order.paid") {
        // fulfill: the payload carries the order id and totals
      }
    
      return new Response(null, { status: 204 });
    }

    No body-parser setting and no raw-body middleware: route handlers hand you the exact bytes the signature signs, and the SDK does the verification.

    Deliveries carry Standard Webhooks headers, and the SDK checks all three with a 300 second tolerance.

    verifyWebhook

    A failed delivery retries with backoff, up to 9 attempts over about three days. A cold start or a bad deploy does not lose a sale.

    retries

    Every retry carries the same id, so deduplication is one lookup.

    webhook-id

    On the wire

    • webhook-id
    • webhook-timestamp
    • webhook-signature

    Events your route branches on

    • order.paid
    • order.refunded
    • subscription.past_due

    Events report what happened to the order, so the handler reads sales, not processor internals. Webhooks guide →

  4. Test it on localhost with real signatures

    flint listen

    The CLI streams your sandbox's events to your dev server, signed like production, with no tunnel and no public URL. Pay the test order with 4242 4242 4242 4242 and watch the route fire.

    flint listen --forward-to http://localhost:3000/api/flint

    The verification code you run in development is the code you ship. CLI docs →

    app.withflintpay.com/developers
    Flint developer console: API keys, webhook endpoints, event stream and request logs inside the merchant dashboard
    The developer console in the dashboard: API keys, webhook endpoints, the event stream, and request logs for every call your app made.

05Month two

The next feature is a call, not a migration.

Stores ask for the same things in the same order: a partial refund, order history, a discount code, a subscription. Each one attaches to the order your server action already creates, so none of them touches your schema.

Refund the croissant, not the whole order.

POST /v1/refunds · line_items[]

Show customers their past orders.

GET /v1/orders · customer_id

Run a 10% off code this weekend.

POST /v1/promotions

Sell the beans as a monthly subscription.

POST /v1/subscriptions

Stop selling what is out of stock.

POST /v1/inventory-reservations

Send a wholesale customer an invoice.

POST /v1/invoices
app.withflintpay.com
The order after the first request. One call named the line and the quantity; Flint worked out the $0.41 tax share, refunded $5.36, and struck the line on the same record the payment settled against.

Every subscription renewal is an order too, so the webhook route you wrote for one sale already handles the recurring ones. Catalog, promotions, inventory, and invoices are the same API under the same key. The commerce API page walks through all of it.

What you get with the key

Your repo keeps the storefront. Flint runs the store behind it.

2
SDK calls from a button to a paid order
0
money tables in your Prisma schema
20
dashboard sections before you write a line
574
API operations under the same key

Stripe processes every card · card data goes straight to Stripe · one processing fee per payment · samples validated against the published OpenAPI spec at build time

Public alpha

Alpha accounts get the changes first.

Real merchants take real payments on Flint today, and new features reach alpha accounts before general rollout. The order you create in the sandbox this afternoon is the same record you go live on.

shipped 2026-09-29 · Codes for saved details default to the auto channel

Alpha features and API change policy →

Building now?

Create an account from your terminal with flint signup. The sandbox is free, with no credit card.

06Start

One prompt, or two files by hand.

Paste into your agent
Add checkout to my Next.js app with Flint. Create
the order and the checkout session in a server
action with the sandbox key in FLINT_API_KEY, send
the buyer to the session URL, and add a webhook
route that verifies the signature with the SDK
and marks the sale fulfilled on order.paid.

Or write it yourself: the server action in the hero and the webhook route in step 3 are the whole integration.

Install the SDK

npm install @flintpay/node

Install the CLI

npm install -g @flintpay/cli

FAQ

Questions worth asking first.

Why use Flint instead of Stripe Checkout in my Next.js app?

Stripe processes every Flint card payment, so the payment itself is the same. The difference is where the store lives. On a payments API your app owns the order model: a Prisma schema for orders and line items, a webhook that copies each sale into your tables, tax and discount math, refund proration, and a second copy of every sale to keep in sync with the processor. On Flint the order is the API's record. Totals and tax compute server-side, refunds take line items, order history is a query, receipts and the buyer account are already running, and your schema keeps zero money tables.

Should I use a server action or a route handler for checkout?

Both work and both stay server-side. Use a server action when a form or button in a server-rendered page starts the purchase: it creates the order and redirects to checkout in one function. Use a route handler when a client component starts it, or when something outside your app posts to you. Webhooks are always a route handler, because the sender is Flint, not your UI.

Can I call Flint from a client component?

Flint API keys are server credentials, so the call goes through a server action or a route handler. The client component collects what the buyer wants and hands it to the server; the browser only ever receives the hosted checkout URL. The key stays out of the bundle and the total stays out of reach of anyone editing JavaScript in a browser.

How do I verify Flint webhooks in the App Router?

Read the raw bytes with request.arrayBuffer(), convert the headers with Object.fromEntries(request.headers), and pass both to the SDK's verifyWebhook with your endpoint secret. It checks the Standard Webhooks headers with a 300 second tolerance and throws on a mismatch. Route handlers do not pre-parse the body, so there is no body-parser setting to turn off and no raw-body middleware to add.

How do I test Flint webhooks in Next.js locally?

Run your dev server, then run flint listen with the forward flag pointed at your webhook route. The CLI streams your sandbox's events and posts them to localhost with a real signature, so the verification code you run in development is the code you ship. No tunnel, no registered endpoint, no fake payloads.

Does this work on Vercel and other serverless hosts?

Yes. The integration is server actions, route handlers, and two environment variables, which is what every Next.js host runs. Failed webhook deliveries retry with backoff, up to 9 attempts over about three days, so a cold start or a bad deploy does not lose a sale. Every retry carries the same webhook-id, which makes deduplication one lookup.

Can Claude Code or Cursor write this integration?

Yes. Connect the Flint docs MCP server or paste SKILL.md into context and the agent works from live endpoint schemas instead of memory. There is little for it to get wrong: two SDK calls in a server action and one webhook route, with the totals, tax, and refund math on Flint's side of the API.

Do I need a database to track orders?

Not for the commerce. The order lives on Flint with its line items, totals, payment state, refunds, and history, and the dashboard reads it without any code from you. Your database holds what is yours: content, accounts, entitlements. A webhook route on order.paid is where a sale updates it.

What does it cost?

Cards cost 3.79% + 35¢, with no monthly fee. The sandbox is free and needs no credit card. One processing fee per payment covers Flint and the card processing underneath, so there is no separate processor bill.

Build the store this afternoon.

Free sandbox, no credit card. One server action and one webhook route, and the checkout, receipts, buyer account, and dashboard are already running behind them.

Skip the wait: npm install -g @flintpay/cli && flint signup creates your account and a sandbox key from the terminal. CLI guide

app/checkout/actions.ts · the first file you write