Integrate
Calling the API directly
Every Troupe operation is an HTTP request that you can send yourself, from any language. This page covers the REST and RPC paths, authentication, the contract header and errors.
In TypeScript, the client SDK does all of this for you.
One contract, three bindings
The runtime's contract has 64 procedures in 14 groups. Each has an id, <group>.<operation>, and every
other name follows from it:
| Binding | Rule | For input.doubleTap |
|---|---|---|
| REST | /v1/<group>/<operation in kebab-case> | POST /v1/input/double-tap |
| RPC | /rpc/v2/<group>/<operation> | /rpc/v2/input/doubleTap |
| MCP tool | The id with _ for the dot | input_doubleTap |
The reference has a page per group, with each procedure's route, scopes, input, output and errors. A local runtime and Troupe Cloud serve the same contract:
| Server | Address |
|---|---|
| A local runtime | The url in $TROUPE_HOME/runtime.json: http://127.0.0.1:<port> |
| Troupe Cloud | https://api.troupe.ali.ve |
REST
- Method.
GETwhen the procedure only reads and its input is flat: scalars, enums and arrays of scalars.POSTotherwise. No other method is used. - Input. A
GETtakes its input as query parameters, with arrays in bracket notation (sources[0]=app). APOSTtakes it as a JSON body. There are no path parameters: ids travel in the input. - Output. JSON, except for two kinds. A binary procedure, such as
media.screenshot, answers with the raw bytes and theirContent-Type. An event iterator, such aslogs.stream, answers withtext/event-stream.
Read the address and the owner token of a local runtime, then call it:
URL=$(jq -r .url ~/.troupe/runtime.json)
TOKEN=$(jq -r .token ~/.troupe/runtime.json)
curl -X POST "$URL/v1/sessions/create" \
-H "Authorization: Bearer $TOKEN" \
-H "Troupe-Contract: 1.0" \
-H "Content-Type: application/json" \
-d '{"spec":{"platform":"ios-simulator"}}'A read is a GET with a query:
curl "$URL/v1/sessions/get?sessionId=ses_01jbx3y5m7c9tq2v4w6x8z0a1b" \
-H "Authorization: Bearer $TOKEN" \
-H "Troupe-Contract: 1.0"A screenshot is a binary answer:
curl -X POST "$URL/v1/media/screenshot" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"sessionId":"ses_01jbx3y5m7c9tq2v4w6x8z0a1b"}' \
--output screen.pngA request body may be at most 4 MiB. Inputs are strict: a key the procedure does not define fails with
VALIDATION.
Event streams
An event iterator stays open and sends one server-sent event per item, each a JSON object with a seq.
curl --no-buffer "$URL/v1/logs/stream?sessionId=ses_01jbx3y5m7c9tq2v4w6x8z0a1b" \
-H "Authorization: Bearer $TOKEN" \
-H "Accept: text/event-stream"The stream ends with event: done. A failure arrives as event: error whose data is the error body. To
resume after a drop, pass the last seq you saw as since. If the server no longer has the events after
it, the first event is a gap.
Read these streams with a streaming HTTP client and the bearer header. A browser's EventSource cannot
send the header.
RPC
/rpc/v2/<group>/<operation> serves the same procedures in the wire format of oRPC's RPC protocol, for a
caller that already uses an oRPC client. v2 is the version of that wire format, not of the contract.
Inputs, outputs, scopes and errors are the same as on REST. Troupe's own client and CLI use REST.
Authentication
Send Authorization: Bearer <token> on every call. Two procedures are public and ignore it:
runtime.version and pairings.redeem.
| Server | Credential | Where it comes from |
|---|---|---|
| A local runtime | The owner token, tr_rt_… | runtime.json. It holds every scope and changes when the daemon restarts |
| A local runtime | A pairing token, tr_pt_… | A paired browser page. It works from one origin only |
| Troupe Cloud | An API key, tr_key_… | API keys |
| Troupe Cloud | An access token | troupe login, or a token exchange in CI |
Each procedure lists the scopes it needs, and the caller must hold all of them.
| Scope | Grants |
|---|---|
runtime:read | The runtime's health, host and environment checks |
runtime:admin | Stopping the runtime, sweeping leaks, managing pairings |
devices:read | Observing: sessions, the accessibility tree, screenshots, watch tickets |
devices:acquire | Creating, keeping alive and releasing sessions |
devices:drive | Input, system settings, launching apps, drive tickets |
apps:install | Installing, uninstalling and resetting apps, and their permissions |
logs:read | Logs and crash reports |
runs:read, runs:report | Reading test runs, and publishing one |
A missing, invalid or expired credential is UNAUTHENTICATED (401). A valid one without the scopes is
FORBIDDEN (403), and its data.requiredScopes names every scope the call needs.
A local runtime also checks where a request comes from:
- The
Hostheader must be127.0.0.1:<port>orlocalhost:<port>. - An
Originheader, which browsers send, must be the runtime's own origin or a paired one. - Requests without a valid credential share a budget of 600 a minute. Past it the answer is
RATE_LIMITED.
Headers
| Header | Direction | Meaning |
|---|---|---|
Authorization | Request | Bearer <token> |
Troupe-Contract | Both | The contract version, 1.0: yours on a request, the server's on a response |
Troupe-Request-Id | Both | A correlation id. The server echoes yours or mints one (req_…). Quote it when you report a problem |
Troupe-Call-Id | Request | The id under which the runtime records this call in a trace |
Retry-After | Response | Whole seconds, on a 429 or 503 that says when to try again |
Troupe-Project | Request | Troupe Cloud: the project to act in, among those the credential can see |
The contract version
The contract's version is major.minor, now 1.0. The path carries the major (/v1). Within a major,
changes only add: new procedures, new optional input fields, new output fields and new enum values. So a
client must ignore output fields and enum values it does not know.
A request whose Troupe-Contract names a major the server does not serve fails with
CONTRACT_UNSUPPORTED (400). runtime.version needs no
credential and says which version and majors a server serves:
curl "$URL/v1/runtime/version"Errors
Every failure has the same body, on every binding:
{
"code": "NOT_FOUND",
"message": "Not found.",
"data": { "kind": "session", "id": "ses_01jbx3y5m7c9tq2v4w6x8z0a1b" },
"retryable": false,
"requestId": "req_…"
}Decide on code, never on the message. data is typed per code; Errors lists
every code with its data and status.
| Status | Codes, for example |
|---|---|
| 400 | VALIDATION, CONTRACT_UNSUPPORTED |
| 401, 403 | UNAUTHENTICATED, FORBIDDEN |
| 404 | NOT_FOUND: unknown, expired, or not visible to the caller |
| 408 | TIMEOUT |
| 409 | CONFLICT, SESSION_NOT_READY, SESSION_BUSY, APP_NOT_RUNNING |
| 410 | SESSION_ENDED |
| 422 | CAPABILITY_UNSUPPORTED, DEVICE_SPEC_INVALID, APP_CRASHED, LOCATOR_NOT_FOUND |
| 429 | RATE_LIMITED, and QUOTA_EXCEEDED in Troupe Cloud |
| 500 | INTERNAL |
| 503 | CAPACITY_UNAVAILABLE, DEVICE_BOOT_FAILED, every ENVIRONMENT_* code |
retryable: truemeans the same request may succeed later. Wait fordata.retryAfterMs, or theRetry-Afterheader, when there is one.- An
ENVIRONMENT_*error carriesdata.fix: one command or instruction that repairs the machine. - A code you do not know comes from a newer server. Treat it by its status.
Conventions
- Ids are a prefix and 26 lower-case characters, such as
ses_…for a session. An id is a handle, not a credential: every call is still authorized. - Timestamps are ISO 8601 strings in UTC. Durations are integers in milliseconds, named
…Ms. - Coordinates are in points, in screen space for the current orientation, from the top left.
- Lists take
limitandcursorand returnnextCursor.
Next
- The client SDK: the same calls, typed
- Sessions: the first calls to make
- Runtime protocol reference: every procedure