Documentation Developers API Reference

API Reference

27 min read·Updated 30 Jul 2026

Every capability in the Arble desktop application is available through this API. Sessions, agents, memory, tools, permissions, desktop and mobile control are all resources you can create, read and act on from your own code.

The API is REST over HTTPS. Requests and responses are JSON. Long-running work streams over Server-Sent Events, and anything that happens outside a request — a permission approval, a completed run — reaches you through webhooks. Authentication is a bearer token on every request.

Quick start

Create a run and get a result in one call:

SHELL
curl https://api.arble.ai/v1/runs \
  -H "Authorization: Bearer $ARBLE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "agent": "agt_5wTn9Kd",
    "input": "Summarize the open PRs in acme/api-gateway"
  }'
JSON
{
  "id": "run_2Kd8vQx",
  "object": "run",
  "status": "queued",
  "agent": "agt_5wTn9Kd",
  "session": "ses_8kQ2mVx",
  "created_at": 1775049600
}

The run is created immediately and executes asynchronously. Poll it, stream it, or receive a run.completed webhook — all three are covered below.

Three things every request needs: an API key in the Authorization header, Content-Type: application/json on any request with a body, and the base URL for your environment.

Authentication

All requests authenticate with a bearer token. There is no other scheme — no query-string keys, no HTTP basic.

SHELL
curl https://api.arble.ai/v1/agents \
  -H "Authorization: Bearer arb_live_9f2c4a..."

Tokens differ in what they can reach, not in how they’re sent.

Token typePrefixScope
Personalarb_user_Everything the user can access, across their organizations
Organizationarb_org_One organization; all its projects
Projectarb_proj_One project. The right default for a deployed service
Sessionarb_ses_One session, expires in ≤ 1 hour. For browser and mobile clients
Devicearb_dev_One paired device, for desktop and mobile control endpoints

Project tokens are what most integrations should use. A token scoped to one project can’t read another project’s memory even if the same key leaks.

Session tokens exist so you never ship a long-lived key to an untrusted client. Mint one server-side, hand it to the browser, and let it expire:

SHELL
curl https://api.arble.ai/v1/tokens \
  -H "Authorization: Bearer $ARBLE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"type": "session", "session": "ses_8kQ2mVx", "expires_in": 900}'
JSON
{
  "object": "token",
  "token": "arb_ses_7Vn2...",
  "type": "session",
  "expires_at": 1775050500
}

Device authentication covers the desktop and mobile endpoints, which act on a specific machine. Pair the device once through the app; the resulting device token is the only credential those endpoints accept.

Rotation. Create the new key, deploy it, then revoke the old one. Both work during the overlap, so rotation needs no downtime. Revocation takes effect immediately, on in-flight requests as well as new ones.

SHELL
curl -X POST https://api.arble.ai/v1/api_keys \
  -H "Authorization: Bearer $ARBLE_ADMIN_KEY" \
  -d '{"name": "api-gateway-prod", "scope": "project"}'

curl -X DELETE https://api.arble.ai/v1/api_keys/key_8fRt2Ls \
  -H "Authorization: Bearer $ARBLE_ADMIN_KEY"

The full key is returned only once, at creation. Store it in a secret manager; there is no endpoint that reads it back.

Base URL

EnvironmentBase URL
Productionhttps://api.arble.ai/v1
Developmenthttps://api.sandbox.arble.ai/v1
Self-hostedhttps://<your-host>/v1

Development is a full instance with separate data and no billing. Runs execute against a smaller model by default, so latency and output differ from production — verify behavior in production before you depend on it.

Regional endpoints pin data residency. Requests to a regional host never leave that region, including model inference:

RegionHost
United Stateshttps://us.api.arble.ai/v1
European Unionhttps://eu.api.arble.ai/v1
Asia Pacifichttps://ap.api.arble.ai/v1

An object created in one region isn’t visible from another. Pick a region per project, not per request.

Making requests

HeaderNotes
AuthorizationBearer <token>. Required on every request.
Content-Typeapplication/json on any request with a body.
Arble-VersionPins the API version. Defaults to your account’s version.
Idempotency-KeyClient-generated key. Safe retries on POST.
Accept-Encodinggzip or br. Responses are compressed when requested.

Idempotency. Send an Idempotency-Key on any POST that creates something. A repeated key returns the original response instead of creating a second object — keys are retained 24 hours:

SHELL
curl https://api.arble.ai/v1/runs \
  -H "Authorization: Bearer $ARBLE_API_KEY" \
  -H "Idempotency-Key: 8f2c4a9e-1b7d-4e3a-9c8f-2d1e6b5a4c30" \
  -H "Content-Type: application/json" \
  -d '{"agent": "agt_5wTn9Kd", "input": "Audit the retry logic"}'

Reusing a key with a different body returns 400 idempotency_key_reused.

Timeouts. The API responds within 30 seconds. Anything longer is asynchronous by design: creating a run returns immediately with status: "queued", and you stream or poll for the result. Set a client timeout of 30 seconds and don’t raise it.

Retries. Retry 429, 500, 502, 503 and 504 with exponential backoff and jitter. Never retry 400, 401, 403, 404 or 422 — the request is wrong and will stay wrong. Always send an idempotency key on retried POSTs.

Agents

An agent is a reusable configuration: a model, a set of tools, and instructions. Runs are executions of an agent. Create the agent once and run it many times.

Create an agent

POST/v1/agents
ParameterTypeNotes
namestringRequired. Unique within the project.
instructionsstringRequired. System instructions.
modelstringDefaults to the project’s model.
toolsarrayTool names the agent may call. Omit for all project tools.
permissionsobjectCapability grants scoped to this agent.
metadataobjectUp to 16 key-value pairs. Returned unmodified.
SHELL
curl https://api.arble.ai/v1/agents \
  -H "Authorization: Bearer $ARBLE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "pr-reviewer",
    "instructions": "Review diffs for missing error handling.",
    "model": "arble-1",
    "tools": ["github.get_pull_request", "github.create_review_comment"],
    "permissions": {"network.http": ["api.github.com"]}
  }'
JSON
{
  "id": "agt_5wTn9Kd",
  "object": "agent",
  "name": "pr-reviewer",
  "model": "arble-1",
  "tools": ["github.get_pull_request", "github.create_review_comment"],
  "permissions": {"network.http": ["api.github.com"]},
  "created_at": 1775049600
}

Errors: 400 parameter_missing · 409 name_already_exists · 422 unknown_tool

List agents

GET/v1/agents

Returns agents in the project, newest first. Accepts limit, cursor and name filters.

JSON
{
  "object": "list",
  "data": [
    {"id": "agt_5wTn9Kd", "object": "agent", "name": "pr-reviewer"},
    {"id": "agt_3pLwRt9", "object": "agent", "name": "ci-triage"}
  ],
  "has_more": true,
  "next_cursor": "cur_agt_3pLwRt9"
}

Errors: 400 invalid_cursor

Retrieve, update, delete

GET/v1/agents/:id
PATCH/v1/agents/:id
DELETE/v1/agents/:id

PATCH accepts any creatable field except name. Updates apply to future runs; in-flight runs keep the configuration they started with. DELETE fails while runs are active unless force=true, which cancels them.

SHELL
curl -X PATCH https://api.arble.ai/v1/agents/agt_5wTn9Kd \
  -H "Authorization: Bearer $ARBLE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model": "arble-1-pro"}'
JSON
{"id": "agt_5wTn9Kd", "object": "agent", "deleted": true}

Errors: 404 agent_not_found · 409 agent_has_active_runs · 422 unknown_tool

Sessions

A session holds context across runs: messages, tool results, and permission grants. Runs in the same session see each other’s history; runs in different sessions are independent.

Create a session

POST/v1/sessions
ParameterTypeNotes
titlestringOptional, for display.
agentstringDefault agent for runs in this session.
metadataobjectUp to 16 key-value pairs.
SHELL
curl https://api.arble.ai/v1/sessions \
  -H "Authorization: Bearer $ARBLE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"title": "Payment retry audit", "agent": "agt_5wTn9Kd"}'
JSON
{
  "id": "ses_8kQ2mVx",
  "object": "session",
  "status": "active",
  "title": "Payment retry audit",
  "agent": "agt_5wTn9Kd",
  "message_count": 0,
  "created_at": 1775049600
}

Errors: 404 agent_not_found

Session lifecycle

A session is active, paused or archived. Each transition is its own endpoint:

ActionEndpointEffect
PausePOST /v1/sessions/:id/pauseCancels active runs, keeps context
ResumePOST /v1/sessions/:id/resumeReturns to active, context intact
ArchivePOST /v1/sessions/:id/archiveRead-only. Context kept, no new runs
DeleteDELETE /v1/sessions/:idRemoves the session and its messages
SHELL
$ curl -X POST https://api.arble.ai/v1/sessions/ses_8kQ2mVx/pause \
    -H "Authorization: Bearer $ARBLE_API_KEY"

{"id": "ses_8kQ2mVx", "object": "session", "status": "paused"}

Errors: 404 session_not_found · 409 invalid_session_state

Deletion is permanent and does not remove memory the session wrote — memory outlives sessions by design. Delete memory entries explicitly if you need them gone.

Export a session

GET/v1/sessions/:id/export

Returns the complete transcript: messages, tool calls with arguments and results, and every permission decision. This is the artifact for an audit or incident review. format accepts json or jsonl.

Errors: 404 session_not_found

Runs

A run is one execution of an agent. Creating a run returns immediately; the run progresses asynchronously through states:

StatusMeaning
queuedAccepted, not yet started
runningExecuting
requires_actionPaused for a permission approval or client-side tool result
completedFinished successfully
failedFinished with an error. See last_error
cancelledCancelled by request
expiredExceeded timeout without completing

requires_action is the state that matters most: the run is waiting on you. Inspect required_action to see what it needs, then either approve a permission or submit a tool result.

Create a run

POST/v1/runs
ParameterTypeNotes
agentstringRequired unless session has a default agent.
inputstringThe instruction. Required.
sessionstringExisting session. A new one is created if omitted.
streambooleanReturn SSE instead of a run object.
toolsarrayNarrow the agent’s tools for this run only.
timeoutintegerSeconds, 1–3600. Default 300.
metadataobjectUp to 16 key-value pairs.
SHELL
curl https://api.arble.ai/v1/runs \
  -H "Authorization: Bearer $ARBLE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "agent": "agt_5wTn9Kd",
    "session": "ses_8kQ2mVx",
    "input": "Review PR 482 for missing error handling",
    "timeout": 600
  }'

Errors: 400 parameter_missing · 404 agent_not_found · 409 session_not_active · 429 concurrency_limit_exceeded

Retrieve a run

GET/v1/runs/:id
JSON
{
  "id": "run_2Kd8vQx",
  "object": "run",
  "status": "completed",
  "agent": "agt_5wTn9Kd",
  "session": "ses_8kQ2mVx",
  "output": "Three handlers retry without backoff: charge.py:118, ...",
  "created_at": 1775049600,
  "started_at": 1775049601,
  "completed_at": 1775049643,
  "usage": {"input_tokens": 4182, "output_tokens": 311, "tool_calls": 9}
}

When status is failed, last_error carries the same shape as an API error.

Errors: 404 run_not_found

Cancel and retry

POST/v1/runs/:id/cancel
POST/v1/runs/:id/retry

Cancelling is cooperative: an in-flight tool call is given the chance to finish before the run stops. Retrying creates a new run with the same agent, input and session — only failed, cancelled and expired runs can be retried, the original is untouched, and retry_of links them.

JSON
{
  "id": "run_9Ln4Ktp",
  "object": "run",
  "status": "queued",
  "retry_of": "run_2Kd8vQx"
}

Errors: 404 run_not_found · 409 run_not_cancellable · 409 run_not_retryable

List runs

GET/v1/runs

Filter by session, agent, status, created_after and created_before. See Filtering.

SHELL
curl "https://api.arble.ai/v1/runs?session=ses_8kQ2mVx&status=failed" \
  -H "Authorization: Bearer $ARBLE_API_KEY"

Errors: 400 invalid_filter

Messages

Messages are the contents of a session. A run appends messages as it works; you append them to continue a conversation.

Send a message

POST/v1/sessions/:id/messages

Appends a message and starts a run to respond to it. This is the conversational form of POST /v1/runs — use it when you’re building a chat interface rather than dispatching a task. Accepts content (required), attachments (file IDs) and stream.

SHELL
curl https://api.arble.ai/v1/sessions/ses_8kQ2mVx/messages \
  -H "Authorization: Bearer $ARBLE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "content": "Which of those is riskiest under load?",
    "attachments": ["file_7Bn3Kqs"]
  }'
JSON
{
  "id": "msg_4Rt8Nvp",
  "object": "message",
  "session": "ses_8kQ2mVx",
  "role": "user",
  "content": "Which of those is riskiest under load?",
  "attachments": ["file_7Bn3Kqs"],
  "run": "run_6Wq2Mdx",
  "created_at": 1775049700
}

Errors: 404 session_not_found · 409 session_not_active · 422 attachment_not_found

List messages

GET/v1/sessions/:id/messages

Returns messages oldest-first by default; pass order=desc to reverse. Assistant messages include the run that produced them and any tool_calls made along the way.

JSON
{
  "object": "list",
  "data": [
    {
      "id": "msg_5Sv9Owq",
      "object": "message",
      "role": "assistant",
      "content": "webhook.py:203 — it retries inside a request handler.",
      "run": "run_6Wq2Mdx",
      "tool_calls": [
        {"tool": "filesystem.read", "duration_ms": 84}
      ],
      "created_at": 1775049714
    }
  ],
  "has_more": false,
  "next_cursor": null
}

Errors: 404 session_not_found

Memory

Memory is per-project and persists across sessions. Agents read it automatically at the start of a run. Entries should be durable facts — a constraint, a decision, a gotcha — not a log of what happened.

POST/v1/memory/search

Semantic search, not substring matching. Returns ranked entries with relevance scores. Accepts query (required), limit (1–100, default 10) and min_score.

SHELL
$ curl https://api.arble.ai/v1/memory/search \
    -H "Authorization: Bearer $ARBLE_API_KEY" \
    -d '{"query": "why the postgres driver is pinned", "limit": 3}'

{
  "object": "list",
  "data": [
    {
      "id": "mem_2fQx8Lp",
      "object": "memory",
      "content": "pg driver pinned to 8.11.3 — 8.12 breaks pgbouncer.",
      "score": 0.91,
      "source": "run_2Kd8vQx",
      "created_at": 1774963200
    }
  ],
  "has_more": false,
  "next_cursor": null
}

Errors: 400 parameter_missing

Insert, update, delete

POST/v1/memory
PATCH/v1/memory/:id
DELETE/v1/memory/:id
SHELL
$ curl https://api.arble.ai/v1/memory \
    -H "Authorization: Bearer $ARBLE_API_KEY" \
    -d '{"content": "Staging shares the production Redis."}'

{
  "id": "mem_8Yt3Rqz",
  "object": "memory",
  "content": "Staging shares the production Redis.",
  "created_at": 1775049800
}

Errors: 404 memory_not_found · 422 content_too_long

Summarize and export

POST/v1/memory/summarize
GET/v1/memory/export

Summarizing collapses related entries into fewer, denser ones — a smaller memory costs less context on every run. Accepts older_than (a duration like 90d) to bound the work. It’s asynchronous and returns a run you can poll. Export returns every entry as JSONL.

Errors: 409 summarization_in_progress · 403 permission_denied

Tools

Tools are the capabilities an agent can call. Agents call them during runs; you can also call them directly, which is useful for testing a tool or reusing one outside an agent loop.

List tools and metadata

GET/v1/tools
GET/v1/tools/:name

The list returns every tool available to the project — built-in, Tool SDK, MCP and connector-provided — in one collection. source says where each came from; nothing else about the shape differs. Retrieving one returns the full JSON Schema for its input and output, which is the same schema the planner reads.

JSON
{
  "name": "github.create_review_comment",
  "object": "tool",
  "source": "connector",
  "input_schema": {
    "type": "object",
    "properties": {
      "pull_request": {"type": "integer"},
      "path": {"type": "string"},
      "line": {"type": "integer"},
      "body": {"type": "string"}
    },
    "required": ["pull_request", "path", "line", "body"]
  },
  "permissions": ["network.http:api.github.com"]
}

Errors: 404 tool_not_found

Execute a tool

POST/v1/tools/:name/execute

Calls the tool directly, bypassing the agent loop. Arguments are validated against the input schema before execution — a malformed call fails with 422 and never runs.

SHELL
$ curl https://api.arble.ai/v1/tools/github.create_review_comment/execute \
    -H "Authorization: Bearer $ARBLE_API_KEY" \
    -d '{"arguments": {"pull_request": 482, "path": "webhook.py",
                       "line": 203, "body": "This retry stacks."}}'

{
  "object": "tool_result",
  "tool": "github.create_review_comment",
  "output": {"id": 1904832, "url": "https://github.com/..."},
  "duration_ms": 212
}

Direct execution still passes the permission gate. If the capability isn’t granted, the call fails with 403 rather than prompting — there’s no interactive approval on a bare HTTP request. Streaming tools accept "stream": true and return SSE.

Errors: 403 permission_denied · 404 tool_not_found · 422 invalid_arguments · 504 tool_timeout

Workflows

A workflow is a sequence of steps — agent runs, tool calls, or conditionals — defined once and executed on demand or on a schedule. Where a run is one execution, a workflow is a repeatable process with state that survives failures.

Create a workflow

POST/v1/workflows
ParameterTypeNotes
namestringRequired. Unique within the project.
stepsarrayRequired. Ordered steps.
schedulestringCron expression. Omit for on-demand only.
onstringEvent name to trigger on.
max_retriesinteger0–10. Default 3.
SHELL
curl https://api.arble.ai/v1/workflows \
  -H "Authorization: Bearer $ARBLE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "nightly-triage",
    "schedule": "0 7 * * 1-5",
    "steps": [
      {"type": "run", "agent": "agt_3pLwRt9", "input": "Summarize CI failures"},
      {"type": "tool", "tool": "slack.post_message",
       "arguments": {"channel": "#eng-alerts"}}
    ]
  }'

Errors: 400 invalid_cron · 409 name_already_exists · 422 invalid_step

Run and control

POST/v1/workflows/:id/run
POST/v1/workflow_runs/:id/pause
POST/v1/workflow_runs/:id/resume
POST/v1/workflow_runs/:id/cancel
JSON
{
  "id": "wfr_7Qm3Bkt",
  "object": "workflow_run",
  "workflow": "wfl_5Kp2Nvx",
  "status": "running",
  "step": 1,
  "steps_total": 2,
  "started_at": 1775049901
}

Pausing takes effect at the next step boundary, not mid-step. Resuming continues from the step that hadn’t started — completed steps never re-execute.

Errors: 404 workflow_not_found · 409 workflow_run_in_progress · 409 invalid_workflow_state

History

GET/v1/workflow_runs

Returns past executions with per-step timing and outcome. Filter by workflow and status to find failures.

JSON
{
  "object": "list",
  "data": [
    {
      "id": "wfr_7Qm3Bkt",
      "object": "workflow_run",
      "status": "failed",
      "step": 2,
      "last_error": {"type": "permission_error", "code": "permission_denied"},
      "duration_ms": 41200
    }
  ],
  "has_more": false,
  "next_cursor": null
}

Permissions

Every tool call passes a permission gate, whether it came from the app, the CLI or this API. A grant is a capability plus an optional scope.

List and grant

GET/v1/permissions
POST/v1/permissions

level is always_allow, ask or never_allow. Omitting agent applies the grant project-wide.

SHELL
$ curl https://api.arble.ai/v1/permissions \
    -H "Authorization: Bearer $ARBLE_API_KEY" \
    -d '{"capability": "network.http", "scope": "api.github.com",
         "level": "always_allow", "agent": "agt_5wTn9Kd"}'

{
  "id": "perm_3Nx8Vqt",
  "object": "permission",
  "capability": "network.http",
  "scope": "api.github.com",
  "level": "always_allow",
  "agent": "agt_5wTn9Kd",
  "created_at": 1775050000
}

Errors: 422 unknown_capability · 409 permission_already_exists

Handle a permission request

POST/v1/permission_requests/:id/approve
POST/v1/permission_requests/:id/deny

When a run needs a capability set to ask, it enters requires_action:

JSON
{
  "id": "run_2Kd8vQx",
  "object": "run",
  "status": "requires_action",
  "required_action": {
    "type": "permission_request",
    "id": "preq_9Ft2Wsm",
    "capability": "github.create_pull_request",
    "scope": "acme/api-gateway",
    "arguments": {"title": "Add backoff to webhook retries"}
  }
}
SHELL
curl https://api.arble.ai/v1/permission_requests/preq_9Ft2Wsm/approve \
  -H "Authorization: Bearer $ARBLE_API_KEY" \
  -d '{"remember": true}'

remember: true promotes the decision to a persistent always_allow grant. A permission request expires after 15 minutes and the run fails with permission_request_expired.

Errors: 404 permission_request_not_found · 409 permission_request_resolved

Revoke and policy

DELETE/v1/permissions/:id
PUT/v1/permissions/policy

Revocation takes effect on the next tool call, including inside runs already executing. The policy endpoint sets defaults for capabilities with no explicit grant — use it to fail closed:

JSON
{"default": "never_allow", "exceptions": {"filesystem.read": "always_allow"}}

Connectors

A connector is an authenticated link to an external service. Installing one adds its tools to the registry; authenticating it makes them usable.

ActionEndpoint
ListGET /v1/connectors
InstallPOST /v1/connectors
RemoveDELETE /v1/connectors/:name
HealthGET /v1/connectors/:name/health
AuthenticatePOST /v1/connectors/:name/authorize
SHELL
$ curl https://api.arble.ai/v1/connectors \
    -H "Authorization: Bearer $ARBLE_API_KEY" \
    -d '{"name": "github"}'

{
  "name": "github",
  "object": "connector",
  "status": "pending_auth",
  "authorize_url": "https://api.arble.ai/v1/connectors/github/authorize",
  "tools": []
}

OAuth can’t complete inside an API call. Redirect the user to authorize_url; the connector becomes connected and its tools appear once they finish.

JSON
{
  "name": "github",
  "object": "connector",
  "status": "connected",
  "scopes": ["repo", "read:org"],
  "expires_at": 1782825600,
  "tools": ["github.get_pull_request", "github.create_review_comment"]
}

Health reports reachability and token validity separately — an expired token is status: "connected" with auth: "expired", which is a different fix from an unreachable service.

Errors: 404 connector_not_found · 409 connector_already_installed · 401 connector_auth_expired

MCP

Arble speaks Model Context Protocol natively. An MCP server’s tools land in the same registry as everything else and pass the same permission gate. See MCP server for the protocol itself.

ActionEndpoint
List serversGET /v1/mcp/servers
InstallPOST /v1/mcp/servers
RemoveDELETE /v1/mcp/servers/:name
HealthGET /v1/mcp/servers/:name/health
RegistryGET /v1/mcp/registry
SHELL
$ curl https://api.arble.ai/v1/mcp/servers \
    -H "Authorization: Bearer $ARBLE_API_KEY" \
    -d '{"name": "acme-internal", "transport": "http",
         "url": "https://mcp.acme.example.com/mcp"}'

{
  "name": "acme-internal",
  "object": "mcp_server",
  "transport": "http",
  "status": "connected",
  "protocol_version": "2025-06-18",
  "tools": 9
}

transport is http for remote servers or stdio for local ones, which take a command instead of a url. Only http is available on hosted Arble — stdio requires a self-hosted instance or a paired device, since it spawns a process.

The registry endpoint lists servers Arble is known to work with, for building an install picker. Per-tool permissions are read and written through Permissions using the tool’s fully qualified name.

Errors: 409 server_already_installed · 422 unsupported_transport · 503 server_unreachable

Desktop

Desktop endpoints act on a specific paired machine and require a device token. Every call requires the desktop capability and appears in the audit log.

ActionEndpoint
Open app or filePOST /v1/desktop/open
ScreenshotPOST /v1/desktop/screenshot
ClipboardGET | PUT /v1/desktop/clipboard
KeyboardPOST /v1/desktop/keyboard
MousePOST /v1/desktop/mouse
List windowsGET /v1/desktop/windows
NotificationPOST /v1/desktop/notifications

Screenshots return a file reference, not inline base64 — fetch the bytes through Files. Mouse coordinates are screen points, origin top-left.

SHELL
$ curl https://api.arble.ai/v1/desktop/screenshot \
    -H "Authorization: Bearer $ARBLE_DEVICE_TOKEN" \
    -d '{"window": "Safari"}'

{
  "object": "screenshot",
  "file": "file_7Bn3Kqs",
  "width": 2560,
  "height": 1440,
  "captured_at": 1775050100
}

$ curl https://api.arble.ai/v1/desktop/mouse \
    -H "Authorization: Bearer $ARBLE_DEVICE_TOKEN" \
    -d '{"action": "click", "x": 640, "y": 480}'

Errors: 401 device_token_required · 403 permission_denied · 404 window_not_found · 503 device_offline

Mobile

Mobile endpoints target a paired phone and also require a device token.

ActionEndpoint
Push notificationPOST /v1/mobile/notifications
ClipboardGET | PUT /v1/mobile/clipboard
Open app or linkPOST /v1/mobile/open
CameraPOST /v1/mobile/camera
MicrophonePOST /v1/mobile/microphone
SHELL
$ curl https://api.arble.ai/v1/mobile/notifications \
    -H "Authorization: Bearer $ARBLE_DEVICE_TOKEN" \
    -d '{"title": "Migration finished", "body": "14 tables, 0 errors"}'

{"object": "notification", "id": "ntf_2Vx8Kmp", "delivered": true}

Capture endpoints return 202 Accepted with a pending capture, since they wait on a human. Poll it, or subscribe to the capture events. Approved captures expose a file; declined ones become status: "denied".

JSON
{
  "object": "capture",
  "id": "cap_6Rn4Wqt",
  "status": "awaiting_approval",
  "expires_at": 1775050400
}

Camera and microphone always prompt on the device itself. No parameter suppresses that, and there is no silent capture.

Errors: 401 device_token_required · 403 capture_denied · 503 device_offline · 504 capture_timeout

Files

Files carry binary data in and out of the API: attachments on messages, screenshots, and tool inputs and outputs.

Upload

POST/v1/files

Send multipart/form-data with a file field and a purpose of attachment, tool_input or resource.

SHELL
$ curl https://api.arble.ai/v1/files \
    -H "Authorization: Bearer $ARBLE_API_KEY" \
    -F purpose=attachment \
    -F file=@./trace.json

{
  "id": "file_7Bn3Kqs",
  "object": "file",
  "filename": "trace.json",
  "purpose": "attachment",
  "bytes": 48219,
  "content_type": "application/json",
  "created_at": 1775050200
}

Errors: 413 file_too_large · 422 unsupported_content_type

Download, metadata, delete

GET/v1/files/:id/content
GET/v1/files/:id
DELETE/v1/files/:id

Content streams the bytes with the original Content-Type. Requesting content with a Range header returns 206 Partial Content, which is how you resume an interrupted download.

Large files

Single-request uploads are capped at 100 MB. Above that, use a resumable upload: POST /v1/uploads returns an upload session, PUT each part to it, then POST /v1/uploads/:id/complete. Parts are 5–100 MB and may be sent in parallel.

JSON
{
  "id": "upl_9Kt3Mvx",
  "object": "upload",
  "status": "pending",
  "part_size": 8388608,
  "expires_at": 1775136600
}

Errors: 409 upload_already_completed · 422 invalid_part_size

Streaming

Pass "stream": true to any run, message or tool execution and the response becomes text/event-stream instead of a JSON object.

HTTP
event: run.created
data: {"id":"run_2Kd8vQx","status":"queued"}

event: run.step
data: {"step":1,"type":"tool_call","tool":"filesystem.read"}

event: progress
data: {"message":"Reading 14 files","percent":35}

event: message.delta
data: {"delta":"Three handlers retry without"}

event: message.delta
data: {"delta":" backoff: charge.py:118, "}

event: run.completed
data: {"id":"run_2Kd8vQx","status":"completed"}
EventMeaning
run.createdThe run exists and is queued
run.stepA step started — a tool call or a model turn
progressIncremental progress from a streaming tool
message.deltaA fragment of assistant output. Concatenate in order
tool.resultA tool returned
run.requires_actionExecution paused. See Permissions
run.completedTerminal. Carries final usage
errorTerminal. Carries a standard error object

A stream always ends with run.completed, error, or a closed connection. Treat a closed connection without a terminal event as a failure and reconcile with GET /v1/runs/:id — never assume success.

Heartbeats. A :ping comment arrives every 15 seconds to keep intermediaries from closing an idle connection. Ignore lines beginning with :.

Closing the connection does not cancel the run. It keeps executing server-side. Call POST /v1/runs/:id/cancel to actually stop it.

Resuming. Reconnect with Last-Event-ID to replay events you missed. Events are retained for the run’s lifetime plus one hour.

Webhooks

Webhooks deliver events that happen outside a request — a run completing, a permission being requested. Use them instead of polling.

Register an endpoint

POST/v1/webhook_endpoints
SHELL
$ curl https://api.arble.ai/v1/webhook_endpoints \
    -H "Authorization: Bearer $ARBLE_API_KEY" \
    -d '{"url": "https://api.example.com/hooks/arble",
         "events": ["run.completed", "run.failed", "permission.requested"]}'

{
  "id": "we_3Lm8Qvx",
  "object": "webhook_endpoint",
  "url": "https://api.example.com/hooks/arble",
  "events": ["run.completed", "run.failed", "permission.requested"],
  "secret": "whsec_9Ft2Wsm4Kp...",
  "status": "enabled"
}

The secret is returned once. Store it — you need it to verify signatures, and it can only be rotated, not read back.

Errors: 422 invalid_url · 422 unknown_event

Verify signatures

Every delivery carries an Arble-Signature header of the form t=1775050300,v1=5257a869e7bcd.... Verify by computing HMAC-SHA256 over {timestamp}.{raw_body} with your endpoint secret, comparing in constant time, and rejecting timestamps older than five minutes.

PYTHON
import hmac, hashlib, time

def verify(raw_body: bytes, header: str, secret: str) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    if abs(time.time() - int(parts["t"])) > 300:
        return False
    expected = hmac.new(
        secret.encode(),
        f"{parts['t']}.".encode() + raw_body,
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(expected, parts["v1"])

Verify against the raw body. Parsing to JSON and re-serializing changes the bytes, and the signature will never match.

Delivery and retries

Respond 2xx within 10 seconds. Anything else is a failure and is retried with exponential backoff over 24 hours: after 10s, 1m, 5m, 30m, 2h, 6h, then hourly. An endpoint failing for 24 consecutive hours is disabled and a webhook.disabled event is sent to your other endpoints.

Deliveries can arrive out of order and more than once. Use the event id for idempotency and created_at for ordering — not arrival time.

Errors: 404 webhook_endpoint_not_found

Events

Every event shares one envelope. data holds the full object the event concerns, in the same shape the corresponding GET returns.

JSON
{
  "id": "evt_8Vn3Kqt",
  "object": "event",
  "type": "run.completed",
  "created_at": 1775050300,
  "data": {
    "id": "run_2Kd8vQx",
    "object": "run",
    "status": "completed",
    "session": "ses_8kQ2mVx"
  }
}
EventFires when
run.startedA run leaves queued and begins executing
run.completedA run finishes successfully
run.failedA run terminates with an error
permission.requestedA run needs approval and entered requires_action
permission.grantedA permission request was approved
memory.updatedAn entry was inserted, changed or deleted
tool.executedA tool call completed, successfully or not
session.closedA session was archived or deleted
connector.connectedA connector finished authenticating
connector.disconnectedA connector was removed or its token expired

GET /v1/events lists past events for replay and reconciliation, filterable by type and created_after. Events are retained 30 days.

permission.requested is the one to wire up first if you run agents unattended — without a handler, those runs sit in requires_action until they expire.

Pagination

List endpoints are cursor-paginated. Cursors are opaque and stable: an object inserted mid-iteration won’t shift a page or cause a duplicate.

ParameterNotes
limit1–100. Default 20.
cursorFrom next_cursor on the previous page.
orderasc or desc by created_at. Default desc.

Every list response has the same envelope:

JSON
{
  "object": "list",
  "data": [],
  "has_more": false,
  "next_cursor": null
}

Iterate until has_more is false. Don’t stop when data is shorter than limit — filtered pages can come back partially full with more results behind them. Don’t construct cursors yourself; the format is not part of the contract.

Errors: 400 invalid_cursor · 400 limit_out_of_range

Filtering

List endpoints accept filters as query parameters, combined with AND.

FilterApplies toExample
statusRuns, workflow runs, sessionsstatus=failed
projectAll resourcesproject=prj_4mNp
sessionRuns, messagessession=ses_8kQ2mVx
agentRunsagent=agt_5wTn9Kd
created_afterAll resourcescreated_after=1775049600
created_beforeAll resourcescreated_before=1775136000
metadata[key]Resources with metadatametadata[env]=staging

status accepts a comma-separated list — status=failed,expired — which is an OR within that one filter. Timestamps are Unix seconds, always UTC.

Filtering on metadata is how you correlate Arble objects with records in your own system. Set metadata at creation to your primary key, then filter by it later.

Errors: 400 invalid_filter · 422 unknown_filter_field

Rate limits

Limits are per token, applied on a sliding window. Concurrency is counted separately from request rate.

LimitDefault
Requests1,000 per minute
Burst100 per second
Concurrent runs25
Concurrent streams50
HTTP
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 947
X-RateLimit-Reset: 1775050360

Exceeding a limit returns 429 with Retry-After in seconds. Exceeding concurrency is a different error — concurrency_limit_exceeded — and retrying immediately won’t help. Wait for a run to finish, or queue work yourself.

Back off on 429 rather than retrying tightly; a client that retries without backoff will stay rate-limited. Read X-RateLimit-Remaining and slow down before you hit zero.

Errors

Every error uses the same shape, whatever the status code:

JSON
{
  "error": {
    "type": "invalid_request_error",
    "code": "parameter_missing",
    "message": "Missing required parameter: input.",
    "param": "input",
    "request_id": "req_5Kp2Nvx"
  }
}

Branch on code, not message. Messages are written for humans and change without notice; codes are part of the contract.

StatusTypeMeaning
400invalid_request_errorMalformed request — bad JSON, missing parameter
401authentication_errorMissing, malformed or revoked token
403permission_errorAuthenticated but not permitted
404not_found_errorNo such object, or not visible to this token
409conflict_errorObject state forbids the operation
422validation_errorWell-formed but semantically invalid
429rate_limit_errorRate or concurrency limit exceeded
500api_errorServer-side fault. Safe to retry
503service_unavailableTemporarily unavailable. Retry with backoff
504timeout_errorUpstream tool or model timed out

Validation errors list every problem at once, rather than failing on the first:

JSON
{
  "error": {
    "type": "validation_error",
    "code": "invalid_parameters",
    "message": "2 parameters are invalid.",
    "errors": [
      {"param": "timeout", "code": "out_of_range",
       "message": "Must be between 1 and 3600."},
      {"param": "tools[1]", "code": "unknown_tool",
       "message": "No tool named 'github.merge'."}
    ],
    "request_id": "req_5Kp2Nvx"
  }
}

The distinction worth encoding in your client: 403 permission_error means a grant is missing and retrying is pointless — surface it. 500 and 503 are transient — retry them.

Every response includes request_id, in the body on errors and in the Arble-Request-Id header always. Log it; it’s the first thing support will ask for.

SDKs

Official libraries wrap authentication, retries with backoff, pagination and SSE parsing. They’re generated from the same OpenAPI spec, so coverage is identical.

LanguagePackage
Pythonpip install arble
TypeScriptpnpm add arble
Gogo get github.com/arble-ai/arble-go
Rustcargo add arble
SwiftSwift Package Manager
Kotlincom.arble:arble-kotlin
PYTHON
from arble import Arble

client = Arble()  # reads ARBLE_API_KEY

run = client.runs.create(
    agent="agt_5wTn9Kd",
    input="Summarize the open pull requests",
)

for event in client.runs.stream(run.id):
    if event.type == "message.delta":
        print(event.delta, end="", flush=True)
PYTHON
import Arble from "arble";

const client = new Arble();

const stream = await client.runs.create({
  agent: "agt_5wTn9Kd",
  input: "Summarize the open pull requests",
  stream: true,
});

for await (const event of stream) {
  if (event.type === "message.delta") process.stdout.write(event.delta);
}

Iterating a list in any SDK paginates automatically — there’s no cursor handling to write:

CODE
for run in client.runs.list(status="failed"):
    print(run.id, run.last_error.code)

Versioning

The API is versioned by date. Your account is pinned to the version current when you created it, and that version’s behavior does not change.

SHELL
curl https://api.arble.ai/v1/runs \
  -H "Authorization: Bearer $ARBLE_API_KEY" \
  -H "Arble-Version: 2026-04-15"

The /v1 path segment denotes the API generation and changes only for a wholesale redesign. Dated versions carry the actual breaking changes.

Additive changes — a new field, a new endpoint, a new event type or enum value — ship to every version without notice. Treat unknown fields and unrecognized enum values as forward-compatible; a client that rejects them will break on a routine release.

Breaking changes only ever arrive in a new dated version. Deprecated versions are supported at least 12 months, and requests to one return an Arble-Deprecation header with the sunset date. To migrate, set Arble-Version explicitly, test, then move your account default.

OpenAPI

The complete specification is published and versioned alongside the API. It’s OpenAPI 3.1, covers every endpoint and schema on this page, and is the source the SDKs are generated from — so it’s exact rather than approximate.

SHELL
curl https://api.arble.ai/v1/openapi.json

npx @openapitools/openapi-generator-cli generate \
  -i https://api.arble.ai/v1/openapi.json \
  -g ruby -o ./arble-ruby

A Postman collection is available at /v1/postman.json, and the spec renders as browsable Swagger UI at https://api.arble.ai/docs. Pin the spec to a dated version with ?version=2026-04-15 when generating a client you intend to keep.

Security

HTTPS and TLS

TLS 1.2 or higher, required. Plain HTTP requests are rejected, not redirected — a redirect would leak the token in transit. HSTS is enforced.

Encryption

Data is encrypted in transit and at rest. Credentials and secrets are encrypted with per-project keys and never returned by any endpoint after creation.

Authentication and permissions

These are separate. A valid token gets you into the API; a permission grant gets a tool call executed. Neither implies the other.

Audit logs

Every request and tool call is recorded with its token, arguments, permission decision and outcome, queryable through GET /v1/audit_logs and retained 90 days.

Least privilege

Use project tokens over organization tokens, scope permissions to specific hosts and paths, and mint short-lived session tokens for anything client-side.

Secrets sent to the API — in tool arguments, memory entries or metadata — are redacted from logs and traces on a best-effort basis. Don’t rely on that: never put a credential in a field whose purpose isn’t credentials.

Best practices

  1. Use project-scoped tokens in deployed services; reserve personal tokens for local development.
  2. Never ship a long-lived key to a browser or mobile client — mint a session token server-side.
  3. Send an Idempotency-Key on every POST that creates something, not just retries.
  4. Pin Arble-Version explicitly rather than relying on the account default.
  5. Branch on error.code, never on error.message.
  6. Retry only 429, 500, 502, 503 and 504, with exponential backoff and jitter.
  7. Treat 403 as terminal and surface it — a missing grant won’t resolve on retry.
  8. Prefer webhooks to polling; poll only to reconcile after a missed delivery.
  9. Verify webhook signatures against the raw request body, in constant time.
  10. Deduplicate webhooks by event id and order by created_at, not arrival time.
  11. Handle permission.requested before running agents unattended, or those runs will expire.
  12. Never assume a closed stream means success — reconcile with GET /v1/runs/:id.
  13. Call POST /v1/runs/:id/cancel to stop a run; closing the connection doesn’t.
  14. Iterate lists until has_more is false, not until a page is short.
  15. Set metadata to your own primary key at creation so you can correlate later.
  16. Ignore unknown fields and unrecognized enum values so additive changes don’t break you.
  17. Write memory entries as durable facts; use session exports for what happened once.
  18. Scope permissions to specific hosts and paths, not bare capabilities.
  19. Log request_id on every failure; it’s what support needs to trace a request.
  20. Keep client timeouts at 30 seconds and let long work be asynchronous, as designed.

Reference

  • Authentication — token types, rotation, revocation
  • Agents — create, list, retrieve, update, delete
  • Runs — lifecycle, cancel, retry, stream
  • Sessions — lifecycle, export
  • Messages — send, list, attachments
  • Memory — search, insert, summarize, export
  • Tools — list, metadata, direct execution
  • Files — upload, download, resumable uploads
  • Permissions — grants, requests, policy
  • Streaming — SSE event types, heartbeats, resume
  • Webhooks — registration, signatures, retries
  • Errors — status codes, error types, validation
  • SDKs — official libraries and examples

FAQ

Do I need the desktop app to use the API?

No. The app and the API are clients of the same runtime. The only endpoints that need a paired machine are Desktop and Mobile, which act on a specific device by definition.

Is a run synchronous or asynchronous?

Asynchronous. POST /v1/runs returns immediately with status: "queued". Stream it, poll it, or take a run.completed webhook — streaming is the lowest-latency option.

Should I use streaming or webhooks?

Streaming when a user is waiting on output. Webhooks for anything unattended. They’re complementary, not alternatives — many integrations use both.

What happens if my stream disconnects mid-run?

The run keeps executing. Reconnect with Last-Event-ID to replay missed events, or fetch the run to get its final state. A dropped connection is never a cancellation.

How do I handle a permission request from a server with no UI?

Either pre-grant the capability with level: "always_allow" so runs never pause, or subscribe to permission.requested and approve programmatically. Runs left in requires_action expire after 15 minutes.

Why did a tool call return 403 instead of prompting me?

Interactive approval requires a client that can prompt. A bare HTTP request has nowhere to show a dialog, so an ungranted capability fails closed. Grant it explicitly.

Can two runs share a session safely?

Yes, but they see each other’s messages, which is usually what you want in a conversation and rarely what you want for parallel independent tasks. Use separate sessions for fan-out.

Does deleting a session delete what it wrote to memory?

No. Memory is project-scoped and outlives sessions deliberately. Delete memory entries explicitly.

How do I test without touching production data?

Use the sandbox base URL. It’s a full instance with separate data. Note that it defaults to a smaller model, so verify latency-sensitive behavior in production.

Are timestamps UTC?

Always. Every timestamp in every response is Unix seconds in UTC. There is no timezone parameter.

What’s the difference between 409 and 422?

409 means the request is valid but the object’s current state forbids it — cancelling a completed run. 422 means the request itself is semantically wrong — a timeout of 5000, or a tool that doesn’t exist.

How long are objects retained?

Runs and messages for the life of their session. Events 30 days, audit logs 90 days, idempotency keys 24 hours. Memory and files persist until deleted.