> ## Documentation Index
> Fetch the complete documentation index at: https://help.exy.sh/llms.txt
> Use this file to discover all available pages before exploring further.

# Order and renew servers with the API

> Quote a server, order it paid from your balance, renew it, switch auto-renew, upgrade it and change its IP.

Every purchase through the API is paid from your account balance in the same request: either it is paid, or nothing happened. There are no coupons, no crypto payments and no partial payments through the API. Top up your balance first in the dashboard, see [Top up account credit](https://help.exy.sh/billing-and-payments/top-up-account-credit.md).

All amounts are whole euro cents, VAT included where the field says so. Read your balance with `GET /v1/balance` (scope `billing.read`).

## What your token needs

| Action | Endpoint | Scope |
| - | - | - |
| Locations, plans, images | `GET /v1/locations`, `GET /v1/plans?location=DE`, `GET /v1/images` | `billing.read` (images: `vms.read`) |
| Price a server | `POST /v1/quotes` | `billing.read` |
| Order a server | `POST /v1/orders` | `billing.manage` |
| Follow an order | `GET /v1/orders/{id}` | `billing.read` |
| Renew a server | `POST /v1/servers/{id}/renew` | `billing.manage` |
| Switch auto-renew | `PATCH /v1/servers/{id}` with `autoRenew` | `billing.manage` |
| Upgrade a server | `GET /v1/servers/{id}/upgrade-options`, `POST /v1/servers/{id}/upgrades` | `billing.read`, `billing.manage` |
| New IPv4 address | `GET /v1/servers/{id}/ip-change`, `POST /v1/servers/{id}/ip-changes` | `billing.read`, `vms.manage` and `billing.manage` |

`billing.manage` spends the account owner's balance, also when a team member's token uses it. Only a member with two-factor authentication can create a token with it.

## Prices come from us, `expectedTotal` protects you

You never send a price. The server computes it from the plan, the term and your confirmed tax country. What you can send is `expectedTotal`: the total you saw and agreed to. If the price at the moment of the purchase is anything else, the request fails with `409 EXPECTED_TOTAL_MISMATCH` and nothing is charged. Quote again and decide.

Where `expectedTotal` comes from:

| Purchase | Send as `expectedTotal` |
| - | - |
| Order | `total` from `POST /v1/quotes` |
| Upgrade | `total` of the option from `GET /v1/servers/{id}/upgrade-options` |
| IP change | `total` from `GET /v1/servers/{id}/ip-change` |
| Renewal | The renewal's total, VAT included. A quote's `renewalTotal` shows what each renewal of the same term costs today |

`expectedTotal` is optional. Without it, you pay the current price.

## The idempotency key

Orders, renewals, upgrades and IP changes need an `Idempotency-Key` header with a UUID. Use one new key per purchase you mean to make, and the same key when you retry that purchase:

* A retry with the same key and the same body returns the same result and charges once. For an upgrade or an IP change it also shows the current status.
* The same key with another body, or for another server, is `409`.
* Keys belong to the account, not to the token: every token of the account shares them. Orders and renewals share one key space; upgrades and IP changes each have their own.

So if a request times out, send it again with the same key. You will never pay twice for it.

## Order a server

**1. Pick location, plan and image**

`GET /v1/locations` lists the location `code`s. `GET /v1/plans?location=DE` lists the plans there with their billing cycles, list prices (`netAmount`, before VAT), whether they are `available`, and whether backups are `purchasable`, `included` or `unavailable`. `GET /v1/images` lists the image slugs.

**2. Get a quote**

`POST /v1/quotes` with `planId`, `locationCode`, `billingCycle` and optionally `backups` returns the exact first-term price under your tax profile: `subtotal`, `taxRate`, `taxAmount`, `total`, and `renewalTotal`. A quote reserves no stock and charges nothing.

**3. Order**

`POST /v1/orders` with the same fields plus `osType` (the image slug), and optionally `name`, `sshKey` (Linux images) and `expectedTotal`. It answers `201` with the order.

**4. Follow the build**

Poll `GET /v1/orders/{id}`. Once the server exists, `serverId` names it; follow it with `GET /v1/servers/{id}`. If you use [webhooks](https://help.exy.sh/api/webhooks.md), `vm_ready` tells you when it is built and running.

```bash theme={null}
curl -X POST https://api.exy.sh/v1/quotes \
  -H "Authorization: Bearer $EXY_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"planId": 3, "locationCode": "DE", "billingCycle": "1month"}'
# {"data":{"subtotal":...,"taxRate":...,"total":1299,"renewalTotal":1299,"currency":"EUR",...}}

curl -X POST https://api.exy.sh/v1/orders \
  -H "Authorization: Bearer $EXY_API_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{"planId": 3, "locationCode": "DE", "billingCycle": "1month",
       "osType": "<image slug>", "expectedTotal": 1299}'
# 201 {"data":{"id":789,"number":"...","kind":"plan_vm","status":"provisioning","serverId":...}}
```

In a real script, generate the key once, store it with the purchase, and reuse it on every retry. A fresh `uuidgen` on each attempt would place a second order.

Why an order can be refused, with nothing charged and no order left behind:

| Answer | Reason |
| - | - |
| `400 TAX_COUNTRY_UNCONFIRMED` | Your account has no confirmed tax country yet. Confirm your billing address in the dashboard, see [How VAT and billing address are applied](https://help.exy.sh/billing-and-payments/how-vat-and-billing-address-are-applied.md) |
| `400 INSUFFICIENT_BALANCE` | The balance does not cover the total |
| `400 PLAN_OUT_OF_STOCK` | The plan is sold out in that location |
| `409 EXPECTED_TOTAL_MISMATCH` | The price is not your `expectedTotal` |
| `429 DAILY_ORDER_LIMIT_EXCEEDED` | 10 new server orders in 24 hours, dashboard orders included. `Retry-After` can be up to 24 hours |

### Order statuses

| `status` | Meaning |
| - | - |
| `pending_payment` | Not paid yet |
| `provisioning` | Paid, the server is being built |
| `completed` | Paid and built |
| `failed` | Paid, but the last build attempt failed. Automatic retries can still run for about 15 minutes and lead to `completed` |
| `refunded` | Closed without a server; the payment went back |
| `cancelled`, `fraud` | Closed without a server |

Poll while the order is `pending_payment` or `provisioning`, and for about 15 minutes after `failed`.

### When a build fails

You never pay for a server that was not built. If a paid server order is still not built 24 hours after its payment, what you paid for it goes back to your account balance automatically and the order becomes `refunded`. You get an email, and an `order_refunded` [webhook](https://help.exy.sh/api/webhooks.md) if you subscribed to it. Until then our team may still retry the build, and the order can still end as `completed`.

## Renew a server

`POST /v1/servers/{id}/renew` with an `Idempotency-Key` and a `billingCycle` (`1month`, `3months`, `6months` or `12months`) adds that term to the server and charges it to your balance. Optionally send `expectedTotal`.

It answers `201` with a renewal order (`kind: "renewal"`). The order is `provisioning` until the new term is applied, then `completed`; follow it with `GET /v1/orders/{id}`. The server's new end date is `expiresAt` in `GET /v1/servers/{id}`.

* **Open renewal invoice:** we raise a renewal invoice 14 days before a server expires. While it is open, the renewal pays that invoice, on its own term. Another `billingCycle` is `409 RENEWAL_INVOICE_OPEN`.
* **Which servers:** running, stopped and suspended servers renew. A server being deleted, deleted or not built yet is `409 VM_STATE_INVALID`.
* **One at a time:** while another paid renewal of the server is still being applied, a new one is `409 CONFLICT`.
* Too little balance is `400 INSUFFICIENT_BALANCE` and changes nothing.

## Auto-renew

```bash theme={null}
curl -X PATCH https://api.exy.sh/v1/servers/123 \
  -H "Authorization: Bearer $EXY_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"autoRenew": true}'
```

With `autoRenew` on, the server is renewed from your balance when its term ends, on its billing cycle. If your balance will not cover the renewal, we notify you 3 days before the server expires, so you can top up in time. `autoRenew` in `GET /v1/servers/{id}` shows the current setting.

## Upgrade a server

**1. List the options**

`GET /v1/servers/{id}/upgrade-options` lists the larger plans sold where the server runs, each priced for the days left in its term (`daysRemaining`), VAT included. An empty list means none fits now.

**2. Buy one**

`POST /v1/servers/{id}/upgrades` with an `Idempotency-Key`, the option's `plan.id` as `planId` and optionally its `total` as `expectedTotal`. It answers `201` with the upgrade.

**3. Wait for it**

The upgrade is `paid`, then `processing`, then `completed`. Poll `GET /v1/servers/{id}` for the new `cores`, `memoryMb` and `diskGb`, or repeat the request with the same key to see the upgrade's status.

> [!WARNING]
>
> A running server is switched off and on again once for the upgrade, without a shutdown, and is offline for about 2 minutes. Save open work first. A stopped server stays stopped.

Only a running or stopped server whose term has not ended can be upgraded (`400 VM_STATE_INVALID` otherwise, also for a suspended one). While another upgrade is paid or being applied, it is `400 UPGRADE_IN_PROGRESS`; without room on the server's host, `400 UPGRADE_NO_CAPACITY`. If an upgrade ends as `failed` before the server was touched, the charge goes back to your balance.

## Change the IP address

`GET /v1/servers/{id}/ip-change` shows whether a new IPv4 address can be bought now (`available`), when the cooldown ends (`cooldownUntil`) and what it costs (`total`, VAT included).

`POST /v1/servers/{id}/ip-changes` with an `Idempotency-Key` buys it. Send `{}` as the body, or `{"expectedTotal": <total>}`. It needs the scopes `vms.manage` and `billing.manage`. It answers `201` with the change, `paid` until it is applied right after; `newIp` holds the new address once one is reserved.

* **The old address stops working.** A running Linux server restarts once to take the new address and is briefly unreachable. A running Windows server switches over live, which ends every connection to the old address. A stopped server takes it when it next starts.
* **Cooldown:** one change per server every 7 days. A change inside it is `409 CONFLICT`, naming when the cooldown ends.
* Only a running or stopped server (`409 VM_STATE_INVALID` otherwise). While another change is paid or being applied, `409 IP_CHANGE_IN_PROGRESS`. No free address on the host is `400`.
* A change that ends as `failed` is not refunded automatically: [contact support](https://panel.exy.sh/tickets).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.