Orders and payment¶
Ordering a server, an upgrade or extra bandwidth creates an order. The order pays for itself from the project owner's account credit when the credit covers it. Otherwise it waits while its invoice is paid in the console. You follow an order from its invoice to the server it delivers.
Endpoints¶
| Endpoint | What it does |
|---|---|
GET / |
List products |
GET / |
List images |
GET / |
List locations |
GET / |
Get pricing |
POST / |
Order a server |
GET / |
List upgrade options |
POST / |
Upgrade a server |
POST / |
Buy extra bandwidth |
GET / |
List orders |
GET / |
Get an order |
How ordering works¶
- Look up what you can order: products, images, locations and prices.
- Place the order, with an
Idempotency-Key. The answer is201with the order. - The order is paid from the project owner's credit if it covers the invoice. Otherwise it waits in
awaiting_payment, with a link to the invoice. - Poll the order until it is
completed. A server order then has the new server's id inserver_id.
Ordering, upgrading and buying bandwidth need the admin or owner role in the project, and are billed to the project owner's account. Orders have a rate limit of their own: 10 a minute per key.
Payment¶
When the order's invoice is created, the project owner's account credit is applied to it if the credit covers what the invoice still owes. The order then moves on by itself.
If the credit doesn't cover it, the order waits in awaiting_payment, and invoice.url links to the invoice, which the project's owner pays in the console. Once it is paid, the order continues, and polling it shows the progress.
The credit used is the project owner's. The owner sees its balance in billing_account.credit_balance of the current user.
The order object¶
{
"id": 55,
"type": "server",
"status": "awaiting_payment",
"server_id": null,
"request": {
"hostname": "web-02", "product_id": 123, "location": "AMS", "image": "almalinux10",
"billing_cycle": "monthly", "ipv4_additional": 0, "ipv6": true,
"ssh_keys": [3731], "snapshot_id": null, "promo_code": null, "has_user_data": false
},
"invoice": {"id": 777, "status": "unpaid", "total": "12.00", "currency": "USD", "url": "https://console.hosthatch.com/billing/invoice/777/"},
"warnings": [],
"created_at": "2026-10-19T10:00:00Z",
"updated_at": "2026-10-19T10:00:00Z"
}
| Field | Type | Description |
|---|---|---|
id |
integer | The order's id. |
type |
string | server, upgrade or bandwidth. |
status |
string | See Order statuses. |
server_id |
string or null | The server. For a server order it is set once the server exists; for an upgrade or bandwidth order, from the start. |
request |
object | What you ordered. For a server, the order's fields with the billing cycle it got, and has_user_data in place of user_data, which is never stored. For an upgrade, product_id and upgrade_disk. For bandwidth, size_gb. |
invoice |
object or null | The invoice: id, status as billing last showed it, such as unpaid, total, currency, and url, where the project's owner can pay it. null while the order has none. |
warnings |
array | Things that went wrong after billing took the order, each with a code and a message. credit_unconfirmed means paying the invoice from account credit couldn't be confirmed, and support checks it. |
created_at, updated_at |
string | When the order was placed, and last changed. |
Order statuses¶
| Status | Meaning |
|---|---|
submitting |
Being placed. It lasts a few seconds. |
awaiting_payment |
Its invoice waits to be paid. Pay it at invoice.url. |
provisioning |
Paid, and being delivered: the server is being set up, or the bandwidth added. |
completed |
Delivered. A server order has its server_id. |
cancelled |
Cancelled in billing: its invoice was cancelled or refunded, or the new server's service was cancelled. |
failed |
Billing refused the order. Nothing was charged, unless its invoice shows paid: then contact support instead of ordering again. |
unknown |
Billing didn't confirm the order when it was placed, so whether it went through is being checked. It is never placed again. Poll it: it moves on once that is settled, by support if billing can't tell. Don't order again meanwhile. |
The catalogue¶
What you can order, as the project's owner can order it. The catalogue can be up to 5 minutes old. The examples' prices are illustrations: the endpoints answer with yours.
List products¶
GET /
The products you can order.
{
"id": 123,
"name": "Cloud VPS 2x4",
"category": "Cloud Servers",
"description": ["2 vCPU", "4 GB RAM", "80 GB NVMe"],
"prices": {"monthly": "12.00", "quarterly": "34.00", "annually": "120.00"},
"setup_fees": {"monthly": "0.00"},
"currency": "USD",
"billing_cycles": ["monthly", "quarterly", "annually"],
"default_billing_cycle": "monthly",
"locations": [{"code": "AMS", "in_stock": true, "upgrade_in_stock": true}],
"images": ["almalinux10", "ubuntu24"]
}
locations says where the product is in stock for new servers (in_stock) and for upgrades (upgrade_in_stock). images lists the images it can be installed with.
List images¶
GET /
The images a server can be installed with. name is what an order and a reinstall take.
{"name": "almalinux10", "distro": "AlmaLinux", "version": "10", "display_name": "AlmaLinux 10"}
List locations¶
GET /
The locations that at least one product is offered in. code is what an order takes.
{"code": "AMS", "city": "Amsterdam", "country": "The Netherlands", "country_code": "NL"}
Get pricing¶
GET /
What a new server's extras cost on top of its product's price: additional IPv4 addresses, IPv6 and extra bandwidth.
{
"currency": "USD",
"ipv4_additional": [
{"count": 0, "prices": {"monthly": "0.00"}, "setup_fees": {"monthly": "0.00"}},
{"count": 1, "prices": {"monthly": "2.00"}, "setup_fees": {"monthly": "0.00"}}
],
"ipv6": {"prices": {"monthly": "0.00"}, "setup_fees": {"monthly": "0.00"}},
"extra_bandwidth": [{"size_gb": 500, "price": "2.00"}, {"size_gb": 1000, "price": "4.00"}]
}
ipv4_additional has an entry for each count of additional addresses an order can ask for, starting at 0. Each entry, and ipv6, has a recurring price and a setup fee per billing cycle. ipv6 is null when no product offers IPv6. These are the prices of the first product that offers each extra: billing charges a product that prices them differently its own price.
extra_bandwidth lists the sizes Buy extra bandwidth takes, each with its one-time price.
Ordering¶
Order a server¶
POST /
Order a new server in the project.
curl -X POST "https://console.hosthatch.com/api/v1/projects/acme/servers/" \
-H "Authorization: Bearer $HOSTHATCH_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 3b9f7c2e-8d41-4a6f-b0c5-9e2a7d1f4c88" \
-d '{
"hostname": "web-02", "product_id": 123, "location": "AMS", "image": "almalinux10",
"billing_cycle": "monthly", "ssh_keys": [3731], "ipv4_additional": 0, "ipv6": true,
"promo_code": null, "snapshot_id": null, "user_data": null
}'
| Field | Type | Description |
|---|---|---|
hostname |
string | Required. The same rules as renaming a server. |
product_id |
integer | Required. A product's id. |
location |
string | Required. A location code where the product is in stock. |
billing_cycle |
string | One of the product's billing_cycles. Leave it out for its default_billing_cycle. |
image |
string or null | One of the product's images. Send image or snapshot_id: one of them, not both. |
snapshot_id |
integer or null | A completed snapshot of the project to create the server from, instead of an image. |
ssh_keys |
array of integers | Ids of the project's SSH keys to install, up to 100. |
ipv4_additional |
integer | How many IPv4 addresses to add to the first one, 0 by default. Pricing lists the counts. |
ipv6 |
boolean | Give the server an IPv6 subnet. |
promo_code |
string or null | A promotion code: letters, digits, hyphens and underscores, up to 64 characters. |
user_data |
string or null | Not available yet: sending it answers 400 feature_unavailable. Cloud-init user data, up to 65536 characters, starting with #cloud-config or #!. |
A choice the product doesn't offer answers 400 validation_error on that field: a location without stock, a billing cycle it isn't sold with, an image it doesn't take, more additional addresses than it allows, IPv6 it can't have, a snapshot whose image it can't be ordered with, or a key or snapshot that isn't the project's. The answer is 201 with the order, of type server.
To put the server on its location's private network, start its enable-private-network action once the order is completed.
List upgrade options¶
GET /
The products the server can be upgraded to, with their prices. One that isn't in_stock can't be ordered in the server's location now.
{"product_id": 41, "name": "Cloud VPS 4x8", "prices": {"monthly": "24.00"}, "currency": "USD", "in_stock": true}
Upgrade a server¶
POST /
Move the server to one of its upgrade options. It keeps its billing cycle, and billing invoices what is left of the current term.
{"product_id": 41, "upgrade_disk": true}
| Field | Type | Description |
|---|---|---|
product_id |
integer | Required. One of the server's upgrade options. Any other answers 400 validation_error. |
upgrade_disk |
boolean | Also grow the server's disk to the new plan's size when the upgrade is applied. Left out or false, the disk keeps its size. |
The answer is 201 with an order of type upgrade, which completes once its invoice is paid. If the server then shows pending_changes: true, a reboot through the API applies the new plan.
A server takes one upgrade at a time. While one is unpaid or its outcome is unknown, another answers 409 conflict, with the open order's id in order_id when it is the project's. An option that isn't in_stock answers 409 insufficient_capacity.
Buy extra bandwidth¶
POST /
Add bandwidth to the server's allowance for this month.
{"size_gb": 1000}
size_gb is one of the sizes in pricing's extra_bandwidth, and the price is the one listed there. The answer is 201 with an order of type bandwidth. It is provisioning while the bandwidth is added, and completed once the server's bandwidth.extra_gb has it.
Orders¶
List orders¶
GET /
The project's orders, newest first. Lists show each order as last recorded: poll the order itself to follow it.
Get an order¶
GET /
One order, checked against billing for its latest state before the answer.
If you lose the answer¶
Send an Idempotency-Key with every order, as for actions. If you send the same order again with the same key, you get the first order back, with 200 and Idempotent-Replayed: true, instead of buying twice. The same key with a different body answers 409 idempotency_key_reused.
When billing doesn't answer, the order is still recorded: the answer is 502 upstream_error with the order's id in order_id, and the order is unknown while it is checked. Poll that order rather than ordering again. If it ends failed and its invoice isn't paid, place a new order with a new key.
Errors¶
| Status | Code | When |
|---|---|---|
| 400 | validation_error |
A field is invalid, or the product doesn't offer that choice. |
| 400 | feature_unavailable |
user_data in an order. |
| 403 | insufficient_role |
Ordering needs the admin or owner role. |
| 403 | read_only_key |
Ordering with a read-only key. |
| 403 | billing_account_required |
The project's owner has no billing account. |
| 403 | project_not_ready |
The project is still being set up. |
| 404 | not_found |
No such order or server in the project. |
| 409 | idempotency_key_reused |
The key was used for another request. |
| 409 | conflict |
The server has an upgrade that isn't paid or settled yet, or its billing service can't be found. |
| 409 | insufficient_capacity |
The upgrade is out of stock in the server's location. |
| 429 | rate_limited |
More than 10 orders a minute with one key. |
| 502 | upstream_error |
Billing failed or didn't answer. With an order_id, poll that order. |
| 503 | upstream_unavailable |
Billing is unavailable for now. |