Billing
OPanel’s built-in billing uses Stripe Checkout for signup and the Stripe Customer Portal for self-service billing. Card data never touches your servers, which keeps you in PCI SAQ-A scope. If you already bill through another system, skip Stripe entirely and drive subscriptions through the API instead.
1. Create products and prices in Stripe
Section titled “1. Create products and prices in Stripe”For each plan you sell, create a Stripe Product with one or more recurring Prices (monthly,
yearly, or both). Then, in Admin → Plans, add a matching price to the plan with the Stripe
price ID (price_...), the interval and the amount, which should match what you set in Stripe.
2. Configure OPanel
Section titled “2. Configure OPanel”Under Admin → Billing (or PATCH /api/v1/billing/settings):
| Field | Notes |
|---|---|
| Provider | stripe |
| Secret key | A restricted key (rk_...) or secret key (sk_...). Use sk_test_... until you’ve verified the whole flow. |
| Public URL | The https:// origin customers use, e.g. https://panel.example.com — Stripe redirects here after checkout and from the portal |
| Grace period | Days a past-due subscription keeps serving before suspension (default 7) |
Keys are encrypted with the installation’s master key before they’re stored.
3. Add the webhook endpoint
Section titled “3. Add the webhook endpoint”In Stripe → Developers → Webhooks, add an endpoint pointing at
https://<panel host>/api/v1/billing/stripe/webhook (the Billing settings page shows the exact
URL), subscribed to:
checkout.session.completedcustomer.subscription.createdcustomer.subscription.updatedcustomer.subscription.deletedcustomer.subscription.pausedcustomer.subscription.resumedinvoice.paidinvoice.payment_failedCopy the endpoint’s signing secret (whsec_...) into the Billing settings. Webhook events are
stored before processing and applied exactly once, so Stripe’s retries are harmless.
The subscription lifecycle
Section titled “The subscription lifecycle”| Stripe event | What happens |
|---|---|
checkout.session.completed |
Customer created, buyer becomes its owner, subscription created, first site created if a domain was entered at signup |
invoice.payment_failed |
Subscription goes past due; sites keep serving through the grace period |
| Grace period elapses | Subscription is suspended: sites show a suspended page, SSH is denied, data is kept |
invoice.paid |
Past-due or suspended subscriptions become active again |
customer.subscription.deleted |
Subscription is cancelled, scheduled for termination |
| Retention period elapses | Subscription is terminated and its sites deleted (files and database dumps kept in trash for the node’s own retention window) |
Every Stripe event is treated as a notification, not a command: the controller re-reads the subscription’s live state from Stripe and applies that, so late, duplicate or out-of-order webhook deliveries can never move a subscription backwards. A subscription suspended by staff, or for non-payment, stays that way until staff or payment resolve it — Stripe events alone never lift it.
External billing: WHMCS, Blesta and HostBill
Section titled “External billing: WHMCS, Blesta and HostBill”If you already run one of these, keep it: OPanel provides billing modules for WHMCS,
Blesta and HostBill, and a provisioning API for any other system. Subscriptions created
this way carry billingProvider: external; OPanel does not attempt to charge them, and the
external system drives every lifecycle transition — activation, suspension, cancellation — through
the same API endpoints staff use.
Point your billing system’s OPanel module at your panel’s API base URL and an API token scoped to
customer:manage, billing:manage and site:create; the module handles provisioning a customer,
subscription and site on order, and suspending or terminating on non-payment, the same way the
built-in Stripe integration does.
Closing a customer account
Section titled “Closing a customer account”Deleting a customer (DELETE /api/v1/customers/{id}) closes the account rather than erasing it —
sites, subscriptions and the audit log all refer to it. It’s refused while any subscription is
active, trialing or past due; cancel those first. A closed customer keeps read access to what
remains and can be reopened (POST /api/v1/customers/{id}/reopen), which does not revive
subscriptions that already finished terminating.
Reseller brands with their own Stripe account
Section titled “Reseller brands with their own Stripe account”A brand can bill through the platform’s Stripe account, or through an account of its own — see Brands.