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, none is applied. The order waits in awaiting_payment, and invoice.url links to the invoice: pay it 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, "private_network": false,
"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. User data is never stored here; has_user_data says whether there was some. |
invoice |
object or null | The invoice: id, status, total, currency, and url, where you can pay it. |
warnings |
array | Things that went wrong without stopping the order, such as a disk expansion that couldn't be scheduled. |
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 |
Its invoice was cancelled. |
failed |
The order was refused, and nothing was bought. |
unknown |
Billing didn't answer when the order was placed, so whether it went through is being checked. Poll it: it moves on once that is settled. 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 /
The prices of extra addresses and extra bandwidth.
{
"currency": "USD",
"ipv4_additional": {"monthly": "2.00"},
"ipv6": {"monthly": "0.00"},
"extra_bandwidth": [{"size_gb": 500, "price": "2.00"}, {"size_gb": 1000, "price": "4.00"}]
}
ipv4_additional and ipv6 have a price per billing cycle. extra_bandwidth lists the sizes Buy extra bandwidth takes.
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,
"private_network": false, "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. |
image |
string | An image the product offers. Send image or snapshot_id, not both. |
snapshot_id |
integer | A completed snapshot of the project to create the server from. |
ssh_keys |
array of integers | Ids of the project's SSH keys to install. |
ipv4_additional |
integer | How many IPv4 addresses to add to the first one. |
ipv6 |
boolean | Give the server an IPv6 subnet. |
private_network |
boolean | Attach it to the private network. |
promo_code |
string or null | A promotion code. |
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, such as a location without stock, an image it doesn't take, or a key or snapshot that isn't the project's, answers 400 validation_error on that field. The answer is 201 with the order, of type server.
List upgrade options¶
GET /
The plans the server can be upgraded to.
{"product_id": 41, "name": "Cloud VPS 4x8", "prices": {"monthly": "24.00"}, "currency": "USD"}
Upgrade a server¶
POST /
Move the server to a bigger plan. It keeps its billing cycle.
{"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. If that can't be scheduled, the order says so in warnings. |
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.
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, 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. |
| 404 | not_found |
No such order or server in the project. |
| 409 | idempotency_key_reused |
The key was used for another request. |
| 409 | project_deleting |
The project is being deleted. |
| 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. |