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
| Plan | How |
|---|---|
| free | Default for npm run issue:key:db teams |
| pro | Stripe Checkout → webhook sets teams.plan = pro |
| enterprise | Operator only: npm run team:plan -- TEAM_ID enterprise (not Stripe self-serve yet) |
| legacy | Single 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
- Stripe Dashboard → Product catalog → Add product
- Name:
MailAgent Pro - Pricing: Recurring · monthly (or yearly) · amount of your choice
- Copy Price ID →
price_…→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
| Field | Value |
|---|---|
| URL | https://api.webmailagent.com/webhooks/stripe |
| Events | checkout.session.completed, customer.subscription.updated, customer.subscription.deleted |
Copy Signing secret → whsec_… → 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
| Secret | Where |
|---|---|
STRIPE_SECRET_KEY | wrangler secret + optional .dev.vars |
STRIPE_WEBHOOK_SECRET | wrangler secret + optional .dev.vars |
STRIPE_PRICE_PRO | wrangler secret + optional .dev.vars |
.dev.vars is for local wrangler dev and setup:stripe-prod — never commit.
Client flow
- Team key from
npm run issue:key:db -- acme GET /v1/me→billing.stripeEnabled,billing.canUpgradePOST /v1/billing/checkout(admin scope) →{ url }→ Stripe Checkout- Webhook →
teams.plan = pro POST /v1/billing/portal→ Customer Portal
Dashboard: dashboard.html — Upgrade 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 showsstripeEnabled: true) - Test checkout with real card → confirm
GET /v1/meplanpro - Cancel in portal → confirm downgrade to
free
Troubleshooting
| Symptom | Fix |
|---|---|
stripe_not_configured | Missing STRIPE_SECRET_KEY or STRIPE_PRICE_PRO on Worker |
invalid_stripe_signature | Wrong STRIPE_WEBHOOK_SECRET or raw body modified |
| Upgrade button missing | Need registered team key + free plan + Stripe configured |
billing_requires_registered_key | npm run issue:key:db (not legacy API_KEY only) |
| Plan stuck on free after pay | Check webhook delivery in Stripe Dashboard; events above subscribed |
Related
- BILLING.md — plans without Stripe
- YOUR-TURN.md — operator secrets
- public/docs/billing.html