Ordering
This guide explains the relationship between the catalog, orders, invoices, and services. If you just want the end-to-end happy path, read Provisioning a VPS first; come back here when you need to understand the order lifecycle in depth.
Mental model
catalog → preview → order → invoice → service → server
- Catalog — read-only list of what you can buy.
- Preview — compute the total without committing. Returns a
quote_token. - Order — created with the preview's
quote_token; charges the invoice. - Invoice — generated automatically by the order. Pay it (or it auto-pays).
- Service — the billing record for a recurring product. One per line item.
- Server — the actual provisioned VPS (server-type products only).
Each of these is a separate resource with its own endpoint. The order is the entry point; the others get created as side effects.
The order state machine
┌───────────────────────────────────────┐
│ ▼
createOrder ─► pending ─► (invoice paid) ─► active ─► (service ids present)
│ │
│ └── (fraud check fails) ─► fraud (manual review)
│ │
│ └── (timeout, customer never paid) ─► cancelled
│
└── (you call cancelPendingOrder) ─► cancelled
Terminal states are active and cancelled. fraud means we're looking
into it — we don't expose which heuristic triggered, since that would be
useful to bad actors. Open a ticket if you believe a fraud flag is
wrong.
quote_token — locking pricing
Catalog prices can change between when you call preview and when you
call create. To protect against this, previewOrder returns a
quote_token valid for 5 minutes. Pass it to createOrder and the order
uses the quoted total even if catalog pricing changed in the window.
If you skip the token, createOrder recomputes pricing at order time.
That's fine for one-shot scripts; for cart UIs always preview-then-create.
Idempotency
Every POST order endpoint accepts an Idempotency-Key header. Same
key + same body within 24h replays the cached response; same key +
different body returns 409 idempotency_conflict.
Use a fresh UUID per logical order — not per HTTP retry. The point of idempotency is "I clicked the button and got a network error — was the order placed?" not "I want to deduplicate identical orders."
Cancelling
cancelPendingOrder— for orders that haven't been paid + provisioned. Voids the invoice. Free.cancelService— for already-active services. Different rules: refunds, end-of-cycle vs immediate, fees. See the services guide.
If the order is active, cancelPendingOrder returns 409. You want
cancelService on the service id in order.service_ids.
What auto_accept does
Our billing engine normally runs provisioning on a periodic cron,
even after you pay the invoice. That's the customer-portal
behaviour. The API offers auto_accept: true on createOrder which
forces synchronous provisioning after the invoice auto-pays — you
get back an operation to poll immediately instead of waiting for the
next cron tick.
Use it for paid-up-front flows. Skip it if you want the customer to pay
manually (the operation will still run automatically once InvoicePaid
fires).
POST /servers vs POST /orders
POST /servers is a shortcut over POST /orders for the
one-VPS-one-plan case. Internally it builds an order with auto_accept
set and returns the operation directly. Use /orders when:
- You're ordering multiple products at once
- You need addons or a promo code
- You want to defer payment (no
auto_accept) - You need to preview-then-create against a
quote_token
For everything else, /servers is simpler.
Errors specific to ordering
| HTTP | code | Meaning | Action |
|---|---|---|---|
| 402 | payment_required | Auto-pay failed (insufficient credit, declined card) | Update payment method, retry, or pay the invoice manually |
| 403 | step_up_required | Destructive verb without X-Confirm-Token | Run vpsctl auth step-up, retry |
| 403 | kyc_required | KYC not complete for high-value orders | Complete KYC at my.vps-server.host |
| 409 | idempotency_conflict | Same Idempotency-Key, different body | Use a fresh key |
| 422 | validation_failed | Bad product_id, billing_cycle, etc. | Check the response's errors[] array |
| 429 | rate_limited | Too many previewOrder calls (more aggressive limit) | Back off |
See errors for the full envelope.
What's next
- Async operations — provisioning is async.
- Webhooks — get notified instead of polling.
- Step-up auth —
X-Confirm-Tokenfor destructive verbs.