CommerceForAI

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_SECRET
app/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 });
}