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

> ## Agent Instructions
> Reactor hosts multiple models, each with its own connect slug (modelName) and command/event schema. The catalog of every model — slug, typed SDK package, and links to its schema — is at /model-api-reference/overview. Some models expose one slug per experience (e.g. HappyOyster); always take the slug from the model's own pages, never guess it.
> Fastest path to a working app: `npx create-reactor-app my-app --model=<slug>` scaffolds a complete app with secure auth wired up. Typed TypeScript SDKs are published as @reactor-models/<model>; Python uses the base reactor-sdk package.
> Auth: exchange an API key (rk_...) for a JWT via POST https://api.reactor.inc/tokens from your server. Never put the API key in client-side code.
> Append .md to any docs URL for clean Markdown. Search these docs via the MCP server at https://docs.reactor.inc/mcp.

# Reactor

> The C++ class for connecting to any Reactor model

`reactor::Reactor` is one session, and the tracks and commands on it. It speaks raw JSON over the
wire: open a session, send commands by name, receive generic message events. It works against any
model — for the commands and events a specific one accepts, see the
[Model API Reference](/model-api-reference/overview), or ask the running model itself with
[`request_schema()`](#request_schema).

```cpp theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
#include <reactor/reactor.hpp>

reactor::Reactor client{"reactor/helios", reactor::ApiKey{std::getenv("REACTOR_API_KEY")}};

auto status = client.on_status([](reactor::Status s) {
  std::cout << reactor::to_string(s) << '\n';
});

client.connect().get();
client.send_command("set_prompt", {{"prompt", "a mountain landscape"}}).get();
client.disconnect().get();
```

**Async calls return `std::future<T>`, and failures arrive as exceptions from `.get()`** — the same
typed hierarchy a synchronous call throws. See
[`ReactorError`](/sdk-reference/cpp/types#reactorerror).

<Note>
  Movable, not copyable — a session has one owner. Destroying a connected client releases the native
  handle, but a creator that goes away without `disconnect()` leaves the session orphaned, and the
  next run cannot start until it clears.
</Note>

***

## Constructors

```cpp Signature theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
Reactor(std::string model, ApiKey key, Options options = {});
Reactor(std::string model, Jwt jwt, Options options = {});
```

<ParamField path="model" type="std::string" required>
  The model to connect to, as `owner/name`. A bare name resolves under `reactor/`.
</ParamField>

<ParamField path="key" type="ApiKey">
  An API key, exchanged for a session-scoped token when connecting. Scoped to this model, so a leak
  is worth a handful of sessions rather than everything the key can reach.
</ParamField>

<ParamField path="jwt" type="Jwt">
  A token minted elsewhere, used as it is. For a server that already holds one, or a client handed
  one by a backend that owns the key.
</ParamField>

<ParamField path="options" type="Options">
  Everything that is not the model or the credential — see below.
</ParamField>

`ApiKey` and `Jwt` are distinct one-field structs rather than two `std::string` parameters, so the
credential you meant is the credential that is used:

```cpp Example theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
reactor::Reactor server{"reactor/helios", reactor::ApiKey{std::getenv("REACTOR_API_KEY")}};
reactor::Reactor client{"reactor/helios", reactor::Jwt{token_from_your_backend}};
```

<Warning>
  Never ship an API key to an end-user's machine. Mint a short-lived JWT on your server and pass it
  as `Jwt`. See [Authentication](/authentication).
</Warning>

### `Options`

```cpp Definition theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
struct Options {
  std::string api_url{DEFAULT_API_URL};   // https://api.reactor.inc
  bool local = false;
  Executor executor;
};
```

<ParamField path="api_url" type="std::string" default="https://api.reactor.inc">
  The coordinator. `reactor::LOCAL_API_URL` is `http://localhost:8080`, for a local runtime.
</ParamField>

<ParamField path="local" type="bool" default="false">
  Accept a dev coordinator's self-signed certificate and speak its local-development protocol. Pair
  it with `api_url = reactor::LOCAL_API_URL`.
</ParamField>

<ParamField path="executor" type="Executor">
  Where control-event handlers run. Empty — the default — means the SDK's own dispatcher thread.
</ParamField>

### `Executor`

```cpp Signature theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
using Executor = std::function<void(std::function<void()>)>;
```

By default the SDK runs control-event handlers on one thread of its own, serialised, never on a
library thread. Give it an executor and it hands that callable each event instead — for a host with
a loop of its own (Qt, ASIO, a game loop) that would rather own when handlers run.

```cpp Example theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
reactor::Options options;
options.executor = [&queue](std::function<void()> event) { queue.post(std::move(event)); };
```

It is called from a library thread, so it must be safe to call from any thread.

<Note>
  Futures do **not** go through the executor. A promise is settled on the library's own completion
  thread, so `connect().get()` on the same thread that would have run the executor cannot deadlock
  against it.
</Note>

<Warning>
  Media handlers do not go through it either. [`on_frame()`](/sdk-reference/cpp/track#on_frame) and
  [`on_audio()`](/sdk-reference/cpp/track#on_audio) run inline on the library's delivery thread, on
  purpose — blocking in one is the backpressure. See
  [`VideoFrame`](/sdk-reference/cpp/track#videoframe).
</Warning>

***

## Connecting

### `connect()`

Creates — or adopts — a session and brings up the transport. Resolves when the session is
[`Ready`](/sdk-reference/cpp/types#status).

```cpp Signature theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
std::future<void> connect(ConnectOptions options = {});
```

```cpp Definition theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
struct ConnectOptions {
  std::optional<std::string> session_id;
  std::optional<std::uint32_t> connection_id;
};
```

<ParamField path="session_id" type="std::optional<std::string>">
  Join a session that already exists rather than creating one. This is how a second client attaches
  to the same session. A session adopted this way is **not** ended by `disconnect()` — it keeps
  running for its owner.
</ParamField>

<ParamField path="connection_id" type="std::optional<std::uint32_t>">
  Adopt a connection slot a backend already registered for this session. The connection-level
  analogue of `session_id`; most callers building one connection per session leave it unset. See
  [Multiple connections per session](/concepts/sessions#multiple-connections-per-session).
</ParamField>

Throws the typed failure: `UnauthorizedError` for a token problem, `ConflictError` for a session a
previous run left orphaned.

```cpp Example theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
reactor::ConnectOptions options;
options.session_id = existing_session;
second_client.connect(options).get();
```

***

### `reconnect()`

Cycles the connection without ending the session — after a transient failure, or deliberately from
`Ready`.

```cpp Signature theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
std::future<void> reconnect();
```

It tears the live connection down itself, so there is no need to `disconnect()` first — and doing so
would end the very session this is about to reuse. Throws when there is no session to reconnect to.

<Note>
  `RecvOnly` tracks resume automatically. `SendOnly` tracks do not: a track published before the
  reconnect is not published after it, so publish again for anything you were sending.
  [`Track::published()`](/sdk-reference/cpp/track#published) says which side of that you are on, and
  [`push_frame()`](/sdk-reference/cpp/track#push_frame) throws rather than pushing into a slot with
  nothing behind it.

  ```cpp theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
  client.reconnect().get();
  camera.publish().get();
  ```
</Note>

***

### `disconnect()`

Ends the session server-side and tears down the transport.

```cpp Signature theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
std::future<void> disconnect();
```

Not recoverable — there is no parameter that keeps the session alive instead. To disconnect and
later resume the same session, call [`reconnect()`](#reconnect). Only ends sessions this client
created; one [adopted via `session_id`](#connect) is left running for its owner.

***

### `status()`

```cpp Signature theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
Status status() const;
```

Where the session is now, as a [`Status`](/sdk-reference/cpp/types#status). Readable before
`connect()` — a client that never connected reports `Disconnected` rather than nothing.

***

### `session_id()`

```cpp Signature theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
std::optional<std::string> session_id() const;
```

The session's id, once there is a session.

***

## Commands and uploads

### `send_command()`

Sends a command to the model and waits for its correlated reply.

```cpp Signature theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
std::future<std::optional<Json>> send_command(
    std::string command,
    Json args = Json::object(),
    std::map<std::string, FileRef> uploads = {});
```

<ParamField path="command" type="std::string" required>
  The command name. Must match a command the model defines.
</ParamField>

<ParamField path="args" type="Json">
  The payload. `reactor::Json` is an alias for `nlohmann::json`.
</ParamField>

<ParamField path="uploads" type="std::map<std::string, FileRef>">
  Files to pass as named parameters — see [`upload_file()`](#upload_file).
</ParamField>

The reply is `{type, data}`, or **empty** when the handler ran and acknowledged the command without
returning a message, as an auto-generated `set_<field>` setter does. Empty is not a failure and is
not folded into one:

```cpp Example theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
auto reply = client.send_command("set_prompt", {{"prompt", "a mountain landscape"}}).get();
if (reply) {
  std::cout << reply->dump() << '\n';
}
```

To fire a command without waiting on the reply, keep the future and call `.get()` later — or drop
it, which sends the command all the same. These futures are settled by a promise rather than by
`std::async`, so dropping one neither blocks nor cancels; it only means a failure has nowhere to be
thrown.

***

### `upload_file()`

Uploads a local file and returns a [`FileRef`](/sdk-reference/cpp/types#fileref) to pass into a
command.

```cpp Signature theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
std::future<FileRef> upload_file(std::string path);
```

Needs a `Ready` session — the upload is created against it. Throws `NotFoundError` when the path
does not exist.

```cpp Example theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
auto ref = client.upload_file("photo.jpg").get();
client.send_command("set_image", {}, {{"image", ref}}).get();
```

<Note>
  Uploads are passed **separately** rather than embedded in `args`. The Python SDK finds a `FileRef`
  sitting in the arguments and pulls it out; C++ has no way to recognise one inside a `Json`, so it
  is named in the third parameter instead. Explicit costs a few characters and cannot silently miss
  one.
</Note>

***

### `upload_bytes()`

The same result as [`upload_file()`](#upload_file), for a caller who has the bytes rather than a
path — a frame just rendered, a buffer just decoded.

```cpp Signature theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
std::future<FileRef> upload_bytes(Bytes data, std::string name, std::string mime_type);
```

`data` is [borrowed for the call only](/sdk-reference/cpp/types#bytes).

```cpp Example theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
auto ref = client.upload_bytes({png.data(), png.size()}, "frame.png", "image/png").get();
```

***

### `request_schema()`

```cpp Signature theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
std::future<Json> request_schema();
```

The model's command schema, as an OpenAPI document — the same schema published on the
[Model API Reference](/model-api-reference/overview) pages, fetched from the running model. What to
read when a command is rejected: it is the model's own account of what it accepts, which is more
current than any documentation.

***

## Recordings

### `request_clip()`

Asks for a clip covering the last `duration_seconds` of the session.

```cpp Signature theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
std::future<Clip> request_clip(double duration_seconds);
```

Resolves when the platform has **accepted** the request, which is not the same as the clip being
ready — [`Clip::download()`](/sdk-reference/cpp/types#clip) is what waits for that.

```cpp Example theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
auto clip = client.request_clip(10.0).get();
clip.download("last-ten-seconds.mp4").get();
```

See [Recordings](/concepts/recordings) for the full flow.

***

### `request_recording()`

```cpp Signature theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
std::future<Clip> request_recording();
```

The same, covering the whole session up to now.

***

## Tracks

### `track()`

The track called `name`, as a [`Track`](/sdk-reference/cpp/track) — the only way to push frames into
one, receive its frames, or pause it.

```cpp Signature theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
Track track(const std::string& name);
```

```cpp Example theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
auto camera = client.track("camera");
camera.publish().get();
```

Throws `NotFoundError`, listing what the session *does* declare, for a name that is not among them.
Before the session has declared anything, any name is allowed: there is nothing yet to contradict,
and the refusals that matter happen when a handler is registered or a frame is pushed.

***

### `tracks()`

Every track the session declared, as a [`TrackList`](/sdk-reference/cpp/track#tracklist) — for
discovery, and for a caller who would rather not hardcode a name.

```cpp Signature theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
TrackList tracks();
```

```cpp Example theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
auto output = client.tracks()
                  .with_direction(reactor::TrackDirection::RecvOnly)
                  .with_kind(reactor::TrackKind::Video)
                  .one();
```

Empty until the model's capabilities arrive, shortly after `connect()`.

***

### `set_bitrate()`

Bounds what the whole connection may allocate, in bits per second.

```cpp Signature theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
std::future<void> set_bitrate(Bitrate bounds);
```

```cpp Definition theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
struct Bitrate {
  std::optional<std::int32_t> min_bps;
  std::optional<std::int32_t> start_bps;
  std::optional<std::int32_t> max_bps;
};
```

<ParamField path="min_bps" type="std::optional<std::int32_t>">
  A floor for the congestion controller; it will not drop below this even on a poor estimate. A
  floor above what the link can sustain trades graceful degradation for a fixed send rate, so choose
  it deliberately.
</ParamField>

<ParamField path="start_bps" type="std::optional<std::int32_t>">
  The initial encoder target. WebRTC starts at \~300 kbps and ramps, which is visible as a few
  seconds of soft video.
</ParamField>

<ParamField path="max_bps" type="std::optional<std::int32_t>">
  A ceiling on the whole connection.
</ParamField>

<Warning>
  There are **two** bitrate ceilings and they are conjunctive — the lower one wins. This is the
  connection-wide one. [`Track::set_bitrate()`](/sdk-reference/cpp/track#set_bitrate) bounds one
  sender's share of it, and that is the one that lifts WebRTC's 2.5 Mbps video default. Raising
  `max_bps` here alone will not make a video track exceed it.

  ```cpp theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
  reactor::Reactor::Bitrate budget;
  budget.start_bps = 4'000'000;
  budget.max_bps = 12'000'000;
  client.set_bitrate(budget).get();

  reactor::Track::Bitrate cap;
  cap.max_bps = 8'000'000;
  client.track("camera").set_bitrate(cap).get();
  ```
</Warning>

Throws on a session that is not `Ready`. The bounds outlive a reconnect.

***

### `get_stats()`

A snapshot of the live connection: RTT, jitter, packet loss, bitrates, the transport ICE selected,
and the WebRTC engine's own per-stream counters.

```cpp Signature theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
std::future<ConnectionStats> get_stats();
```

Returns a [`ConnectionStats`](/sdk-reference/cpp/types#connectionstats). Throws on a session that is
not `Ready`.

```cpp Example theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
auto stats = client.get_stats().get();
if (stats.rtt_ms) {
  std::cout << *stats.rtt_ms << " ms, " << stats.candidate_type.value_or("unknown") << '\n';
}
```

***

## Events

Every `on_*` returns a [`Subscription`](/sdk-reference/cpp/types#subscription): an RAII token that
unregisters the handler when it goes out of scope.

<Warning>
  **Discarding the return value unregisters immediately.** `client.on_status(...);` as a statement
  registers a handler and cancels it on the same line. Hold the token, or call `.detach()` to say
  the handler should live as long as the client does. This is the one place the C++ surface diverges
  from the Python one, which offers `off(event, handler)` — two `std::function`s cannot be compared,
  so a token is the only honest removal.
</Warning>

| Handler              | Payload               | Fires when                                                        |
| -------------------- | --------------------- | ----------------------------------------------------------------- |
| `on_status`          | `Status`              | The connection status changed                                     |
| `on_message`         | `const Json&`         | The model sent an application message, as `{type, data}`          |
| `on_runtime_message` | `const Json&`         | The platform sent one — session lifecycle notices, clip readiness |
| `on_track`           | `Track`               | An incoming track was received                                    |
| `on_error`           | `const ReactorError&` | A failure arrived that no call was waiting on                     |

```cpp Signature theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
Subscription on_status(std::function<void(Status)> handler);
Subscription on_message(std::function<void(const Json&)> handler);
Subscription on_runtime_message(std::function<void(const Json&)> handler);
Subscription on_track(std::function<void(Track)> handler);
Subscription on_error(std::function<void(const ReactorError&)> handler);
```

```cpp Example theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
auto messages = client.on_message([](const reactor::Json& message) {
  std::cout << message.value("type", "") << '\n';
});

auto errors = client.on_error([](const reactor::ReactorError& error) {
  if (error.recoverable()) { /* reconnect */ }
});
```

Model messages and platform messages are separate events because they are separate things: a caller
reading only `on_message` never has to filter the platform's out of it.

<Note>
  There is no client-wide frame event. Media is delivered per track, through
  [`Track::on_frame()`](/sdk-reference/cpp/track#on_frame) and
  [`Track::on_audio()`](/sdk-reference/cpp/track#on_audio) — a single handler fed every incoming
  track of a kind at once could not tell them apart.
</Note>

<Note>
  `on_error` hands you a `ReactorError`, the same type a failed call throws. Match on `code()`, or
  branch on `recoverable()` when the specific code does not matter. See
  [`ReactorError`](/sdk-reference/cpp/types#reactorerror).
</Note>

***

## `time_micros()`

The engine's monotonic clock, in microseconds — the epoch a frame's capture time is read in.

```cpp Signature theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
std::int64_t time_micros() noexcept;
```

Read it **once per unit of produced media** and stamp every track with that one value: tracks are
synchronised by sharing a capture time, not by reaching the encoder at the same moment.

```cpp Example theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
const auto captured_at = reactor::time_micros();

reactor::Track::FrameOptions options;
options.capture_time_us = captured_at;
camera.push_frame({bgra.data(), bgra.size()}, 1280, 720, options);
microphone_track.push_audio({pcm.data(), pcm.size()});
```

Unrelated to the system clock — a UNIX timestamp is not a substitute.
