Skip to content

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.

SDK endpoints take two headers on every request:

HeaderDescription
X-Project-IDThe project UUID
X-Project-SecretThat 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.


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:

EntryContents
data.jsonAn array of logs
sessions.jsonAn 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.

{
"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.

StatusMeaningWhat a client should do
200StoredDelete the batch locally
413Over the upload capSend fewer logs per batch
429Over the project’s daily capWait for Retry-After, then resume
400Malformed archiveDo 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.


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.


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.

See Watching a Device Live.


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.


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.


{"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.