# Vantix API

Lease dedicated Mac minis from your own code. The API lets you check stock,
order machines billed to your card on file, manage the SSH keys installed on
new machines, and share unbranded browser desktops.

**Base URL:** `https://panel.vantixservers.com/api/v1`

## Contents

- [Quickstart](#quickstart)
- [Authentication](#authentication)
- [Conventions](#conventions)
- [Stock](#stock)
- [Orders](#orders)
- [Instances](#instances)
- [SSH keys](#ssh-keys)
- [Console links](#console-links)
- [Account](#account)
- [Billing and cancelling](#billing-and-cancelling)
- [Errors](#errors)
- [Limits](#limits)

## Quickstart

1. Sign in at [panel.vantixservers.com](https://panel.vantixservers.com). Under
   **Billing**, add a card. Adding a card doesn't charge it.
2. Under **API keys**, create a key and store it somewhere safe. It's shown once.
3. Check what's in stock:

   ```sh
   export VANTIX_KEY=vx_...
   curl -s https://panel.vantixservers.com/api/v1/stock -H "Authorization: Bearer $VANTIX_KEY"
   ```

4. Order a machine, with your SSH key so you can log straight in:

   ```sh
   curl -s https://panel.vantixservers.com/api/v1/orders \
     -H "Authorization: Bearer $VANTIX_KEY" \
     -H "Idempotency-Key: $(uuidgen)" \
     -H "Content-Type: application/json" \
     -d "{\"configuration\":\"16\",\"ssh_keys\":[\"$(cat ~/.ssh/id_ed25519.pub)\"]}"
   ```

   The response lists your new machine under `instances`, for example `mac-03`.

5. Wait for `ssh.keys_status` to become `installed`, usually a minute or two,
   then connect:

   ```sh
   curl -s https://panel.vantixservers.com/api/v1/instances/mac-03 -H "Authorization: Bearer $VANTIX_KEY"
   ssh vantix@203.0.113.3
   ```

6. To give someone the desktop in their browser, create a
   [console link](#console-links).

## Authentication

Send your API key as a bearer token on every request:

```
Authorization: Bearer vx_0123456789abcdef...
```

Keys start with `vx_`. Create and revoke them in the panel under
**API keys**. Only accounts with a card on file can create keys, and each
account can have up to 10.

A key acts as your account. Anyone holding it can order machines billed to
your card, read your machines' passwords and open their desktops. So:

- Keep keys on your servers. Don't put them in browser code, mobile apps or
  public repositories. The API doesn't accept requests from other websites'
  browser JavaScript.
- Use one key per system, so you can revoke one without breaking the others.
- Revoke a key in the panel the moment it may have leaked. Requests using it
  fail straight away.

A missing, malformed or revoked key gets `401 unauthorized`.

## Conventions

- **JSON.** Requests with a body send `Content-Type: application/json`.
  Unknown fields are rejected with `400`, so typos don't pass silently.
- **Money.** Amounts are integers in cents, in US dollars. `46500` is $465.00.
- **Times.** Timestamps are RFC 3339, such as `2026-09-26T10:15:02Z`.
- **IDs.** Machines have short IDs such as `mac-03`. Orders, SSH keys and
  console links have UUIDs.
- **Errors.** Every error has the same shape. See [Errors](#errors).
- **Idempotency.** `POST /orders` accepts an `Idempotency-Key` header. See
  [Retrying safely](#retrying-safely).

## Stock

### `GET /stock`

Lists the configurations you can order right now, with live availability and
prices. Prices are per Mac, per billing period.

```json
{
  "ordering_enabled": true,
  "configurations": [
    {
      "id": "16",
      "name": "Mac mini M4",
      "chip": "Apple M4 · 16GB · 512GB",
      "available": 3,
      "max_per_order": 5,
      "pricing": {
        "quarterly": { "amount": 46500, "currency": "usd", "interval_months": 3 },
        "monthly": { "amount": 17800, "currency": "usd", "interval_months": 1 }
      }
    }
  ]
}
```

| Field | Description |
| --- | --- |
| `id` | Pass this as `configuration` when ordering. |
| `available` | Machines ready to hand over now. `0` means sold out. |
| `max_per_order` | The most you can order in one request. |
| `pricing.quarterly` | Billed every 3 months. Always present. |
| `pricing.monthly` | Billed monthly with no commitment. Only present when offered. |

`ordering_enabled` is `false`, with an empty list, while ordering is paused.

## Orders

### `POST /orders`

Leases machines and charges your card on file for the first billing period.
The machines are yours as soon as the request succeeds.

| Field | Required or default | Description |
| --- | --- | --- |
| `configuration` | required | An `id` from [`GET /stock`](#stock). |
| `quantity` | default `1` | From 1 to the configuration's `max_per_order`. |
| `term` | default `"quarterly"` | `"quarterly"` or `"monthly"`. |
| `ssh_keys` | optional | Up to 10 public keys, added to your account before the machines are reserved, so they're installed at handover. See [SSH keys](#ssh-keys). |
| `instance_ssh_keys` | optional | One array of public keys per ordered Mac, up to 10 keys per Mac. These keys are exclusive to that Mac's lease; account keys are excluded. Cannot be combined with `ssh_keys`. |

Keys in `ssh_keys` are added to your account first, all or none. If they
don't fit under the 10-key limit, the order stops with `409 limit_reached`
before anything is reserved or charged. Once added they stay on your account
even if the order then fails, for example for lack of stock.

```json
{ "configuration": "16", "quantity": 2, "term": "monthly", "ssh_keys": ["ssh-ed25519 AAAA... ci@build"] }
```

For resellers, give each Mac a separate key set:

```json
{
  "configuration": "16",
  "quantity": 2,
  "term": "monthly",
  "instance_ssh_keys": [
    ["ssh-ed25519 AAAA... customer-a@laptop"],
    ["ssh-ed25519 AAAA... customer-b@laptop"]
  ]
}
```

Each inner array belongs to one Mac, in the returned `instances` order for a
fully fulfilled order. To wait and set keys after ordering, use an empty
array for that Mac, for example `"instance_ssh_keys": [[], []]`. This still
excludes account keys. Add its keys later with `PUT /instances/{id}/ssh-keys`.
Keys supplied this way are saved with the pending order and attached only
when payment succeeds, including when payment is resolved later. They never
enter your account key list. If an order reports a provisioning shortfall,
read each assigned Mac's key selection to identify its customer.

Response:

```json
{
  "id": "5a0c7c1e-3f55-4a53-9d0e-2b0f4d8f6e21",
  "status": "active",
  "configuration": "16",
  "quantity": 2,
  "term": "monthly",
  "instances": ["mac-03", "mac-04"],
  "created_at": "2026-09-26T10:15:02Z"
}
```

| Status | Meaning |
| --- | --- |
| `201 Created` | Paid. `instances` lists your new machines. |
| `200 OK` | A repeat of an earlier request with the same `Idempotency-Key`. The body is that order, in its current state. |
| `202 Accepted` | We couldn't confirm the payment outcome yet. The order is `pending`; poll [`GET /orders/{id}`](#get-ordersid). It becomes `active` or `failed` within about 15 minutes. |
| `400` | Invalid request: `invalid_request`, `unknown_configuration`, `term_unavailable` or `invalid_quantity`. |
| `402` | `payment_method_required`: add a card first. `payment_failed`: the card was declined, and the message says why. Nothing was charged. |
| `409` | `insufficient_stock`: fewer machines are available than you asked for. `error.available` says how many are. Nothing was charged. |
| `422` | `invalid_ssh_key` (the message names the bad key) or `idempotency_key_reused`. |

Stock is held for you while your card is charged, so a `201` never comes back
without machines. In the rare case that some can't be handed over, the order
is still `active` and its `error` says how many are still being provisioned.
We follow up by email.

Your card must allow charges without you present. If your bank requires a
verification step for every payment, the order fails with `payment_failed`.
Use another card or contact your bank.

#### Retrying safely

Networks fail, and a timed-out order request may or may not have gone through.
Send an `Idempotency-Key` header with a unique value, such as a UUID, on every
order. If you retry with the same key, you get the original order back rather
than a second one. Reusing a key with different fields, including different
`ssh_keys` or `instance_ssh_keys`, returns `422 idempotency_key_reused`. Keys can be 1 to 255 printable ASCII characters.

### `GET /orders/{id}`

The order's current state, in the same shape as above.

| Status | Meaning |
| --- | --- |
| `pending` | Payment is being confirmed. `instances` is empty. |
| `active` | Paid. `instances` lists the machines. |
| `failed` | Not charged, or the charge was declined. `error` says why. |

## Instances

An instance is a Mac mini on your account.

### `GET /instances`

```json
{
  "instances": [
    {
      "id": "mac-03",
      "name": "Vantix-MacMini-03",
      "status": "active",
      "configuration": "16",
      "ip_address": "203.0.113.3",
      "ssh": {
        "host": "203.0.113.3",
        "port": 22,
        "user": "vantix",
        "command": "ssh vantix@203.0.113.3",
        "key_mode": "account",
        "keys_status": "installed",
        "keys_installed_at": "2026-09-26T10:16:40Z"
      },
      "mac_user": "vantix",
      "console_url": "https://panel.vantixservers.com/machines/mac-03/guac",
      "assigned_at": "2026-09-26T10:15:02Z"
    }
  ]
}
```

| Field | Description |
| --- | --- |
| `ssh.key_mode` | Present when SSH handover is enabled. `account`: uses your account keys at handover. `instance`: uses only this Mac's keys. |
| `ssh.keys_status` | `installed`: your keys are on the machine. `pending`: installing, usually a minute or two. `no_keys`: the selected key set is empty; add account keys or this Mac's keys according to `key_mode`. `unmanaged`: contact support to have keys added. |
| `mac_user` | The macOS account, for SSH and the desktop. |
| `console_url` | The desktop in a browser where you're signed in to the panel. To give the desktop to someone else, use a [console link](#console-links). |

### `GET /instances/{id}`

One machine, in the same shape, plus `mac_password`: the macOS account
password, used for `sudo` and to unlock the screen.

Don't change the macOS account password. The browser console and console
links sign in with it, so changing it locks you out of the desktop. If you
need it changed, contact support.

Another account's machine returns `404`.

## SSH keys

SSH on your machines accepts keys only, no passwords. By default, your account
keys are installed on each new machine once, at handover, a minute or two after
it becomes yours. A Mac in `instance` mode receives only its own key set. In the same step Vantix's own access is removed, so from then
on only you can change the machine's keys.

That means:

- An account key you add reaches machines in `account` mode whose `keys_status` is `no_keys` or `pending`. It never reaches a Mac in `instance` mode.
- It doesn't reach a machine that's already `installed`. To add a key there,
  SSH in with an existing key and append it to
  `/etc/ssh/mosyle_authorized_keys/<mac_user>` using `sudo`.
- Removing a key from your account doesn't remove it from machines that have it.

To have account keys on new machines from the start, pass `ssh_keys` when
ordering, or add them first. Use `instance_ssh_keys` for separate customers or
machines. In the panel, choose **Set keys separately for each Mac** at checkout,
then set each Mac's keys on the dashboard. The browser desktop remains usable
while SSH waits for keys.

Ed25519, ECDSA, security-key types (`sk-ssh-ed25519`, `sk-ecdsa-sha2-nistp256`)
and RSA of 3072 bits or more are accepted. Paste the single line from your
`.pub` file. Options before the key type are rejected, and the comment is kept
for display but never written to a machine.

### `GET /ssh-keys`

```json
{
  "ssh_keys": [
    {
      "id": "8c1d2e3f-0000-4000-8000-000000000001",
      "type": "ssh-ed25519",
      "fingerprint": "SHA256:Xb2n1s9…",
      "comment": "ci@build",
      "created_at": "2026-09-26T10:14:00Z"
    }
  ]
}
```

### `POST /ssh-keys`

```sh
curl -s https://panel.vantixservers.com/api/v1/ssh-keys \
  -H "Authorization: Bearer $VANTIX_KEY" \
  -H "Content-Type: application/json" \
  -d "{\"public_key\":\"$(cat ~/.ssh/id_ed25519.pub)\"}"
```

Returns `201` with the key. Errors: `invalid_ssh_key` (`422`), and
`duplicate_ssh_key` or `limit_reached` (`409`).

### `DELETE /ssh-keys/{id}`

Removes the key from your account. Returns `204`.

### `GET /instances/{id}/ssh-keys`

Returns the key selection for a Mac you own:

```json
{
  "mode": "instance",
  "editable": true,
  "ssh_keys": [
    {
      "id": "8c1d2e3f-0000-4000-8000-000000000001",
      "type": "ssh-ed25519",
      "fingerprint": "SHA256:Xb2n1s9…",
      "comment": "customer-a@laptop",
      "created_at": "2026-09-26T10:14:00Z"
    }
  ]
}
```

In `account` mode this lists the current account keys. In `instance` mode it
lists this lease's keys. This is the panel's configuration, not a live read
of the Mac's key file. `editable` becomes false when handover first starts.

### `PUT /instances/{id}/ssh-keys`

Replaces the entire selection **before handover starts**. The default mode is
`instance`. Keys are validated together; a bad key leaves the selection unchanged.

```json
{"mode": "instance", "ssh_keys": ["ssh-ed25519 AAAA... customer-a@laptop"]}
```

- Up to 10 keys per Mac, independently of the account's 10-key limit.
- Account keys are excluded in `instance` mode.
- `{"mode":"instance","ssh_keys":[]}` clears the selection and waits for keys.
- `{"mode":"account"}` switches back to the account keys before handover.
- Returns `204` on success, `422 invalid_ssh_key` for invalid keys, or
  `409 ssh_keys_locked` if handover has started. A different account's Mac
  returns `404`.
- Once handover starts, use an existing SSH key or the browser console to
  edit `/etc/ssh/mosyle_authorized_keys/<mac_user>` with `sudo`. Contact support
  if setup has failed. The API cannot change keys on a handed-over Mac.
- Separate keys are cleared when the lease changes; they never carry over to
  the next customer, even if the same account leases the Mac again.

An account-key handover may start immediately after assignment. Choose
`instance_ssh_keys` when ordering if you need guaranteed time to select keys.

## Console links

A console link opens a machine's desktop in any browser, with no sign-in and
no Vantix branding. Use them to give your own users or customers a desktop,
or to embed one in your own site.

A link gives full control of the machine's desktop, so treat it like a
password. It stops working when:

- it expires,
- you revoke it, or
- the machine's lease ends.

Revoking a link, or the lease ending, also disconnects desktops already open
through it within a few seconds. Expiry only stops new visits: a desktop opened
just before expiry stays connected until it's closed. Prefer short-lived links
created when they're needed.

### `POST /instances/{id}/console-links`

| Field | Required or default | Description |
| --- | --- | --- |
| `title` | optional | Shown in the browser tab and the toolbar, up to 80 characters. Default `"Remote desktop"`. |
| `expires_in` | default `86400` | Seconds until the link stops working, from `300` (5 minutes) to `2592000` (30 days). |

The body is optional.

```sh
curl -s https://panel.vantixservers.com/api/v1/instances/mac-03/console-links \
  -H "Authorization: Bearer $VANTIX_KEY" \
  -H "Content-Type: application/json" \
  -d '{"title":"Acme build machine","expires_in":3600}'
```

```json
{
  "id": "2f7c9a10-0000-4000-8000-000000000002",
  "instance": "mac-03",
  "url": "https://panel.vantixservers.com/console/vxc_0123…",
  "title": "Acme build machine",
  "expires_at": "2026-09-26T11:15:02Z",
  "created_at": "2026-09-26T10:15:02Z",
  "last_used_at": null
}
```

The `url` is only returned here. We store a hash of it, so it can't be shown
again. Create a new link if you lose it.

To hide the toolbar, add `?toolbar=0`. That suits embedding in your own page:

```html
<iframe src="https://panel.vantixservers.com/console/vxc_0123…?toolbar=0"
        style="width:100%;height:720px;border:0"
        allow="clipboard-read; clipboard-write"></iframe>
```

The toolbar holds the **Paste** button, which types text into the session, and
a **Reconnect** button. Without the toolbar, people can still type and use the
mouse normally.

A link that is invalid, expired or revoked shows a plain "invalid or expired"
page. It doesn't say which.

### `GET /instances/{id}/console-links`

The machine's live links, in the same shape but without `url`.

### `DELETE /console-links/{id}`

Revokes one link. Returns `204`.

### `DELETE /instances/{id}/console-links`

Revokes every link to the machine. Returns how many were revoked:

```json
{ "revoked": 3 }
```

## Account

### `GET /account`

```json
{
  "id": "aaaaaaaa-0000-4000-8000-00000000000a",
  "email": "you@example.com",
  "card": { "brand": "Visa", "last4": "4242", "exp_month": 4, "exp_year": 2030 },
  "can_order": true,
  "api_key_id": "f30e043c-0000-4000-8000-000000000003",
  "instances": 2
}
```

`card` is `null` when there's no card on file. `can_order` is `false` until you
add one. `api_key_id` identifies the key that made the request.

## Billing and cancelling

- Each order is its own subscription. It renews automatically every quarter
  or month, depending on its `term`, and charges your card on file.
- Change cards with **Replace card** in the panel. The new card is used for
  new orders and for every renewal of orders placed through the API. Leases
  bought through checkout in the panel keep the card used at checkout; change
  those in **Manage billing**.
- Manage or cancel subscriptions with **Manage billing** in the panel. You can't
  cancel through the API.
- When a subscription ends, its machines are taken back and wiped for the next
  customer. Copy off anything you need first.

## Errors

Errors have one shape:

```json
{
  "error": {
    "code": "insufficient_stock",
    "message": "Not enough machines of this configuration are available right now.",
    "available": 1
  }
}
```

Branch on `code`, which is stable. `message` is written for people and may change.

| Status | Code | Meaning |
| --- | --- | --- |
| `400` | `invalid_request` | Malformed JSON, an unknown field or a bad value. |
| `400` | `unknown_configuration` | Not an `id` from `/stock`, or not orderable now. |
| `400` | `term_unavailable` | Monthly billing isn't offered for this configuration. |
| `400` | `invalid_quantity` | Below 1 or above `max_per_order`. |
| `401` | `unauthorized` | Missing, malformed or revoked API key. |
| `402` | `payment_method_required` | Add a card in the panel. |
| `402` | `payment_failed` | The card was declined. Nothing was charged. |
| `404` | `not_found` | No such endpoint, or it isn't on your account. |
| `405` | `method_not_allowed` | Wrong HTTP method for the endpoint. |
| `409` | `insufficient_stock` | Not enough machines. See `available`. |
| `409` | `conflict` | A concurrent request is using the same `Idempotency-Key`. Retry. |
| `409` | `ssh_keys_locked` | Handover has started; change keys on the Mac itself. |
| `409` | `duplicate_ssh_key` | That key is already on your account. |
| `409` | `limit_reached` | Too many SSH keys or console links. See [Limits](#limits). |
| `422` | `idempotency_key_reused` | Same `Idempotency-Key`, different order. |
| `422` | `invalid_ssh_key` | Not a usable public key. The message says why. |
| `429` | `rate_limited` | Wait the number of seconds in `Retry-After`. |
| `500` | `internal_error` | Retry after a short wait. |
| `502` | `payment_provider_unavailable` | Couldn't reach the card processor. Retry. |
| `503` | `unavailable`, `ordering_disabled`, `ssh_keys_unavailable`, `console_unavailable` | Temporarily unavailable. |

Retry `429` and `5xx` with backoff. For `POST /orders`, always retry with the
same `Idempotency-Key`.

## Limits

| Limit | Value |
| --- | --- |
| Requests | 120 a minute per API key |
| Orders | 10 order requests a minute per account |
| API keys | 10 per account |
| SSH keys | 10 per account, plus 10 per Mac in instance mode |
| Console links | 100 live links per account |
| Opening a console link | 30 times a minute per link |

Need more? Contact [team@usevantix.com](mailto:team@usevantix.com).
