Stripe setup (Pro self-serve)

June 9, 2026 · View on GitHub

MailAgent billing is optional. Without STRIPE_* secrets the API returns stripe_not_configured and teams stay on free (or operator-assigned pro / enterprise).

When you have a Stripe account, follow this once — same pattern as OIDC (wizard:auth0).

What you get

PlanHow
freeDefault for npm run issue:key:db teams
proStripe Checkout → webhook sets teams.plan = pro
enterpriseOperator only: npm run team:plan -- TEAM_ID enterprise (not Stripe self-serve yet)
legacySingle wrangler API_KEY (no team billing)

Quick path (operator)

# 1) Dashboard checklist + save keys to .dev.vars
npm run wizard:stripe

# 2) Push secrets to Cloudflare Worker
npm run wizard:stripe -- --deploy
# or: npm run setup:stripe-prod

# 3) Verify
npm run doctor:billing
npm run test:contract:qa:billing

Stripe Dashboard (test mode first)

1. Product + Price

  1. Stripe DashboardProduct catalogAdd product
  2. Name: MailAgent Pro
  3. Pricing: Recurring · monthly (or yearly) · amount of your choice
  4. Copy Price IDprice_…STRIPE_PRICE_PRO

Use test mode keys (sk_test_…, price_… from test catalog) until go-live.

2. Customer Portal

Settings → Billing → Customer portal → enable:

  • Cancel subscription
  • Update payment method
  • Invoice history

Return URL is set by API (dashboard.html?billing=portal).

3. Webhook

Developers → Webhooks → Add endpoint

FieldValue
URLhttps://api.webmailagent.com/webhooks/stripe
Eventscheckout.session.completed, customer.subscription.updated, customer.subscription.deleted

Copy Signing secretwhsec_…STRIPE_WEBHOOK_SECRET

Local dev (optional):

stripe listen --forward-to http://127.0.0.1:8787/webhooks/stripe
# use the whsec_ from CLI output in .dev.vars for local only

4. API key

Developers → API keys → Secret key (sk_test_… or restricted rk_test_…)

Recommended: restricted key with:

  • Checkout Sessions: Write
  • Customers: Write
  • Billing Portal: Write
  • Prices: Read

STRIPE_SECRET_KEY

Secrets map

SecretWhere
STRIPE_SECRET_KEYwrangler secret + optional .dev.vars
STRIPE_WEBHOOK_SECRETwrangler secret + optional .dev.vars
STRIPE_PRICE_PROwrangler secret + optional .dev.vars

.dev.vars is for local wrangler dev and setup:stripe-prod — never commit.

Client flow

  1. Team key from npm run issue:key:db -- acme
  2. GET /v1/mebilling.stripeEnabled, billing.canUpgrade
  3. POST /v1/billing/checkout (admin scope) → { url } → Stripe Checkout
  4. Webhook → teams.plan = pro
  5. POST /v1/billing/portal → Customer Portal

Dashboard: dashboard.htmlUpgrade to Pro / Manage billing.

Go-live checklist

  • Switch Dashboard to live mode
  • Create live Product + Price → update STRIPE_PRICE_PRO
  • Live webhook endpoint (same URL)
  • Live STRIPE_SECRET_KEY + STRIPE_WEBHOOK_SECRET
  • npm run setup:stripe-prod
  • npm run doctor:billing (prod shows stripeEnabled: true)
  • Test checkout with real card → confirm GET /v1/me plan pro
  • Cancel in portal → confirm downgrade to free

Troubleshooting

SymptomFix
stripe_not_configuredMissing STRIPE_SECRET_KEY or STRIPE_PRICE_PRO on Worker
invalid_stripe_signatureWrong STRIPE_WEBHOOK_SECRET or raw body modified
Upgrade button missingNeed registered team key + free plan + Stripe configured
billing_requires_registered_keynpm run issue:key:db (not legacy API_KEY only)
Plan stuck on free after payCheck webhook delivery in Stripe Dashboard; events above subscribed