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

# REST API

> HTTP endpoints for console access, experience metadata, module inspection, timeline replay, JSON-RPC ingress, and blobs.

*Mirrored from Meerkat Meta main at [26f5ae9da1fd](https://github.com/lukacf/meerkat/commit/26f5ae9da1fd3caac378f1cd847ce50f9ec5ac92) on 2026-11-03; Meta version 0.9.9 at that commit.*

Meta serves a REST API over HTTP using Axum. The API provides console access, runtime inspection, timeline replay, JSON-RPC ingress, legacy console send compatibility, multipart upload/send, and blob retrieval.

## Base URL

```
<http_base_url>
```

The console/admin `meta_gateway` and SDK-facing persistent `rpc_gateway`
bind an available loopback port and report the exact `http_base_url` in their
`meta/init` result. Use that value for every route on this page; do not
assume port 8080. A Rust library host chooses its own listener and must supply
that base URL to its clients.

***

## Console endpoints

### `GET /console`

Returns the admin console HTML page. The page is a self-contained single-page React application with no external dependencies.

**Response:** `text/html`

### `GET /console/assets/console-app.js`

Returns the console React bundle as JavaScript.

**Response:** `application/javascript`

### `GET /console/assets/console-app.css`

Returns the console stylesheet.

**Response:** `text/css`

***

## JSON API endpoints

### `GET /console/experience`

Returns experience metadata describing the current runtime state. This is the primary data source for the console UI.

**Response:**

```json theme={null}
{
  "agent_sidebar": {
    "title": "Agents",
    "live_snapshot": {
      "agents": [
        {
          "agent_id": "agent-001",
          "member_id": "lead-1",
          "label": "Team Lead",
          "kind": "autonomous_host"
        }
      ]
    }
  },
  "activity_feed": { "title": "Activity" },
  "chat_inspector": { "title": "Chat" },
  "topology": {
    "title": "Topology",
    "live_snapshot": {
      "nodes": [...],
      "node_count": 5,
      "mob_id": "ops-team"
    }
  },
  "health_overview": {
    "title": "Health",
    "live_snapshot": {
      "loaded_modules": ["router", "delivery", "memory"],
      "loaded_module_count": 3,
      "running": true
    }
  }
}
```

When a Meerkat mob runtime is configured, the response includes live agent data from the mob. Without a mob runtime, only module-level information is returned. When console UI config is loaded, the response includes `console_config`, the view-level contract consumed by the bundled console.

An agent row in `agent_sidebar.live_snapshot.agents`, and its rows in
`identity_status` and `health_overview`, can carry `last_activity_ms`: the
newest time, in milliseconds since the epoch, that the console's frame log
recorded the agent acting (a live runtime event, a console send or a tool
application). Frames rebuilt from session history or synthesized by the
console do not count, and neither do lifecycle records (respawn, retire,
reset, delete), interaction failures and memory events, which record
something done to the agent. The field is advisory: a row without it has no
known activity, and reading it wakes no agent. A row answered pending (below)
keeps it, since the frame log answers it, not the read model.

The gateway reads each agent's newest acting frame from the log on the first
experience after it starts that lists the agent, 256 agents per store call,
and keeps the time in memory after that. At 10,000 agents that first read
takes about 56 ms in all when the agents' newest frames record them acting,
and longer when many of those frames do not (rebuilt history, for example),
at about 0.4 to 0.9 microseconds per such frame. A retire, a delete or a
clear made meanwhile waits for at most one such call, and so do the console
writes queued behind it.

`topology.live_snapshot.mob_id` is the id of the mob whose members the
topology shows, when one mob is known: a runtime's console reports its own
mob, and a shared console the mob of its one registered runtime. It is absent
on a shared console over several runtimes or none, and from a snapshot a host
builds without it. The console names the topology's team from it, never from
`runtime_id`.

`health_overview.live_snapshot.running` is the mob owner's answer when the
experience is read. A console served from the console aggregator answers for
its registered runtimes: `true` only while every registered runtime's mob is
creating or running, and `false` when at least one is not. Without a runtime
that can answer (no live mob runtime, or an aggregator with no registered
runtime), it is `null`, and the snapshot names it in an `unobserved` marker:
`{"reason": "not_observed", "fields": ["running"]}`.

An identity-first member's records take its session from the identity
runtime, never from a walk made under an earlier identity binding. When the
member's binding has moved (after a reset, retire or delete), a read collects
the member again first and waits at most 2 s. Past that bound, the member's
sidebar row answers with the identity runtime's session, the member's run
`progress` and state `unknown`, and offers no send or retire. Each record of
the member (the sidebar row, the `identity_status` and `health_overview`
rows, and the topology node) withholds the other facts the earlier walk
found and names them in an `unobserved` marker with reason
`refresh_pending`, for example
`{"reason": "refresh_pending", "fields": ["label", "role", "state", "wired_to", "addressable"]}`
on the topology node. Access and visibility still decide on the last
observed facts, so a label-scoped deny keeps such a member hidden.

### `GET /console/modules`

Returns the configured module IDs from `RuntimeDecisionState`. This response
does not contain per-module health or startup state.

**Response:**

```json theme={null}
{
  "contract_version": "0.5.0",
  "modules": [
    "router",
    "delivery",
    "memory"
  ]
}
```

### `GET /console/identities`

Returns visibility-filtered identities from the console aggregator, including
ordinary runtime/worker identities; an identity-first runtime is not required.
Without a console aggregator, this route returns `404` with
`error: "unavailable"`.

**Response:** `{identities: ConsoleIdentityRecord[]}`. Each record carries
`identity`, `display_name`, `runtime_key`, `runtime_member_id`, optional
`session_id`, optional `member_id` (Core's mob member id of the identity's
last binding, live or retired: the identity's roster member id, as Core's
member tokens name it, which is Meta's comms-safe encoding of the identity and
starts with `mk--` when the identity has a `:`; absent when Core holds no
binding of the identity and on records of raw runtime members),
`visibility`, `addressable`, `health`, `labels`, and
`topology_peers` when non-empty. Use
[`meta/console/inspect_identity`](https://docs.rkat.ai/meta/api/rpc#meta%2Fconsole%2Finspect_identity)
for identity/peer inspection and `/console/experience` for affordances;
neither identities nor inspection records contain an affordances field.

### `GET /console/timeline`

Returns console timeline frames from the Meta console log store. This endpoint is the REST equivalent of `meta/console/query_timeline`.

> [accordion] Query parameters
>
> | Param | Type | Required | Description |
> | - | - | - | - |
> | `identity` | `string` | no | Restrict results to one console identity. |
> | `conversation_id` | `string` | no | Restrict results to one conversation. |
> | `mode` | `"since"` or `"recent"` | no | `since` scans forward after `after`; `recent` returns the latest visible frames in display order. Defaults to `since` for compatibility. |
> | `after` | `console:<seq>` | no | Exclusive lower cursor bound for continuation. |
> | `before` | `console:<seq>` | no | Exclusive upper cursor bound, primarily for older-history paging with `recent`. |
> | `limit` | `usize` | no | Visible frame limit, clamped by the server. |

Response frames are visibility-filtered and returned in display order. `next_cursor` is safe to use as the next `after` cursor for `since` pagination; `latest_cursor` is the latest cursor for the target window and is useful for live continuation after a `recent` seed.

The log keeps a streamed block's `text_delta` and `reasoning_delta` frames until the block's `text_complete` or `reasoning_complete` frame is stored, then removes all of them but the first, which keeps the block's place in the timeline: a later read returns that first frame and the complete frame, whose text supersedes it, and live streams have already delivered each delta. A block that never completes keeps its deltas. Cursors are never renumbered, so the cursor of a removed frame still works as `after` or `before`.

```json theme={null}
{
  "frames": [],
  "next_cursor": "console:240",
  "latest_cursor": "console:240",
  "exhausted": false
}
```

### `GET /console/timeline/stream`

Streams timeline replay followed by live console frames (`text/event-stream`).
It accepts the same query parameters as `GET /console/timeline`, but `limit`
does not bound the whole replay: `recent` produces one bounded seed page
(`limit` sizes it), while anchored `since` replays all retained matching
backlog, sent page by page as it is read, whatever `limit` says. Without
`after`, `since` switches to `recent`. A non-empty `Last-Event-ID` header
overrides the URL's `after` parameter. See the
[timeline SSE contract](https://docs.rkat.ai/meta/api/sse.md#console-timeline-stream) for event envelopes
and snapshot markers.

If the requested cursor can no longer be replayed, the route returns HTTP **409** with `{"error":"replay_unavailable"}` and cursor context. Clients should refetch a recent page and resume from `latest_cursor`.

An anchored replay can also end after the stream opened: if the log no longer holds what follows the frames sent (it was cleared during the replay), the stream sends a `replay_unavailable` event and ends, to be handled as the `409`; if a later page cannot be read, the stream ends after the frames it sent, with no `snapshot_complete`, and the client reconnects with the last frame's cursor as `Last-Event-ID`.

### `GET /console/identity/{identity}/stream`

Identity-filtered convenience route for the same console timeline stream. It sets the `identity` filter from the path and accepts the remaining timeline query parameters.

### `POST /console/send`

Legacy console send route backed by the console aggregator. New clients should prefer `POST /console/rpc` with `meta/console/send`.

### `POST /console/send/recover`

REST twin of [`meta/console/recover_send`](https://docs.rkat.ai/meta/api/rpc):
the body is `{identity, origin, idempotency_key}` and the caller is the
request's authenticated principal. It runs the same gate as `/console/send`
(read-only and `agent.send` refusals). A send the caller does not own, or
never made, answers HTTP **404** with `{"error":"send_not_found"}`. A
candidate launch (`runtime_options.mob_composition.authority =
"candidate"`) settles no send and answers HTTP **409** with
`{"error":"candidate_launch"}`, as `/console/send` does: recover the send on
the authoritative launch. It waits for the runtime's admission evidence as
the RPC does: up to 45 seconds in all when the store is slow (after a restart,
reading the evidence loads the session the send recorded), so an HTTP client
whose timeout is shorter gives up first. `console_fetch_timeout_ms`, the bundled
console's fetch timeout, should stay above 45 seconds.

### `POST /console/rpc`

JSON-RPC 2.0 endpoint for console and runtime operations.

### `POST /console/rpc/multipart`

Multipart JSON-RPC endpoint. Supported methods:

| Method | Purpose |
| - | - |
| `meta/console/send` | Send console content with image upload placeholders. |
| `meta/blob/upload` | Store a single blob/image and return a blob ID. |

For `meta/blob/upload`, send a `payload` part containing this JSON-RPC
request, plus exactly one binary part named `file:upload-1` with
`Content-Type: image/png`:

```json theme={null}
{
  "jsonrpc": "2.0",
  "id": "upload-1",
  "method": "meta/blob/upload",
  "params": {
    "upload": {
      "upload_id": "upload-1",
      "type": "image_upload",
      "media_type": "image/png"
    }
  }
}
```

The upload fields may instead be directly in `params`. `part_name` is an
alias for `upload_id`; both identify the suffix after `file:`, with
`upload_id` taking precedence if both are present. `type` is optional and,
when supplied as a string, must be `image_upload`. Optional `media_type`
must match the file part's MIME type. Supported types are `image/png`,
`image/jpeg`, `image/webp`, and `image/gif`, with a **25 MiB per-file** limit.
The result contains `blob_id`, `media_type`, and `size` in bytes, not `url`
or `content_type`. Fetch the bytes through `GET /blobs/{blob_id}`.
Console sends with image placeholders remain subject to the multipart
image-count and per-file byte limits.

### `GET /blobs/{blob_id}`

Returns blob/image content by ID when a blob store is configured.

***

## Authentication

The reference router keeps its frontend shell and bootstrap assets public:

* `/`, `/favicon.ico`, and `/healthz`
* `/console`, `/console/`, and `/console/assets/*`
* `/flow-editor`, `/flow-editor/`, and `/flow-editor/assets/*`

When `ConsolePolicy.require_app_auth` is `true`, the console JSON, JSON-RPC,
timeline, blob, Flow Editor RPC, and SSE surfaces require a valid JWT. Clients
may use either a bearer header:

```
Authorization: Bearer eyJhbGciOiJSUzI1NiIs...
```

or an exact query parameter:

```
?auth_token=<url-encoded-token>
```

Query values are percent-decoded. URL-encode reserved characters such as `=`,
`&`, `+`, and `%`. Prefer the header except for browser or SSE bootstrap cases.
The token is validated against the fixed trusted OIDC snapshot and configured
auth policy. See [authentication](https://docs.rkat.ai/meta/guides/authentication.md) for details.

Meta's built-in console auth is bearer-token based and does not rely on
cookies. Deployments that add cookie-backed console auth at a reverse proxy must
also add CSRF/origin protection for mutating console routes.

Set `ConsolePolicy.read_only` to `true` to serve the console in view-only mode.
Read endpoints and streams remain available, while mutating JSON-RPC methods
such as `meta/console/send`, `meta/blob/upload`, lifecycle controls, gating
decisions, and metadata writes are omitted from `meta/capabilities` and return
a read-only error if called directly.

## Error responses

API errors return standard HTTP status codes with a JSON body:

```json theme={null}
{
  "error": "unauthorized",
  "message": "Email not in allowlist"
}
```

| Status | Meaning |
| - | - |
| `200` | Success |
| `400` | Bad request (invalid parameters) |
| `401` | Unauthorized (missing or invalid token) |
| `403` | Forbidden (read-only policy or insufficient access grants) |
| `404` | Not found |
| `409` | Console timeline replay unavailable or console-send conflict/refusal |
| `429` | Console-send admission backlog full (`admission_backlog_full`) |
| `500` | Internal server error |
| `503` | Console-send actor probe unhealthy or reload refused |
| `504` | Console-send admission or reload timed out |

Structured `/console/send` admission failures carry `{error, message, data}`:
`429` uses `admission_backlog_full`, `503` uses `actor_probe_unhealthy` or
`reload_refused`, and `504` uses `admission_timeout` or `reload_timed_out`.
Preserve `data` when reporting these failures; a timeout does not by itself
establish execution fate or retryability. These REST status mappings are
distinct from the JSON-RPC policy codes documented in the
[RPC reference](https://docs.rkat.ai/meta/api/rpc.md#meta-specific-error-codes).

## See also

* [JSON-RPC](https://docs.rkat.ai/meta/api/rpc.md) -- full programmatic API
* [SSE API](https://docs.rkat.ai/meta/api/sse.md) -- event streaming protocol
* [Console guide](https://docs.rkat.ai/meta/guides/console.md) -- admin console details


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.