TanStarter Docs

Creem

How to set up and use Creem for payments and subscriptions

TanStarter uses Creem for payment management, supporting both one-time payments and subscriptions.

Setup

TanStarter template provides three pricing plans by default: a free plan, a pro subscription plan (monthly/yearly), and a lifetime plan (one-time payment). Follow these steps to set up:

Create Creem Account

Create a Creem account at creem.io. No credit card required to sign up.

Get API Keys

Get your API keys from the Creem Dashboard:

  • Go to Creem Dashboard > Developers > API & Webhooks, create a new API Key
  • Copy the API key (Note: test mode starts with creem_test_, production mode starts with creem_live_)
  • Save it to your environment file as CREEM_API_KEY

Set Up Webhook

Set up Webhook and get your Webhook secret:

  • Go to Creem Dashboard > Developers > API & Webhooks, create a new Webhook
  • Add Webhook URL: https://YOUR-DOMAIN.com/api/webhooks/creem
  • Creem automatically listens for all payment and subscription related events, including:
    • checkout.completed
    • subscription.active
    • subscription.paid
    • subscription.update
    • subscription.canceled
    • subscription.scheduled_cancel
    • subscription.expired
    • subscription.past_due
    • subscription.unpaid
    • subscription.paused
    • subscription.trialing
  • Copy the Webhook signing secret
  • Save it to your environment file as CREEM_WEBHOOK_SECRET

Create Products and Pricing Plans

Create products in Creem and set up pricing plans:

  • Go to Creem Dashboard > Commerce > Products
  • Create the Pro subscription plan product:
    • Click to create a product
    • Name: Pro Plan
    • Description: Premium features with subscription pricing
    • Add monthly subscription price:
      • Price: $9.90 (currency: USD)
      • Billing type: recurring
      • Billing period: every-month
      • Save and copy the Product ID (starts with prod_), this will be used for VITE_CREEM_PRODUCT_PRO_MONTHLY
    • Copy the monthly subscription product, then add yearly subscription price:
      • Price: $99.00 (currency: USD)
      • Billing type: recurring
      • Billing period: every-year
      • Save and copy the Product ID (starts with prod_), this will be used for VITE_CREEM_PRODUCT_PRO_YEARLY
  • Create the Lifetime plan product:
    • Click to create a product
    • Name: Lifetime Plan
    • Description: One-time payment for lifetime access
    • Add price:
      • Price: $199.00 (currency: USD)
      • Billing type: one_time
      • Save and copy the Product ID (starts with prod_), this will be used for VITE_CREEM_PRODUCT_LIFETIME

Configure Environment Variables

Add the following environment variables:

.env
# Payment provider
VITE_PAYMENT_PROVIDER=creem

# Set to 'true' to use Creem test API, omit or set to 'false' for production
# CREEM_DEBUG=true

# API Key and Webhook Secret
CREEM_API_KEY=creem_test_...
CREEM_WEBHOOK_SECRET=...

# Product IDs
VITE_CREEM_PRODUCT_PRO_MONTHLY=prod_...
VITE_CREEM_PRODUCT_PRO_YEARLY=prod_...
VITE_CREEM_PRODUCT_LIFETIME=prod_...

Update Payment Configuration

Pricing plans — amounts, currencies, intervals, promotion codes, and plan metadata — are now defined in src/config/payment.ts instead of src/config/website.ts. The enable, provider, and priceId fields are automatically resolved from your environment variables (VITE_PAYMENT_PROVIDER and VITE_CREEM_PRODUCT_*), so you don't need to hardcode them. Creem ignores allowPromotionCode.

You must configure this section to match the products you created in Creem:

src/config/payment.ts
export function createPaymentConfig(env, locale?) {
  const provider = paymentProviderSchema.parse(env.VITE_PAYMENT_PROVIDER); // ← 'creem'
  const providerPriceIds = {
    creem: {
      proMonthly: env.VITE_CREEM_PRODUCT_PRO_MONTHLY,
      proYearly: env.VITE_CREEM_PRODUCT_PRO_YEARLY,
      lifetime: env.VITE_CREEM_PRODUCT_LIFETIME,
    },
    // ...other providers
  };
  const priceIds = provider ? providerPriceIds[provider] : undefined;

  return {
    enable: provider !== '',
    provider: provider || undefined,
    price: {
      plans: {
        free: {
          id: 'free',
          prices: [],
          isFree: true,
          isLifetime: false,
        },
        pro: {
          id: 'pro',
          prices: [
            {
              type: 'subscription',
              priceId: priceIds?.proMonthly ?? '', // ← auto: from VITE_CREEM_PRODUCT_PRO_MONTHLY
              amount: 990,                         // amount in cents ($9.90)
              currency: 'USD',
              interval: 'month',
              allowPromotionCode: true,
            },
            {
              type: 'subscription',
              priceId: priceIds?.proYearly ?? '',  // ← auto: from VITE_CREEM_PRODUCT_PRO_YEARLY
              amount: 9900,                        // amount in cents ($99.00)
              currency: 'USD',
              interval: 'year',
              allowPromotionCode: true,
            },
          ],
          isFree: false,
          isLifetime: false,
          popular: true,
        },
        lifetime: {
          id: 'lifetime',
          prices: [
            {
              type: 'one_time',
              priceId: priceIds?.lifetime ?? '',   // ← auto: from VITE_CREEM_PRODUCT_LIFETIME
              amount: 19900,                       // amount in cents ($199.00)
              currency: 'USD',
              allowPromotionCode: true,
            },
          ],
          isFree: false,
          isLifetime: true,
        },
      },
    },
  } satisfies PaymentConfig;
}

If you are setting up your environment, you can now go back to the Environment Configuration and continue. The rest of this document can be read later.

Environment Configuration

Set up environment variables


Development Environment

Keep the application on its normal local origin and use an HTTPS tunnel only for webhook delivery. Configure .env.local with Test Mode values:

.env.local
VITE_BASE_URL=http://localhost:3000
VITE_PAYMENT_PROVIDER=creem
CREEM_DEBUG=true
CREEM_API_KEY=creem_test_...
CREEM_WEBHOOK_SECRET=...
VITE_CREEM_PRODUCT_PRO_MONTHLY=prod_...
VITE_CREEM_PRODUCT_PRO_YEARLY=prod_...
VITE_CREEM_PRODUCT_LIFETIME=prod_...

Start the app, then expose port 3000 with Cloudflared in another terminal:

pnpm dev

# Run this in a second terminal
cloudflared --config /dev/null tunnel \
  --url http://localhost:3000 \
  --no-autoupdate \
  --protocol quic

In the Creem Dashboard, switch to Test Mode and register the generated URL as https://xxxx.trycloudflare.com/api/webhooks/creem. The tunnel is only for Creem's webhook request; keep VITE_BASE_URL=http://localhost:3000 while using the site locally.

After a successful checkout, Creem returns the browser through /settings/payment?request_id=.... TanStarter waits for the payment row with that same reference, then redirects to /settings/billing.

The template also includes a Creem Test Mode E2E suite:

pnpm e2e:creem

Demo video: https://cdn.mksaas.com/mksaas/video/mksaas-creem-payment.mp4

Creem provides a full test environment. When using API keys starting with creem_test_, all operations run in a sandbox and no real transactions are made.

Production Environment

  1. Go to Creem Dashboard > Developers > API & Webhooks
  2. Add the production Webhook URL: https://YOUR-DOMAIN.com/api/webhooks/creem
  3. Switch the API key from test key (creem_test_) to production key (creem_live_)
  4. Make sure your environment variables are updated with production keys and product IDs

Webhook Events

Creem supports the following webhook events:

EventDescription
checkout.completedCheckout session completed
subscription.activeNew subscription created (for sync only)
subscription.paidSubscription payment successful (use to activate access)
subscription.updateSubscription updated (plan change, period renewal)
subscription.canceledSubscription canceled
subscription.scheduled_cancelSubscription marked to cancel at end of billing period
subscription.past_duePayment failed, subscription awaiting retry
subscription.expiredPayment expired but can still recover; stored as past_due
subscription.unpaidCollection attempts are exhausted; access is suspended
subscription.trialingSubscription entered trial period
subscription.pausedSubscription paused

Creem recommends using the subscription.paid event to activate user access, rather than subscription.active. subscription.active is only for data synchronization.

Only subscription.canceled is terminal. An expired or past-due subscription can become active again after a successful payment.

Webhook Responses and Retries

ResultHTTP status
Payment is disabled200
Creem is not the configured payment provider404
Missing payload or creem-signature header400
Invalid signature400
Successfully processed or safely duplicated200
Temporary provider or database failure500

TanStarter returns 500 when a lifecycle event arrives before checkout.completed has created its payment row, allowing Creem to retry it. Handlers are idempotent, so repeated deliveries do not create duplicate payments.

Creem refund and dispute events are not handled by the template. Refunding a payment in Creem does not automatically revoke access; revoke it manually or add a handler in src/payment/provider/creem.ts.

Customer Portal

After every successful payment, your customers will receive an email with a link to their Customer Portal. The Customer Portal allows them to:

  • View subscription details
  • Manage subscriptions (upgrade, pause, cancel)
  • View payment history
  • Update payment methods

Test Cards

To test Creem integration, use Creem's test mode with test credit cards:

  • 4242 4242 4242 4242 - Successful payment
  • 4000 0000 0000 0002 - Payment failure

When using test mode (API keys starting with creem_test_), all transactions run in a sandbox environment with no real charges.

Best Practices

  1. Protect API keys: Never expose your Creem API key in client-side code
  2. Validate webhook signatures: Always verify the creem-signature of webhook events
  3. Handle errors gracefully: Provide user-friendly error messages when payments fail
  4. Test webhooks thoroughly: Make sure all webhook events are handled correctly

References

Last updated on

On this page