Billing & plans
Multi-gateway billing with Stripe (cards) + manual offline payments (bank transfer, crypto, mobile money). Promo codes, multi-currency, one-time-per-account trials, plan swaps with prorated charges, add-ons stack on top.
Setup flow
- Currencies — confirm your default currency at
/admin/currencies. - Payment methods — add Stripe credentials at
/admin/payment-methods → New → Stripe. Publish webhook endpoint URL into Stripe Dashboard. - Billing plans — create at least one plan with monthly + yearly prices.
- Promo codes (optional) — percentage / fixed-amount discounts at
/admin/promo-codes. - Quota tiers — link each customer-group to a plan slug so the upgrade flow auto-assigns the correct tier.
Stripe configuration
- Admin → Payment methods → New → Stripe.
- Paste Publishable key + Secret key (test or live).
- Copy the displayed webhook URL — looks like
https://app.example.com/stripe/webhook. - In Stripe Dashboard → Developers → Webhooks → Add endpoint, paste the URL, select these events:
customer.subscription.createdcustomer.subscription.updatedcustomer.subscription.deletedinvoice.paidinvoice.payment_failedcharge.refunded
- Copy the Webhook signing secret back into KodMail.
- Save. Click Test connection — should report
OKfrom a one-shot probe ping.
/stripe/webhook is exempted from CSRF in bootstrap/app.php. Authenticity is verified via the signing secret — never disable that exemption.
Manual / offline payments
For markets where cards don't work — bank transfer, M-Pesa, crypto, Wise, etc:
- Admin → Payment methods → New → Manual.
- Name (e.g. Bank transfer).
- Instructions HTML — what the customer sees on checkout.
- Dynamic fields — reference number / proof-of-payment screenshot / etc. The customer fills these at checkout.
- Customer submits → admin sees in Admin → Manual payments queue → Approve / Reject.
- On approval, the customer's plan activates immediately.
Plans + prices
Each plan has 1..N prices (monthly / quarterly / yearly etc):
Creating a plan
- Admin → Billing plans → New plan.
- Name + slug (used in URLs).
- Customer group link — when a customer subscribes, their
customer_group_idauto-updates to match. - Features (string list) — shown in the pricing card on the landing page.
- Add price points: monthly ($X), yearly ($Y), each with trial days.
- Sync to Stripe (creates Stripe Price IDs). Manual gateways skip the sync.
Trial behaviour
Trial is once per account lifetime (industry standard — Linear, Vercel, Notion). The first plan a customer takes that has trial_days > 0 triggers the trial. trial_used_at stamps the customer's record; subsequent upgrades charge immediately at full price.
Admin escape hatch: reset trial under Admin → Customer → Edit → Reset trial.
Promo codes
| Field | Meaning |
|---|---|
| Code | What the customer types at checkout (case-insensitive). |
| Type | percentage or fixed. |
| Value | e.g. 20 for 20% off, or 500 for $5 fixed off (cents). |
| Max redemptions | Total usage cap across all customers. |
| Max per customer | How many times one customer can use it. |
| Plan scope | Restrict to specific plan slugs. |
| Expires at | Datetime cutoff. |
Plan swap (upgrade / downgrade)
When a customer moves between plans:
- Stripe → Stripe — Cashier proration. Charge today = new plan price minus unused credit from old plan.
- Manual → Stripe — manual sub deleted; new Stripe sub created. Unused manual credit applied as Stripe customer balance.
- Stripe → Manual — Stripe sub cancelled; customer submits new manual payment. New plan activates on admin approval.
- Same gateway, same plan, different price — direct price update, prorated.
Financial report
Admin → Financial report shows:
- KPIs — gross / refunds / net / transactions / paying customers / ARPU with period-over-period delta.
- MRR + ARR normalised across cycles (yearly subs ÷ 12).
- Per-plan MRR breakdown.
- Per-gateway revenue — Stripe vs manual vs others.
- Payment health — success rate, awaiting approval queue, recent failures.
- Subscription state — active / trialing / past due / cancelling / cancelled / incomplete + churn rate.
- Daily revenue chart for 30/90/365 day windows.
documentation/screenshots/admin-financial.png/admin/financial — MRR, ARR, gateway breakdown, churn.
Cancellation + grace period
Customer cancels:
- Subscription stays active until current period ends.
- At
ends_at, status →canceled. - For
billing.grace_period_daysdays more, customer has read-only access (lists / reports visible, sending blocked). - After grace, customer must subscribe again to send.