Skip to content

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 /projects/{slug}/products/ List products
GET /projects/{slug}/images/ List images
GET /projects/{slug}/locations/ List locations
GET /projects/{slug}/pricing/ Get pricing
POST /projects/{slug}/servers/ Order a server
GET /projects/{slug}/servers/{id}/upgrade-options/ List upgrade options
POST /projects/{slug}/servers/{id}/upgrade/ Upgrade a server
POST /projects/{slug}/servers/{id}/bandwidth/ Buy extra bandwidth
GET /projects/{slug}/orders/ List orders
GET /projects/{slug}/orders/{id}/ Get an order

How ordering works

  1. Look up what you can order: products, images, locations and prices.
  2. Place the order, with an Idempotency-Key. The answer is 201 with the order.
  3. 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.
  4. Poll the order until it is completed. A server order then has the new server's id in server_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 /projects/{slug}/products/

Any member · Read-only keys: yes · Returns 200 Product[]

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 /projects/{slug}/images/

Any member · Read-only keys: yes · Returns 200 Image[]

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 /projects/{slug}/locations/

Any member · Read-only keys: yes · Returns 200 Location[]

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 /projects/{slug}/pricing/

Any member · Read-only keys: yes · Returns 200 Pricing

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 /projects/{slug}/servers/

Admin or owner · Read-only keys: no · Returns 201 Order

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 /projects/{slug}/servers/{id}/upgrade-options/

Any member · Read-only keys: yes · Returns 200 UpgradeOption[]

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 /projects/{slug}/servers/{id}/upgrade/

Admin or owner · Read-only keys: no · Returns 201 Order

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 /projects/{slug}/servers/{id}/bandwidth/

Admin or owner · Read-only keys: no · Returns 201 Order

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 /projects/{slug}/orders/

Any member · Read-only keys: yes · Returns 200 Order[]

The project's orders, newest first. Lists show each order as last recorded: poll the order itself to follow it.

Get an order

GET /projects/{slug}/orders/{id}/

Any member · Read-only keys: yes · Returns 200 Order

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.