# Streaming Hub Core API

Complete reference for authors of external applications and native clients.

HTTP contract: v1. Component release verification pending.

Content updated: 2026-10-25. Plugins required for these examples: None.

Core, Client/Contracts package versions and Host Plugin API versions are independent. HTTP v1 is not an application or package version. Match this document to the running Core's negotiated contract and live schemas.

## Reading this file with an agent

This is a standalone Markdown document: the following table of contents links to the complete articles embedded below. Code fences contain copyable examples. Website-only Editor and Plugin SDK references use absolute URLs.

Start with discovery, pairing and handshake. Ask the user to approve the requested scopes through Core's Tray. Obtain secrets only through the approved protocol; keep them out of generated source, logs, command lines and project files. Read current identity, revision and schemas before editing. Preserve command IDs and the paired principal when recovering uncertain outcomes.

The current deployment supports local native Windows clients over loopback HTTP/WebSocket. It does not expose a browser or LAN API. Core owns runtime and files; a client owns its own interface and drafts.

## Contents

- [Streaming Hub API overview and installation](#streaming-hub-api-overview-and-installation)
- [Connection, pairing and scopes](#connection-pairing-and-scopes)
- [Manual HTTP testing in PowerShell](#manual-http-testing-in-powershell)
- [Your first .NET client](#your-first-net-client)
- [Partial changes and conflicts](#partial-changes-and-conflicts)
- [Command recovery and uncertain outcomes](#command-recovery-and-uncertain-outcomes)
- [Streams, lifecycle and compatibility](#streams-lifecycle-and-compatibility)
- [Installation updates and client preparation](#installation-updates-and-client-preparation)
- [Console examples and manual test workflow](#console-examples-and-manual-test-workflow)
- [HTTP routes and .NET client reference](#http-routes-and-net-client-reference)

---

## Streaming Hub API overview and installation

Website: https://streaminghub.tools/docs/api-client/overview-and-installation

Updated: 2026-10-25.

Verified with: Release component versions pending verification; do not infer package versions from HTTP v1.

Streaming Hub API lets an external application read and control Streaming Hub Core. Build a desktop controller, monitoring tool or alternative editor while Core owns the active project, plugin connections, automation execution and saved files.

### Choose the right documentation

| You are building | Use |
| --- | --- |
| An application that connects to Core | This Streaming Hub API section |
| Automations in the supplied graphical application | [Streaming Hub Editor documentation](https://streaminghub.tools/docs) |
| Actions, signals or integrations loaded inside Core | [Plugin SDK](https://streaminghub.tools/docs/0.2/plugin-sdk) |

A client does not need WPF, the supplied Editor, Core internals or Plugin SDK. Closing your client leaves Core running. Themes, panel layout and unsent drafts belong to your application.

### Choose your transport

The native API uses authenticated HTTP under `/api/v1` and bounded WebSocket streams. Its current deployment is local Windows applications: Core listens on `127.0.0.1` at a dynamically allocated port. Browser-origin requests and LAN connections are not supported.

Any language with an HTTP client can implement the protocol. Start with [PowerShell HTTP requests](#manual-http-testing-in-powershell) to see the wire format. For .NET, use `StreamingHub.Api.Client` and `StreamingHub.Api.Contracts`; both target .NET 8. The client handles discovery validation, pairing calls, session negotiation, typed contracts and observation helpers.

HTTP API v1, Core versions, Client/Contracts package versions and Host Plugin API versions identify different things. Use Client and Contracts from the same verified package set. A public NuGet feed is not currently advertised.

### Prepare Core

Use a matching Core package with its Tray approval surface, and open a copy of a test project. Keep the Tray available: external clients need the user's approval before accessing state.

From a repository checkout with the .NET 8 SDK, build the solution before starting Core:

```powershell
dotnet build Streaming-HUB.slnx
dotnet run --project StreamingHub.CoreHost -- --data-dir C:\StreamingHubApiTest --project C:\StreamingHubApiTest\Test.shubproj
```

Create the test directory and project copy first. A custom data directory isolates settings, profiles, plugin state, credentials and discovery; it still permits only one Core owner per Windows user. Exit an existing Core before starting a different test installation. A packaged Core starts through `StreamingHub.CoreHost.exe` and includes its Tray.

### Find the current endpoint

Without a custom data directory, discovery is at:

```text
%LOCALAPPDATA%\StreamingHub\runtime\api-discovery.json
```

With `--data-dir`, use `api-discovery.json` in that directory. The file contains `endpoint`, `hostInstanceId` and `apiMajorVersions`. Read it again after Core restarts; do not assume a fixed port or reuse the previous Host identity. Discovery is not permission to access Core.

### Follow the client lifecycle

1. Read discovery and validate the loopback endpoint and HTTP major.
2. [Request permissions and pair](#connection-pairing-and-scopes) through the user's Tray.
3. Exchange the approved credential for a session and complete the handshake.
4. Read a coherent snapshot, then [observe changes](#streams-lifecycle-and-compatibility).
5. Submit identity- and revision-bound commands and [recover uncertain results](#command-recovery-and-uncertain-outcomes) using their original IDs.

Use API operations to change and save a project. Do not overwrite its active project file or sidecars directly.

Continue with [manual HTTP testing](#manual-http-testing-in-powershell) or [your first .NET client](#your-first-net-client).

---

## Connection, pairing and scopes

Website: https://streaminghub.tools/docs/api-client/connection-pairing-and-scopes

Updated: 2026-10-25.

Verified with: Release component versions pending verification; do not infer package versions from HTTP v1.

Discovery identifies the active Host endpoint and identity. It does not authorize access, and a client cannot approve its own pairing request.

### Pair

Read the current discovery file with `ApiHostDiscovery.ReadAsync`, construct `StreamingHubApiClient(discovery)`, and call `RequestPairingAsync` with a stable client ID, claimed display name/publisher and requested scopes.

The user approves or denies through Core's tray. Poll the returned handle with `PollPairingAsync`; only an approved result returns the credential, once. Connect using `ConnectWithCredentialAsync` and the required capabilities.

Store a retained credential using protected local storage. Keep credentials, sessions and pairing handles out of logs, URLs, command lines and project files.

Pairing requests expire after two minutes; at most 16 can be pending. Credentials last 30 days and are bound to the stable client ID. HTTP sessions last at most two minutes, bounded by credential expiry. Exchange a still-valid credential again and complete handshake to renew without changing grants. Client names and publisher labels are claimed text, not verified publisher identity.

The [PowerShell walkthrough](#manual-http-testing-in-powershell) includes every HTTP request. [Your first .NET client](#your-first-net-client) implements the same flow with typed methods.

### Select permissions

| Scope | Purpose |
| --- | --- |
| `host.status` | Host identity and safe status |
| `host.shutdown` | Explicit Host shutdown; separate from status access |
| `project.read` / `project.configuration.read` | Structural state / content-bearing configuration |
| `project.write` / `project.files` | Apply and recovery / project file operations |
| `catalog.read` | Declared actions, signals and schemas |
| `settings.read` / `settings.write` | Plugin/Core settings and versioned writes |
| `runtime.read` / `runtime.control` | Runtime observations/recovery and execution control |
| `runtime.values.read` | Live variables, service values and statistics |
| `diagnostics.read` | Bounded diagnostic metadata |
| `diagnostics.content.read` | Sensitive diagnostic/log content and sanitized signal samples |
| `inspector.read` | Bounded traces for the selected run, subject to ownership |
| `history.read` / `history.manage` | Persisted run data / retention and clear |
| `updates.read` / `updates.manage` / `updates.prepare` | Update observations, exact-plan control and client preparation |
| `plugins.trust.read` / `plugins.trust.manage` | Trust candidate reads / decisions |
| `interaction.read` / `interaction.respond` | List pending interactions / claim, release and respond |
| `components.read` / `components.install` | Read installation component catalog / plan installation |
| `project.layout.read` / `project.layout.write` | Retired; layout is client-local and routes return `editor_layout_local` |

Some methods require combined scopes. Runtime value reads, sensitive diagnostics and interaction responses have additional dedicated permissions; inspect the negotiated capabilities and method contract.

Dynamic editor options require `catalog.read`, `project.configuration.read` and `settings.read` together. Context-file pickers require `project.files`. Reading a catalog does not grant directory access. Responding to sensitive interactions also requires the underlying operation's scope. Runtime control does not grant runtime command-result reads.

Handshake's `requiredCapabilities` selects capabilities from the approved grant and rejects unavailable ones. Keep that list explicit. An application providing only status should request `host.status`; one that reads a project adds `project.read`; one that applies edits also needs `project.write`. Do not copy the supplied Editor's full grant into every client.

Renewal cannot expand a grant. A disconnected session can reconnect using an unrevoked credential; revoked or expired access must be resolved explicitly. A fresh pairing is a different principal for command recovery.

Disconnecting one session does not revoke its approved credential. Revocation invalidates associated sessions and subscriptions. Core's trusted client-access controls own these administrative actions; there is no public HTTP approval endpoint for a client to authorize itself.

### Browser boundary

The current API accepts local native clients. Core does not enable a LAN listener, CORS or browser-origin access. Do not expose it through a web proxy as a substitute for an approved browser security design.

---

## Manual HTTP testing in PowerShell

Website: https://streaminghub.tools/docs/api-client/manual-http-testing

Updated: 2026-10-25.

Verified with: Release component versions pending verification; do not infer package versions from HTTP v1.

Use PowerShell 7 on Windows to exercise the local HTTP protocol your application will use. This walkthrough requests only `host.status` and reads Host information and API schemas. It does not edit or run a project.

Start Core with its Tray first. Paste the blocks into the same PowerShell session. Responses containing handles, credentials or session tokens stay in variables; do not print them or capture them in a transcript.

### Read discovery and prepare requests

For a custom data directory, change the discovery path:

```powershell
$discoveryPath = Join-Path $env:LOCALAPPDATA 'StreamingHub\runtime\api-discovery.json'
$discovery = Get-Content -LiteralPath $discoveryPath -Raw | ConvertFrom-Json
$endpoint = [uri]$discovery.endpoint
if ($endpoint.Scheme -ne 'http' -or $endpoint.Host -ne '127.0.0.1' -or
    $endpoint.UserInfo -or $endpoint.Query -or $endpoint.Fragment -or
    $endpoint.AbsolutePath -ne '/' -or $discovery.apiMajorVersions -notcontains 1) {
    throw 'Unsupported Core discovery endpoint or contract.'
}
$apiBase = $endpoint.AbsoluteUri.TrimEnd('/') + '/api/v1'
$clientId = 'example.powershell-client'
$scopes = @('host.status')

function Send-CoreRequest {
    param(
        [Parameter(Mandatory)][string]$Path,
        [ValidateSet('Get', 'Post')][string]$Method = 'Get',
        [object]$Body,
        [hashtable]$Headers = @{}
    )
    $requestOptions = @{
        Uri = "$apiBase/$Path"
        Method = $Method
        Headers = $Headers
        NoProxy = $true
        MaximumRedirection = 0
        TimeoutSec = 15
        ErrorAction = 'Stop'
    }
    if ($PSBoundParameters.ContainsKey('Body')) {
        $requestOptions.ContentType = 'application/json'
        $requestOptions.Body = $Body | ConvertTo-Json -Depth 20 -Compress
    }
    Invoke-RestMethod @requestOptions
}
```

Use the discovered literal loopback address. Disabling proxies and redirects keeps credentials on that endpoint. Do not add an `Origin` header: Core rejects browser-origin requests.

### Request access and approve it in Tray

```powershell
$pairing = Send-CoreRequest -Path 'pairing/requests' -Method Post -Body @{
    clientId = $clientId
    name = 'PowerShell API test'
    publisher = 'Local manual test (claimed)'
    scopes = $scopes
}
Write-Host 'Approve PowerShell API test in the Core tray.'

do {
    if ([datetimeoffset]::UtcNow -ge [datetimeoffset]$pairing.expiresAt) {
        throw 'Pairing request expired. Start a new request.'
    }
    $approval = Send-CoreRequest -Path 'pairing/requests/status' -Method Post -Body @{
        handle = $pairing.handle
    }
    if ($approval.status -eq 'pending') { Start-Sleep -Milliseconds 500 }
} while ($approval.status -eq 'pending')

if ($approval.status -ne 'approved' -or -not $approval.credential) {
    throw "Pairing ended: $($approval.status)"
}
$credential = $approval.credential
```

Approve **PowerShell API test** in Tray while polling continues. Requests expire after two minutes. Only approval returns the secret, once. The client cannot approve itself.

### Exchange the credential and negotiate

```powershell
$session = Send-CoreRequest -Path 'credentials/exchange' -Method Post -Body @{
    clientId = $clientId
    secret = $credential.secret
}
$authHeaders = @{ Authorization = "Bearer $($session.accessToken)" }
$handshake = Send-CoreRequest -Path 'handshake' -Method Post -Headers $authHeaders -Body @{
    contractMajor = 1
    requiredCapabilities = $scopes
}
if ($handshake.hostInstanceId -ne $discovery.hostInstanceId) {
    throw 'Core restarted. Read discovery again before continuing.'
}
$handshake | Select-Object contractMajor, hostInstanceId, capabilities
```

The credential creates sessions; HTTP requests use a short-lived session token in the `Authorization` header. The handshake is required before normal calls. Do not put either secret in a URL.

### Read Host and live API reference

```powershell
$hostInfo = Send-CoreRequest -Path 'host' -Headers $authHeaders
$hostInfo | Select-Object hostInstanceId, coreVersion

$openApi = Send-CoreRequest -Path 'openapi.json' -Headers $authHeaders
$openApi.paths.PSObject.Properties.Name | Sort-Object

$schema = Send-CoreRequest -Path 'schemas/HandshakeRequest' -Headers $authHeaders
$schema | ConvertTo-Json -Depth 20
```

These routes require `host.status`. OpenAPI and named schemas describe the running Core; use them to implement clients in other languages. See the [route and method reference](#http-routes-and-net-client-reference).

### Renew the session

Sessions last at most two minutes. Repeat exchange and handshake with the still-valid credential, replacing `$authHeaders` with the new token. This retains the same principal and grants. New approval is required if the credential expired, was revoked or needs additional scopes.

To read a project, include `project.read` in the original scopes, obtain approval for the new grant and complete exchange/handshake. Then request `projects/active/snapshot` with a project open. That response can be paginated: a complete client must preserve identity and revision across every page, as the .NET `GetCompleteSnapshotAsync` helper does.

### Expected failures

| Result | Check |
| --- | --- |
| Missing discovery or connection refused | Start Core; select the correct data directory and read current discovery |
| `origin_forbidden` | Use the discovered literal IPv4 endpoint from a native client |
| `session_required` / `session_expired` | Exchange the credential and send the new Bearer token |
| `handshake_required` | Complete the handshake before normal calls |
| `capability_forbidden` | Request the specific scope and obtain approval |
| `credential_invalid` | Check client ID, expiration and revocation |
| `pairing_unknown` | The handle is unknown or its one-time result was consumed |

### Finish

```powershell
Remove-Variable pairing, approval, credential, session, authHeaders, handshake -ErrorAction SilentlyContinue
```

Removing variables drops console references; it does not revoke access or guarantee memory erasure. Revoke the test client through Core's client-access controls when finished. After a mutation timeout, [recover the original command](#command-recovery-and-uncertain-outcomes).

---

## Your first .NET client

Website: https://streaminghub.tools/docs/api-client/first-dotnet-client

Updated: 2026-10-25.

Verified with: Release component versions pending verification; do not infer package versions from HTTP v1.

This console application uses only public Client/Contracts libraries. It requests `host.status`, waits for user approval and prints the connected Host identity. It works even when Core has no open project.

### Add the client library

Create the app outside the Streaming Hub checkout:

```powershell
dotnet new console --framework net8.0 --name MyStreamingHubClient
cd MyStreamingHubClient
```

Obtain matching Client/Contracts `.nupkg` files from the API Client archive of the preview package you are evaluating. Extract them into a local package directory. Replace both placeholders with that package set's exact values:

```powershell
$packageVersion = 'REPLACE_WITH_EXACT_PACKAGE_VERSION'
$packageDirectory = 'C:\StreamingHubApiPackages'
dotnet add package StreamingHub.Api.Client --version $packageVersion --source $packageDirectory
```

This does not imply publication on a public feed. When developing against this repository instead, add a project reference to `StreamingHub.Api.Client/StreamingHub.Api.Client.csproj`; Contracts is its dependency. Do not reference CoreHost or Editor.

### Replace Program.cs

```csharp
using StreamingHub.Api.Client;
using StreamingHub.Api.Contracts;

using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(110));
Console.CancelKeyPress += (_, e) => {
    e.Cancel = true;
    deadline.Cancel();
};
var token = deadline.Token;
const string clientId = "example.my-streaminghub-client";
string[] scopes = ["host.status"];

try
{
    var discovery = await ApiHostDiscovery.ReadAsync(
        args.Length > 0 ? args[0] : null, token);
    using var client = new StreamingHubApiClient(discovery);
    var request = await client.RequestPairingAsync(
        new PairingRequestDto(clientId, "My Streaming Hub client",
            "Local example (claimed)", scopes), token);

    Console.WriteLine("Approve My Streaming Hub client in the Core tray.");
    PairingResult result;
    do
    {
        result = await client.PollPairingAsync(request, token);
        if (result.Status == "pending")
            await Task.Delay(500, token);
    } while (result.Status == "pending");

    if (result.Credential is not { } credential)
    {
        Console.WriteLine($"Pairing ended: {result.Status}");
        return;
    }

    await client.ConnectWithCredentialAsync(
        clientId, credential.Secret, scopes, token);
    var host = await client.GetHostAsync(token);
    Console.WriteLine($"Connected to Core {host.HostInstanceId}");
}
catch (ApiClientException error)
{
    Console.Error.WriteLine(error.Code);
}
catch (OperationCanceledException)
{
    Console.Error.WriteLine("Cancelled or timed out.");
}
catch (HttpRequestException)
{
    Console.Error.WriteLine("Core could not be reached. Check discovery and Core status.");
}
```

The example keeps the credential in memory for this run. A returning application should store it in protected local storage, reuse its stable client ID and reconnect with the same credential. A new pairing creates a new principal with a separate command recovery context.

### Run

With Core and Tray running:

```powershell
dotnet run
```

For a custom data directory:

```powershell
dotnet run -- C:\StreamingHubApiTest\api-discovery.json
```

Approve only `host.status` in Tray. The console prints Host identity. Denial, timeout and incompatibility produce an explicit result.

### Extend your application

Request `project.read` too, obtain approval for those rights, then call `GetCompleteSnapshotAsync(ct: token)` once a project is open. Use the returned identity and revision when [building a change](#partial-changes-and-conflicts). Renewal cannot add scopes to an existing grant.

Keep network work cancellable and [observe Core](#streams-lifecycle-and-compatibility) without blocking your interface. The [repository example](#console-examples-and-manual-test-workflow) adds open, switch, rename, run and Save.

---

## Partial changes and conflicts

Website: https://streaminghub.tools/docs/api-client/partial-changes-and-conflicts

Updated: 2026-10-25.

Verified with: Release component versions pending verification; do not infer package versions from HTTP v1.

A mutation binds to Host identity, project identity and a base revision. Read a coherent snapshot and find the intended resource before building a ChangeSet.

### Edit a macro name

This fragment assumes an approved, connected client, a selected `macroId` and a cancellation token:

```csharp
using System.Text.Json;
using StreamingHub.Api.Client;

var snapshot = await client.GetCompleteSnapshotAsync(ct: token);
var command = new ChangeSetDraft(snapshot)
    .SetMacro(macroId, "Name", JsonSerializer.SerializeToElement("Updated name"))
    .Build();
await client.ValidateAsync(command, token);
var result = await client.ApplyAsync(command, token);
```

Select a macro by its stable resource ID, not display text. With `project.read`, call `GetResourcesAsync("macro", ct: token)` and follow `NextPageToken` where present. Read the selected resource before editing. Changes require `project.write`; reading content-bearing configuration can additionally require `project.configuration.read`.

Validate is a dry run, not a reservation. Another client can commit before Apply. On `revision_conflict`, retain the draft, refresh and show the differences. Do not silently rewrite its base revision.

### Parameters and resources

Use step addresses read from the graph and field IDs from the current schema. A step path is revision-relative, not a permanent ID. Batch compatible semantic operations for atomic publication.

Plugin entity/settings writes use their own resource versions. Detached validation must not mutate live plugin state. An active run keeps the configuration captured for it; an edit affects later accepted work.

Undo is a new compensation command against current state. It can conflict, does not reverse external effects and is not a persistent undo history across Host restarts.

### Run and save separately

Applying an edit changes Core's active definitions. It does not implicitly start an automation or save the project file. With the respective approved scopes, these fragments start a selected automation and track Save:

```csharp
// Requires project.read, runtime.control and runtime.read.
var snapshot = await client.GetCompleteSnapshotAsync(ct: token);
var start = new RuntimeStartCommand(Guid.NewGuid(), snapshot.Identity, automationId);
var acceptedRun = await client.StartRunAsync(start, token);
if (acceptedRun.RunId is { } runId)
{
    var run = await client.WaitForRunAsync(runId, ct: token);
    Console.WriteLine(run.Status);
}
```

```csharp
// Requires project.read and project.files. Core must have an open project.
var snapshot = await client.GetCompleteSnapshotAsync(ct: token);
var save = new ProjectFileCommand(Guid.NewGuid(),
    snapshot.Identity.HostInstanceId, snapshot.Identity.ProjectId);
var acceptedSave = await client.SaveAsync(save, token);
var operation = await client.WaitForOperationAsync(acceptedSave.OperationId, ct: token);
Console.WriteLine(operation.Status);
```

These fragments also need `StreamingHub.Api.Contracts`. Inspect terminal status and safe errors rather than treating acceptance as completion. `waiting_for_interaction` needs an authorized responder. Running an automation can perform external effects. Keep each command ID before sending it and use [command recovery](#command-recovery-and-uncertain-outcomes) if the response becomes uncertain.

---

## Command recovery and uncertain outcomes

Website: https://streaminghub.tools/docs/api-client/command-recovery

Updated: 2026-10-25.

Verified with: Release component versions pending verification; do not infer package versions from HTTP v1.

Assign one command ID to a logical mutation and freeze its body. Reusing the ID for different content is rejected.

### A response was lost

The Client's supported project/file/runtime helpers do not repeat a mutation POST automatically. They attempt bounded lookup of its original ID. An unknown result exposes `ApiCommandOutcomeUnknownException.CommandId`; caller cancellation can expose `ApiCommandCanceledException` without proving Core cancelled the command.

Reconnect with the same approved credential/principal and inspect `GetCommandResultAsync(originalCommandId, token)`. Continue polling an existing operation rather than starting another one. Update methods have their own explicit lookup workflow.

Persist a journal of command IDs and frozen payloads before transmission when your application needs recovery across its own restarts. Store the approved credential separately using protected storage. A new command ID represents new work, not a retry marker.

For HTTP clients, use `GET /api/v1/commands/{commandId}` for the supported project/file/runtime/settings families, `GET /api/v1/runtime/commands/{commandId}` for runtime starts, or `GET /api/v1/updates/commands/{commandId}` for update commands. Lookup is authorized for that operation's family and principal. A `runtime.control` grant alone does not authorize runtime lookup; include `runtime.read`.

If a command lookup identifies a file operation or run, continue tracking that existing ID. An unknown or evicted result is not evidence that nothing happened. Refresh state and let the user decide how to proceed when external effects cannot be established.

### Retention limits

Command results are bounded and tied to the running Host. Expiry, eviction or Host restart can make lookup unknown. Reconcile actual state before deciding whether to issue a new action.

There is no general exactly-once guarantee for external effects. A timeout can occur after an external action was accepted.

Run history and a durable installation result are separate from command caches. Retaining a run does not restore an old session or grant permission to replay a mutation.

---

## Streams, lifecycle and compatibility

Website: https://streaminghub.tools/docs/api-client/streams-and-lifecycle

Updated: 2026-10-25.

Verified with: Release component versions pending verification; do not infer package versions from HTTP v1.

Streams carry bounded observations and cursors. They do not make a client an authoritative owner of Core's state.

### Observe

Use `ObserveProjectAsync` for a complete snapshot followed by its cursor-bound stream. The lower-level subscription methods accept a cursor read from the corresponding snapshot or prior event.

Handle `subscribed`, `resync_required`, diagnostic gaps and explicit errors. After a replay gap, fetch a new snapshot. Preserve and compare pending UI drafts instead of silently replacing them with the refreshed model.

### WebSocket protocol for other languages

Use the discovered endpoint with scheme `ws` and path `/api/v1/streams`. Authenticate the upgrade with the negotiated session in the `Authorization: Bearer <accessToken>` header. Do not use a token query parameter. After connecting, send a UTF-8 JSON text subscription:

```json
{
  "type": "subscribe",
  "stream": "project",
  "cursor": "opaque-cursor-from-the-project-snapshot"
}
```

Use the actual cursor returned by Core. Never parse, increment or invent it. `project` uses the project snapshot cursor; `shell`, `runtime`, `status`, `diagnostics` and `logs` each have their own snapshot endpoint and permission. One stream's cursor cannot be reused for another.

Read control frames (`subscribed`, `resync_required`, `gap`, `error`) and event envelopes with stream, event type, Host/project identity and cursor. Inspect `ProjectStreamSubscribeDto`, `ProjectStreamControlDto`, `ProjectStreamEventDto` and `HostStreamEventDto` through the [live schemas](#http-routes-and-net-client-reference) for exact fields. The normal client defaults to a 2 MiB maximum message; respect Core's bounded retention and limits too.

Store the last consumed cursor only after processing its event. After a gap, discard that observation cache, obtain a new coherent snapshot and subscribe from its cursor. Do not apply events from a previous Host/project to the new state.

### .NET observation helpers

`SubscribeProjectAsync`, `SubscribeShellAsync`, `SubscribeRuntimeAsync`, `SubscribeStatusAsync`, `SubscribeDiagnosticsAsync` and `SubscribeLogsAsync` expose explicit controls and events. `ApiStreamMessage.RequiresSnapshot` identifies a gap or resync request. A completed enumeration does not silently renew approval or replace UI drafts.

For coordinated project, shell and runtime observations, `ApiConnection` accepts your reconnect factory. It reads coherent snapshots and moves through `Connecting`, `Syncing`, `Ready`, `Lost` and `Incompatible`. The factory should read fresh discovery and reconnect with an approved credential using `host.status`, `project.read` and `runtime.read`. Your application owns credential storage and renewal policy.

### Lifecycle

A Core restart replaces Host identity and invalidates old sessions/cursors. A project switch replaces project identity. Stop issuing edits against the previous identity and read current state before continuing.

Dispose hidden/closed observations and join cancellation. A slow client must not stall automation execution; bounded retention means it may need to resynchronize.

Handshake checks contract major, capabilities and permissions. Missing scopes or an incompatible client should produce a clear refusal rather than fallback to a weaker private interface.

See [Pairing](#connection-pairing-and-scopes) and [Command recovery](#command-recovery-and-uncertain-outcomes).

---

## Installation updates and client preparation

Website: https://streaminghub.tools/docs/api-client/installation-updates

Updated: 2026-10-25.

Verified with: Release component versions pending verification; do not infer package versions from HTTP v1.

Core owns one shared installation-update operation. API access to updates is separate from project/settings access and from plugin execution trust.

### Review and stage

Use `updates.read` to inspect the installation and operation. With `updates.manage`, check approved sources, inspect supported legacy packages, and plan the complete compatible component set.

Display required components, publisher evidence, unsigned-package acknowledgements and new permissions. Approval binds the exact plan and current operation/registry revision; changing the plan invalidates old consent.

Poll package progress and operation state. An accepted command is not the same as a completed installation. After an uncertain update response, recover the original command through the update lookup rather than repeating a new POST.

### Prepare a native client

`updates.prepare` permits registration and readiness responses; it does not grant update-management authority. Use `ApiUpdateParticipant` with callbacks that durably preserve supported drafts, freeze edits and handle release/close requests.

Respond only to the exact Host/operation/preparation/plan/client round. A readiness receipt is not a request to upload draft contents or local checkpoint paths. Refusal, expiry, unsupported drafts or Save failure can postpone installation.

Approved installation can continue after prepared clients close. Reconnect to the new Host and read the recorded outcome; do not reuse old sessions or command cursors.

See [User update recovery](https://streaminghub.tools/docs/0.2/diagnostics/update-recovery).

---

## Console examples and manual test workflow

Website: https://streaminghub.tools/docs/api-client/examples

Updated: 2026-10-25.

Verified with: Release component versions pending verification; do not infer package versions from HTTP v1.

The repository console example connects using public Client/Contracts references. It demonstrates user pairing, coherent reads, a partial edit, runtime execution and tracked Save.

For raw HTTP use [PowerShell testing](#manual-http-testing-in-powershell). To create a separate app use [your first .NET client](#your-first-net-client).

### Run the repository example

Start Core and Tray, and open a copied test project. From the checkout root:

```powershell
$discovery = Join-Path $env:LOCALAPPDATA 'StreamingHub\runtime\api-discovery.json'
dotnet run --project Examples/ApiClients/StreamingHub.Api.Example -- $discovery status
```

For `--data-dir`, set `$discovery` to that directory's `api-discovery.json`. Approve **API console example** in Tray. The console prints Host/project identity, revision and automation IDs.

### Supported commands

Replace sample paths and IDs with your test project and selected resources:

```powershell
dotnet run --project Examples/ApiClients/StreamingHub.Api.Example -- $discovery open 'C:\StreamingHubApiTest\Test.shubproj'
dotnet run --project Examples/ApiClients/StreamingHub.Api.Example -- $discovery switch 'C:\StreamingHubApiTest\Other.shubproj'
dotnet run --project Examples/ApiClients/StreamingHub.Api.Example -- $discovery rename-macro '<macro-id>' 'API test name'
dotnet run --project Examples/ApiClients/StreamingHub.Api.Example -- $discovery run '<automation-id>'
dotnet run --project Examples/ApiClients/StreamingHub.Api.Example -- $discovery save
```

Use `open` when Core has no project and `switch` when it does. A dirty project can refuse switching. Choose a known macro ID for rename; `status` lists automation IDs only. An approved client can discover macros with the resource-list route in the reference.

`rename-macro` applies the change but does not save it; `save` is separate. `run` executes configured actions, which may contact external services. Choose a harmless test automation.

Every invocation requests only its command's scopes and keeps credentials in memory. Each invocation pairs again, so the sample is not a persistent interactive CLI or command journal.

### Test failure handling

| Scenario | Expected client behavior |
| --- | --- |
| Deny pairing | Report denial without reading project data |
| Cancel a wait | Stop waiting without claiming Host work was cancelled |
| Request an ungranted capability | Report the refusal |
| Apply against an old revision | Preserve the draft and report a conflict |
| Restart Core | Read discovery and negotiate again |
| Lose a mutation response | Keep the original command ID and principal for lookup |

The deadline is 110 seconds. Pending password/trust interactions need an authorized responder. `--verify` belongs to automated disposable fixtures and edits/runs/saves predefined resources; it is not a manual command for a real project.

See [partial changes](#partial-changes-and-conflicts), [recovery](#command-recovery-and-uncertain-outcomes) and [streams](#streams-lifecycle-and-compatibility) when adapting this example.

---

## HTTP routes and .NET client reference

Website: https://streaminghub.tools/docs/api-client/api-and-client-methods

Updated: 2026-10-25.

Verified with: Release component versions pending verification; do not infer package versions from HTTP v1.

All paths below are relative to the current discovered endpoint. After [pairing and handshake](#connection-pairing-and-scopes), send the session token as `Authorization: Bearer <accessToken>`. Requests and responses use JSON with camelCase field names.

### Live contract and schemas

With `host.status`, read `GET /api/v1/openapi.json` for the running Core's methods and `GET /api/v1/schemas/{name}` for a named DTO. In .NET use `GetOpenApiAsync` and `GetSchemaAsync`. `GET /api/v1/examples/{name}` / `GetExampleAsync` exposes supported named examples.

The route table is checked against the repository's public `ApiSurface.Endpoints` and `ApiClientCoverage.Methods`. Request and response names identify public Contracts types; query parameters and full field definitions are described by the live OpenAPI and named schemas. Generate or validate non-.NET payloads against that running contract. [PowerShell testing](#manual-http-testing-in-powershell) shows how to retrieve it.

### Route table

`GetFileLocationsAsync` lists approved logical folder IDs, names, operation rights and flags for the current project. Machine paths are excluded. Folder approval, rebinding and revocation belong to the trusted Core Settings window; `settings.write`, `project.write` and runtime scopes cannot modify those local approvals. Catalog IDs beginning `Core.Files.` require the project's separate folder permissions when executed.

`GetFileDirectoryAsync(locationId)` requires `project.files` plus the location's local List and inspect permission. It supplies bounded relative file/folder metadata for path selection, including empty folders, with no content or absolute paths. Subfolder permissions, truncation and permission-revision checks remain enforced.

`—` means no typed body/schema. `public`, `session`, `pipe ticket` and `stream-specific` describe authorization stages, not grantable scopes. Comma-separated permissions are required together; command lookup authorizes the specific command family, not every scope shown as an alternative.

`202` accepts work; inspect the returned operation/run until completion. `101` is a WebSocket upgrade. The retired layout routes return `editor_layout_local`; store layout in your client. Named Pipe bootstrap is an internal local-launch path with only status access and is not a substitute for external-client pairing.

| HTTP route | Permission / authorization | Request | Response | Success | .NET method |
| --- | --- | --- | --- | --- | --- |
| `GET /api/v1/plugins/trust` | `plugins.trust.read` | `—` | `PluginTrustSnapshotDto` | 200 | `GetPluginTrustAsync` |
| `POST /api/v1/plugins/trust` | `plugins.trust.manage` | `PluginTrustCommand` | `PluginTrustCommandResult` | 200 | `SetPluginTrustAsync` |
| `GET /api/v1/plugins/trust/commands/{commandId}` | `plugins.trust.manage` | `—` | `PluginTrustCommandResult` | 200 | `GetPluginTrustCommandAsync` |
| `GET /api/v1/components/catalog` | `components.read` | `—` | `ComponentCatalogDto` | 200 | `GetComponentCatalogAsync` |
| `POST /api/v1/components/plan` | `components.install` | `ComponentCatalogPlanCommand` | `UpdateCommandResultDto` | 200 | `PlanCatalogInstallationAsync` |
| `GET /api/v1/updates` | `updates.read` | `—` | `UpdateStateDto` | 200 | `GetUpdatesAsync` |
| `GET /api/v1/updates/operations/{operationId}` | `updates.read` | `—` | `UpdateOperationDto` | 200 | `GetUpdateOperationAsync` |
| `GET /api/v1/updates/commands/{commandId}` | `updates.manage` | `—` | `UpdateCommandResultDto` | 200 | `GetUpdateCommandAsync` |
| `POST /api/v1/updates/check` | `updates.manage` | `UpdateCheckCommand` | `UpdateCommandResultDto` | 200 | `CheckUpdatesAsync` |
| `POST /api/v1/updates/plan` | `updates.manage` | `UpdatePlanCommand` | `UpdateCommandResultDto` | 200 | `PlanUpdatesAsync` |
| `POST /api/v1/updates/inspect` | `updates.manage` | `UpdateInspectCommand` | `UpdateCommandResultDto` | 200 | `InspectUpdatePackagesAsync` |
| `POST /api/v1/updates/approve` | `updates.manage` | `UpdateApproveCommand` | `UpdateCommandResultDto` | 200 | `ApproveUpdatesAsync` |
| `POST /api/v1/updates/install` | `updates.manage` | `UpdateInstallCommand` | `UpdateCommandResultDto` | 200 | `InstallUpdatesAsync` |
| `POST /api/v1/updates/cancel` | `updates.manage` | `UpdateCancelCommand` | `UpdateCommandResultDto` | 200 | `CancelUpdateAsync` |
| `POST /api/v1/updates/prepare` | `updates.manage` | `UpdatePrepareCommand` | `UpdateCommandResultDto` | 200 | `PrepareUpdateClientsAsync` |
| `POST /api/v1/updates/maintenance` | `updates.manage` | `UpdateMaintenanceCommand` | `UpdateCommandResultDto` | 200 | `BeginUpdateMaintenanceAsync` |
| `POST /api/v1/updates/shutdown` | `updates.manage` | `UpdateShutdownCommand` | `UpdateCommandResultDto` | 200 | `CommitUpdateShutdownAsync` |
| `POST /api/v1/updates/clients/refuse-close` | `updates.prepare` | `UpdateCloseRefusal` | `UpdateClientDto` | 200 | `RefuseUpdateCloseAsync` |
| `POST /api/v1/updates/clients/closed` | `updates.prepare` | `UpdateClientCloseCommand` | `UpdateClientClosedDto` | 200 | `CompleteUpdateCloseAsync` |
| `POST /api/v1/updates/clients/register` | `updates.prepare` | `UpdateClientRegistration` | `UpdateClientDto` | 200 | `RegisterUpdateClientAsync` |
| `POST /api/v1/updates/clients/respond` | `updates.prepare` | `UpdateClientResponse` | `UpdateClientDto` | 200 | `RespondUpdateClientAsync` |
| `DELETE /api/v1/updates/clients/{clientId}` | `updates.prepare` | `—` | `UpdateClientClosedDto` | 200 | `UnregisterUpdateClientAsync` |
| `GET /api/v1/projects/active/files/locations` | `settings.read` | `—` | `FileLocationsDto` | 200 | `GetFileLocationsAsync` |
| `GET /api/v1/projects/active/files/directory` | `project.files` | `—` | `FileLocationDirectoryDto` | 200 | `GetFileDirectoryAsync` |
| `GET /api/v1/projects/active/context/roots` | `project.files` | `—` | `ContextRootsDto` | 200 | `GetContextRootsAsync` |
| `GET /api/v1/projects/active/context/directory` | `project.files` | `—` | `ContextDirectoryDto` | 200 | `GetContextDirectoryAsync` |
| `GET /api/v1/projects/active/context/text` | `project.files` | `—` | `ContextTextDto` | 200 | `GetContextTextAsync` |
| `POST /api/v1/projects/active/context/commands` | `project.files` | `ContextFileCommand` | `ContextFileResultDto` | 200 | `SendContextFileCommandAsync` |
| `POST /api/v1/projects/create` | `project.files` | `ProjectFileCommand` | `ProjectFileAcceptedDto` | 202 | `CreateProjectAsync` |
| `GET /api/v1/projects/active/persistence` | `project.read` | `—` | `ProjectPersistenceStatusDto` | 200 | `GetPersistenceAsync` |
| `POST /api/v1/projects/active/autosave` | `project.files` | `AutosaveCommand` | `ProjectPersistenceStatusDto` | 200 | `ConfigureAutosaveAsync` |
| `GET /api/v1/projects/active/layout` | `project.layout.read` | `—` | `ProjectLayoutDto` | — | `GetLayoutAsync` |
| `POST /api/v1/projects/active/layout` | `project.layout.write` | `ProjectLayoutCommand` | `ProjectLayoutDto` | — | `SaveLayoutAsync` |
| `GET /api/v1/runtime/runs/{runId}/inspector` | `inspector.read` | `—` | `RuntimeInspectorDto` | 200 | `GetInspectorAsync` |
| `GET /api/v1/runtime/history` | `history.read` | `—` | `RunHistoryPageDto` | 200 | `GetRunHistoryAsync` |
| `GET /api/v1/runtime/history/{runId}` | `history.read` | `—` | `RunHistoryDetailDto` | 200 | `GetRunHistoryDetailAsync` |
| `GET /api/v1/runtime/history/settings` | `history.read` | `—` | `RunHistoryStatusDto` | 200 | `GetRunHistorySettingsAsync` |
| `POST /api/v1/runtime/history/settings` | `history.manage` | `RunHistorySettingsCommand` | `RunHistoryStatusDto` | 200 | `ConfigureRunHistoryAsync` |
| `POST /api/v1/runtime/history/clear` | `history.manage` | `RunHistoryClearCommand` | `RunHistoryStatusDto` | 200 | `ClearRunHistoryAsync` |
| `GET /api/v1/interactions` | `interaction.read` | `—` | `InteractionPageDto` | 200 | `GetInteractionsAsync` |
| `POST /api/v1/interactions/claim` | `interaction.respond` | `InteractionClaimCommand` | `InteractionResultDto` | 200 | `ClaimInteractionAsync` |
| `POST /api/v1/interactions/release` | `interaction.respond` | `InteractionClaimCommand` | `InteractionResultDto` | 200 | `ReleaseInteractionAsync` |
| `POST /api/v1/interactions/respond` | `interaction.respond` | `InteractionResponseCommand` | `InteractionResultDto` | 200 | `RespondInteractionAsync` |
| `POST /api/v1/pairing/requests` | `public` | `PairingRequestDto` | `PairingStart` | 202 | `RequestPairingAsync` |
| `POST /api/v1/pairing/requests/status` | `public` | `PairingStatusRequestDto` | `PairingResult` | 200 | `PollPairingAsync` |
| `POST /api/v1/credentials/exchange` | `public` | `CredentialExchangeRequestDto` | `CredentialSessionDto` | 200 | `ConnectWithCredentialAsync` |
| `POST /api/v1/bootstrap/exchange` | `pipe ticket` | `TicketRequest` | `CredentialSessionDto` | 200 | `ConnectWithBootstrapAsync` |
| `POST /api/v1/handshake` | `session` | `HandshakeRequest` | `HandshakeResponseDto` | 200 | `ConnectWithCredentialAsync, ConnectWithBootstrapAsync` |
| `GET /api/v1/host` | `host.status` | `—` | `HostInfoDto` | 200 | `GetHostAsync` |
| `POST /api/v1/host/shutdown` | `host.shutdown` | `HostShutdownCommand` | `RuntimeControlResultDto` | 200 | `ShutdownHostAsync` |
| `GET /api/v1/openapi.json` | `host.status` | `—` | `JsonElement` | 200 | `GetOpenApiAsync` |
| `GET /api/v1/schemas/{name}` | `host.status` | `—` | `JsonElement` | 200 | `GetSchemaAsync` |
| `GET /api/v1/examples/{name}` | `host.status` | `—` | `JsonElement` | 200 | `GetExampleAsync` |
| `GET /api/v1/streams/shell/snapshot` | `host.status` | `—` | `HostStreamSnapshotDto` | 200 | `GetShellSnapshotAsync` |
| `GET /api/v1/streams/runtime/snapshot` | `runtime.read` | `—` | `HostStreamSnapshotDto` | 200 | `GetRuntimeSnapshotAsync` |
| `GET /api/v1/streams` | `stream-specific` | `—` | `—` | 101 | `SubscribeProjectAsync, SubscribeStatusAsync, SubscribeDiagnosticsAsync, SubscribeShellAsync, SubscribeRuntimeAsync` |
| `GET /api/v1/streams/status/snapshot` | `host.status` | `—` | `HostStreamSnapshotDto` | 200 | `GetStatusSnapshotAsync` |
| `GET /api/v1/streams/logs/snapshot` | `diagnostics.content.read` | `—` | `HostStreamSnapshotDto` | 200 | `GetLogsSnapshotAsync` |
| `GET /api/v1/streams/diagnostics/snapshot` | `diagnostics.read` | `—` | `HostStreamSnapshotDto` | 200 | `GetDiagnosticsSnapshotAsync` |
| `GET /api/v1/projects/active/snapshot` | `project.read` | `—` | `ProjectSnapshotDto` | 200 | `GetSnapshotAsync` |
| `GET /api/v1/projects/active/resources/{kind}` | `project.read` | `—` | `ProjectResourcePageDto` | 200 | `GetResourcesAsync` |
| `GET /api/v1/projects/active/resources/{kind}/{id}` | `project.read` | `—` | `ProjectResourceDto` | 200 | `GetResourceAsync` |
| `GET /api/v1/projects/active/configuration/{kind}` | `project.configuration.read` | `—` | `ProjectResourcePageDto` | 200 | `GetConfigurationAsync` |
| `GET /api/v1/projects/active/graph/{kind}/{id}` | `project.read` | `—` | `ProjectGraphDto` | 200 | `GetGraphAsync` |
| `POST /api/v1/projects/active/undo` | `project.write` | `UndoCommand` | `ChangeSetResult` | 200 | `UndoAsync` |
| `GET /api/v1/projects/active/references` | `project.read` | `—` | `AvailableReferencePageDto` | 200 | `GetReferencesAsync` |
| `GET /api/v1/projects/active/plugins/{pluginId}/settings` | `project.read` | `—` | `PluginResourceDto` | 200 | `GetPluginSettingsAsync` |
| `GET /api/v1/projects/active/plugins/{pluginId}/entities/{entityTypeId}` | `project.read` | `—` | `PluginEntityCollectionDto` | 200 | `GetPluginEntitiesAsync` |
| `GET /api/v1/projects/active/plugin-editors` | `project.read` | `—` | `PluginEditorInventoryDto` | 200 | `GetPluginEditorsAsync` |
| `GET /api/v1/plugins/{pluginId}/local-settings` | `settings.read` | `—` | `PluginLocalSettingsDto` | 200 | `GetLocalSettingsAsync` |
| `POST /api/v1/plugins/local-settings` | `settings.write` | `PluginLocalSettingCommand` | `PluginSettingsOperationDto` | 200 | `SetLocalSettingAsync` |
| `POST /api/v1/plugins/local-settings/batch` | `settings.write` | `PluginLocalSettingsBatchCommand` | `PluginSettingsOperationDto` | 200 | `SetLocalSettingsBatchAsync` |
| `GET /api/v1/plugins/inventory` | `settings.read` | `—` | `HostPluginInventoryDto` | 200 | `GetHostPluginsAsync` |
| `POST /api/v1/plugins/settings-actions` | `settings.write` | `PluginSettingsActionCommand` | `PluginSettingsOperationDto` | 200 | `StartSettingsActionAsync` |
| `GET /api/v1/plugins/settings-operations/{operationId}` | `settings.read` | `—` | `PluginSettingsOperationDto` | 200 | `GetSettingsOperationAsync` |
| `POST /api/v1/plugins/settings-operations/authorization` | `settings.write` | `PluginSettingsOperationCommand` | `PluginAuthorizationDto` | 200 | `ClaimSettingsAuthorizationAsync` |
| `POST /api/v1/plugins/entities/export` | `project.configuration.read` | `PluginEntityExportCommand` | `PluginEntityExportDto` | 200 | `ExportPluginEntitiesAsync` |
| `POST /api/v1/plugins/settings-operations/response` | `settings.write` | `PluginClientResponseCommand` | `PluginSettingsOperationDto` | 200 | `RespondSettingsInteractionAsync` |
| `POST /api/v1/plugins/settings-operations/cancel` | `settings.write` | `PluginSettingsOperationCommand` | `PluginSettingsOperationDto` | 200 | `CancelSettingsOperationAsync` |
| `GET /api/v1/catalog` | `catalog.read` | `—` | `CatalogPageDto` | 200 | `GetCatalogAsync` |
| `GET /api/v1/catalog/{id}` | `catalog.read` | `—` | `CatalogSchemaDto` | 200 | `GetCatalogSchemaAsync` |
| `POST /api/v1/projects/active/editor-options` | `catalog.read, project.configuration.read, settings.read` | `EditorOptionsRequest` | `EditorOptionsDto` | 200 | `GetEditorOptionsAsync` |
| `POST /api/v1/projects/active/plugin-entity-form` | `project.read, project.configuration.read` | `PluginEntityFormRequest` | `PluginEntityFormDto` | 200 | `GetPluginEntityFormAsync` |
| `POST /api/v1/projects/active/changesets/validate` | `project.write` | `ChangeSetCommand` | `ChangeSetResult` | 200 | `ValidateAsync` |
| `POST /api/v1/projects/active/changesets` | `project.write` | `ChangeSetCommand` | `ChangeSetResult` | 200 | `ApplyAsync` |
| `GET /api/v1/commands/{commandId}` | `project.write \| project.files \| runtime.read \| settings.write` | `—` | `ApiCommandLookupDto` | 200 | `GetCommandResultAsync` |
| `GET /api/v1/operations/{operationId}` | `project.files` | `—` | `ProjectFileOperationDto` | 200 | `GetOperationAsync` |
| `POST /api/v1/runtime/values` | `runtime.control` | `RuntimeValueCommand` | `RuntimeControlResultDto` | 200 | `SetRuntimeValueAsync` |
| `GET /api/v1/runtime/values` | `runtime.values.read` | `—` | `RuntimeObservationSnapshotDto` | 200 | `GetRuntimeValuesAsync` |
| `GET /api/v1/runtime/status` | `runtime.read` | `—` | `RuntimeStatusDto` | 200 | `GetRuntimeStatusAsync` |
| `POST /api/v1/runtime/signals/sample` | `diagnostics.content.read` | `SignalLiveSampleRequest` | `SignalLiveSampleDto` | 200 | `GetSignalLiveSampleAsync` |
| `POST /api/v1/runtime/hotkeys/capture` | `runtime.control` | `HotkeyCaptureCommand` | `HotkeyCaptureDto` | 200 | `CaptureHotkeyAsync` |
| `POST /api/v1/runtime/runs` | `runtime.control` | `RuntimeStartCommand` | `RuntimeCommandResult` | 202 | `StartRunAsync` |
| `GET /api/v1/runtime/test-chains` | `runtime.read` | `—` | `TestChainRuntimeDto` | 200 | `GetTestChainRuntimeAsync` |
| `POST /api/v1/runtime/test-chains` | `runtime.control` | `TestChainStartCommand` | `RuntimeCommandResult` | 202 | `StartTestChainAsync` |
| `POST /api/v1/runtime/test-chains/{runId}/stop` | `runtime.control` | `RuntimeControlCommand` | `RuntimeControlResultDto` | 200 | `StopTestChainAsync` |
| `GET /api/v1/runtime/runs/{runId}` | `runtime.read` | `—` | `RuntimeRunStatusDto` | 200 | `GetRunAsync` |
| `GET /api/v1/runtime/commands/{commandId}` | `runtime.read` | `—` | `RuntimeCommandResult` | 200 | `GetRuntimeCommandResultAsync` |
| `POST /api/v1/projects/active/save` | `project.files` | `ProjectFileCommand` | `ProjectFileAcceptedDto` | 202 | `SaveAsync` |
| `POST /api/v1/projects/open` | `project.files` | `ProjectFileCommand` | `ProjectFileAcceptedDto` | 202 | `OpenAsync` |
| `POST /api/v1/projects/active/switch` | `project.files` | `ProjectFileCommand` | `ProjectFileAcceptedDto` | 202 | `SwitchAsync` |
| `POST /api/v1/projects/active/import` | `project.files` | `ProjectFileCommand` | `ProjectFileAcceptedDto` | 202 | `ImportAsync` |
| `POST /api/v1/projects/active/export` | `project.files` | `ProjectFileCommand` | `ProjectFileAcceptedDto` | 202 | `ExportAsync` |
| `POST /api/v1/runtime/runs/{runId}/stop` | `runtime.control` | `RuntimeControlCommand` | `RuntimeControlResultDto` | 200 | `StopRunAsync` |
| `POST /api/v1/runtime/automations/stop` | `runtime.control` | `RuntimeControlCommand` | `RuntimeControlResultDto` | 200 | `StopAutomationsAsync` |
| `POST /api/v1/runtime/stop` | `runtime.control` | `RuntimeControlCommand` | `RuntimeControlResultDto` | 200 | `StopRuntimeAsync` |
| `POST /api/v1/runtime/resume` | `runtime.control` | `RuntimeControlCommand` | `RuntimeControlResultDto` | 200 | `ResumeRuntimeAsync` |

### Read coherent state

`GetCompleteSnapshotAsync` follows snapshot pages while pinning Host/project identity, revision and cursor. Resource lists use `pageToken`, `pageSize` and optional search where supported. Encode path segments and query values; do not treat a resource's display name as its ID.

Ordinary `project.read` responses omit content-bearing configuration. Additional `project.configuration.read` can be required for definitions and editor forms; protected fields remain redacted. Dynamic options require `catalog.read`, `project.configuration.read` and `settings.read` together. Context file operations require `project.files`.

Project revision, plugin resource version, local settings version and runtime observations are distinct. Read the relevant version before a write. A runtime variable observation is not an edit to its saved default.

### Operations, recovery and limits

`WaitForOperationAsync` waits for a terminal file outcome or `waiting_for_interaction`. An authorized interaction responder must claim/respond where required; this is not implicit permission to approve passwords or trust. `WaitForRunAsync` waits for a run's terminal state. Cancelling a wait does not cancel Core work.

The default .NET client bounds responses to 2 MiB and HTTP calls to 15 seconds. Core also enforces endpoint/body/page/stream limits and rate limits; handle explicit refusal instead of interpreting a truncated observation as complete state.

Handle `revision_conflict`, `schema_changed`, stale identity, incompatible contracts, revoked/expired access and unknown command outcomes separately. See [partial edits](#partial-changes-and-conflicts), [command recovery](#command-recovery-and-uncertain-outcomes), [streams](#streams-lifecycle-and-compatibility) and [installation cooperation](#installation-updates-and-client-preparation).
