Creem
Creem checkout and webhook provider for the Next.js template.
Creem is the default provider. It implements the shared
PaymentProvider interface in
src/lib/payment/providers/creem.ts: it creates hosted checkouts, verifies
webhook signatures, and handles a couple of lifecycle events.
Configuration
| Variable | Purpose |
|---|---|
NEXT_PUBLIC_PAYMENT_PROVIDER | Set to creem (the default) to select this provider. |
CREEM_SERVER_IDX | Selects the Creem server (sandbox vs. live). Defaults to 0. |
CREEM_API_KEY | Server key for creating checkouts. |
CREEM_WEBHOOK_SECRET | HMAC secret for verifying webhook signatures. |
NEXT_PUBLIC_CREEM_PRODUCT_PRO_MONTHLY / _PRO_YEARLY / _LIFETIME | Public Creem product IDs for the Pro monthly, Pro yearly, and Lifetime plans. |
Prices vs. product IDs
src/config/price-config.ts holds the display prices and plan metadata.
Each plan's productId comes from a NEXT_PUBLIC_CREEM_PRODUCT_* variable and
is what Creem actually charges for. The display amount and the Creem product are
independent — keep them consistent yourself.
The create route inserts the payment row with the display amount from
price-config.ts, while the real charge is determined by the Creem product.
Reconcile the stored amount/currency with the Creem product if you rely on
it.
Checkout
createCheckout builds a Creem checkout for the requested productId and
returns its URL. The create route redirects the browser there, with success URL
{origin}/pay-success?orderId={orderNo}.
Webhook
- Path:
POST /api/payment/notify/creem(served by the shared/api/payment/notify/[provider]route). - Signature: HMAC-SHA256 of the raw body using
CREEM_WEBHOOK_SECRET, compared against the hexcreem-signatureheader with a constant-time check. A missing/invalid signature makes the route answer400. - Handled events:
checkout.completed— marks the matchingpaymentrowsuccessand stores the Creem order/customer IDs.- Subscription lifecycle —
subscription.active,subscription.paid,subscription.update,subscription.trialing,subscription.paused,subscription.past_due,subscription.scheduled_cancel,subscription.canceled, andsubscription.expiredupsert the user'ssubscriptionrow to the matching status, period, and cancel timestamps. refund.created— marks the matchingpaymentrowrefunded.
Local testing
- Use the Creem sandbox server (
CREEM_SERVER_IDX) and sandbox product IDs. - Expose your local webhook with a tunnel (e.g.
cloudflared/ngrok) and registerhttps://<tunnel>/api/payment/notify/creemin Creem. - Confirm the
paymentrow flips tosuccessaftercheckout.completed.
Production notes
- Switch to the live Creem server and live product IDs (rebuild for the public
NEXT_PUBLIC_*IDs to take effect). - Webhooks can be retried; the handlers are not idempotent, so add event-id dedup if you extend them with non-terminal side effects.
Verify
- Pricing CTA creates a checkout and redirects to Creem.
- After paying in sandbox, the success page shows the order.
- The webhook marks the
paymentrowsuccess. - A subscription product creates/updates the
subscriptionrow.
