Add a store to a Next.js app
A complete App Router integration: server-rendered product pages, a checkout route, a thank-you page and an optional webhook receiver. The API key stays on the server.
Client
lib/commerce.ts
import "server-only";
import { Commerce } from "commerceforai";
export const commerce = new Commerce(process.env.COMMERCEFORAI_API_KEY);Product pages
Store your page slug in product metadata, then filter on it.
app/shop/[handle]/page.tsx
import { notFound } from "next/navigation";
import { commerce } from "@/lib/commerce";
import { BuyButton } from "./buy-button";
export default async function ProductPage({ params }: PageProps<"/shop/[handle]">) {
const { handle } = await params;
const { data } = await commerce.products.list({ active: true, metadata: { handle } });
const product = data?.data[0];
if (!product) notFound();
return (
<main>
<h1>{product.name}</h1>
{product.variants.map((v) => (
<BuyButton
key={v.id}
variantId={v.id}
label={`${v.name} · $${(v.price / 100).toFixed(2)}`}
soldOut={v.inventory.available === 0}
/>
))}
</main>
);
}Checkout route
app/api/checkout/route.ts
import { commerce } from "@/lib/commerce";
export async function POST(req: Request) {
const { variantId, quantity = 1 } = await req.json();
const site = process.env.NEXT_PUBLIC_SITE_URL!;
const { data, error } = await commerce.checkouts.create({
items: [{ variant_id: variantId, quantity }],
success_url: `${site}/thanks?session={CHECKOUT_SESSION_ID}`,
cancel_url: `${site}/`,
shipping_rates: [{ name: "Standard", amount: 500, min_days: 3, max_days: 7 }],
});
if (error) {
const status = error.type === "out_of_stock" ? 409 : 400;
return Response.json({ error: error.message }, { status });
}
return Response.json({ url: data.url });
}app/shop/[handle]/buy-button.tsx
"use client";
export function BuyButton({ variantId, label, soldOut }: { variantId: string; label: string; soldOut: boolean }) {
return (
<button
disabled={soldOut}
onClick={async () => {
const res = await fetch("/api/checkout", { method: "POST", body: JSON.stringify({ variantId }) });
const { url, error } = await res.json();
if (url) window.location.href = url;
else alert(error);
}}
>
{soldOut ? "Sold out" : label}
</button>
);
}Thank-you page
Show a friendly confirmation, but don't treat it as proof of payment. Orders are created from Stripe's signed events, which can arrive a moment after the redirect (or days later for bank payments).
React to orders (optional)
Register an endpoint once and save the returned secret. Then verify every delivery with the raw body.
const { data } = await commerce.webhooks.create({
url: "https://example.com/api/commerce-webhook",
events: ["order.*"],
});
// save data.secret as COMMERCEFORAI_WEBHOOK_SECRETapp/api/commerce-webhook/route.ts
import { verifyWebhook } from "commerceforai";
export async function POST(req: Request) {
const event = await verifyWebhook({
payload: await req.text(),
headers: req.headers,
secret: process.env.COMMERCEFORAI_WEBHOOK_SECRET!,
});
if (event.type === "order.paid") {
// notify Slack, start a fulfillment workflow, ...
}
return new Response(null, { status: 204 });
}