Accept Payments
Choose a gateway, connect your products, and complete your first sandbox checkout.
Connect one gateway first. Your goal is a complete sandbox purchase: a signed-in user checks out, the webhook arrives, and the app shows the correct plan.
Before You Start
Have a working app and a gateway account with sandbox/test access. Prepare the plans you want to sell, their amounts, currency, and billing intervals. For webhooks, use a publicly reachable HTTPS test deployment or a provider-supported forwarding tool.
Keep live credentials and real charges out of this first test. You can keep config.payments.enableBilling=false until configuration is ready.
1. Choose One Gateway
| Gateway | Setup guide | Main credential |
|---|---|---|
| Creem | Creem | CREEM_API_KEY |
| Stripe | Stripe | STRIPE_SECRET_KEY |
| Dodo Payments | Dodo | DODO_PAYMENTS_API_KEY |
| Waffo | Waffo | WAFFO_MERCHANT_ID and WAFFO_PRIVATE_KEY |
Choose based on merchant eligibility, markets, payment methods, and business needs. Availability depends on the provider and your account; you do not need all four accounts.
2. Connect Your Plans
In packages/config/src/index.ts, keep your stable app plan IDs in payments.productCatalog. In your selected app:
src/config/saas/pricing-desc.ts: plan titles, descriptions, features.src/config/saas/pricing-desc-usage.ts: amounts, currency, recurring intervals, and purchasable items.
Create matching products/prices in the gateway dashboard. Follow its guide for the exact ID to copy. Map each configured plan to that gateway's environment variables. For example, a Stripe setup in the app .env.local uses:
BILLING_DEFAULT_GATEWAY="muse_stripe"
STRIPE_SECRET_KEY="YOUR_TEST_SECRET_KEY"
STRIPE_WEBHOOK_SECRET="YOUR_TEST_WEBHOOK_SECRET"
STRIPE_PRODUCT_ID_PRO_MONTHLY="YOUR_MONTHLY_PRICE_ID"
STRIPE_PRODUCT_ID_PRO_YEARLY="YOUR_YEARLY_PRICE_ID"
STRIPE_PRODUCT_ID_LIFETIME="YOUR_ONE_TIME_PRICE_ID"Keep provider price, currency, and interval aligned with the app. originalAmount only displays a comparison price; it does not change the gateway price. Workers local settings also go in .dev.vars; production secrets belong on your host.
3. Register the Webhook
Use your test origin plus the gateway path:
| Gateway | Path |
|---|---|
| Creem | /api/muse-billing/notify/muse_creem |
| Stripe | /api/muse-billing/notify/muse_stripe |
| Dodo | /api/muse-billing/notify/muse_dodo |
| Waffo | /api/muse-billing/notify/muse_waffo |
Copy the provider's signing secret where applicable and enable the events listed in its guide. Waffo uses its SDK signature verifier and WAFFO_ENVIRONMENT=test, without a separate webhook-secret variable. A browser success redirect alone does not establish paid access; the server processes webhook state.
4. Complete a Sandbox Purchase
Enable payments.enableBilling, restart/redeploy with your test settings, and sign in as a development user. Open /pricing, choose a configured plan, and complete checkout with the provider's sandbox method. Wait for the webhook, then inspect the user's plan and account billing page.
Check Your Result
- Pricing shows the intended plan and the provider checkout uses the expected amount, currency, and interval.
- The provider reports successful webhook delivery.
- The app shows the correct plan/access window for the purchasing user after refresh.
- Subscription management works through the gateway portal when using a subscription plan.
- A failed or canceled checkout does not grant paid access.
Common Problems
| Problem | Fix |
|---|---|
| Gateway is missing or checkout fails | Check the selected gateway's credentials and product mappings; restart after local changes |
| Product/price not found | Check the exact ID type and sandbox/live environment in the gateway guide |
| Webhook never arrives | Use a public HTTPS endpoint or forwarding tool; check the provider delivery log and URL |
| Signature is rejected | Match the endpoint's signing secret; for Waffo check signature header and environment |
| Checkout succeeds but the plan stays unchanged | Inspect webhook delivery, server errors, and contract matching rather than only the return URL |
Before switching to live mode, create live products, replace test credentials and webhook configuration, and complete the Release Checklist. Do not use development seed accounts in production.