# Ordering guide

Order an instant server end to end: get ready, pick an offer, check the total with a dry run, order, and wait until the server is ready.

## Before your first order

Ordering is off until the account owner sets it up. In the client area, on **Settings → API**:

1. Make a key with the **order** permission. It needs a monthly spend limit for that key: a whole amount in the account's currency, 1 to 100000. Orders that would take the key over it in 30 days are refused. The limit cannot be raised later; make a new key instead. An IP list on ordering keys is strongly suggested.
2. Turn on **Ordering through the API** (a recent sign-in and an emailed code, as for keys).
3. Wait 24 hours. Ordering opens 24 hours after it was turned on and after a new ordering key was made. Until then orders answer `403 cooling_off`, with `details.retry_after` in seconds.
4. Add funds. Orders through the API are paid from account credit only. With `read_billing`, `GET /account` shows the balance.

## Account limits

On top of each key's own monthly limit, the account has limits counted across all its keys over rolling windows:

| Limit | Default | `details.cap` when reached |
|---|---|---|
| Servers ordered in a day | 3 | `servers_day` |
| Servers ordered in 30 days | 10 | `servers_month` |
| Orders paid but not delivered yet, at once | 2 | `open` |
| Money spent in 30 days | the equivalent of €2,000 | `money_month` |
| Monthly renewals added in 30 days | the equivalent of €2,000 | `renewal_month` |
| The key's own monthly limit | set by the owner | `key_month` |

The account limits are set by RedSwitches for each account; the defaults are above. Contact support if you need a higher one. Raising a limit does not pause ordering. The key's own limit is set when the key is made and cannot be raised: make a new key instead.

Over a limit the order is refused with `403 cap_reached` and nothing is ordered. The details say which limit, its value and its window:

```json
{
  "error": {
    "code": "cap_reached",
    "message": "This order would go over a limit of this key or of the account. Nothing was ordered.",
    "request_id": "01JCX7Q2M8ZK4T6V9B3N5R1W2Y",
    "details": { "cap": "servers_day", "limit": 3, "window": "day" }
  }
}
```

For a money limit, `limit` is an amount in your account's currency, in minor units: `{"cap": "money_month", "limit": {"amount": "200000", "currency": "EUR"}, "window": "30_days"}` is 2,000.00 EUR in 30 days. `window` is `day`, `30_days`, or `open` for orders paid but not delivered yet.

## 1. Pick an offer

```bash
curl -sS https://api.redswitches.com/v1/offers \
  -H "Authorization: Bearer $REDSWITCHES_API_KEY"
```

```ts
const { data: offers } = await rs.offers.list();
const offer = offers.find((o) => o.location?.city === "Amsterdam" && o.stock !== 0)!;
const os = offer.os.find((x) => x.name.startsWith("Ubuntu"))!;
```

```python
offers = client.offers.list().data
offer = next(o for o in offers if o.location and o.location.city == "Amsterdam")
os_choice = next(x for x in offer.os if x.name.startswith("Ubuntu"))
```

```go
offers, err := client.Offers.List(ctx)
if err != nil {
	log.Fatal(err)
}
offer := offers.Data[0]
osID := offer.OS[0].ID
```

```json
{
  "data": [
    {
      "id": "ofr_12",
      "name": "Instant Dedicated Server E-2388G",
      "location": { "city": "Amsterdam", "country": "NL" },
      "cpu": "Intel Xeon E-2388G (8 cores, 3.2 GHz)",
      "ram": "64 GB DDR4",
      "disks": "2 x 960 GB NVMe SSD",
      "port_speed": "1 Gbps",
      "traffic": "50 TB",
      "currency": "USD",
      "prices": [{ "cycle": "monthly", "price": "129.00", "setup": "0.00" }],
      "os": [{ "id": "os_31", "name": "Ubuntu 24.04" }],
      "stock": "5+",
      "as_of": "2026-11-15T09:30:00Z"
    }
  ]
}
```

Offer prices are before tax. The amount you pay comes from the dry run.

## 2. Dry run

A dry run checks everything a real order checks (offer, OS, SSH key, stock, limits, credit) and returns the exact total. Nothing is ordered or held.

```bash
curl -sS -X POST "https://api.redswitches.com/v1/orders?dry_run=true" \
  -H "Authorization: Bearer $REDSWITCHES_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"offer_id":"ofr_12","billing_cycle":"monthly","os_id":"os_31","ssh_key_id":"sshkey_7"}'
```

```ts
const body = { offer_id: offer.id, billing_cycle: "monthly", os_id: os.id, ssh_key_id: "sshkey_7" } as const;
const quote = await rs.orders.create(body, { dryRun: true });
console.log("Will pay", quote.total.amount, quote.total.currency, "(minor units)");
```

```python
body = dict(offer_id=offer.id, billing_cycle="monthly", os_id=os_choice.id, ssh_key_id="sshkey_7")
quote = client.orders.create(**body, dry_run=True)
print("Will pay", quote.total.amount, quote.total.currency, "(minor units)")
```

```go
req := &redswitches.OrderCreateRequest{OfferID: offer.ID, BillingCycle: "monthly", OSID: osID, SSHKeyID: "sshkey_7"}
quote, err := client.Orders.Create(ctx, req, &redswitches.OrderOptions{DryRun: true})
```

```json
{
  "dry_run": true,
  "offer_id": "ofr_12",
  "billing_cycle": "monthly",
  "total": { "amount": "15480", "currency": "USD" },
  "recurring": { "amount": "12900", "currency": "USD" },
  "paid_from_credit": { "amount": "15480", "currency": "USD" }
}
```

`"15480"` is 154.80 USD. Show it to whoever approves the spend.

## 3. Order

Send the same body with `expected_total` set to the dry run's `total`, and a new Idempotency-Key that you keep until you have the answer. If the price moved in between, the order is refused with `409 price_changed` and the new total in `details.total`; nothing is charged.

```bash
curl -sS -X POST https://api.redswitches.com/v1/orders \
  -H "Authorization: Bearer $REDSWITCHES_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 7d0e4b1c-2f3a-4c5d-8e9f-0a1b2c3d4e5f" \
  -d '{"offer_id":"ofr_12","billing_cycle":"monthly","os_id":"os_31","ssh_key_id":"sshkey_7","expected_total":{"amount":"15480","currency":"USD"}}'
```

```ts
import { randomUUID } from "node:crypto";

const idempotencyKey = randomUUID(); // store it with your job until the answer arrives
const order = await rs.orders.create({ ...body, expected_total: quote.total }, { idempotencyKey });
```

```python
import uuid

key = str(uuid.uuid4())  # store it with your job until the answer arrives
order = client.orders.create(**body, expected_total=quote.total, idempotency_key=key)
```

```go
req.ExpectedTotal = quote.Total
order, err := client.Orders.Create(ctx, req, &redswitches.OrderOptions{IdempotencyKey: uuid.NewString()})
```

```json
{
  "id": "ord_456",
  "invoice_id": "inv_789",
  "server_ids": ["srv_123"],
  "status": "paid",
  "delivery": "queued",
  "billing_cycle": "monthly",
  "total": { "amount": "15480", "currency": "USD" },
  "recurring": { "amount": "12900", "currency": "USD" },
  "paid_from_credit": { "amount": "15480", "currency": "USD" },
  "poll_after": 30
}
```

`201` means the order exists and is paid from credit. The invoice is already settled.

### If the answer is lost

Send exactly the same request again, with the same Idempotency-Key and the same body. You get:

- `201` with the same order, if it went through;
- the same error, if it was refused;
- `409 in_progress` while it is still running: wait `details.retry_after` seconds and ask again.

You never get a second server. Never change the body under the same key: that is `422 idempotency_conflict`.

## 4. Wait for delivery

Poll the order, waiting `poll_after` seconds between requests, until every server's `delivery` is `ready`.

```bash
while true; do
  r=$(curl -sS https://api.redswitches.com/v1/orders/ord_456 \
    -H "Authorization: Bearer $REDSWITCHES_API_KEY")
  echo "$r" | jq -c '.services'
  wait=$(echo "$r" | jq -r '.poll_after // empty')
  [ -z "$wait" ] && break
  sleep "$wait"
done
```

```ts
const ready = await rs.orders.waitUntilReady(order.id);
const server = await rs.servers.get(ready.services[0].id);
console.log(server.primary_ip);
```

```python
ready = client.orders.wait_until_ready(order.id)
server = client.servers.get(ready.services[0].id)
print(server.primary_ip)
```

```go
for {
	o, err := client.Orders.Get(ctx, order.ID)
	if err != nil {
		log.Fatal(err)
	}
	if o.PollAfter == nil {
		break
	}
	time.Sleep(time.Duration(*o.PollAfter) * time.Second)
}
```

```json
{
  "id": "ord_456",
  "date": "2026-11-15T09:30:00Z",
  "status": "active",
  "payment": "paid",
  "invoice": "inv_789",
  "services": [{ "id": "srv_123", "delivery": "ready" }],
  "poll_after": null
}
```

Delivery goes `assigning` → `installing` → `ready`. `poll_after` becomes null when nothing is still being delivered. If a server shows `attention`, our staff are already looking at it; do not order again.

When it is `ready`, the welcome email with the login details is in the account owner's mailbox. The root password never appears in the API. If you ordered with `ssh_key_id`, you can log in with that key straight away.

## When an order is refused

| Answer | What happened | What to do |
|---|---|---|
| `402 insufficient_credit` | Not enough credit. `details.needed` is the shortfall in minor units. | Add funds, then order with a new Idempotency-Key. |
| `409 price_changed` | The total moved. `details.total` is the new one. | Confirm it, then order with a new key and the new `expected_total`. |
| `409 sold_out` | The last unit went. | Pick another offer. |
| `403 cap_reached` | A limit would be passed. `details.cap`, `details.limit` and `details.window` say which. | Wait for the window to pass, or ask support to raise an account limit. |
| `403 cooling_off` | Ordering is not open yet. | Wait `details.retry_after` seconds. |
| `403 ordering_not_enabled` | Ordering is off for the account. | The owner turns it on. |
| `403 not_eligible` | The account may not order now. | The owner sees why on the API page. |
| `403 ordering_paused` | RedSwitches paused API ordering. | Try later. |
| `409 order_not_placed` | Nothing was created or charged. | You may try again with a new key. |
| `409 order_under_review` | The order exists but staff must check it before it is paid or delivered. `details.order` is its id. | Do not order again. We will contact you. |
| `422 not_orderable` | Offer, OS, SSH key or cycle not allowed. | Check the ids against `GET /offers` and `GET /ssh-keys`. |
