> ## 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
> To build and serve your own model, start at /deploy/development/quickstart and /deploy/development/overview. Deploying is the default path: reactor init scaffolds a workspace, reactor auth login authenticates, and reactor model deploy registers the model, publishes the release with the weights/ folder, and activates it on Reactor's GPUs, in one command from that workspace. Docker must be running, because the publish step builds the image locally. Bump model.version in reactor.yaml before redeploying a change, because a release that already has an image is reactivated as it is. Deployment access is granted per account, so contact team@reactor.inc if a deploy is refused. Every key in reactor.yaml is documented at /deploy/platform/reactor-yaml. Model code imports reactor_runtime; Python client code imports reactor_sdk. The runtime overview explains the model interface. Running the model on your own machine with reactor run is optional and needs a GPU you attach with --gpus; /deploy/development/local-testing covers that loop and pairs a complete brightness model with a Python client test in a separate brightness-test workspace.
> Reactor hosts multiple models, each with its own connect slug (modelName) and command/event schema. The video model catalog — slug, typed SDK package, and links to its schema — is at /model-api-reference/overview. Robotics policy documentation starts at /robotics/overview; X-WAM observations, actions, and client integration are under /robotics/xwam/; Cosmos3 Nano Policy DROID is under /robotics/cosmos/nano-policy-droid/. 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.

# C++ SDK types

> Type definitions for the Reactor C++ SDK

Everything here lives in namespace `reactor` and arrives with `#include <reactor/reactor.hpp>`.

## `Status`

```cpp Definition theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
enum class Status : std::uint8_t {
  Disconnected,   // no session, or the last one ended
  Connecting,     // the session is being created or adopted
  Waiting,        // the session exists; waiting for the runtime, negotiating transport
  Ready,          // transport is up: commands can be sent and tracks flow
};
```

Four states, in the order they happen. Read the current one with
[`client.status()`](/sdk-reference/cpp/reactor#status), or subscribe with
[`on_status()`](/sdk-reference/cpp/reactor#events).

```cpp Signature theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
std::string_view to_string(Status status) noexcept;
Status status_from_string(std::string_view text) noexcept;
```

| Argument | Type | Description |
| - | - | - |
| `status` | `Status` | For `to_string()`: status to convert to its wire spelling. |
| `text` | `std::string_view` | For `status_from_string()`: wire spelling to parse. |

`status_from_string` reads an unrecognised spelling as `Disconnected`: a client that cannot
understand what the library is telling it should behave as though it has no session, rather than
assume the most capable state it knows.

<Note>
  Several calls require `Ready` and throw [`InvalidStateError`](#reactorerror) otherwise —
  [`upload_file()`](/sdk-reference/cpp/reactor#upload_file),
  [`publish()`](/sdk-reference/cpp/track#publish),
  [`push_frame()`](/sdk-reference/cpp/track#push_frame),
  [`set_bitrate()`](/sdk-reference/cpp/track#set_bitrate).
</Note>

***

## `ReactorError`

Thrown when an operation fails — from `.get()` on a future, or directly from a synchronous call —
**and** the payload [`on_error`](/sdk-reference/cpp/reactor#events) delivers. The same class either
way: what the event hands you is not a separate type that happens to agree with the exception, it
*is* the exception.

```cpp Definition theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
class ReactorError : public std::runtime_error {
 public:
  const ErrorDetails& details() const noexcept;

  const std::string& code() const noexcept;
  const std::string& message() const noexcept;
  bool recoverable() const noexcept;
  const std::optional<int>& status() const noexcept;
  const std::optional<std::string>& operation() const noexcept;
  const std::optional<double>& retry_after_ms() const noexcept;
  const std::optional<double>& timestamp_ms() const noexcept;
};
```

| Parameter | Type | Description |
| - | - | - |
| `code` | `std::string` | A stable, matchable code — see the [table](#error-codes) below, or a code the platform sent for a request it rejected. Never empty. |
| `message` | `std::string` | The human-readable explanation, and the only field guaranteed to be worth printing. |
| `recoverable` | `bool` | Whether the same call could succeed later. `true` is about the moment — a timeout, a 5xx, a transport that dropped — so waiting or reconnecting is worth something. `false` is about the request itself. |
| `status` | `std::optional<int>` | The HTTP status, when the failure came from one. |
| `operation` | `std::optional<std::string>` | Which call failed, e.g. `"connect"`, `"send_command"`. Empty for a failure not tied to a specific call, like a transport that dropped on its own. |
| `retry_after_ms` | `std::optional<double>` | A backoff hint, when the platform sent one. |
| `timestamp_ms` | `std::optional<double>` | When this happened. Only ever set on the `on_error` event — a thrown exception is already happening now, so there is nothing this would tell you that catching it does not. |

Every code in the table below has its own subclass, so a specific failure is caught by class rather
than by parsing `code()`:

```cpp Example theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
try {
  client.connect().get();
} catch (const reactor::UnauthorizedError&) {
  token = refresh();                    // a specific, actionable failure
} catch (const reactor::ConflictError&) {
  // a previous run left the session orphaned
} catch (const reactor::ReactorError& error) {
  if (error.recoverable()) {            // a class of failures, by property
    client.reconnect().get();
  } else {
    throw;
  }
}
```

`ReactorError` is the base of all of them, so `catch (const ReactorError&)` still catches every one.

<Note>
  `recoverable` is not computed in C++. The core decides it from the code and sends it, so two SDKs
  cannot disagree about whether a timeout is worth retrying.
</Note>

<Note>
  There is no `component` field. Which tier of the platform failed is not something a caller can act
  on, and splitting codes by it once produced two names for one failure — a 401 during connect that
  the caller saw as `UNAUTHORIZED` and not recoverable while the event called it `CONNECTION_FAILED`
  and recoverable, so anything listening to the event reconnected in a loop against a token that
  would never work. Match on `code()` alone.
</Note>

### Error codes

| Code | Exception class | Recoverable | Description |
| - | - | - | - |
| `INVALID_STATE` | `InvalidStateError` | No | The call is not allowed from the state the client is in — most often one needing a live session, made before `connect()` or after `ready` was lost. |
| `DISCONNECTED` | `DisconnectedError` | Yes | The connection went away: dropped while a request was in flight, or lost after it had been established. `reconnect()` is the way back. |
| `NETWORK_ERROR` | `NetworkError` | Yes | The request never got a reply — DNS, TLS, a refused socket. |
| `REQUEST_TIMEOUT` | `RequestTimeoutError` | Yes | Sent, and nothing came back in time. |
| `TRANSPORT_ERROR` | `TransportError` | Yes | The media transport failed. |
| `UNAUTHORIZED` | `UnauthorizedError` | No | The token is missing, expired, or not scoped for this (HTTP 401 / 403). |
| `NOT_FOUND` | `NotFoundError` | No | No such model, session or upload (HTTP 404). |
| `CONFLICT` | `ConflictError` | No | The session is in a state that does not allow this (HTTP 409) — usually one left orphaned by a run that went away without disconnecting. |
| `RATE_LIMITED` | `RateLimitedError` | Yes | Too many requests (HTTP 429). |
| `BAD_REQUEST` | `BadRequestError` | No | The request itself was wrong — a 4xx other than the above, or an argument rejected here before it was sent. |
| `SERVER_ERROR` | `ServerError` | Yes | The platform failed, and the same request may work later (HTTP 5xx). |
| `VERSION_MISMATCH` | `VersionMismatchError` | No | This client and the platform disagree on the protocol (HTTP 426 / 501). Download a newer release archive. |
| `DECODE_FAILED` | `DecodeError` | No | A reply arrived and could not be understood. |
| `SESSION_TERMINAL` | `SessionTerminalError` | No | The session reached a state it cannot leave. Start a new one. |
| `MESSAGE_TOO_LARGE` | `MessageTooLargeError` | No | The payload exceeds what the data channel accepts. Use `upload_file()` instead. |
| `ABORTED` | `AbortedError` | No | The operation was abandoned before it finished. |
| `RECORDER_DISABLED` | `RecorderDisabledError` | No | A clip/recording request failed because the model's recorder is disabled or has crashed. |

The spellings are also available as constants under `reactor::codes` —
`reactor::codes::UNAUTHORIZED` and so on.

<Note>
  **Codes are open-ended.** A command or control request the model itself rejects reports the
  platform's own code, which this list cannot enumerate — that arrives as the base `ReactorError`
  with `code()` set to whatever came. Match on `code()` for anything not in the table, and never
  assume an unrecognised code means the payload was malformed.
</Note>

***

## `Subscription`

What every `on_*` registration hands back. An RAII token: it unregisters the handler when it goes
out of scope.

```cpp Definition theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
class Subscription {
 public:
  void remove() noexcept;    // unregister now; idempotent, safe after the client is gone
  void detach() noexcept;    // keep the handler registered, stop tracking it here
  bool active() const noexcept;
};
```

```cpp Example theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
{
  auto status = client.on_status([](reactor::Status s) { log(s); });
  // handler live
}                                     // and unregistered here
```

`remove()` and the destructor also wait for a delivery already in flight on another thread. When
either returns, the handler is not in a call, so whatever it captured is safe to destroy.

<Warning>
  **Discarding the return value unregisters immediately** — `client.on_status(...);` as a statement
  registers a handler and cancels it on the same line, which is usually a bug and always a quiet one.
  Hold the token, or call `detach()` for a handler meant to live as long as the client does:

  ```cpp theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
  client.on_error([](const reactor::ReactorError& e) { log(e.what()); }).detach();
  ```
</Warning>

This is the one place the C++ surface diverges from the
[Python one](/sdk-reference/python/reactor#off), which offers `off(event, handler)`. Two
`std::function`s cannot be compared, so a C++ binding has no way to find "the handler you passed
earlier" — a token is the only honest answer, and it doubles as RAII.

Move-only, like every other handle here.

***

## `FileRef`

A file the platform is holding, ready to be passed into a command. Returned by
[`upload_file()`](/sdk-reference/cpp/reactor#upload_file) and
[`upload_bytes()`](/sdk-reference/cpp/reactor#upload_bytes).

```cpp Definition theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
struct FileRef {
  std::string upload_id;
  std::string name;
  std::string mime_type;
  std::uint64_t size = 0;
};
```

A parameter that takes one file is handed to
[`send_command()`](/sdk-reference/cpp/reactor#send_command) as a named upload rather than embedded
in the arguments — the platform resolves the reference on its side, so the bytes cross the wire
once. A parameter that takes several has no named slot. Put those references in the arguments: a
`FileRef` converts to `Json` as that same reference, and `Json::array({first, second})` is the list.
See [File Uploads](/concepts/file-uploads).

***

## `Clip`

A clip or a full-session recording, once the platform has **accepted** the request — which is not
the same as it being ready. Returned by [`request_clip()`](/sdk-reference/cpp/reactor#request_clip)
and [`request_recording()`](/sdk-reference/cpp/reactor#request_recording).

```cpp Definition theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
class Clip {
 public:
  const std::string& playlist_url() const noexcept;
  const std::string& session_id() const noexcept;
  const std::string& kind() const noexcept;      // "snap" or "recording"

  double start_marker() const noexcept;          // the window, in the session's media clock
  double end_marker() const noexcept;
  double now_marker() const noexcept;
  double predicted_ready_at_ms() const noexcept;

  std::future<void> download(std::string path, DownloadOptions options = {}) const;
};
```

### `download()`

Fetches and assembles the clip into one playable file at `path`, waiting for it to become ready.

| Argument | Type | Description |
| - | - | - |
| `path` | `std::string` | Destination file path. |
| `options` | `DownloadOptions` | Optional progress callback and readiness timeout; defaults to `{}`. |

```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();
```

The file is created before anything is asked of the network, so an unwritable path fails
immediately; a download that fails part-way leaves nothing behind, because a truncated clip opens,
plays some of itself, and gives no reason to suspect the download.

```cpp Definition theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
struct DownloadOptions {
  std::function<void(std::uint32_t done, std::uint32_t total)> on_progress;
  std::optional<double> ready_timeout_seconds;
};
```

| Parameter | Type | Description |
| - | - | - |
| `on_progress` | `std::function<void(std::uint32_t, std::uint32_t)>` | Called after each segment is written, with how many of how many. Invoked on the downloader's own thread — fine for a counter or a log line, and not a place to touch anything that is not thread-safe. |
| `ready_timeout_seconds` | `std::optional<double>` | Grace past `predicted_ready_at_ms`. Empty — the default — waits as long as the session lives. |

<Note>
  Empty is the right default for a model generating slower than real time: a clip becomes ready
  *because* the model keeps generating, and once the session is gone a "not yet" is a "not yet"
  forever, which is what ends the wait. `predicted_ready_at_ms` is a wall clock plus media seconds,
  so it is only right for a model generating at real time — treat it as an anchor, never a deadline.
</Note>

Reactor does not host clips: `playlist_url()` names a short-lived HLS playlist, and the fragments
behind it have to be fetched and assembled. That assembly lives in the Rust core, so this SDK
carries no HTTP client and no playlist parser of its own. See [Recordings](/concepts/recordings).

***

## `ConnectionStats`

Returned by [`get_stats()`](/sdk-reference/cpp/reactor#get_stats).

```cpp Definition theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
struct ConnectionStats {
  std::optional<double> rtt_ms;                          // from the candidate pair carrying the media
  std::optional<double> jitter_s;                        // on the received video stream
  std::optional<double> packet_loss_ratio;               // cumulative, 0 to 1
  std::optional<double> incoming_bitrate_bps;            // measured over the window since the last call
  std::optional<double> outgoing_bitrate_bps;            // the same window, on the same terms
  std::optional<double> available_incoming_bitrate_bps;  // what the path can carry, not what flows
  std::optional<double> available_outgoing_bitrate_bps;
  std::optional<double> target_bitrate_bps;               // what the encoders aim at, summed
  std::optional<double> frames_per_second;                // on the received video stream
  std::optional<std::string> candidate_type;              // "host", "srflx", "prflx", or "relay"
  std::optional<std::string> relay_protocol;               // "udp", "tcp", or "tls" when relayed
  std::optional<std::string> candidate_pair_state;
  std::uint64_t packets_received = 0;
  std::int64_t packets_lost = 0;
  std::uint64_t packets_sent = 0;
  std::uint64_t bytes_received = 0;
  std::uint64_t bytes_sent = 0;
  double timestamp_ms = 0.0;                               // when the sample was taken, in Unix ms
  std::vector<InboundStream> inbound;
  std::vector<OutboundStream> outbound;
  std::vector<CandidatePair> candidate_pairs;
};
```

An unmeasured field is empty, never zero. A missing RTT does not mean a zero-latency link.
`candidate_type` reads `"relay"` when the media goes through a TURN server. Check this field first
when latency is bad.

The SDK measures the two bitrates on the candidate pair that carries the media, over the window
since the previous call. Both read empty on the first call after connecting, again after a
reconnect, and until ICE selects a pair. A call made less than 200 ms after the last one also reads
empty for them. Every other field is on every call.

The SDK reads `jitter_s`, `packet_loss_ratio`, and `frames_per_second` from the received video
stream. With no video stream, the first two fall back to whatever receive streams exist, and
`frames_per_second` stays empty.

`reactor-webrtc` 0.15 brought this shape to field-for-field parity with the browser SDK's own
`getStats()` — same candidate pair, same byte counters, same video stream. `inbound`, `outbound`,
`candidate_pairs`, and `relay_protocol` have no match there. The browser SDK reports none of them.

***

## `InboundStream`

One receive stream's counters, as the WebRTC engine reports them. Read from
[`ConnectionStats::inbound`](#connectionstats).

```cpp Definition theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
struct InboundStream {
  std::uint32_t ssrc = 0;
  std::optional<std::string> kind;    // "audio", "video", or empty when the engine reported no kind
  std::uint32_t packets_received = 0;
  std::int32_t packets_lost = 0;      // signed: duplicates can take it negative
  std::uint64_t bytes_received = 0;
  double jitter_s = 0.0;
  std::uint32_t nack_count = 0;
  double total_decode_time_s = 0.0;   // cumulative
  double frames_per_second = 0.0;     // video only; zero until a window is measured
  std::uint32_t frames_decoded = 0;
  std::uint32_t frames_dropped = 0;
  std::uint32_t frame_width = 0;      // decoded frame size; zero for audio
  std::uint32_t frame_height = 0;
};
```

***

## `OutboundStream`

One send stream's counters. Read from [`ConnectionStats::outbound`](#connectionstats).

```cpp Definition theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
struct OutboundStream {
  std::uint32_t ssrc = 0;
  std::optional<std::string> kind;    // "audio", "video", or empty
  std::uint64_t packets_sent = 0;
  std::uint64_t retransmitted_packets_sent = 0;
  std::uint64_t bytes_sent = 0;
  double target_bitrate_bps = 0.0;    // what the encoder aims at
  double frames_per_second = 0.0;     // video only; zero until a window is measured
  std::uint32_t frames_sent = 0;
  std::uint32_t frame_width = 0;      // encoded frame size; zero for audio
  std::uint32_t frame_height = 0;
  double round_trip_time_s = 0.0;
  double total_round_trip_time_s = 0.0;
  double fraction_lost = 0.0;         // 0 to 1
  std::int32_t packets_lost = 0;      // signed
};
```

The last four fields come from the far end's report about this client. They stay at zero until it
sends one. A zero there means "not measured yet", not a perfect connection.

***

## `CandidatePair`

One ICE candidate pair. Read from [`ConnectionStats::candidate_pairs`](#connectionstats).

```cpp Definition theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
struct CandidatePair {
  double current_round_trip_time_s = 0.0;
  double total_round_trip_time_s = 0.0;
  std::uint64_t priority = 0;
  std::string state;                  // "succeeded", "waiting", "in-progress", "failed", or "cancelled"
  bool nominated = false;             // whether ICE selected this pair
  bool writable = false;
  double available_outgoing_bitrate_bps = 0.0;
  double available_incoming_bitrate_bps = 0.0;
  std::uint64_t bytes_sent = 0;       // everything the pair carried, RTCP and the data channel included
  std::uint64_t bytes_received = 0;
  std::uint64_t packets_sent = 0;
  std::uint64_t packets_received = 0;
  std::optional<std::string> local_candidate_type;  // "host", "srflx", "prflx", "relay", or empty
  std::optional<std::string> local_relay_protocol;  // "udp", "tcp", "tls", or empty when not relayed
};
```

A connection gathers many pairs, and exactly one is `nominated`. Only that pair carries traffic. The
rest report zeros. Read `nominated` instead of averaging across the list.

***

## `Bytes`

A borrowed run of bytes — what `std::span<const std::uint8_t>` would be, which C++17 does not have.

```cpp Definition theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
struct Bytes {
  const std::uint8_t* data = nullptr;
  std::size_t size = 0;

  bool empty() const noexcept;
  const std::uint8_t* begin() const noexcept;
  const std::uint8_t* end() const noexcept;
};
```

Borrowed, always: it is valid for the call it is passed to, or for the handler it is delivered in,
and no longer.

```cpp Example theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
camera.push_frame({bgra.data(), bgra.size()}, 1280, 720);
```

***

## `Samples`

The same, for interleaved 16-bit PCM.

```cpp Definition theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
struct Samples {
  const std::int16_t* data = nullptr;
  std::size_t size = 0;

  bool empty() const noexcept;
};
```

A length, not just a pointer, so [`push_frame()`](/sdk-reference/cpp/track#push_frame) can check it
against the channel count instead of trusting it — the native layer reads what it is told to read.

***

## `Json`

```cpp Definition theme={"theme":{"light":"github-light","dark":"github-dark-high-contrast"}}
namespace reactor {
using Json = nlohmann::json;
}
```

JSON is the native layer's own currency: capabilities, errors, command arguments and replies all
cross the boundary as JSON text. [nlohmann/json](https://github.com/nlohmann/json) is the de facto
standard for reading it in C++, and it is exposed under this alias so there is one seam to change if
that ever stops being true.

The archive carries a copy, and a consumer who already pins their own gets theirs.


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