# API overview

https://instabox.sh/docs/api.md

Authentication, operations, idempotency and errors: what every endpoint shares.

### Base URL
```
https://api.instabox.sh
```
Every request and response body is JSON. Times are RFC 3339, in UTC.

### Authentication
Send an [API key](https://instabox.sh/docs/keys-sdk.md) in the `X-API-Key` header (or as `Authorization: Bearer`). Create keys in the dashboard: **API keys** in the sidebar; a restricted key only reaches the endpoints its scopes allow.
```
curl https://api.instabox.sh/v1/machines -H "X-API-Key: $INSTABOX_API_KEY"
```

### Changes run as operations
Creating, starting, stopping, pausing, resuming and deleting a machine answer `202 Accepted` with an **operation** at once. Follow it with [Get an operation](https://instabox.sh/docs/api/operations.md): it ends `succeeded` or `failed`, and a machine runs one operation at a time.

### Idempotency
Endpoints that list `Idempotency-Key` under **Headers** need one: any unique string, such as a UUID. Retrying a completed request with the same key returns its response instead of acting twice; the same key on a different request is `409 idempotency_conflict`. Keys are kept for 7 days.

### Confirming deletes
[Delete a machine](https://instabox.sh/docs/api/machines.md#delete) and [Delete a snapshot](https://instabox.sh/docs/api/snapshots.md#delete) take an optional `X-Confirm-Delete` header: the ID or exact name of what you mean to delete. Any other value is `409 delete_confirmation_required` and nothing is deleted, so a mistyped ID cannot remove another machine; the same `Idempotency-Key` then works for the corrected request. Without the header the delete goes ahead. The CLI's `rm` and `snapshot rm` send it.

### Errors
Errors use the HTTP status and a JSON body whose `code` says what went wrong. `retryable` is true when the same request may succeed later; `retryAfterSeconds`, when present (`no_capacity`, `rate_limited`, `machine_cooldown`, agent sign-up `quota_exceeded`), is how many seconds to wait first; an error response also sends it as the `Retry-After` header, while a failed operation carries only the field, in its `error`.
```
{
  "error": {
    "code": "machine_not_running",
    "message": "machine is stopped; start it first",
    "requestId": "req_fbckiomplsq3fqin5dvsvzpnli",
    "retryable": false,
    "operationId": null
  }
}
```
| Status | Codes |
|---|---|
| 400 | `bad_request`, `unknown_field` (a body field the request does not have, on create, fork, restore, start, stop, suspend, resume, wake and update of a machine; the message suggests the closest), `spec_not_allowed` (a size your plan does not offer, a disk size at create, or growing a free machine's disks; `GET /v1/orgs` lists each workspace's as `allowedSpecs`) |
| 401 | `missing_key`, `malformed_key`, `invalid_key`, `expired_key`, `revoked_key`, `session_expired` ([authentication errors](https://instabox.sh/docs/api.md#authentication-errors)), `unauthenticated` (a bad refresh token or ticket) |
| 403 | `permission_denied`, `missing_scope` (the key lacks the scope), `template_not_allowed` (the free plan creates from agent-browser or ci only; a snapshot is not limited) |
| 404 | `not_found` |
| 409 | `operation_conflict` (another operation runs, which `operationId` names, or the change is not allowed now), `idempotency_conflict`, `machine_not_running`, `delete_confirmation_required` (`X-Confirm-Delete` names neither the ID nor the name), `fork_unavailable` (the machine cannot be forked on its host: suspended, or an older host; save a snapshot instead) |
| 429 | `quota_exceeded` (a [limit](https://instabox.sh/docs/limits.md) was reached; the message says which) |
| 503 | `no_capacity` (with `retryAfterSeconds` and `Retry-After` when retryable), `host_unavailable` (usually retryable) |

`no_capacity` (no host has room right now, as a 503 or a failed operation) is retryable: wait `retryAfterSeconds` (60; the 503 also sends it as the `Retry-After` header), then retry with exponential backoff (for example every few minutes), or try another size or template. A create whose operation failed replays it for the same `Idempotency-Key`, so retry with a new key. With `--fail-fast` the CLI's `create` and `run` exit 75 on it (otherwise 1).

### Authentication errors
Each credential problem has its own `code` (`reason` repeats it), with `hint` saying how to fix it and `docs` linking here. All are HTTP 401. Older servers sent `unauthenticated`, `api_key_expired` or `api_key_revoked` instead; treat those as credential errors too.
| `code` | When |
|---|---|
| `missing_key` | No `Authorization` or `X-API-Key` header, or an empty `Bearer` token (an unset variable) |
| `malformed_key` | Not an API key (`ibk_` and 52 characters) or access token, or `Authorization` without `Bearer` |
| `invalid_key` | Well-formed, but no such key or access token |
| `expired_key` | The key or token expired |
| `revoked_key` | The key was revoked, or the session signed out |
| `session_expired` | The dashboard's session cookie is no longer valid |
```
curl https://api.instabox.sh/v1/machines -H 'Authorization: Bearer garbage'
{
  "error": {
    "code": "malformed_key",
    "reason": "malformed_key",
    "message": "malformed API key: expected an API key (\"ibk_...\") or an access token (\"iba_...\"); create an API key at …",
    "hint": "create an API key at https://dashboard.instabox.sh/dashboard/api-keys and send it as \"Authorization: Bearer <key>\" or \"X-API-Key: <key>\"",
    "docs": "https://instabox.sh/docs/api#authentication-errors",
    ...
  }
}
```
A key without the scope a route needs gets `403 missing_scope` (older servers: `insufficient_scope`) with the scopes the route accepts in `requiredScopes` (the message names them too, such as `this API key lacks the machines:delete scope this route needs`) and a `hint` to create a key with one.

### OpenAPI
The full machine-readable description, every public endpoint included, is at [https://api.instabox.sh/v1/openapi.json](https://api.instabox.sh/v1/openapi.json).
