AkaPhoneDocs
REST API

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#

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:

Shell
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 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:

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;
}

Plan 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.