# API Reference

> Complete reference for the Cephable Automate HTTP Server — base URL, authentication, every endpoint, request and response schemas, status codes, and error shapes.

Source: https://developers.cephable.com/docs/automate-http-server/api-reference

---
## Base URL

```
http://127.0.0.1:4317
```

The server binds `127.0.0.1` only — never `0.0.0.0`, never a LAN address. Remote hosts cannot reach it.

If the preferred port is occupied, Cephable tries the next eleven ports (`4317`–`4328`) and reports the one it bound. Always read the endpoint from the extension detail view, an environment variable, or a discovery sweep — never assume `4317`. See [Finding the port](/docs/automate-http-server/troubleshooting).

`http://localhost:4317` usually works too, but prefer the literal `127.0.0.1`: on some systems `localhost` resolves to `::1` first, and the server is not listening there.

---

## Authentication

Every route, including `/health`, requires the access key as a bearer token:

```http
Authorization: Bearer <access key>
```

- Comparison is constant-time. A missing, malformed, or wrong token returns `401` before routing, so an unauthenticated request cannot even discover which paths exist.
- The key is generated by the app (32 random bytes, base64url — 43 characters), stored encrypted in OS secure storage, and must be at least 24 characters.
- Regenerating the key in the app invalidates the old one immediately.
- The key is deliberately **not** accepted as a command-line argument, because operating systems expose process command lines to other local processes.

---

## Conventions

| | |
|---|---|
| Content type | `application/json; charset=utf-8` on every response, with `Cache-Control: no-store` |
| Request bodies | JSON. Max **1 MiB** — larger bodies are rejected with `400` |
| Concurrency | **One run at a time.** `/v1/runs`, `/v1/chat/completions`, and `/v1/automate/models/select` return `409` while a run is in flight |
| Streaming | Not supported. Run endpoints block until the run reaches a terminal state |
| CORS | No CORS headers and no `OPTIONS` handler. A browser page on an `http(s)://` origin **cannot** call this server; use a native, Node, Electron main-process, or server-side client |
| Method mismatch | Returns `404`, not `405` — `GET /v1/runs` is "not found" |

---

## Endpoint summary

| Method | Path | Purpose | Allowed while busy |
|---|---|---|---|
| `GET` | `/health` | Readiness, runtime, device, and workspace info | Yes |
| `GET` | `/v1/models` | OpenAI-shaped model list (always `cephable-agent`) | Yes |
| `GET` | `/v1/automate/models` | The on-device GGUF catalog with availability | Yes |
| `POST` | `/v1/automate/models/select` | Pin a model for this app session | No — `409` |
| `POST` | `/v1/automate/cancel` | Stop whatever is running, whoever started it | Yes |
| `POST` | `/v1/runs` | Run an Automate task (native contract) | No — `409` |
| `POST` | `/v1/runs/{resumeToken}/tool-results` | Resume a run parked on your own tools | Yes — by design |
| `POST` | `/v1/chat/completions` | Run an Automate task (OpenAI contract) | No — `409`, unless it carries tool results for a parked run |

---

## GET /health

No parameters. Returns the state of the server, the agent worker, the model, and the device.

```bash
curl -sS http://127.0.0.1:4317/health -H "Authorization: Bearer $CEPHABLE_AUTOMATE_KEY"
```

| Field | Type | Notes |
|---|---|---|
| `status` | `"ok"` | Constant |
| `service` | `"cephable-agent"` | Use this to confirm a listener is Cephable when sweeping ports |
| `appVersion` | `string` | Desktop app version |
| `platform` | `string` | `win32` · `darwin` · `linux` |
| `architecture` | `string` | `x64` · `arm64` |
| `osRelease` | `string` | OS kernel/release string |
| `cpu` | `string` | First CPU model string, or `"unknown"` |
| `logicalCpuCount` | `number` | Logical cores |
| `totalMemoryBytes` | `number` | Physical RAM |
| `workflowStatus` | `string` | Agent status — see [workflow statuses](#workflow-statuses) |
| `activeRequestId` | `string \| null` | The run in flight, if any |
| `modelName` | `string \| null` | GGUF **file name** currently loaded |
| `workerRunning` | `boolean` | Whether the workflow utility process is warm |
| `busy` | `boolean` | Whether *this server* is executing a run — including one parked awaiting your tool results |
| `awaitingToolResults` | `boolean` | A run is parked awaiting caller-executed tool results. Lets a client recovering from a crash tell "someone else's run" from "my own run waiting on me" |
| `workspace` | `string` | Absolute path of the Automate workspace folder |
| `backend` | `object \| null` | `{ flavorId, accelerator, cpuFallback }` — the llama.cpp acceleration actually in use |
| `contextSize` | `number \| null` | The context window this device got. Sized from system RAM, so it varies per machine — record it in any comparison between runs |
| `launchModelSelectionError` | `string \| null` | Why a `--model`/`--family`/`--size` launch argument could not be applied |
| `automateModelSelection` | `object \| null` | The model pinned via `/v1/automate/models/select`, or `null` when the app's own selection is in effect |

**Ready to run** means `busy === false` and `workflowStatus` is `idle` or `terminated`.

```jsonc
{
  "status": "ok",
  "service": "cephable-agent",
  "appVersion": "4.2.1",
  "platform": "win32",
  "architecture": "x64",
  "osRelease": "10.0.26200",
  "cpu": "AMD Ryzen 9 7940HS",
  "logicalCpuCount": 16,
  "totalMemoryBytes": 68719476736,
  "workflowStatus": "idle",
  "activeRequestId": null,
  "modelName": "gemma-4-4b-it-Q4_K_M.gguf",
  "workerRunning": true,
  "busy": false,
  "workspace": "C:\\Users\\you\\AppData\\Roaming\\Cephable\\automate-http-workspace",
  "backend": { "flavorId": "vulkan", "accelerator": "vulkan", "cpuFallback": false },
  "contextSize": 16384,
  "launchModelSelectionError": null,
  "automateModelSelection": null
}
```

---

## GET /v1/models

The OpenAI-compatible model list. There is exactly one entry, and it represents the whole assistant rather than a raw model.

```jsonc
{
  "object": "list",
  "data": [{ "id": "cephable-agent", "object": "model", "owned_by": "cephable" }]
}
```

To see the actual GGUF catalog, use `/v1/automate/models`.

---

## GET /v1/automate/models

The on-device model catalog as Cephable knows it, including which entries this device can run and which are already downloaded.

```jsonc
{
  "object": "list",
  "data": [
    {
      "name": "Gemma 4 M",
      "family": "Gemma",
      "sizeCode": "M",
      "parameterCountBillions": 4,
      "fileSizeMb": 2650,
      "supportsTools": true,
      "availableForDevice": true,
      "downloaded": true,
      "selected": true
    }
  ],
  "catalog": { "status": "ready", "modelCount": 7 }
}
```

| Field | Notes |
|---|---|
| `name` | Exact catalog name — the value a `modelName` selector matches against |
| `family` / `sizeCode` | Coarse selectors, e.g. `Gemma` + `M` |
| `parameterCountBillions` | May be `null` |
| `fileSizeMb` | May be `null` |
| `supportsTools` | **Must be `true`** to be selectable; Automate requires tool calling |
| `availableForDevice` | `false` when this device cannot run it (RAM, GPU, OS) |
| `downloaded` | `false` means it must be downloaded in the Cephable UI first |
| `selected` | The model that will serve the next run |
| `catalog.status` | `"loading"` while the catalog is still syncing from Cephable; `"ready"` once available |
| `catalog.modelCount` | Number of catalog entries |

Poll until `catalog.status` is `"ready"` before selecting a model at startup — the catalog syncs asynchronously after app launch.

---

## POST /v1/automate/models/select

Pin a specific on-device model for the current app session. This is a **session** preference: it overrides the account's synced model choice for the running process without changing the user's saved setting, and is cleared when the server stops.

**Request** — supply any combination of the three fields; the result must match exactly one catalog entry.

```jsonc
{ "modelName": "Gemma 4 M" }
```

```jsonc
{ "family": "Gemma", "sizeCode": "M" }
```

| Field | Type | Notes |
|---|---|---|
| `modelName` | `string?` | Exact catalog name, case-insensitive |
| `family` | `string?` | Case-insensitive family match |
| `sizeCode` | `string?` | Case-insensitive size code, e.g. `S`, `M`, `L` |

An **empty object** (`{}`) restores the app's own model selection and unpins yours.

**Response**

```jsonc
{
  "selected": { "name": "Gemma 4 M", "downloaded": true, "selected": true },
  "models": [ ]
}
```

`selected` is the full model option for the pinned model (or `null` when unpinning and the app has no preference); `models` is the refreshed catalog.

**Errors**

| Status | Cause |
|---|---|
| `409` | A run is in flight |
| `400` | No catalog match, ambiguous match, model not compatible with the device, model not tool-capable, model not downloaded, or the workflow is not idle |

Switching models terminates a warm worker, so the next run pays a cold start. Select once at integration startup, not per request.

---

## POST /v1/automate/cancel

Stops whatever the assistant is doing — a run you started, a run the user started in the panel, or a run another integration started. This is the one route that is **not** refused while a run is in flight, and it is safe to call when nothing is running, so a client recovering from a dropped connection can call it unconditionally.

**Request** (body optional)

```jsonc
{ "force": false }
```

| Field | Default | Behavior |
|---|---|---|
| `force: false` | default | Asks the agent loop to unwind. The worker stays warm and the conversation survives, but it only lands **between steps** — a task already blocked inside a tool call will not notice |
| `force: true` | | Kills the workflow worker outright, exactly as the panel's Stop button does. Always works, at the cost of a cold start on the next run |

**Response** — always `200`

```jsonc
{ "stopped": true, "mode": "cancel", "requestId": "automate-run-…", "workflowStatus": "executing-progress" }
```

| Field | Notes |
|---|---|
| `stopped` | `false` when there was nothing to stop |
| `mode` | `"cancel"` · `"terminate"` · `"none"` |
| `requestId` | The run that was stopped, or `null` |
| `workflowStatus` | Status observed at the moment of the call |

A run stopped by **any** route — including the panel's own Stop button — settles the blocking `/v1/runs` request that started it, rather than leaving it open until its timeout. That request then returns a record with `status: "canceled"` or `"terminated"` under `HTTP 500`.

---

## POST /v1/runs

The native contract. Runs one Automate task and blocks until it reaches a terminal state.

Full treatment of every option, with examples, is in [Running Automate tasks](/docs/automate-http-server/runs). The schema summary:

### Request

| Field | Type | Default | Notes |
|---|---|---|---|
| `prompt` | `string` | **required** | The task. Must be a non-empty string |
| `taskId` | `string?` | — | Your own correlation id, echoed back untouched |
| `timeoutMs` | `number?` | `900000` (15 min) | Minimum `1000`. On timeout the run is canceled and the request returns `400` |
| `thinkingLevel` | `"low" \| "medium" \| "high" \| "max"` | user setting | Reasoning budget, applied per run |
| `additionalWorkflowPrompt` | `string?` | — | Advisory preferences appended to the system prompt |
| `answerContract` | `string?` | — | A binding required shape for the closing message. Returns `finalAnswer` alongside the untouched `answer` |
| `include` | `object?` | all `true` | `{ steps?: boolean, trace?: boolean, events?: boolean }` — set `false` to omit a diagnostic block |
| `restrictToWorkspace` | `boolean?` | `false` | Confine file and CLI tools to the `workspace` folder. Can only narrow — a user's own restriction always wins |
| `continuation` | `boolean?` | `false` | Continue the previous run's conversation instead of starting fresh |
| `selectedSkillIds` | `string[]?` | — | AI Skills to force into this run. Omit to let Cephable select by relevance |
| `selectedMcpServerIds` | `string[]?` | — | **Configured** MCP servers to force into this run. Omit for relevance-based selection |
| `mcpServers` | `object[]?` | — | **Inline** MCP servers for this run only — your own tools, without the user configuring anything. Max 8. See [Custom tools](/docs/automate-http-server/custom-tools) |
| `clientTools` | `object[]?` | — | Tools you declare by JSON Schema and execute yourself. The run parks when the agent calls one. Max 32. See [Custom tools](/docs/automate-http-server/custom-tools) |
| `hitlAnswers` | `Record<string, string \| string[]>?` | — | Pre-supplied answers for `request_user_input` questions, keyed by field id |
| `allowDestructiveTools` | `boolean?` | `false` | Approve destructive-tool requests (`delete_path`, `run_command`, `move_path`) instead of rejecting them |

### Response

```jsonc
{
  "schemaVersion": 1,
  "requestId": "automate-run-9c2e…",
  "taskId": "nightly-42",
  "status": "completed",
  "answer": "…",
  "finalAnswer": "…",
  "errorCode": "TOOL_TIMEOUT",
  "startedAt": "2026-09-14T18:02:11.004Z",
  "completedAt": "2026-09-14T18:02:48.771Z",
  "durationMs": 37767,
  "model": "gemma-4-4b-it-Q4_K_M.gguf",
  "appVersion": "4.2.1",
  "backend": { "flavorId": "vulkan", "accelerator": "vulkan", "cpuFallback": false },
  "steps": [ ],
  "trace": [ ],
  "usage": { "inputTokens": 5120, "outputTokens": 344, "generationMs": 8345, "ttftMs": 610, "tps": 41.2 },
  "events": [ ]
}
```

| Field | Always present | Notes |
|---|---|---|
| `schemaVersion` | Yes | Always `1`. The reliable signal that a run actually happened |
| `requestId` | Yes | Server-minted, `automate-run-<uuid>` |
| `taskId` | — | Only when you sent one |
| `status` | Yes | `completed` · `failed` · `canceled` · `terminated` · `awaiting_tool_results` |
| `toolCalls` | — | Only when `status` is `awaiting_tool_results`: the caller-executed tool calls to run |
| `resumeToken` | — | Only when `status` is `awaiting_tool_results`: post results to `/v1/runs/{resumeToken}/tool-results` |
| `answer` | Yes | The closing message, exactly as a person would have seen it |
| `finalAnswer` | — | Only when `answerContract` was set: the text after the last `FINAL ANSWER:` marker, or the whole answer if no marker was emitted |
| `errorCode` | — | See [workflow error codes](#workflow-error-codes) |
| `startedAt` / `completedAt` | Yes | ISO 8601 |
| `durationMs` | Yes | Wall clock, including model load and cold start |
| `model` | Yes | GGUF **file name** actually loaded, or `"unknown"` |
| `appVersion` | Yes | Desktop app version |
| `backend` | Yes | May be `null` if the runtime could not be queried |
| `steps` | unless excluded | Ordered tool calls with args, result summaries, produced paths and content |
| `trace` | unless excluded | Raw model message list |
| `usage` | Yes | `null` when no metrics were emitted |
| `events` | unless excluded | Every raw channel event. **Large** — the full step list is rebroadcast on every update, so a research run's stream runs to hundreds of KB |

### Status codes

| Status | Body | Meaning |
|---|---|---|
| `200` | run record | `status === "completed"`, or `awaiting_tool_results` — a parked run is a successful exchange |
| `500` | run record | The run ran and ended `failed`, `canceled`, or `terminated`. **Not** a server fault — `schemaVersion` is still `1` |
| `400` | `{ error }` | Invalid request, body over 1 MiB, malformed JSON, run timeout, or an initialization failure |
| `401` | `{ error }` | Bad or missing bearer token |
| `409` | `{ error }` | A run is already active or preparing |

---

## POST /v1/runs/{resumeToken}/tool-results

Resume a run parked awaiting caller-executed tool results. Deliberately **not** refused while busy — the run it resumes is the reason the server is busy.

**Request**

```jsonc
{
  "results": [
    { "id": "client_tool_5f2a…", "result": "Order 4471: 2x widget, shipped 2026-09-02" },
    { "id": "client_tool_9b13…", "error": "the orders database is unreachable" }
  ]
}
```

| Field | Type | Notes |
|---|---|---|
| `results` | `object[]` | **Required.** One entry per call you are answering |
| `results[].id` | `string` | **Required.** The `id` from the `toolCalls` entry |
| `results[].result` | any | What the model sees. A string passes through; anything else is JSON-encoded |
| `results[].error` | `string?` | Report a failure instead. The agent adapts rather than dying |

Results are matched by `id`, not order. An `id` that matches nothing pending is ignored, which makes a retried resume safe.

**Response** — the run's next outcome: another `awaiting_tool_results` record (an agent loop can need several rounds), or the finished record. Same shape and status codes as `/v1/runs`, plus:

| Status | Cause |
|---|---|
| `404` | No parked run matches that token — it timed out (two minutes per round) or was cancelled |
| `400` | Malformed `results` |

Full treatment in [Custom tools](/docs/automate-http-server/custom-tools).

---

## POST /v1/chat/completions

The OpenAI-compatible contract over the same run engine. Non-streaming; each request executes the complete assistant loop. See [OpenAI compatibility](/docs/automate-http-server/openai-compatibility) for the mapping, the `cephable` extension object, and the unsupported parameters.

```jsonc
{
  "model": "cephable-agent",
  "messages": [
    { "role": "system", "content": "Prefer metric units." },
    { "role": "user",   "content": "Summarize today's notes file." }
  ],
  "timeout_ms": 600000,
  "cephable": {
    "include": { "events": false },
    "answerContract": "End with FINAL ANSWER: <one sentence>"
  }
}
```

The full native run record is returned in the response's `cephable` field.

---

## Error shape

Every non-run error uses the same envelope:

```jsonc
{ "error": { "message": "prompt must be a non-empty string", "type": "invalid_request_error" } }
```

| `type` | Status | Cause |
|---|---|---|
| `authentication_error` | `401` | Missing or wrong bearer token |
| `not_found` | `404` | Unknown path, or the right path with the wrong method |
| `conflict` | `409` | A run is already active or preparing |
| `invalid_request_error` | `400` | Validation failure, oversized or malformed body, run timeout, model-selection problem, initialization failure |

Distinguish a failed *run* from a failed *request* by checking for `schemaVersion === 1` rather than by HTTP status.

---

## Workflow statuses

Reported by `/health` as `workflowStatus` and by run records as `status`.

**Ready:** `idle`, and `terminated` (the worker is gone but nothing is running)

**In progress:** `initializing` · `planning` · `thinking` · `executing-start` · `executing-progress` · `executing-complete` · `executing-org-block` · `executing-destructive-block` · `executing-type-focus-block` · `executing-permission-block` · `waitingForUser`

**Terminal for a run:** `completed` · `failed` · `canceled` · `terminated`

**Run-record only:** `awaiting_tool_results` — not a workflow status. A run parked awaiting caller-executed tool results, still alive and still holding the inference slot. See [Custom tools](/docs/automate-http-server/custom-tools).

---

## Workflow error codes

Returned as `errorCode` on a non-completed run.

| Code | Meaning |
|---|---|
| `MODEL_LOAD_FAILED` | The GGUF could not be loaded |
| `PLANNING_TIMEOUT` | The model did not produce a plan in time |
| `TOOL_TIMEOUT` | A tool call exceeded its budget |
| `EXECUTION_TIMEOUT` | The run exceeded its execution budget |
| `EXECUTION_FAILED` | A step failed unrecoverably |
| `CONTEXT_LIMIT_EXCEEDED` | The conversation outgrew `contextSize` |
| `UNSUPPORTED_DEVICE` | The device cannot run AI Workflows |
| `UNSUPPORTED_OS_VERSION` | The OS is below the supported minimum |
| `DISABLED_BY_POLICY` | An organization policy blocked the run |
| `SCHEMA_INVALID` | A tool call did not match its schema |
| `SERVER_LOST` | The llama.cpp server went away mid-run |
| `CANCELED` | Cancelled by a client, the panel, or a timeout |
