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.
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#
https://akaphone.abhgnx-417.workers.devAll resource endpoints are under /v1. GET /healthz and GET /v1/profiles work without authentication.
Authentication#
Send an API key as a Bearer token:
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. A key isak_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 gets403 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 gets400 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 arenull. - 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) or204(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:
{
"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:
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;
}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 bodyPlan limits (concurrent devices, snapshots, retention) are listed in 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 on your server.
Resources#
| Resource | Endpoints |
|---|---|
| Devices | profiles, create, list, get, stop, screenshot, input, install app, install status, snapshot, stream token |
| Tasks | run, list, get, cancel, artifacts |
| Snapshots | list, restore, delete |
| Uploads | create, complete |
| Live stream | WebSocket frames, events and input |
| API keys | list, create, revoke |
| Usage | minutes by day and device |
| Billing | ledger, checkout |
| Me | account, plan and balance |
There is no official SDK or CLI yet; the examples use fetch and requests directly. The MCP server is the packaged client for coding agents.