Blog ·
ISPConfig billing automation
ISPConfig answers HTTP 200 to everything, including failures. What that means for a billing hook, and how the account lifecycle maps onto its remote API.
By Provibr
ISPConfig has a command for every moment where money and machines touch each other: create the account, suspend it, move it to another package, remove it. What it does not have is anything that decides when to run them.
Wiring the two halves together is mostly ordinary work. Two things about ISPConfig are not ordinary, and both of them will quietly break a first attempt: the status code of a call tells you nothing at all, and an account is not a customer.
The status code says nothing
Everything comes back as HTTP 200. Validation errors, "that does not exist", a wrong password, a method the remote user is not allowed to call: all of them, 200. The verdict is a code field inside the JSON body.
code | Meaning |
|---|---|
ok | It worked; response carries the outcome |
remote_fault | It failed: validation, missing object, authentication, permission. message explains |
invalid_method | Unknown method, or a method not in that class |
permission_denied | The remote user is missing that function |
Classify on code and on the text, never on the status. The reason this matters more for a billing integration than for anything else is the poll: a client that treats 200 as success reads an authentication failure as a successful empty inventory, and then marks every one of that customer's accounts as gone. One expired session, and your reconciliation deletes a hosting business.
The message field comes in two flavours, and the difference is useful. Validation errors from the panel's own form layer are a <br>-separated list of translation keys, not sentences: domain_error_unique for a domain that is already hosted. Checks in the remote library are ordinary English sentences: Invalid client_id 99999. Match the keys, quote the sentences.
One account is one website, not one client
ISPConfig separates two things that most control panels fuse. A client is a panel login with account-wide limits. A web_domain is one hosted site. One client can own several websites.
A Provibr hosting account is one website, identified by its domain. Creating one therefore needs an owner, so the agent creates a fresh client and a website under it, and derives the client login from the domain so it can always find the owner again.
That choice is what makes the delete path safe, and the delete path is where this integration nearly went wrong. More on that below.
The lifecycle, mapped
| Billing event | ISPConfig call | Answer on success |
|---|---|---|
| Order paid | client_add, then sites_web_domain_add | code: ok, twice |
| Invoice overdue | sites_web_domain_set_status(id, "inactive") | code: ok |
| Payment received | sites_web_domain_set_status(id, "active") | code: ok |
| Upgrade / downgrade | client_update with a new client template | code: ok |
| Cancellation | sites_web_domain_delete, then maybe the client | code: ok |
Suspending is idempotent in both directions
Suspension flips the website's active flag between y and n. There are no bitmasks and no partial states: a site is on or it is off.
A second suspend on an already suspended site answers code: ok with a response of 0, meaning zero rows changed. That is not an error, and a dunning run that suspends everything overdue, including what it suspended yesterday, has nothing to apologise for. It is worth appreciating how rare that is: as Automating HestiaCP billing and provisioning shows, the same second suspend there is an HTTP 401, indistinguishable from a wrong secret, on a panel that bans your address after five refused logins.
One thing to know about, even though the integration does not use it: ISPConfig also has a client-level lock, and setting it flips every website of that client to inactive at once. A customer who locks themselves in the panel, or a reseller lock above them, therefore shows up in the poll as suspended. That is correct, and it is not something your billing system did.
Changing a package is a real action here
An ISPConfig package is a client template: a named set of limits, built by an administrator in the panel. The remote API cannot create one, which is fine, because a plan should refer to a package rather than invent it. Provibr passes the package name and the agent looks up the template id.
That makes ISPConfig one of the control panels where a plan change is genuinely a plan change. CloudPanel and CyberPanel have to refuse it outright; here it happens and the limits move at once.
Two measured cautions if you write this yourself. Send a minimal update, carrying nothing but the new template: an update with the whole client record came back as remote_fault with no usable message, and so did one that combined an explicit template with explicit limits. And know that the template overrides limits you also send, which is the right way round but surprises people the first time.
The ready signal is server-global
Creating a website answers in milliseconds, and that answer means accepted, not done. The database row exists; the virtual host and the document root are laid down asynchronously by the server daemon.
The signal to wait on is the job queue count for that server, which was measured falling from three to zero over about eight seconds on a quiet box, and about twenty-three seconds on an earlier run. It is a global counter, not a per-command one. On a busy multi-tenant server it can stay above zero because of other customers' jobs, or reach zero while your work is still pending.
So the rule is a timeout with a truthful message rather than an infinite wait: poll until the queue is empty or until the create budget of 180 seconds runs out, and then report the account as created, because it is. "The site is still being set up" is a better thing to tell a paying customer than a spinner that never stops.
Deleting is a privacy question, not a cleanup question
This is the part of the article to keep if you keep nothing else, and it is not really about ISPConfig.
The delete path had a defect that no fixture could see: it compared two fields that are never actually equal, so the branch that removed the owning client was dead code. Every deletion left behind a panel login carrying the customer's name, e-mail address, contact details and password. The command answered ok, the website was gone, the counter went to zero, and the personal data of a person who had asked to leave stayed on the node.
Nothing above the panel can see that. A purge worker and a data-subject export clean up our side; they have no view of what is left on your hosting node. The rule that came out of it is worth stealing:
> Measure a delete path against the panel's own database, and never against "the command returned ok".
On ISPConfig that is four counts, and all four have to be zero: websites, clients, system logins, and the client directory on disk. And measure it with the panel rather than with your own test's cleanup helper, because the helper can carry the same misunderstanding and then the box looks clean while the integration leaks.
The safe deletion rule itself is one line: remove the website; remove the owning client only if it has no websites left. ISPConfig has a call that deletes a client and everything under them, and on an imported account that would take the customer's other sites with it. Never delete more than was asked.
Nothing is instant
Your panel 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 until its status is final: done, failed, timed out or cancelled. Reading never works this way, because an inventory or an invoice 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. Anything that creates something also accepts an idempotency key of your own making, so a retried webhook and a double click cost you nothing.
Where the invoice lives
Provibr does not invoice your customers, does not chase them, and does not know what you charge. That relationship is yours.
What Provibr bills is the capacity you reserve: how many hosting accounts you may run at once, at a per-unit price that falls in tiers as the count rises. For a hosting account there is no "stopped": there is active and suspended, and a suspended account does not count. So the dunning action you already wanted for commercial reasons is also the one that frees the slot you are paying for.
What is not automated
Provibr manages accounts, not the panel. It does not install ISPConfig, does not update it, and does not touch mailboxes, databases or DNS: a customer who deletes their own mailbox does so without anything above the panel knowing or caring. There is no console for a hosting account, no reinstall, and no partial suspension.
If you are earlier in this than the billing part, the companion piece is how to sell web hosting with ISPConfig, which covers the remote user, the scoping and the one-panel-several-servers model. The credentials, the certificate and the package screen are documented on the ISPConfig documentation page.
Frequently asked questions
Why does every ISPConfig API call return HTTP 200?
Because its remote API carries the verdict in the body rather than in the status line. The code field says ok, remote_fault, invalid_method or permission_denied. A client that trusts the status reads an authentication failure as a successful empty answer.
Is one ISPConfig account one customer or one website?
One website. ISPConfig keeps the client, which is a panel login with limits, separate from the web_domain, which is a hosted site, and one client can own several sites. A hosting account here is identified by its domain.
Can a billing system suspend the same ISPConfig site twice?
Yes. The second call answers ok with zero rows changed, which is not an error. Unsuspending is idempotent as well, so a nightly dunning run needs no state of its own.
Does deleting an ISPConfig account remove the customer's panel login?
It has to, and it is worth verifying on your own install. The correct behaviour is to remove the website and then the owning client only when that client has no websites left. A deletion that leaves the client behind leaves the customer's name, address and password on the node.
Can I change an ISPConfig package from a billing system?
Yes. A package is a client template, and assigning a different one moves the limits immediately. Send only the new template in the update; a fuller payload was measured failing without a usable error message.