# Orders

Place an order for an instant server, paid from your account credit, and follow your orders until the server is ready. Reading needs `read`; ordering needs `order`. For a step-by-step walk-through see the [Ordering guide](guide-ordering.md).

## Place an order

`POST /orders` orders one instant server. It is paid from the account's credit at once; there is no card or invoice step. Needs an `Idempotency-Key` (except for a dry run).

| Body field | Required | Rule |
|---|---|---|
| `offer_id` | yes | An offer id from [Offers](offers.md), such as `"ofr_12"`. |
| `billing_cycle` | yes | `"monthly"`. No other cycle can be ordered through the API. |
| `os_id` | yes | One of the offer's `os` ids, such as `"os_31"`. |
| `ssh_key_id` | no | A saved key from [SSH keys](ssh-keys.md), such as `"sshkey_7"`. It is installed on the server. |
| `label` | no | 1 to 60 characters: letters, digits, spaces, `.`, `-`, `_`. Set as the server's label once it is paid. |
| `expected_total` | yes, except in a dry run | `{"amount": "15480", "currency": "USD"}`. The total you expect to pay: `amount` in **minor units** (cents) as a string of digits, `currency` as three capital letters. Take it from a dry run. |

Any other field, a field of the wrong type, or a name given twice is `400 invalid_body`. The configuration is always the offer's default one with your chosen OS. Quantity is one server per order.

### Dry run

Add `?dry_run=true` to see the exact total and check the order would go through. Nothing is ordered, reserved or charged. No `Idempotency-Key` is needed, and `expected_total` may be left out.

```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 quote = await rs.orders.create(
  { offer_id: "ofr_12", billing_cycle: "monthly", os_id: "os_31", ssh_key_id: "sshkey_7" },
  { dryRun: true },
);
```

```python
quote = client.orders.create(
    offer_id="ofr_12", billing_cycle="monthly", os_id="os_31", ssh_key_id="sshkey_7", dry_run=True
)
```

```go
quote, err := client.Orders.Create(ctx, &redswitches.OrderCreateRequest{
	OfferID: "ofr_12", BillingCycle: "monthly", OSID: "os_31", SSHKeyID: "sshkey_7",
}, &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" }
}
```

The answer is `200`. `total` is the first payment including setup and tax; `recurring` is the monthly renewal amount before tax. Both are minor units. A dry run runs the same checks as a real order (stock, limits, credit), so it fails with the same errors.

### Order

Send the same body with `expected_total` set to the dry run's `total`:

```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: order-web-1-2026-11-15" \
  -d '{"offer_id":"ofr_12","billing_cycle":"monthly","os_id":"os_31","ssh_key_id":"sshkey_7","label":"web-1","expected_total":{"amount":"15480","currency":"USD"}}'
```

```ts
const order = await rs.orders.create(
  {
    offer_id: "ofr_12",
    billing_cycle: "monthly",
    os_id: "os_31",
    ssh_key_id: "sshkey_7",
    label: "web-1",
    expected_total: quote.total,
  },
  { idempotencyKey: "order-web-1-2026-11-15" },
);
```

```python
order = client.orders.create(
    offer_id="ofr_12",
    billing_cycle="monthly",
    os_id="os_31",
    ssh_key_id="sshkey_7",
    label="web-1",
    expected_total=quote.total,
    idempotency_key="order-web-1-2026-11-15",
)
```

```go
order, err := client.Orders.Create(ctx, &redswitches.OrderCreateRequest{
	OfferID: "ofr_12", BillingCycle: "monthly", OSID: "os_31", SSHKeyID: "sshkey_7", Label: "web-1",
	ExpectedTotal: quote.Total,
}, &redswitches.OrderOptions{IdempotencyKey: "order-web-1-2026-11-15"})
```

```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
}
```

The answer is `201 Created`: the order exists and is paid. Delivery starts on its own. Follow it with `GET /orders/{id}`.

The root password never appears in the API. The welcome email with the login details goes to the account owner, as for any order.

### Order errors

| Status | Code | What to do |
|---|---|---|
| 402 | `insufficient_credit` | `details.needed` is the credit missing (minor units). Add funds in the client area. |
| 403 | `ordering_not_enabled` | The owner turns ordering on in Settings → API. |
| 403 | `cooling_off` | Ordering opens 24 hours after it was turned on and after a new ordering key. Wait `details.retry_after` seconds. |
| 403 | `not_eligible` | The account may not order now. The owner sees why on the API page. |
| 403 | `cap_reached` | `details.cap` names the limit, `details.limit` its value and `details.window` its window. Account limits are set by RedSwitches: contact support to raise one. See the [Ordering guide](guide-ordering.md). |
| 403 | `ordering_paused` | RedSwitches paused API ordering. Try later. |
| 409 | `price_changed` | `details.total` is the current total. Check it, then order again with a **new** Idempotency-Key and the new `expected_total`. |
| 409 | `sold_out` | Pick another offer. |
| 409 | `order_not_placed` | Nothing was created or charged. Staff have been told. |
| 409 | `order_under_review` | The order exists but needs a staff check before it is paid or delivered. `details.order` is its id. Do not order again. |
| 409 | `in_progress` | The same Idempotency-Key is still running. Ask again after `details.retry_after` seconds with the same key. |
| 422 | `not_orderable` | The offer, OS, SSH key or billing cycle cannot be ordered through the API. |
| 422 | `idempotency_conflict` | The Idempotency-Key was used with another body. |

If an order times out, send exactly the same request again with the same Idempotency-Key. You get the original result; you never get a second server. See [Idempotency and retries](idempotency.md).

## List orders

`GET /orders` returns the account's orders, newest first. Paged with `limit` (1 to 100, default 50) and `cursor` (`ord_…`).

```bash
curl -sS "https://api.redswitches.com/v1/orders?limit=20" \
  -H "Authorization: Bearer $REDSWITCHES_API_KEY"
```

```ts
const page = await rs.orders.list({ limit: 20 });
```

```python
page = client.orders.list(limit=20)
```

```go
page, err := client.Orders.List(ctx, &redswitches.ListOptions{Limit: 20})
```

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

## Get an order

`GET /orders/{id}` returns one order. Poll it after ordering until every service's `delivery` is `ready`.

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

```ts
const ready = await rs.orders.waitUntilReady("ord_456");
```

```python
ready = client.orders.wait_until_ready("ord_456")
```

```go
order, err := client.Orders.Get(ctx, "ord_456")
```

```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
}
```

| Field | Type | Meaning |
|---|---|---|
| `id` | string | `ord_<n>`. |
| `date` | string or null | When the order was placed (UTC). |
| `status` | string | `pending`, `active`, `cancelled`, `on_hold` or `other`. |
| `payment` | string | `paid`, `free`, `unpaid`, `partially_paid`, `pending`, `cancelled`, `refunded`, `collections` or `unknown`. |
| `invoice` | string or null | The invoice id, or null while there is none. |
| `services` | array | Each server of the order: `id` and `delivery`. |
| `services[].delivery` | string or null | `awaiting_payment`, `assigning`, `installing`, `ready`, `attention` or `closed`. Null for a server that is not an instant server. |
| `poll_after` | integer or null | Seconds until it is worth asking again. Null when nothing is still being delivered. |

Delivery states:

| State | Meaning |
|---|---|
| `awaiting_payment` | The order is not paid yet. |
| `assigning` | A server is being picked for you. |
| `installing` | The OS is being installed. |
| `ready` | Done. The login details are in the owner's email. |
| `attention` | Delivery needs a look by our staff. They are on it; you do not need to order again. |
| `closed` | The order was cancelled, refunded or is in collections, or the server was suspended or has ended. |
