---
title: API conventions
description: Auth, ids, idempotency, pagination, versioning and retries — the things every operation assumes.
---

The [operation reference](/docs/api) is generated from the same oRPC contract the API implements
and both SDKs type themselves against, so a route cannot exist in one without existing in the
others. This page is what that document assumes you already know.

## Base URL and versioning

```
https://api.example.com/v1
```

The version is **part of the path**, not a header. `RASTER_BASE_URL` includes it, and so does
every example here.

Within a version, fields are added and never repurposed. Enums are closed sets that may gain a
member, so parse defensively: a client that throws on an unknown `MachineEventType` breaks on a
release that adds one.

## Authentication

```
Authorization: Bearer sk_...
```

An organization API key, or — for a browser — the httponly `computer_session` cookie. There is
no third way in. See [organizations and keys](/docs/reference/organizations) for scopes and how
they intersect with the holder's role.

A cookie-authenticated **write** additionally requires an `Origin` the API recognises.

## Identifiers

Every id is prefixed, so a value is self-describing in a log or an error, and the prefix is part
of the public contract:

| Prefix | Resource     |     | Prefix | Resource       |
| ------ | ------------ | --- | ------ | -------------- |
| `usr_` | user         |     | `snp_` | snapshot       |
| `org_` | organization |     | `tpl_` | template       |
| `mem_` | membership   |     | `img_` | image          |
| `inv_` | invitation   |     | `key_` | api key        |
| `mch_` | machine      |     | `evt_` | event          |
| `ses_` | session      |     | `prv_` | published port |
| `lea_` | input lease  |     | `sec_` | machine secret |

Ids are opaque past the prefix. Do not parse them for ordering or timestamps.

## Requests and responses

JSON in, JSON out. Field names are `snake_case` on the wire; both SDKs present them in the host
language's own style at the wrapper layer and pass them through untouched on `client.api`.

Timestamps are RFC 3339 in UTC. Billing quantities are **decimal strings**, not numbers, because
a byte-second over a month exceeds what a double holds exactly.

## Errors

Every non-2xx response is the same envelope:

```json
{
  "error": {
    "code": "quota_exceeded",
    "message": "your plan allows 3 running machines",
    "request_id": "req_...",
    "details": [{ "field": "size", "message": "must be one of small, standard, large" }],
    "quota": {
      "limit": "running_machines",
      "current": 3,
      "maximum": 3,
      "plan_id": "developer",
      "upgrade_url": "..."
    }
  }
}
```

Switch on `code`. It is a closed set with a fixed HTTP status per member. `message` is written
for a person and may be reworded between releases. See [errors](/docs/reference/errors).

## Idempotency

Any unsafe request accepts a key:

```
Idempotency-Key: <your unique value>
```

That header is what makes a retry safe. A repeat replays the first response instead of creating a
second machine.

- The stored record holds a hash of the **method, path and body**, so reusing a key with a
  different body is a `conflict` rather than a wrong replay.
- The insert is the lock: two concurrent requests with the same key race on a unique index and
  exactly one proceeds. The loser is told to retry rather than being given a half-finished
  answer.
- Keys are retained for 24 hours.

Both SDKs take an `idempotencyKey` / `idempotency_key` argument on every unsafe call.

## Retries

The SDKs retry a request only when repeating it can produce nothing but the effect it already
had: a `GET` or `HEAD`, or an unsafe request **carrying an idempotency key**. Everything else is
attempted once.

Retried codes are `rate_limited`, `internal_error` and `compute_unavailable`, with exponential
backoff to a 2 second ceiling. `compute_unavailable` is on that list deliberately — a machine
that is running but whose agent has not published its address yet answers with it on every boot,
and that is a normal state to wait through rather than a failure to report.

Default timeout is 30 seconds per attempt. A machine that is still booting is **refused, not
waited on**; waiting is `waitForDesktop`'s job, not the transport's.

## Pagination

Every list answers the same shape:

```json
{ "data": [...], "next_cursor": "...", "has_more": true }
```

Pass `next_cursor` back as `cursor`, with `limit` to size the page. A cursor is an opaque
encoding of the last row's sort key — **never construct one**, and never substitute an offset: an
offset skips or repeats rows on a list that changes while it is being read.

`has_more` is known without a second count query, because the query over-fetches by one.

Both SDKs iterate pages for you, so a cursor never appears in your code:

```ts
for await (const machine of client.machines.list()) {
  /* every page */
}
```

## Rate limits

Signup and login are rate-limited — failed logins per email, signups per IP, within a window —
and a successful login clears the counter so a good login does not count against the next. No
other route is rate-limited by the API today.

The gateway limits input messages per second per socket separately, and a viewer that does not
coalesce pointer moves is meant to hit it.

## Request ids

Every response the API produces carries `request_id`, and so does every error. Quote it in a
support request; it is the only thing that identifies one call among a day of them.

## What the API never returns

No substrate identifier, host address, environment id or guest bearer token appears in any
response, and there is no route that would return one. The SDKs cannot expose them because the
API does not.
