Creem
Creem product mapping, sandbox testing, and webhook setup
Last updated November 3, 2026
Creem is a Merchant of Record: it collects in its own name and handles tax and refunds, so the site receives net settlements. Both subscriptions and one-time payments are supported.
Steps
- Create products in the Creem dashboard: recurring products for subscriptions (one per
interval) and one-time products for lifetime purchases.
- Fill in the environment variables:
``dotenv VITE_PAYMENT_PROVIDER=creem CREEM_API_KEY=creem_xxx CREEM_WEBHOOK_SECRET=whsec_xxx CREEM_PRODUCTS=pro_monthly=prod_xxx,pro_yearly=prod_yyy,lifetime=prod_zzz ``
The left side of CREEM_PRODUCTS is the reference on the right-hand side of prices in src/config/products.ts; the right side is the Creem product ID (prod_*). Separate entries with commas, and map each reference to exactly one product ID.
- Deploy:
pnpm run deploy. - In the Creem dashboard add the webhook endpoint
<your-site-domain>/api/webhooks/creem, use
CREEM_WEBHOOK_SECRET as the signing secret, and subscribe to the events below.
The endpoint only responds while Creem is the active provider: when the site runs a different channel it answers 404, and Creem stops retrying.
Events
| Event | Purpose |
|---|---|
checkout.completed | Checkout finished; one-time orders are recorded here |
subscription.paid | The authoritative subscription payment event, sent for the first payment and every renewal |
subscription.* | Subscription lifecycle; scheduled_cancel normalizes to active with cancel-at-period-end |
The creem-signature header is the HMAC-SHA256 hex digest of the raw request body; an invalid signature returns 400. Ownership travels in the checkout metadata.userId / productId / offerId, and subscription metadata wins when present. If both are missing, the write fails and Creem retries — a payment is never attributed to a guessed user.
Sandbox testing
With CREEM_DEBUG=true requests go to the sandbox at test-api.creem.io (production is api.creem.io). Test with a sandbox API key (starting with creem_test_) and sandbox product IDs; before launch set it back to false and switch to production keys and product IDs.
Differences from Stripe
| Dimension | Creem | Stripe |
|---|---|---|
| Products and prices | The product is the price; there is no lookup key and checkout takes product_id | Prices and products are separate objects, linked by lookup_key |
| Fetching prices | One GET /v1/products/{id} per reference | Prices are queried in batches by lookup key |
| Cancel redirect | No cancel_url; cancelling lands on Creem's own page | Has cancel_url, returning to the page that started the purchase |
| Amounts | Pre-tax; the checkout page adds tax, so the displayed price may differ from the charged total | Displayed price equals the charged amount |
| Subscription status | expired is recorded as past_due (still recoverable); unknown statuses become incomplete, withholding access for now | Status is taken from Stripe as-is |
| Refunds | No in-app refund entry point; refunds are started in the Creem dashboard | No in-app refund entry point; refunds are started in the Stripe dashboard and charge.refunded comes back |
Existing subscriptions do not migrate when you switch providers: switching means changing the merchant of record, so handle the existing subscribers yourself.
Verify
pnpm run check:envpasses, so the provider, credentials, and product mapping are in place.- The Creem dashboard's webhook log shows deliveries from your site (
200means the signature
verified).