Blog ·
How to sell web hosting with HestiaCP
HestiaCP has an API but no ordering, invoicing or capacity layer. What its API actually returns, where it bites, and how to build a hosting product on top.
By Provibr
HestiaCP is the healthiest open control panel around. Measured on GitHub on 21 August 2026: 4,481 stars, 345 contributors, and three releases inside a single week. It gives you accounts, packages, DNS, mail, databases and a clean interface, and it costs nothing.
What it does not give you is a way to sell any of that. There is no order form, no invoice, no dunning, no "this customer stopped paying, switch them off". Search for hestiacp billing and, measured the same day, the top results are forum threads: people asking how to sell HestiaCP accounts, and other people answering with half a script.
This page is about the layer above: what HestiaCP's API really returns, where it will bite you, and what has to be right before you can put a Buy button in front of a stranger.
What HestiaCP already gives you
Two things, and they are the right two.
Packages. A HestiaCP package describes what an account may use: disk space, bandwidth, and how many domains, databases and mailboxes. That is your product catalogue, defined once and referred to by name from everywhere else.
An account lifecycle that maps onto a subscription. Create, suspend, unsuspend, move to another package, delete. Those are exactly the states a paid hosting account moves through, and there is a command for each:
| What you sell | HestiaCP command |
|---|---|
| Order placed and paid | v-add-user + v-add-web-domain |
| Invoice overdue | v-suspend-user |
| Invoice paid after all | v-unsuspend-user |
| Upgrade or downgrade | v-change-user-package |
| Cancellation | v-delete-user |
The rest of this page is about the gap between that table and something you can rely on at three in the morning.
The API has two doors, and one of them lies
POST https://host:8083/api/ accepts two kinds of credentials, and which one you send decides which code path handles your request. The old path takes a username and password. The newer path, available since HestiaCP 1.6, takes an access key: a 20-character id and a 40-character secret.
They do not behave the same. Sending a command that does not exist:
| Path | HTTP | Hestia-Exit-Code | Body |
|---|---|---|---|
| Access key | 401 | 10 | Error: Command v-does-not-exist not found |
| Legacy password | 200 | (header absent) | OK |
The legacy path reports success for a command that was never run. The cause is in HestiaCP's own source: it echoes OK when the authentication returned zero and the command produced no output on stdout. It never looks at the command's own exit code, and never sets an HTTP status or the exit-code header for the result. Any command that fails quietly comes back as OK.
So: use access keys. Not only because the legacy path can lie, but because a key can be revoked on its own, is listed in the panel by name, and does not put your panel's admin password in someone else's vault. Do not build a fallback from one to the other either: two doors means two failure surfaces, and here the fallback door is the unreliable one.
The HTTP status is not the answer: the exit code is
HestiaCP folds about twenty internal exit codes onto four HTTP statuses. Two consequences, both measured:
A successful mutation returns 204, not 200. Every v-add-user, v-suspend-user, v-delete-user, v-add-web-domain and v-change-user-package comes back as HTTP 204 with an empty body. A client that only accepts 200 rejects every call that worked.
HTTP 401 means four different things: wrong secret (exit 9), key outside its permissions (10), address not allowed (10), and "this user is already suspended" (5). A client that reads 401 as "credentials are wrong" will tell you your key expired when all you did was suspend an account twice.
Read Hestia-Exit-Code. The status is a derivative.
The one that will actually take you down
Five refused calls and HestiaCP bans your address. This is not a rate limit you can back off from. It is fail2ban, configured by default with maxretry = 5 against /var/log/hestia/auth.log, and it ends in an iptables DROP. After that the panel does not refuse your calls, it stops answering them: your monitoring shows timeouts and it looks like a network problem.
The part that catches people is what counts as a failed login. Not only a wrong secret: HestiaCP's key check writes the same failed to login line when a key is used for a command outside its permission set. Measured, in the panel's own auth log:
12:57:52 <key> 84.85.173.142 v-list-users successfully launched
12:57:52 <key> 84.85.173.142 failed to login ← a command outside the key's scopeThree rules follow, and they are not optional:
- Never retry automatically after an authentication or permission error. A retry loop on a wrong key locks you out within a minute, and by then the message your customer sees is "panel unreachable" instead of "key is wrong", the worst possible signal, because it points at the wrong problem.
- The key must be unscoped, and therefore the admin's. Empty permissions means every command, and HestiaCP only accepts that for the root user. The bundled
billingpermission set comes close but leaves outv-list-user-packagesandv-list-web-domains, the two commands that read your package catalogue and an account's domains. With that set both come back empty, and a scoped key hitting them once per poll cycle bans you on schedule. - Your error text has to mention this, because clearing it needs a shell on the panel:
v-delete-firewall-ban <address> HESTIA.
Empty means nobody
One more first-run trap. HestiaCP keeps a list of addresses allowed to use the API, in API_ALLOWED_IP. On a fresh install that list is empty, and empty here means nobody, not everybody. Every call comes back 401 with IP is not allowed to connect with API while your key is perfectly fine. This is the most likely first failure of the whole integration, and it deserves its own error message rather than a generic "authentication failed".
Creating an account takes two calls, and that gap is real
DirectAdmin, cPanel and Virtualmin create an account together with its domain in one call. HestiaCP does not:
v-add-user USER PASSWORD EMAIL [PACKAGE] [NAME] → creates the user
v-add-web-domain USER DOMAIN [IP] [RESTART] [ALIASES] → attaches the domainIf the second call fails, because the domain already belongs to someone or its format is rejected, you are left with a real unix user, with a home directory, no website, and a row in every count you run. Measured with a deliberately invalid domain: the user existed afterwards with U_WEB_DOMAINS = 0. Anything you build has to compensate: if step two fails, delete the user and report the original error.
What Provibr does with all of this
Provibr connects to HestiaCP through an agent that runs inside your own network and dials outbound, so nothing on the panel has to be exposed. You give it four things: the panel address including port 8083, the access key id, the secret, and the SHA-256 fingerprint to pin to while the panel still uses its self-signed certificate. The agent keeps those in its own vault on your host; the platform cannot read them back.
From there:
- Packages become sellable. A web hosting plan points at one package on one integration, under a name of your own, at a price of your own.
- Five actions, addressed by username: create with a domain and a package, suspend, resume, move package, delete. The two-call create is compensated as described above.
- Usage comes free with the poll. Every read of
v-list-userscarriesDISK_QUOTA,U_DISK,BANDWIDTHandU_BANDWIDTHfor every account, all in megabytes, so limits and usage land on the server page and in the graphs without a second call that could lag behind the first. - Accounts that already exist can be imported. The agent lists what is on the panel and you tick which of them become servers in Provibr.
What it does not do
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; those stay in HestiaCP, where you and your customers keep using them. There is no console for a hosting account, because there is nothing to attach one to.
And one honest performance note: reading a panel with many accounts costs one extra call per account, because HestiaCP has no command that lists every account's domains at once. Measured on 26 accounts, serially that is roughly 8.5 seconds; in parallel it comes back under two. If you write this yourself, do not do it serially. At around 60 accounts a serial poll starts colliding with its own next round.
You do not have to pick one panel and live with it. Provibr runs its control panels, hypervisors and game panels side by side today. They share one inventory, one permission model and one activity trail. HestiaCP for the customers who want an open panel, something else for the ones who ask for cPanel by name, and one place where you can see both.