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

  1. Currencies — confirm your default currency at /admin/currencies.
  2. Payment methods — add Stripe credentials at /admin/payment-methods → New → Stripe. Publish webhook endpoint URL into Stripe Dashboard.
  3. Billing plans — create at least one plan with monthly + yearly prices.
  4. Promo codes (optional) — percentage / fixed-amount discounts at /admin/promo-codes.
  5. Quota tiers — link each customer-group to a plan slug so the upgrade flow auto-assigns the correct tier.

Stripe configuration

  1. Admin → Payment methods → New → Stripe.
  2. Paste Publishable key + Secret key (test or live).
  3. Copy the displayed webhook URL — looks like https://app.example.com/stripe/webhook.
  4. In Stripe Dashboard → Developers → Webhooks → Add endpoint, paste the URL, select these events:
    • customer.subscription.created
    • customer.subscription.updated
    • customer.subscription.deleted
    • invoice.paid
    • invoice.payment_failed
    • charge.refunded
  5. Copy the Webhook signing secret back into KodMail.
  6. Save. Click Test connection — should report OK from a one-shot probe ping.
CSRF + webhook /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:

  1. Admin → Payment methods → New → Manual.
  2. Name (e.g. Bank transfer).
  3. Instructions HTML — what the customer sees on checkout.
  4. Dynamic fields — reference number / proof-of-payment screenshot / etc. The customer fills these at checkout.
  5. Customer submits → admin sees in Admin → Manual payments queue → Approve / Reject.
  6. On approval, the customer's plan activates immediately.

Plans + prices

Each plan has 1..N prices (monthly / quarterly / yearly etc):

Creating a plan

  1. Admin → Billing plans → New plan.
  2. Name + slug (used in URLs).
  3. Customer group link — when a customer subscribes, their customer_group_id auto-updates to match.
  4. Features (string list) — shown in the pricing card on the landing page.
  5. Add price points: monthly ($X), yearly ($Y), each with trial days.
  6. 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

FieldMeaning
CodeWhat the customer types at checkout (case-insensitive).
Typepercentage or fixed.
Valuee.g. 20 for 20% off, or 500 for $5 fixed off (cents).
Max redemptionsTotal usage cap across all customers.
Max per customerHow many times one customer can use it.
Plan scopeRestrict to specific plan slugs.
Expires atDatetime cutoff.

Plan swap (upgrade / downgrade)

When a customer moves between plans:

Financial report

Admin → Financial report shows:

Financial report · documentation/screenshots/admin-financial.png

/admin/financial — MRR, ARR, gateway breakdown, churn.

Cancellation + grace period

Customer cancels:

  1. Subscription stays active until current period ends.
  2. At ends_at, status → canceled.
  3. For billing.grace_period_days days more, customer has read-only access (lists / reports visible, sending blocked).
  4. After grace, customer must subscribe again to send.