Creem checkout in Next.js

An animated cart drawer that pays through Creem: a server action prices the cart, creates one Creem checkout session for the total and sends the buyer to Creem's hosted page.

Stack Cart Drawer, preview
One payment$3

By buying you agree to the Terms.

Ready forCursorv0Lovableshadcn CLI

Full product pageReact · Tailwind · TypeScript

Use it in Next.js

app/shop/page.tsx

"use client";
import StackCartDrawer from "@/components/StackCartDrawer";
import { createCreemCheckout } from "./actions";

export default function Shop() {
  return (
    <div className="h-dvh">
      <StackCartDrawer
        autoplayDemo={false}
        onCheckout={async (items) => {
          try {
            const url = await createCreemCheckout(items);
            window.location.assign(url);
            return { ok: true };
          } catch {
            return { ok: false }; // the drawer says the payment failed and keeps the cart
          }
        }}
      />
    </div>
  );
}

app/shop/actions.ts

"use server";
import { Creem } from "creem";
import { NW } from "@/data/northwind"; // the same product list the drawer shows

const creem = new Creem({
  apiKey: process.env.CREEM_API_KEY!,
  server: process.env.CREEM_MODE === "live" ? "prod" : "test",
});

export async function createCreemCheckout(items: { id: string; qty: number }[]) {
  // Work the total out on the server from your own price list, never from the browser.
  const lines = items.map((i) => {
    const p = NW.shop.products.find((x) => x.id === i.id);
    if (!p || !Number.isInteger(i.qty) || i.qty < 1) throw new Error("Unknown item in cart");
    return { id: p.id, name: p.name, qty: i.qty, cents: Math.round(p.price * 100) * i.qty };
  });
  const subtotal = lines.reduce((sum, l) => sum + l.cents, 0);
  const shipping = subtotal >= NW.shop.freeShipping * 100 ? 0 : Math.round(NW.shop.shipping * 100);

  const checkout = await creem.checkouts.create({
    productId: process.env.CREEM_ORDER_PRODUCT_ID!, // one one-time product, e.g. "Order"
    customPrice: subtotal + shipping, // in cents, replaces that product's price for this session
    successUrl: `${process.env.NEXT_PUBLIC_SITE_URL}/thanks`,
    metadata: { cart: lines.map((l) => `${l.id}x${l.qty}`).join(",") },
  });
  if (!checkout.checkoutUrl) throw new Error("Creem sent no checkout URL");
  return checkout.checkoutUrl;
}

Good to know

  • Type-checked against creem 1.13 and next 15.5 with TypeScript 5.9. We haven't run a live Creem checkout with it.
  • A Creem checkout session takes one product id, plus units for a quantity. It has no list of products like some other providers. So the whole cart goes through one one-time product (call it Order) with custom_price set to the cart total.
  • Creem's API reference says custom_price is in cents, per unit, and only works for one-time payment products. Creem's checkout page and receipt then show that one product's name, so the item list travels in metadata for your own records.
  • The total is worked out on the server from the same product list the drawer shows. Never send a price from the browser. Anyone can change it there.
  • server: "test" points the SDK at test-api.creem.io. Use a test mode key and product while you build, then switch the env var.
  • Give access or ship the order from Creem's webhook (onCheckoutCompleted in @creem_io/nextjs), not from the /thanks page. Anyone can open /thanks without paying.
  • Creem is the merchant of record and handles tax at checkout, so the total in the drawer is only a preview.
  • If you sell one product or a subscription, you don't need a cart. Creem's own CreemCheckout button from @creem_io/nextjs takes a productId and is the simpler, free route.

Install

  1. Copy from the zipPut react/src/components + react/src/data into src/, then npm i framer-motion creem.
  2. Or ask your AIPaste PROMPT.md into Cursor, Lovable or v0.

Props

onCheckout
(items, total) → { ok }
data
store name, products, free-shipping line
autoplayDemo
false for a real shop

How qodesign compares

  • qodesign vs 21st.dev21st.dev is best for: browsing a huge marketplace and copying many components a month.
  • qodesign vs Skiper UISkiper UI is best for: unusual interaction details for landing pages: image reveals, cursor trails, tooltips, drag-and-scroll galleries.

All comparisons, with prices read on each library's own site.

Questions

Why not one checkout per product?

Creem's checkout takes one product id per session, so a cart of three products would mean three payments. One Order product with a custom price keeps it to one.

Can I prefill the buyer's email?

Yes. Pass customer with the email in checkouts.create when you know who is signed in.

Does it work for subscriptions?

No. Creem only takes custom_price on one-time products. For plans, a pricing page with monthly and yearly fits better than a cart.

What if Creem answers with an error?

The action throws, onCheckout returns { ok: false } and the drawer says the payment failed. The cart stays as it was.

Can Cursor or v0 add it for me?

Yes. Each download has a PROMPT.md. Paste it into Cursor, Lovable, v0 or Claude Code and it fits the piece into your app.

Can I use it in client work?

Yes, in personal and client projects. You can't resell the code itself as a kit.