# REST API

> Base URL, authentication with ak_live_ Bearer keys, request and response conventions, the error format with every code and HTTP status, rate limits and list limits.

Source: https://akaphone-docs.pages.dev/api/rest/

The AkaPhone API is JSON over HTTPS. Everything the dashboard does goes through it, so anything you can do in the dashboard you can script.

## Base URL

```text
https://akaphone.abhgnx-417.workers.dev
```

All resource endpoints are under `/v1`. `GET /healthz` and `GET /v1/profiles` work without authentication.

## Authentication

Send an API key as a Bearer token:

```bash
curl https://akaphone.abhgnx-417.workers.dev/v1/me -H "Authorization: Bearer ak_live_urQuj41lKvo2VYp2kBoFqRGw5ig661RoAtM5uTvyrhB"
```

- Create keys in the dashboard under **API keys**, or with [`POST /v1/keys`](https://akaphone-docs.pages.dev/api/keys/). A key is `ak_live_` followed by 43 letters and digits. It is shown once; AkaPhone stores only its SHA-256 hash.
- A key has the full rights of your account. There are no scopes. Use one key per machine or pipeline so you can revoke them independently.
- A missing, malformed or revoked key gets `401 unauthorized`. A suspended account gets `403 forbidden`.

The dashboard authenticates with a session cookie instead. Cookie-authenticated requests that change something are accepted only from the dashboard's own origin, so the cookie cannot be used from other sites.

## Requests and responses

- Send bodies as JSON with `Content-Type: application/json`. Unknown fields are ignored. A body that is not valid JSON gets `400 invalid_request` (*Request body must be valid JSON*). Endpoints whose fields are all optional accept an empty body.
- Times are ISO 8601 strings in UTC, for example `"2026-10-26T09:14:02.000Z"`. Fields that have not happened yet are `null`.
- IDs are prefixed strings: `usr_`, `key_`, `dev_`, `tsk_`, `snp_`, `upl_`, `led_`, `ins_`, `art_`, `nod_`. Treat them as opaque.
- Money is integer **credits** (1 credit = 1 device-minute).
- Successful writes return `201` (created), `202` (accepted, work continues in the background) or `204` (no body).

### Lists

List endpoints return everything that matches, newest first, with no cursor:

| Endpoint | Returns | Limit |
|---|---|---|
| `GET /v1/devices` | a JSON array | active devices; `?status=all` for the 200 most recent |
| `GET /v1/snapshots` | a JSON array | the 200 most recent |
| `GET /v1/tasks` | a JSON array | `?limit=` 1–200, default 50 |
| `GET /v1/keys` | `{ "keys": [...] }` | all active keys (at most 25) |
| `GET /v1/billing/ledger` | `{ "entries": [...] }` | `?limit=` 1–500, default 50 |
| `GET /v1/tasks/:id/artifacts` | `{ "files": [...] }` | all files of the task |

## Errors

Every error has the same shape:

```json
{
  "error": {
    "code": "concurrency_limit",
    "message": "Your plan allows 1 concurrent device"
  }
}
```

Branch on `code`; `message` is for people and may change.

| HTTP | `code` | Meaning |
|---|---|---|
| 400 | `invalid_request` | The body or query failed validation. The message names the field, e.g. `instruction: Too small: expected string to have >=1 characters`. |
| 401 | `unauthorized` | No credentials, or the key or session is invalid or revoked. |
| 402 | `insufficient_credits` | Your balance is below 1 credit. Buy a pack. |
| 403 | `forbidden` | The account is suspended, or a cookie request came from another origin. |
| 404 | `not_found` | The resource does not exist or belongs to someone else. Also returned for unknown routes. |
| 409 | `concurrency_limit` | You already have as many devices as your plan allows. |
| 409 | `conflict` | The request clashes with current state: the device is not `running`, it already has an active task, a snapshot quota is reached, an upload is not complete, the device's host is not connected, too many API keys. |
| 426 | `invalid_request` | A plain HTTP request to the stream endpoint, which needs a WebSocket upgrade. |
| 429 | `rate_limited` | Too many requests. See `Retry-After`. |
| 500 | `internal` | Something went wrong on our side. Safe to retry with backoff. |
| 503 | `no_capacity` | Reserved. Devices queue instead of failing when no slot is free, so you will see `no_capacity` as a device's `error` field, not as an HTTP error. |
| 504 | `internal` | A screenshot could not get a frame from the device within 8 seconds. Retry. |

## Rate limits

| Bucket | Limit |
|---|---|
| All `/v1` requests | 120 per minute per account |
| Device creation (including restores) | 10 per minute |
| Task creation | 30 per minute |
| Snapshot creation | 5 per hour |

A limited request gets `429` with a `Retry-After` header in seconds. There are no `X-RateLimit-*` headers. A simple retry wrapper:

```typescript title="TypeScript"
async function pf(path: string, init: RequestInit = {}, tries = 4): Promise<any> {
  const res = await fetch(`https://akaphone.abhgnx-417.workers.dev${path}`, {
    ...init,
    headers: {
      Authorization: `Bearer ${process.env.AKAPHONE_API_KEY}`,
      "Content-Type": "application/json",
      ...init.headers,
    },
  });
  if (res.status === 429 && tries > 1) {
    const wait = Number(res.headers.get("retry-after") ?? "1");
    await new Promise((r) => setTimeout(r, wait * 1000));
    return pf(path, init, tries - 1);
  }
  if (res.status === 204) return null;
  const body = await res.json();
  if (!res.ok) throw new Error(`${res.status} ${body.error.code}: ${body.error.message}`);
  return body;
}
```
```python title="Python"
import os, time, requests

API = "https://akaphone.abhgnx-417.workers.dev"
session = requests.Session()
session.headers["Authorization"] = f"Bearer {os.environ['AKAPHONE_API_KEY']}"

def pf(method: str, path: str, tries: int = 4, **kwargs):
    r = session.request(method, API + path, **kwargs)
    if r.status_code == 429 and tries > 1:
        time.sleep(int(r.headers.get("Retry-After", "1")))
        return pf(method, path, tries - 1, **kwargs)
    if r.status_code == 204:
        return None
    body = r.json()
    if not r.ok:
        raise RuntimeError(f"{r.status_code} {body['error']['code']}: {body['error']['message']}")
    return body
```

Plan limits (concurrent devices, snapshots, retention) are listed in [Limits and quotas](https://akaphone-docs.pages.dev/guides/limits-and-quotas/).

## CORS

The API sends CORS headers only to AkaPhone's own sites. Call it from your server, scripts or CI, not from a browser page on another domain. For a browser live view on your own site, mint a [stream token](https://akaphone-docs.pages.dev/api/stream/#authentication) on your server.

## Resources

| Resource | Endpoints |
|---|---|
| [Devices](https://akaphone-docs.pages.dev/api/devices/) | profiles, create, list, get, stop, screenshot, input, install app, install status, snapshot, stream token |
| [Tasks](https://akaphone-docs.pages.dev/api/tasks/) | run, list, get, cancel, artifacts |
| [Snapshots](https://akaphone-docs.pages.dev/api/snapshots/) | list, restore, delete |
| [Uploads](https://akaphone-docs.pages.dev/api/uploads/) | create, complete |
| [Live stream](https://akaphone-docs.pages.dev/api/stream/) | WebSocket frames, events and input |
| [API keys](https://akaphone-docs.pages.dev/api/keys/) | list, create, revoke |
| [Usage](https://akaphone-docs.pages.dev/api/usage/) | minutes by day and device |
| [Billing](https://akaphone-docs.pages.dev/api/billing/) | ledger, checkout |
| [Me](https://akaphone-docs.pages.dev/api/me/) | account, plan and balance |

There is no official SDK or CLI yet; the examples use `fetch` and `requests` directly. The [MCP server](https://akaphone-docs.pages.dev/mcp/tools/) is the packaged client for coding agents.
