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.
A task is a plain-English instruction carried out by the ARTEMIS agent on a running device. Read Tasks for profiles, statuses and timeouts.
The task object#
{
"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 and cancel: [{ "n", "label", "ms" }]. |
created_at, started_at, ended_at |
string or null | Timestamps. |
Run a task#
/v1/devices/:id/tasksStarts a task on a running device. Returns immediately; poll get a task or watch the stream 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. |
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"}'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();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
{
"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.
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}'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`);
}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#
/v1/tasksReturns a JSON array of tasks, newest first, without steps.
| Query | Description |
|---|---|
device_id |
Only tasks on this device. |
limit |
1–200, default 50. |
curl "https://akaphone.abhgnx-417.workers.dev/v1/tasks?device_id=dev_s6zr7mr4f2f1&limit=10" \
-H "Authorization: Bearer $AKAPHONE_API_KEY"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());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
[
{ "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#
/v1/tasks/:idcurl https://akaphone.abhgnx-417.workers.dev/v1/tasks/tsk_01m5vqsywgehe4jhva3g0r \
-H "Authorization: Bearer $AKAPHONE_API_KEY"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());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.
{
"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#
/v1/tasks/:id/cancelCancels 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.
curl -X POST https://akaphone.abhgnx-417.workers.dev/v1/tasks/tsk_01m5vqsywgp6df4zpzd4h9/cancel \
-H "Authorization: Bearer $AKAPHONE_API_KEY"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());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.
{ "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#
/v1/tasks/:id/artifactsLists 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.
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"; doneconst { 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");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
{
"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.