> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Orby Assistant API

> Drive the in-dashboard Orby operator assistant — turns, threads, and tool confirmations — from your own UI

# Orby Assistant API

Orby is Orbit's in-dashboard operator assistant. These endpoints power the Orby chat panel: you send a message, Orby streams back a response over Server-Sent Events (SSE), and — when a turn proposes an action that changes data (such as sending an SMS or reassigning a conversation) — it pauses for an explicit approval before running it.

Orby is **operator-facing**. Every endpoint runs in the context of the signed-in dashboard user, scoped to that user's organization and tenant. There is no server-to-server API-key path for Orby.

**Base path:** `/api/v1/orby`

***

## Authentication

Orby uses the **dashboard user (Bearer token)** method, not an API key. Send the signed-in operator's JWT:

```bash theme={null}
curl https://api.orbit.devotel.io/api/v1/orby/threads \
  -H "Authorization: Bearer eyJhbG..."
```

A request authenticated with an API key — whether sent as `X-API-Key` or as a `dv_*` Bearer token — is rejected with `403 ORBY_SESSION_REQUIRED`. There is no server-to-server path.

Every Orby endpoint requires one of these operator roles: `owner`, `admin`, `developer`, `viewer`, or `billing`. Two knowledge-base maintenance endpoints (`POST /kb/reindex` and `GET /kb/status`) additionally require super-admin and return `403 INSUFFICIENT_PERMISSIONS` otherwise.

A thread belongs to the operator who created it. You can only list, read, and post turns to your own threads — requesting another operator's thread returns `404`.

***

## Rate limits

| Surface                                        | Limit                              |
| ---------------------------------------------- | ---------------------------------- |
| `POST /assistant` (turns)                      | 60 turns per minute per operator   |
| `POST /tool-actions/:id/approve` and `/reject` | 60 requests per minute per tenant  |
| `GET /tool-actions/:id` (poll)                 | 120 requests per minute per tenant |

When you exceed the turn limit, `POST /assistant` returns `429 ORBY_RATE_LIMITED` with a `Retry-After` header (seconds).

***

## Run a turn

`POST /api/v1/orby/assistant`

Start a new Orby conversation or continue an existing thread. The response is an **SSE stream** (`Content-Type: text/event-stream`), one event per token and per lifecycle step, terminated by a `done` event.

<ParamField body="message" type="string" required>
  The operator's message. 1–8000 characters.
</ParamField>

<ParamField body="thread_id" type="string">
  Continue an existing thread. Omit to start a new one — the response stream reports the new `thread_id` in the final `response` event, and the thread is auto-titled from your first message.
</ParamField>

<ParamField body="page" type="object">
  Optional context describing the dashboard page the operator is on, so Orby can ground its answer. All fields are optional.

  <Expandable>
    <ParamField body="page.page" type="string">Route or page identifier (max 200 chars).</ParamField>
    <ParamField body="page.contextRef" type="string">Identifier of the record in view, e.g. a conversation id (max 200 chars).</ParamField>
    <ParamField body="page.contextLabel" type="string">Human-readable label for that record (max 200 chars).</ParamField>
    <ParamField body="page.extras" type="object">Free-form key/value context.</ParamField>
  </Expandable>
</ParamField>

Pass an optional `Idempotency-Key` header to make a retried turn safe against double-execution of any tool it runs.

<RequestExample>
  ```bash theme={null}
  curl -N -X POST https://api.orbit.devotel.io/api/v1/orby/assistant \
    -H "Authorization: Bearer eyJhbG..." \
    -H "Content-Type: application/json" \
    -d '{
      "message": "How many conversations are unassigned right now?",
      "page": { "page": "/inbox", "contextLabel": "Inbox" }
    }'
  ```
</RequestExample>

### Stream events

Each SSE frame has an `event:` name and a JSON `data:` payload. Read them in order until you receive `done` (success) or `error` (failure); the connection then closes.

| Event          | `data` payload                                                            | Meaning                                                                                                                                    |
| -------------- | ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `router`       | `{ intent, confidence }`                                                  | Orby classified the message intent.                                                                                                        |
| `status`       | `{ status, detail? }`                                                     | Progress marker (e.g. `thinking`, `approving`).                                                                                            |
| `token`        | `{ content }`                                                             | One fragment of the streamed answer. Concatenate in order.                                                                                 |
| `tool_start`   | `{ toolCallId, tool, input, cost_estimate_cents, requires_confirmation }` | Orby is running a tool.                                                                                                                    |
| `tool_pending` | `{ toolCallId, tool, input, cost_estimate_cents, pending_action_id }`     | The tool needs explicit approval before it runs. Hold here and redeem `pending_action_id` (see [Tool confirmations](#tool-confirmations)). |
| `tool_end`     | `{ toolCallId, tool, input, output, status, cost_cents }`                 | A tool finished.                                                                                                                           |
| `refusal`      | `{ message }`                                                             | Orby declined the request.                                                                                                                 |
| `clarify`      | `{ question }`                                                            | Orby needs more information to proceed.                                                                                                    |
| `response`     | `{ response, turn_id, thread_id, tokens_used, cost_cents_billable }`      | The completed turn. Persist `thread_id` to continue the conversation.                                                                      |
| `error`        | `{ code, message, details? }`                                             | The turn failed.                                                                                                                           |
| `done`         | `{ ok: true }`                                                            | The stream is finished.                                                                                                                    |

```
event: token
data: {"content": "You currently have "}

event: token
data: {"content": "7 unassigned conversations."}

event: response
data: {"response": "You currently have 7 unassigned conversations.", "turn_id": "orbyTurn_abc", "thread_id": "othr_123", "tokens_used": 142, "cost_cents_billable": 1}

event: done
data: {"ok": true}
```

<Note>
  When Orby pauses on a `tool_pending` event, you can resume the same turn either by redeeming the pending action through the [tool-actions endpoints](#tool-confirmations), or by sending a follow-up turn on the same thread whose `message` is a plain `confirm` / `yes` / `cancel`. The chat reply and the explicit approve/reject button resolve through the same path, so the audit trail is identical.
</Note>

***

## Threads

### List threads

`GET /api/v1/orby/threads`

Return the operator's most recent threads (up to 50, newest first) for the "Past chats" panel.

<ResponseExample>
  ```json 200 theme={null}
  {
    "data": {
      "threads": [
        {
          "id": "othr_123",
          "title": "Unassigned conversations",
          "metadata": { "startPage": "/inbox" },
          "created_at": "2026-06-30T12:00:00Z",
          "updated_at": "2026-06-30T12:01:00Z",
          "archived_at": null
        }
      ]
    },
    "meta": {
      "request_id": "req_orby_001",
      "timestamp": "2026-06-30T12:01:00Z"
    }
  }
  ```
</ResponseExample>

### Create a thread

`POST /api/v1/orby/threads`

Pre-allocate an empty thread before the first message (the chat panel uses this to reserve a thread id up front). You can also let `POST /assistant` create the thread implicitly by omitting `thread_id`.

<ParamField body="title" type="string">Optional thread title (max 200 chars). Left empty, the thread is auto-titled from the first message.</ParamField>

<ParamField body="page" type="object">
  Optional page context stored on the thread.

  <Expandable>
    <ParamField body="page.page" type="string">Route or page identifier (max 200 chars).</ParamField>
    <ParamField body="page.contextRef" type="string">Record identifier (max 200 chars).</ParamField>
    <ParamField body="page.contextLabel" type="string">Record label (max 200 chars).</ParamField>
  </Expandable>
</ParamField>

<ResponseExample>
  ```json 200 theme={null}
  {
    "data": {
      "id": "othr_123",
      "title": null,
      "created_at": "2026-06-30T12:00:00Z",
      "updated_at": "2026-06-30T12:00:00Z"
    },
    "meta": {
      "request_id": "req_orby_002",
      "timestamp": "2026-06-30T12:00:00Z"
    }
  }
  ```
</ResponseExample>

### Get thread messages

`GET /api/v1/orby/threads/{id}/messages`

Replay a thread's persisted messages (oldest-first) to re-render history on reload.

<ParamField path="id" type="string" required>Thread id.</ParamField>

<ResponseExample>
  ```json 200 theme={null}
  {
    "data": {
      "messages": [
        {
          "id": "omsg_001",
          "role": "user",
          "content": { "text": "How many conversations are unassigned right now?" },
          "created_at": "2026-06-30T12:00:00Z"
        },
        {
          "id": "omsg_002",
          "role": "assistant",
          "content": { "text": "You currently have 7 unassigned conversations." },
          "created_at": "2026-06-30T12:00:02Z"
        }
      ]
    },
    "meta": {
      "request_id": "req_orby_003",
      "timestamp": "2026-06-30T12:00:05Z"
    }
  }
  ```
</ResponseExample>

***

## Tool confirmations

When a turn proposes an action that changes data, Orby does not run it immediately. It emits a `tool_pending` event carrying a `pending_action_id` and waits. Approve to execute, or reject to cancel. A pending action expires **5 minutes** after it is created; an expired action returns `409 PENDING_ACTION_EXPIRED`.

### Approve an action

`POST /api/v1/orby/tool-actions/{id}/approve`

Execute the pending action. Returns the tool's result once it runs.

<ParamField path="id" type="string" required>The `pending_action_id` from the `tool_pending` event.</ParamField>

<RequestExample>
  ```bash theme={null}
  curl -X POST https://api.orbit.devotel.io/api/v1/orby/tool-actions/pact_abc123/approve \
    -H "Authorization: Bearer eyJhbG..."
  ```
</RequestExample>

<ResponseExample>
  ```json 200 theme={null}
  {
    "data": {
      "id": "pact_abc123",
      "status": "done",
      "result": { "sanitized": "Message queued to +1•••••••00." }
    },
    "meta": {
      "request_id": "req_orby_004",
      "timestamp": "2026-06-30T12:02:00Z"
    }
  }
  ```
</ResponseExample>

### Reject an action

`POST /api/v1/orby/tool-actions/{id}/reject`

Cancel the pending action without running it.

<ParamField path="id" type="string" required>The `pending_action_id`.</ParamField>

<ResponseExample>
  ```json 200 theme={null}
  {
    "data": {
      "id": "pact_abc123",
      "status": "cancelled"
    },
    "meta": {
      "request_id": "req_orby_005",
      "timestamp": "2026-06-30T12:02:00Z"
    }
  }
  ```
</ResponseExample>

### Poll an action

`GET /api/v1/orby/tool-actions/{id}`

Read the current state of a pending action, including its `expires_at` so you can render a countdown. A `pending` action whose window has elapsed is reported as `cancelled` with `error_code: PENDING_ACTION_EXPIRED`.

<ParamField path="id" type="string" required>The `pending_action_id`.</ParamField>

<ResponseExample>
  ```json 200 theme={null}
  {
    "data": {
      "id": "pact_abc123",
      "tool": "send_sms",
      "args": { "to": "+15555550100", "body": "Your order shipped." },
      "cost_estimate_cents": 1,
      "status": "pending",
      "result": null,
      "error_code": null,
      "error_message": null,
      "created_at": "2026-06-30T12:00:00Z",
      "expires_at": "2026-06-30T12:05:00Z",
      "executed_at": null,
      "cancelled_at": null
    },
    "meta": {
      "request_id": "req_orby_006",
      "timestamp": "2026-06-30T12:00:30Z"
    }
  }
  ```
</ResponseExample>

<Note>
  Action states are `pending`, `executing`, `done`, `undone`, `failed`, and `cancelled`. Only a `pending` action can be approved or rejected — acting on any other state returns `409 PENDING_ACTION_NOT_APPROVABLE`, and a concurrent second approve returns `409 PENDING_ACTION_RACE`. Both approve and reject are idempotent in effect: the action runs (or cancels) exactly once. A `done` action moves to `undone` when the operator reverses it within the tool's undo window — for example, a sent message rolled back inside its 30-second grace period. Treat `undone` as a terminal state whose side effect has been undone; it differs from `cancelled`, which marks an action that was rejected (or expired) before it ever ran.
</Note>

***

## Knowledge-base search

Orby can search the Orbit product documentation so operators can answer "where do I configure X" without leaving the dashboard. These endpoints sit under `/api/v1/orby/kb`.

### Search docs

`GET /api/v1/orby/kb/search`

Hybrid search over the Orbit docs index. Available to any authenticated operator role.

<ParamField query="query" type="string" required>Search text. 1–500 characters.</ParamField>
<ParamField query="limit" type="integer" default="10">Results to return (max 50).</ParamField>
<ParamField query="filter_section" type="string">Restrict to a docs section.</ParamField>
<ParamField query="filter_category" type="string">Restrict to a docs category.</ParamField>

<ResponseExample>
  ```json 200 theme={null}
  {
    "data": {
      "results": [
        {
          "doc_id": "quiet-hours",
          "title": "Quiet hours",
          "section": "messaging",
          "category": "compliance",
          "url": "https://docs.orbit.devotel.io/compliance/quiet-hours",
          "snippet": "Set the hours during which campaigns pause…",
          "score": 0.82,
          "last_updated_at": "2026-06-01T00:00:00Z"
        }
      ]
    },
    "meta": {
      "request_id": "req_orby_007",
      "timestamp": "2026-06-30T12:00:00Z"
    }
  }
  ```
</ResponseExample>

### Find a setting

`POST /api/v1/orby/kb/find-setting`

Map a plain-language description to the dashboard route where the setting lives. Available to any authenticated operator role.

<ParamField body="description" type="string" required>What the operator wants to configure. 1–500 characters.</ParamField>
<ParamField body="limit" type="integer" default="10">Matches to return (max 20).</ParamField>

<ResponseExample>
  ```json 200 theme={null}
  {
    "data": {
      "matches": [
        {
          "setting_path": "settings.messaging.quiet_hours",
          "dashboard_url": "/settings/messaging#quiet-hours",
          "label": "Quiet hours",
          "description": "Pause outbound campaigns during set hours.",
          "doc_url": "https://docs.orbit.devotel.io/compliance/quiet-hours",
          "snippet": "Set the hours during which campaigns pause…",
          "score": 0.9
        }
      ]
    },
    "meta": {
      "request_id": "req_orby_008",
      "timestamp": "2026-06-30T12:00:00Z"
    }
  }
  ```
</ResponseExample>

### Reindex and status (super-admin)

`POST /api/v1/orby/kb/reindex` triggers a rebuild of the docs index and returns immediately with `{ "scheduled": true }` (or `{ "ran_inline": true }` when the corpus is available in-process). `GET /api/v1/orby/kb/status` returns a health snapshot: total and stale chunk counts, the last run timestamp, the current version, and cache hit rate. Both require the super-admin role and return `403 INSUFFICIENT_PERMISSIONS` otherwise.
