# Tasks API

> Run plain-English tasks on a device with your model key, poll their status and steps, cancel them, and download their video, screenshots, trace and log.

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

A task is a plain-English instruction carried out by the ARTEMIS agent on a running device. Read [Tasks](https://akaphone-docs.pages.dev/concepts/tasks/) for profiles, statuses and timeouts.

## The task object

```json
{
  "id": "tsk_01m5vqsywgehe4jhva3g0r",
  "device_id": "dev_s6zr7mr4f2f1",
  "instruction": "Open Settings > About phone and report the Android version",
  "profile": "flash",
  "provider": "gemini",
  "status": "succeeded",
  "result": "Android version: 14",
  "error": null,
  "step_count": 4,
  "created_at": "2026-10-26T09:15:10.000Z",
  "started_at": "2026-10-26T09:15:11.000Z",
  "ended_at": "2026-10-26T09:15:29.000Z"
}
```

| Field | Type | Description |
|---|---|---|
| `id` | string | `tsk_…` |
| `device_id` | string | The device it runs on. |
| `instruction` | string | What you asked for. |
| `profile` | string | `flash` or `pro`. |
| `provider` | string or null | The `X-Model-Provider` you sent, or `null` (Gemini is used). |
| `status` | string | `queued`, `running`, `succeeded`, `failed` or `cancelled`. |
| `result` | string or null | The agent's final answer when it finishes. |
| `error` | string or null | Why it failed, e.g. `timeout`. |
| `step_count` | integer | Steps taken so far. |
| `steps` | array | Only on [get](#get-a-task) and [cancel](#cancel-a-task): `[{ "n", "label", "ms" }]`. |
| `created_at`, `started_at`, `ended_at` | string or null | Timestamps. |

## Run a task

`POST /v1/devices/:id/tasks`

Starts a task on a `running` device. Returns immediately; poll [get a task](#get-a-task) or watch the [stream](https://akaphone-docs.pages.dev/api/stream/#events) for progress.

| Header | Required | Description |
|---|---|---|
| `X-Model-Key` | yes, in practice | Your model provider key. Used for this task only and never stored. |
| `X-Model-Provider` | no | `gemini` (default), `anthropic` or `openai`. |

| Body field | Type | Default | Description |
|---|---|---|---|
| `instruction` | string | required | 1–8,000 characters, in plain English. |
| `profile` | string | `"flash"` | `flash` for short, direct actions; `pro` for multi-step flows. |

```bash title="curl"
curl -X POST https://akaphone.abhgnx-417.workers.dev/v1/devices/dev_s6zr7mr4f2f1/tasks \
  -H "Authorization: Bearer $AKAPHONE_API_KEY" \
  -H "X-Model-Key: $GEMINI_API_KEY" \
  -H "X-Model-Provider: gemini" \
  -H "Content-Type: application/json" \
  -d '{"instruction": "Open Settings > About phone and report the Android version", "profile": "flash"}'
```
```typescript title="TypeScript"
const res = await fetch("https://akaphone.abhgnx-417.workers.dev/v1/devices/dev_s6zr7mr4f2f1/tasks", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.AKAPHONE_API_KEY}`,
    "X-Model-Key": process.env.GEMINI_API_KEY!,
    "X-Model-Provider": "gemini",
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    instruction: "Open Settings > About phone and report the Android version",
    profile: "flash",
  }),
});
const task = await res.json();
```
```python title="Python"
import os, requests

task = requests.post(
    "https://akaphone.abhgnx-417.workers.dev/v1/devices/dev_s6zr7mr4f2f1/tasks",
    headers={
        "Authorization": f"Bearer {os.environ['AKAPHONE_API_KEY']}",
        "X-Model-Key": os.environ["GEMINI_API_KEY"],
        "X-Model-Provider": "gemini",
    },
    json={"instruction": "Open Settings > About phone and report the Android version", "profile": "flash"},
).json()
```

**Response** `202 Accepted`

```json
{
  "id": "tsk_01m5vqsywgehe4jhva3g0r",
  "device_id": "dev_s6zr7mr4f2f1",
  "instruction": "Open Settings > About phone and report the Android version",
  "profile": "flash",
  "provider": "gemini",
  "status": "queued",
  "result": null,
  "error": null,
  "step_count": 0,
  "created_at": "2026-10-26T09:15:10.000Z",
  "started_at": null,
  "ended_at": null
}
```

| Error | When |
|---|---|
| `404 not_found` | Unknown device. |
| `409 conflict` | The device is not `running`; it already has a `queued` or `running` task (the message includes its id); or its host is not connected. |
| `400 invalid_request` | Empty or too long `instruction`, unknown `profile`, or `X-Model-Provider` not one of the three. |
| `429 rate_limited` | More than 30 tasks in a minute. |

## Wait for a result

There is no blocking variant; poll every few seconds until the status is final.

```bash title="curl"
while :; do
  TASK=$(curl -s https://akaphone.abhgnx-417.workers.dev/v1/tasks/$TASK_ID -H "Authorization: Bearer $AKAPHONE_API_KEY")
  case $(echo "$TASK" | jq -r .status) in succeeded|failed|cancelled) break;; esac
  sleep 3
done
echo "$TASK" | jq '{status, result, error}'
```
```typescript title="TypeScript"
async function waitForTask(id: string, timeoutMs = 35 * 60_000) {
  const deadline = Date.now() + timeoutMs;
  while (Date.now() < deadline) {
    const t = await fetch(`https://akaphone.abhgnx-417.workers.dev/v1/tasks/${id}`, {
      headers: { Authorization: `Bearer ${process.env.AKAPHONE_API_KEY}` },
    }).then((r) => r.json());
    if (["succeeded", "failed", "cancelled"].includes(t.status)) return t;
    await new Promise((r) => setTimeout(r, 3000));
  }
  throw new Error(`task ${id} still running`);
}
```
```python title="Python"
import time

def wait_for_task(task_id: str, timeout_s: int = 35 * 60):
    deadline = time.time() + timeout_s
    while time.time() < deadline:
        t = requests.get(
            f"https://akaphone.abhgnx-417.workers.dev/v1/tasks/{task_id}",
            headers={"Authorization": f"Bearer {os.environ['AKAPHONE_API_KEY']}"},
        ).json()
        if t["status"] in ("succeeded", "failed", "cancelled"):
            return t
        time.sleep(3)
    raise TimeoutError(task_id)
```

Polling every 3 seconds is 20 requests a minute, well inside the 120 per minute limit even with a few devices.

## List tasks

`GET /v1/tasks`

Returns a JSON **array** of tasks, newest first, without `steps`.

| Query | Description |
|---|---|
| `device_id` | Only tasks on this device. |
| `limit` | 1–200, default 50. |

```bash title="curl"
curl "https://akaphone.abhgnx-417.workers.dev/v1/tasks?device_id=dev_s6zr7mr4f2f1&limit=10" \
  -H "Authorization: Bearer $AKAPHONE_API_KEY"
```
```typescript title="TypeScript"
const tasks = await fetch("https://akaphone.abhgnx-417.workers.dev/v1/tasks?device_id=dev_s6zr7mr4f2f1&limit=10", {
  headers: { Authorization: `Bearer ${process.env.AKAPHONE_API_KEY}` },
}).then((r) => r.json());
```
```python title="Python"
tasks = requests.get(
    "https://akaphone.abhgnx-417.workers.dev/v1/tasks",
    params={"device_id": "dev_s6zr7mr4f2f1", "limit": 10},
    headers={"Authorization": f"Bearer {os.environ['AKAPHONE_API_KEY']}"},
).json()
```

**Response** `200 OK`

```json
[
  { "id": "tsk_01m5vqsywgp6df4zpzd4h9", "device_id": "dev_s6zr7mr4f2f1", "instruction": "Turn on Dark theme in Settings > Display", "profile": "flash", "provider": null, "status": "running", "result": null, "error": null, "step_count": 2, "created_at": "2026-10-26T09:18:01.000Z", "started_at": "2026-10-26T09:18:02.000Z", "ended_at": null },
  { "id": "tsk_01m5vqsywgehe4jhva3g0r", "device_id": "dev_s6zr7mr4f2f1", "instruction": "Open Settings > About phone and report the Android version", "profile": "flash", "provider": "gemini", "status": "succeeded", "result": "Android version: 14", "error": null, "step_count": 4, "created_at": "2026-10-26T09:15:10.000Z", "started_at": "2026-10-26T09:15:11.000Z", "ended_at": "2026-10-26T09:15:29.000Z" }
]
```

Tasks older than your plan's retention (7, 30 or 90 days) are deleted.

## Get a task

`GET /v1/tasks/:id`

```bash title="curl"
curl https://akaphone.abhgnx-417.workers.dev/v1/tasks/tsk_01m5vqsywgehe4jhva3g0r \
  -H "Authorization: Bearer $AKAPHONE_API_KEY"
```
```typescript title="TypeScript"
const task = await fetch("https://akaphone.abhgnx-417.workers.dev/v1/tasks/tsk_01m5vqsywgehe4jhva3g0r", {
  headers: { Authorization: `Bearer ${process.env.AKAPHONE_API_KEY}` },
}).then((r) => r.json());
```
```python title="Python"
task = requests.get(
    "https://akaphone.abhgnx-417.workers.dev/v1/tasks/tsk_01m5vqsywgehe4jhva3g0r",
    headers={"Authorization": f"Bearer {os.environ['AKAPHONE_API_KEY']}"},
).json()
```

**Response** `200 OK`: the task with its `steps`.

```json
{
  "id": "tsk_01m5vqsywgehe4jhva3g0r",
  "device_id": "dev_s6zr7mr4f2f1",
  "instruction": "Open Settings > About phone and report the Android version",
  "profile": "flash",
  "provider": "gemini",
  "status": "succeeded",
  "result": "Android version: 14",
  "error": null,
  "step_count": 4,
  "created_at": "2026-10-26T09:15:10.000Z",
  "started_at": "2026-10-26T09:15:11.000Z",
  "ended_at": "2026-10-26T09:15:29.000Z",
  "steps": [
    { "n": 1, "label": "Open the Settings app", "ms": 2140 },
    { "n": 2, "label": "Scroll to About phone", "ms": 1920 },
    { "n": 3, "label": "Tap About phone", "ms": 1610 },
    { "n": 4, "label": "Read the Android version", "ms": 2380 }
  ]
}
```

## Cancel a task

`POST /v1/tasks/:id/cancel`

Cancels a `queued` or `running` task and stops ARTEMIS on the host. Cancelling a task that has already finished changes nothing and returns it as it is.

```bash title="curl"
curl -X POST https://akaphone.abhgnx-417.workers.dev/v1/tasks/tsk_01m5vqsywgp6df4zpzd4h9/cancel \
  -H "Authorization: Bearer $AKAPHONE_API_KEY"
```
```typescript title="TypeScript"
const task = await fetch("https://akaphone.abhgnx-417.workers.dev/v1/tasks/tsk_01m5vqsywgp6df4zpzd4h9/cancel", {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.AKAPHONE_API_KEY}` },
}).then((r) => r.json());
```
```python title="Python"
task = requests.post(
    "https://akaphone.abhgnx-417.workers.dev/v1/tasks/tsk_01m5vqsywgp6df4zpzd4h9/cancel",
    headers={"Authorization": f"Bearer {os.environ['AKAPHONE_API_KEY']}"},
).json()
```

**Response** `200 OK`: the task with `steps`, status `cancelled`.

```json
{ "id": "tsk_01m5vqsywgp6df4zpzd4h9", "device_id": "dev_s6zr7mr4f2f1", "instruction": "Turn on Dark theme in Settings > Display", "profile": "flash", "provider": null, "status": "cancelled", "result": null, "error": null, "step_count": 2, "created_at": "2026-10-26T09:18:01.000Z", "started_at": "2026-10-26T09:18:02.000Z", "ended_at": "2026-10-26T09:18:09.000Z", "steps": [{ "n": 1, "label": "Open the Settings app", "ms": 2050 }, { "n": 2, "label": "Tap Display", "ms": 1700 }] }
```

## List artifacts

`GET /v1/tasks/:id/artifacts`

Lists the files the host uploaded for a finished task, each with a presigned download URL valid for **15 minutes**. Request the list again for fresh URLs.

```bash title="curl"
curl https://akaphone.abhgnx-417.workers.dev/v1/tasks/tsk_01m5vqsywgehe4jhva3g0r/artifacts \
  -H "Authorization: Bearer $AKAPHONE_API_KEY" \
  | jq -r '.files[] | "\(.name) \(.url)"' \
  | while read -r NAME URL; do curl -s -o "$NAME" "$URL"; done
```
```typescript title="TypeScript"
const { files } = await fetch("https://akaphone.abhgnx-417.workers.dev/v1/tasks/tsk_01m5vqsywgehe4jhva3g0r/artifacts", {
  headers: { Authorization: `Bearer ${process.env.AKAPHONE_API_KEY}` },
}).then((r) => r.json());
const video = files.find((f: { kind: string }) => f.kind === "video");
```
```python title="Python"
files = requests.get(
    "https://akaphone.abhgnx-417.workers.dev/v1/tasks/tsk_01m5vqsywgehe4jhva3g0r/artifacts",
    headers={"Authorization": f"Bearer {os.environ['AKAPHONE_API_KEY']}"},
).json()["files"]
for f in files:
    open(f["name"], "wb").write(requests.get(f["url"]).content)
```

**Response** `200 OK`

```json
{
  "files": [
    { "id": "art_01m5vqsywgjpffndevq1tx", "kind": "video", "name": "video.mp4", "size_bytes": 3482113, "url": "https://<account>.r2.cloudflarestorage.com/akaphone-artifacts/artifacts/usr_01m5vqsywg3mxj7dyxv9q9/tsk_01m5vqsywgehe4jhva3g0r/video.mp4?X-Amz-Algorithm=AWS4-HMAC-SHA256&..." },
    { "id": "art_01m5vqsywgxexz9wabrvn9", "kind": "screenshot", "name": "before.png", "size_bytes": 412877, "url": "https://…/before.png?…" },
    { "id": "art_01m5vqsywg7jzjtbbkkczn", "kind": "screenshot", "name": "after.png", "size_bytes": 398120, "url": "https://…/after.png?…" },
    { "id": "art_01m5vqsywgskq594xgaqcm", "kind": "log", "name": "stdout.log", "size_bytes": 18233, "url": "https://…/stdout.log?…" }
  ]
}
```

`kind` is `video`, `screenshot`, `trace` or `log`. The list is empty while a task is running, and for a few seconds after it ends while the host finishes uploading. Download URLs need no `Authorization` header; do not send yours to them. Artifacts are deleted after your plan's retention period.
