Blog ·
Automating HestiaCP billing and provisioning
From a paid order to a live HestiaCP account, and from an overdue invoice to a suspended one. The commands, the measured traps, and where the invoice lives.
By Provibr
An automated hosting product has exactly three moments where money and machines touch each other:
- The order is paid. An account has to exist, with the right package, within seconds.
- The invoice is overdue. The account has to go dark, and come back the moment it is paid.
- The subscription ends. The account has to be removed, for real, including its data.
HestiaCP has a command for every one of those. What it does not have is anything that decides when to run them. This page is about wiring the two halves together, and about the specific places where HestiaCP's API answers in a way that will quietly break a first attempt.
The three moments, and the commands under them
| Moment | HestiaCP | Answer on success |
|---|---|---|
| Paid order | v-add-user then v-add-web-domain | 204, empty body (twice) |
| Overdue | v-suspend-user | 204, empty body |
| Paid after all | v-unsuspend-user | 204, empty body |
| Upgrade / downgrade | v-change-user-package | 204, empty body |
| Cancellation | v-delete-user | 204, empty body |
Note the answer column. A successful mutation on HestiaCP is HTTP 204 with nothing in the body. Not 200. A client written against the assumption that success is 200 rejects every call that worked, and then usually retries, which is how a working integration turns into a locked-out one (see below).
The real answer lives in a response header, Hestia-Exit-Code, which carries a number out of a fixed table. The HTTP status is derived from it and collapses about twenty codes onto four statuses. Read the header; treat the status as a hint.
Suspension is not symmetric, and it matters here more than anywhere
This is the trap that specifically hits a billing integration, because a billing integration is the only thing that suspends the same account twice.
Measured, on the same account:
v-suspend-user on an active account → 204, exit 0
v-suspend-user on a suspended account → 401, exit 5 ("user X is suspended")
v-unsuspend-user on a suspended account → 204, exit 0
v-unsuspend-user on an active account → 204, exit 0Unsuspending is idempotent. Suspending is not. And the failure it produces is HTTP 401, the same status you get for a wrong secret, an out-of-scope key, and a blocked address.
So a dunning run that suspends everything overdue, including things it already suspended yesterday, will report authentication failures for accounts that are in exactly the state you wanted. Exit code 5 on v-suspend-user means "already that way" and has to be read as success.
Never retry on an authentication error
HestiaCP bans an address after five refused logins, using fail2ban with maxretry = 5 and an iptables DROP. Once that fires, calls stop timing out at the application level and start timing out at the network level: your monitoring says the panel is unreachable, and nothing in your logs points at a credential problem.
For a billing integration this matters more than for anything else, because a billing integration is what runs unattended at night. Two consequences:
- Exit codes 9, 10 and 21 must never trigger an automatic retry. Those are wrong secret, permission refused, and API disabled respectively. None of them is a transient failure, and all three count towards the ban.
- A command outside the key's permissions also counts as a failed login. That is not obvious: the same log line is written for a wrong secret and for a valid key reaching for a command it is not allowed to run. A key scoped with HestiaCP's bundled
billingpermission set is missingv-list-user-packagesandv-list-web-domains, so a poll cycle that reads packages and domains burns through the ban counter on schedule. Use an unscoped key for the admin user.
Clearing a ban needs a shell on the panel host: v-delete-firewall-ban <address> HESTIA. If the software you build cannot say that sentence, whoever is on call at three in the morning will spend the night on the wrong problem.
A paid order takes two calls, so it needs a rollback
HestiaCP has no command that creates a user and attaches a domain at once. That leaves a window:
v-add-user provibrtest1 … → 204, exit 0
v-add-web-domain provibrtest1 "not a valid domain" → 422, exit 2
v-list-user provibrtest1 → the account exists, with zero domainsA real unix user, with a home directory, no website, and a row in every count. If the customer's domain was already taken on your panel (which happens, because customers move), the order fails and you are left holding a half account that nobody will ever notice.
The rule is simple and it has to be built in: if step two fails, delete the user and report the original error. A refused domain must never leave half an account behind.
Provibr does exactly that, and the error the customer sees is the one HestiaCP gave for the domain, not a secondary error about a cleanup.
Where the invoice actually lives
Worth being explicit, because it is the most common misunderstanding: Provibr does not invoice your customers. It does not send them a bill, does not chase them, and does not know what you charge them. That relationship is yours.
What Provibr bills is the capacity you reserve: how many hosting accounts you may run at once, per month, at a tiered unit price. What you charge your own customers for a HestiaCP account, and how, stays entirely inside your own billing system.
There are two ways to connect the two halves:
A WHMCS module. If you already run WHMCS, install the Provibr provisioning module and point one WHMCS product at one Provibr web plan. A paid order creates the account; a suspension in WHMCS suspends it; a termination removes it. Your customer keeps one account, one invoice and one login. The module is a plain zip you install yourself, and it runs inside your WHMCS; there is nothing to enable on our side.
The REST API. If you run FOSSBilling, Blesta, something in-house, or a spreadsheet and a cron job, drive it directly. One key, one base URL, and exactly the permissions the member behind the key already has.
The one rule for driving it from your own code
Everything that touches a machine is asynchronous, and it has to be. Your infrastructure is reachable only through the agent inside your own network, and that agent dials outbound. So an endpoint that changes an account cannot answer with the result. It answers that the order was accepted and hands you a command to follow, which you read until its status is final: done, failed, timed out or cancelled. A failed command carries the agent's own error code with its arguments, the same one the panel renders on screen.
Reading never works this way. An inventory, an address or an invoice answers immediately, because that answer comes from a database and not from your host.
The practical consequence for a billing hook: do not mark the order complete on the accepted answer. On HestiaCP, v-add-user takes around a second and v-delete-user around three, so the wait is short, but the answer you get first is "we sent it", not "it worked".
Suspension is also your own cost control
One connection that is easy to miss. In Provibr a suspended server does not count towards your capacity, and for a hosting account there is no "stopped": there is active and suspended.
So the dunning action you already wanted to run for commercial reasons is also the one that frees the slot you are paying for. Suspend an overdue account and it stops counting immediately; delete it and it stops counting too; leave it running and unpaid and you are paying for it.
What this does not automate
Provibr manages accounts, not the panel. It does not install HestiaCP, does not update it, does not change server settings, and does not touch DNS, mailboxes or databases; a customer who deletes their own mailbox through HestiaCP does so without anything above it knowing or caring. There is no console for a hosting account. And there is no partial suspension: an account is active or it is not.
If you also sell VPS, game servers or cPanel accounts, the same three moments apply to all of them, and they can live in one inventory. Provibr runs its control panels, hypervisors and game panels side by side today. They share one set of permissions and one activity trail across the lot.