Skip to content

Blog ·

Automating Plesk billing and provisioning

From a paid order to a live subscription, and from an overdue invoice to a dark one. The calls, the measured timings, and four traps that only bite unattended.

By Provibr

An automated hosting product has three moments where money and machines touch each other:

  1. The order is paid. A subscription has to exist, on the right service plan, within seconds.
  2. The invoice is overdue. The subscription has to go dark, and come back the moment it is paid.
  3. The subscription ends. It has to be removed, for real, including its data.

Plesk has calls for all three. What it does not have is anything that decides when to run them, and that is the half you are building. This page is about wiring the two together, and about four behaviours that are harmless when you click them by hand and dangerous when a scheduler runs them at three in the morning.

Everything here was measured against a running Plesk Obsidian 18.0.80 server in August 2026.

The three moments and the calls under them

MomentAPICallMeasured duration
Paid orderRESTPOST /clients then POST /domains0.6 to 0.7 s + 4.1 to 4.5 s
OverdueXML<webspace><set> with gen_setup.status0.78 s
Paid after allXMLthe same call, status back to 00.78 s
Upgrade or downgradeXML<webspace><switch-subscription>1.5 s
CancellationRESTDELETE /domains/{id} then DELETE /clients/{id}7.7 s +
Reading stateXML<webspace><get> for the whole server0.33 s

Two of those numbers shape the design. A subscription takes four and a half seconds to create and nearly eight to delete. That is long enough that an order form has to say something is happening, long enough that a timeout budget written for a fast API will cut a working call off halfway, and long enough that two orders arriving together are genuinely concurrent rather than theoretically so.

Note the read row, too. One XML call returns every subscription with its plan, status, quota and usage in a third of a second, regardless of how many there are, and a billing system reconciles against the panel constantly.

The suspend trap is a billing bug, not an API detail

Plesk offers two ways to suspend, and they write different fields. In a hosting product this stops being trivia.

Measured, on the same subscription:

What you doXML gen_info.statusREST subscription status
nothing0active
PUT /domains/{id}/status to suspended0suspended
XML <webspace><set> status 1616suspended

A dunning run that suspends over REST writes a field the bulk read does not look at, so your platform keeps reporting the account as active. Here is why that hurts billing specifically: when the invoice is finally paid, your automation looks at its own state, sees an account that was never suspended, and does not resume it. The customer has paid and stays dark, and nothing has recorded an error.

The rule is one line: suspend through the field you read. Provibr writes <webspace><set><gen_setup><status>, after which both views agree.

gen_info.status is a bitmask; we measured 0 for active, 1 for webspace-off and 16 for disabled by the administrator. Treat anything non-zero as suspended, the only reading that survives a value nobody has seen yet.

One honest gap: we did not measure suspending an already-suspended subscription. On HestiaCP that case is not idempotent and returns an authentication-shaped error, exactly what a dunning run trips over daily, so establish it on your own server before relying on it.

An expired key looks exactly like an empty server

This is the failure that can do real damage, and it takes one sentence to describe.

The XML API returns HTTP 200 for a wrong key. The error sits inside the document, as code 11003. A client that checks the status code therefore reads an authentication failure as a successful round that found no subscriptions.

Now put that inside a nightly reconciliation job. A successful round that found nothing means, to any hosting platform, that every account there yesterday has been deleted. One expired key, and overnight your system marks your entire customer base as gone.

Two rules come out of it, and they apply to any panel that reports failure with a 200, which, of the panels we have integrated, is most of them:

  • On the XML API the status code carries no information at all, not even about authentication. Parse the document.
  • An unreadable answer is unknown, never empty. An empty server does exist, and it produces a single empty result rather than an error. Those two are genuinely different, and telling them apart is the whole job.

REST fails differently again: an unusable key comes back as HTTP 401 with a completely empty body. So a connection test has to probe both surfaces and report them separately. A green REST result with a silent XML API means the account list, the plan list and every plan change will stay empty, and the tempting conclusion is that the customer has no accounts.

The error code does not tell you what went wrong

Plesk's REST errors carry a numeric code, and it is tempting to switch on it. Measured, it does not classify:

  • 1007 covers both "that domain already exists" and "that client login already exists".
  • 1013 covers both "there is no such service plan" (REST) and "domain does not exist" (XML).
  • 0 means no code, not no error.

For a billing integration the damage lands in retry logic. "Domain already exists" after a paid order may mean the previous attempt half-succeeded and you should look before trying again; "no such service plan" means the order will never succeed and belongs in front of a human. They share a family of codes. So the text classifies, and that mapping belongs in exactly one place in your code rather than being re-derived at every call site.

There is a fifth failure shape as well: ask REST for a route that does not exist and it answers with an HTML page, not JSON.

A failed order can block the retry

Measured: creating a domain against a service plan that does not exist returns HTTP 404 with a clear message, and the domain object exists afterwards anyway, with no hosting type and an empty document root. Plesk creates the object first and applies the plan second, and does not roll back when the second step fails.

In a billing flow that reads: the customer paid, provisioning failed, and the retry now fails differently, because the domain is in use by an account they never received. The second failure looks like a duplicate order, which is one of the more confusing things to debug from a support ticket.

Two cheap things follow. Resolve the plan before you create anything. You already have the plan list, so look the name up locally and fail the order before touching the panel, which removes this failure rather than handling it. And clean up after a failed create, because leaving the wreckage turns one failed order into a permanently blocked domain name.

For contrast, a field validation error such as a duplicate client login returns HTTP 400 and leaves nothing behind. That check runs before creation; only the plan lookup happens too late to save you.

Two calls that each have a middle

Removing a subscription means DELETE /domains/{id} and then DELETE /clients/{id}. If the first succeeds and the second does not, you are left with a client record that owns nothing: invisible in most views, and a duplicate-login error the next time that customer comes back. At 7.7 seconds the delete is also long enough for a network to interrupt it, so cancellation deserves the same treatment as provisioning: read the panel back afterwards and reconcile against the bulk read, not against the response you got.

Upgrades and downgrades have their own corner. <webspace><switch-subscription> does not accept a plan name, only the plan's GUID or external ID, where everywhere else in Plesk a package is referenced by name. Provibr keeps the name as the thing humans see and does the name-to-GUID translation internally from the plan list the poll already fetches. Storing a GUID in your billing system instead works right up until somebody rebuilds a plan.

Where the invoice lives

The three moments above are only half a hosting product. The other half is what your own bill looks like.

You pay for capacity, per resource type, and a hosting account is one of them. Every subscription counts whether it is running or not; only a suspended one does not. That is the right way round for dunning. Suspending an overdue customer frees the slot while their data stays untouched, and resuming takes it again.

Going over your limit is not blocked at the moment it happens. It is detected, you are told which resource type and by how much, and you get a working day to fix it. If that expires, the newest servers above the limit are switched off, newest first, so what you built earliest survives. A second overage inside a week gets an hour instead of a day, because otherwise the grace period could be renewed forever by dipping under the limit and going over again. Nothing is started back up for you when it is resolved: switching a machine on is a decision, and the platform does not make it while nobody is watching.

Each period produces a proforma you can read and print, with the definitive invoice arriving from the bookkeeping as a file. An invoice line records the package name as it was at the moment of invoicing, so renaming a plan later does not rewrite history.

What Provibr does, and what it deliberately does not

Provibr sits between your billing and your Plesk server. A subscription is an ordinary row: order it, suspend it, resume it, switch its service plan, delete it. Suspension goes through the field the poll reads. The plan is resolved before the create, and a failed create is cleaned up. Every mutation ends by reading the subscription back, so what is stored is what was observed rather than what was requested. The agent dials outward from your own network, so port 8443 does not have to face the internet.

What it does not do: manage the Plesk server itself, or reach inside a subscription. It also never asks Plesk for the datasets containing the FTP password or the customer's password hash, both of which the panel hands over in plain text if you request them.

Measured against Plesk Obsidian 18.0.80 in August 2026, with responses recorded as fixtures. Repeated suspension, reseller-scoped keys and behaviour under real traffic load were not measured, and are marked as such above.

Start with one host and one agent

Create an account, install the agent with a single command and connect your first Proxmox host. The trial runs for 14 days and asks for no payment details.