Server REST API
Most of the server is a web dashboard, and its routes exist to serve HTML to a browser. This page covers the part a client talks to.
Authentication
Section titled “Authentication”SDK endpoints take two headers on every request:
| Header | Description |
|---|---|
X-Project-ID | The project UUID |
X-Project-Secret | That project’s secret |
Dashboard endpoints take a cs_session cookie, set by signing in.
The MCP endpoint takes a personal access token as Authorization: Bearer csp_…, created under
Personal settings in the dashboard.
POST /api/logs/dump
Section titled “POST /api/logs/dump”Upload a batch.
Content-Type: multipart/form-data, with a field named file holding a gzipped tar.
The archive has up to two entries, read by name rather than by position, so a newer SDK can add a file without an older server refusing the whole batch:
| Entry | Contents |
|---|---|
data.json | An array of logs |
sessions.json | An array of the sessions those logs belong to. Optional. |
{ "id": "uuid-v4", "session_id": "uuid-v4", "level": "info", "message": "User signed in", "error": null, "stack_trace": [ {"index": 0, "method": "LoginBloc.onSubmit", "path": "lib/login/bloc.dart", "line": 42, "column": 12} ], "metadata": {"userId": "12345"}, "tags": ["auth"], "timestamp": "2026-01-15T10:30:00Z", "is_network_call": 0, "request_id": null, "call_phase": null}The last three fields describe network capture. is_network_call is the integer 0 or 1, not a
boolean. A network log is what a captured call writes for its request, and for its response or its
error. It has is_network_call 1, the call’s request_id, and a call_phase of request,
response or error.
request_id can also be set on a log with is_network_call 0 and call_phase null. That is a log
the app linked to a call, such as the error its model threw on the call’s response body, and it
looks like this, with the other fields as above:
{ "level": "error", "message": "Could not read GET /v2/cart", "error": "type 'int' is not a subtype of type 'double' in type cast", "is_network_call": 0, "request_id": "5c67a6d7-7663-4274-9152-ce71784c3aa1", "call_phase": null}The server pairs logs into a call by request_id, and only logs with is_network_call 1 take part,
so a linked log is shown beside its call and never changes the call’s status, duration or count.
request_id must be a UUID or null. A value that does not parse fails the decode of the whole
data.json, and the batch is refused with a 500.
A session
Section titled “A session”{ "id": "uuid-v4", "installation_id": "uuid-v4", "user_id": "u_8812", "device_model": "Pixel 7", "os_name": "Android", "os_version": "14", "app_version": "3.11.2", "build_number": "418", "sdk_version": "1.4.0", "metadata": {"plan": "free"}, "started_at": "2026-01-15T10:29:00Z", "last_seen_at": "2026-01-15T10:31:00Z"}Sessions are re-sent with every batch and upserted on id. That is what lets setUser() land
mid-session, and what stops a failed first upload from orphaning later logs.
Responses
Section titled “Responses”| Status | Meaning | What a client should do |
|---|---|---|
200 | Stored | Delete the batch locally |
413 | Over the upload cap | Send fewer logs per batch |
429 | Over the project’s daily cap | Wait for Retry-After, then resume |
400 | Malformed archive | Do not retry unchanged |
Ingest is idempotent on the log’s id. A batch replayed after a lost 200 inserts nothing
and is not charged against the daily cap.
A capped project is refused whole, never partly. Retry-After is in delta-seconds.
GET /api/validate
Section titled “GET /api/validate”Checks credentials, and carries the project’s live configuration back.
{ "id": "a3f2c7d1-4e88-4b21-9f60-1c2d3e4f9c41", "name": "Ledger Mobile", "session_sample_rate": 0.25}session_sample_rate is a fraction from 0 to 1. The SDK uses whichever is lower, its own rate
or this one, so a server can quieten a noisy app but never make one send more than its developer
asked for.
GET /api/live/socket
Section titled “GET /api/live/socket”A WebSocket upgrade. The device’s end of a live session.
The first frame pairs:
{ "code": "4K7Q2P", "device": { "session_id": "uuid-v4", "installation_id": "uuid-v4", "device_model": "Pixel 7", "os_name": "Android", "os_version": "14", "app_version": "3.11.2", "build_number": "418", "user_id": "u_8812" }}The server answers {"ok": true, "session_id": "..."}, or {"ok": false, "error": "..."} and
hangs up. Every frame after that carries logs:
{"logs": [{"level": "error", "message": "Payment declined", "timestamp": "2026-01-15T10:30:00Z"}]}An empty logs array is a heartbeat, saying the app is awake even when it is quiet.
Credentials are checked before the code is ever read, so the code decides which session a device joins, never whether it may join one. A code is single use and expires after five minutes.
POST /api/mcp
Section titled “POST /api/mcp”The Model Context Protocol endpoint, over streamable HTTP. This is what an AI coding agent connects to.
Authorization: Bearer csp_…, a personal access token. A missing or bad token is refused with
401 and a JSON body before any protocol exchange happens, so a misconfigured client fails at
setup rather than inside a tool call.
The session is stateless: each POST is a complete exchange, answered with one application/json
body. There is no Mcp-Session-Id to track and no stream held open, which also means a reverse
proxy needs no special configuration for this route.
Eighteen tools, all read only, covering projects, log search, error groups, sessions, devices, network calls, live sessions and a paired device’s local databases. None of them takes an operation, a statement or a value to write.
A token carries its owner’s access. A project the owner cannot read answers “project not found”, identically to one that does not exist.
See Reading CodeScout with an AI agent.
POST /api/auth/submit
Section titled “POST /api/auth/submit”Sign in, or register on a fresh instance.
First run: name, email, password, confirm_password. The account created becomes the
super admin.
After that: email and password.
Sets an HttpOnly cs_session cookie valid for 30 days.
POST /api/auth/logout
Section titled “POST /api/auth/logout”Errors
Section titled “Errors”{"code": 47000001, "message": "Human readable message", "errors": []}errors carries one entry per rejected field when a form was the problem, and is empty otherwise.
A 429 adds retry_after_seconds, the same number as the Retry-After header. It is repeated in
the body because proxies rewrite headers, and a client reading a non-200 already has the body open.
Codes use a 47XXXXXX scheme. The HTTP status is what a client should branch on; the code is
for reporting a problem precisely.