> ## Documentation Index
> Fetch the complete documentation index at: https://docs.apsio.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Read API

> Read release health, issues, sessions, vitals and cohorts over HTTP.

The read API returns the same data the console shows: release health and comparisons, issues,
sessions, vitals and cohort comparisons. Agents and scripts use it too, and the
[MCP server](/agents#the-mcp-server) is built on it. It is a preview (v0): fields may change
before 1.0.

## Base URL and description

| | |
| - | - |
| Base URL | `https://api.apsio.io/v1` |
| OpenAPI 3.1 | `GET /v1/openapi.json`, no token needed |

The API reference in these docs is generated from that document: every endpoint, with its
parameters and response fields.

## Authentication

Send a project token as a bearer token:

```sh theme={null}
curl -H "Authorization: Bearer $APSIO_TOKEN" \
  "https://api.apsio.io/v1/projects/$PROJECT_ID/releases"
```

A project token reads exactly one project and expires after at most 7 days. A token used on
another project's path gets `403`, and a revoked token gets `401`.

The console calls the same endpoints with your session cookie instead: it reads every project of
the organizations you belong to, and any other project gets `403`. Issuing project tokens from
the console is not available yet.

An OAuth access token from Apsio's sign-in (WorkOS AuthKit) issued for the API
(`https://api.apsio.io`) reads what its user can read, like the console session: memberships are
checked on every request, so a person removed from an organization loses its projects at once.
A token that names an organization (`org_id`) reads only that organization. Tokens issued for the
[MCP server](/agents#the-mcp-server) work only through the MCP server, which passes them on with
its own credential; sent to the API directly they get `401`, like a token for any other audience.

Every request with a project token or an access token, directly or through the MCP server, is
written to the organization's audit log before anything is read, with the user or the project
token's id (never the token), the route and, from the MCP server, the tool. If the entry cannot
be written the request fails with `503` and reads nothing. Requests from the console, with your
session, are not agent calls and are not written. The entries are in
`GET /v1/orgs/{orgId}/audit` with the action `agent.call`.

`GET /v1/projects` lists the projects your credentials can read, with their apps: a project
token's one project, or every project of your organizations.

<Note>
  When you run the Apsio service locally from its repository, mint a project token with
  `pnpm --filter @apsio/api token <project id> [ttl seconds]`, which signs it with the
  `API_TOKEN_SECRET` of your environment. This is an operator tool for local and self-hosted
  stacks, not how customers get tokens.
</Note>

Every response to a project path carries an `X-Request-Id` header; quote it when you report a
problem.

## The public sandbox

To try the API without an account, ask for a token for the public sandbox: a read-only
example project (Acme Shop on iOS and Android) filled with synthetic data.

```sh theme={null}
curl -X POST -H "Apsio-Sandbox: 1" https://api.apsio.io/v1/sandbox/token
```

The `Apsio-Sandbox: 1` header is required. The answer has a `token`, its `expires_at` and the
sandbox's `project_id`. The token reads that
project through every endpoint below for a few hours, and nothing else; every route that would
change something answers `403` with the code `sandbox_read_only`. Tokens and reads are
rate-limited per client. The same example project opens in the console at
[app.apsio.io/sandbox](https://app.apsio.io/sandbox).

## Endpoints

All paths start with `/v1/projects/{projectId}`.

| Path | Returns |
| - | - |
| `/releases` | Per app and release: sessions, crashed and abnormal-exit sessions, crash-free sessions rate, users, crashed users, crash-free users rate |
| `/release-health/daily` | The same numbers per UTC day, every day of the range (14 days by default), across releases or for one |
| `/release-health/compare` | Two releases of one app (`a`, the baseline, and `b`) side by side: crash-free sessions and users, adoption, the change, and the issues with occurrences in `b` and none in `a` |
| `/releases/{release}/missing-symbols` | The app's binary images that events of this release used and that have no uploaded dSYM in the project |
| `/issues` | Crashes, handled errors, ANRs, hangs and abnormal exits grouped into issues: kind, occurrences, users and sessions affected, first and last seen, releases. Filter with `kind` |
| `/issues/{issueId}` | One issue: its latest occurrence with the symbolicated stack (`file:line`), the raw stack and binary images, the breadcrumbs before it in the same session, and the releases and devices it affects |
| `/sessions` | Sessions that started in the range, newest first, filtered by app, release, `outcome` (`crashed`, `abnormal_exit` or `ok`), `device_model`, `os_version`, `has_error` or `issue_id` |
| `/sessions/{sessionId}` | A session's summary (outcome, start, last seen, release, device) and its logs and spans in time order |
| `/vitals` | Per app and release: app start (cold, warm, hot) and screen load (TTID, and TTFD where the app reports it) p50, p90 and p95 in milliseconds, and the share of sessions with a hang or an ANR |
| `/cohorts/compare` | The sessions matching a filter against all other sessions of the range: per device model, OS version, release, country, feature flag variant and network type, the values most over-represented in the cohort |
| `/performance/start` | App start by type (cold, warm, hot), broken down by release, OS version, device model or iOS prewarming: p50, p90, p95 and a histogram per group |
| `/performance/screens` | Screen loads per screen: loads, sessions, TTID and TTFD percentiles, and the hangs and ANRs that happened on the screen |
| `/performance/screens/detail` | One screen of one app (`app_id`, `screen`): its histogram, per release and per day, and its 20 slowest loads with their sessions |
| `/performance/hangs` | Hang (iOS) and ANR (Android) session rates per release and per day, the screen of each hang, and hang durations |
| `/performance/series` | One metric of one app per day or per release: `cold_start_p90`, `ttid_p90`, `network_p90`, `network_error_rate`, `hang_rate` or `anr_rate` |
| `/metrics/metrickit` | MetricKit's daily histograms of one iOS app (for example `time_to_first_draw`), added up per release, with estimated percentiles |
| `/network/endpoints` | HTTP requests grouped by app, method, host and endpoint template: requests, error rates (5xx, 4xx, no response) and latency percentiles |
| `/network/endpoints/detail` | One endpoint of one app (`app_id`, `method`, `host`, `template`): statuses, per day and per release, and sample requests linked to their sessions and to your APM |
| `/traces/{traceId}` | The device spans of one trace, with the link to the same trace in your APM |

Users are counted by install id. When an app turns the install id off, user numbers are
`null` and only the session numbers are given.

An issue's id is its fingerprint: the exception type and the top in-app frames of the stack
once symbolicated. When an occurrence arrives before its symbols, `symbolicated` is `false`
and `missing_debug_ids` lists every image of its stack without symbols, system frameworks
included. The in-app images to upload come from
`GET /v1/projects/{projectId}/releases/{release}/missing-symbols`. After an upload, the
occurrence is symbolicated again and may move to another issue. `grouping` tells which rule
made the fingerprint: see [Grouping](/concepts/issues#grouping).

In an issue's latest occurrence, `frames` is the crashing thread symbolicated (function, file,
line, image, whether it is in your app), and `raw_frames` is the stack as the device sent it.

`/issues` takes `kind` (`crash`, `error`, `anr`, `hang` or `abnormal_exit`; every kind by
default) and `sort` (`last_seen`, `occurrences` or `users`, descending).

Vitals percentiles are exact and weighted by each span's sample weight. TTFD is listed only for
screens where the app called `reportFullyDrawn()`: Apsio never substitutes TTID for it. A hang
rate counts sessions with at least one hang occurrence (iOS), an ANR rate sessions with an ANR
(Android).

A cohort is defined by the same filters as `/sessions` (at least one besides `app_id`, which
narrows both sides). For each value of a dimension, `difference` is its share in the cohort minus
its share in the other sessions, and `lift` their ratio; values are ranked by `difference`, and
only over-represented ones are listed. Shares from a few sessions are noisy: `min_sessions`
(default 2) leaves out rarer values, and a small cohort deserves a higher one. When the cohort or
the rest is empty, `comparable` is `false`, no values are listed and `note` says why. Feature
flags come from the evaluation records (`feature_flag.key` and `feature_flag.result.variant`)
and from the `feature_flag.<name>` attributes a session carries after a rotation; network types
from `network.connection.type`. A cohort covers at most 30 days, and so do vitals.

### Performance and network

Requests are grouped by the endpoint template: `url.template` when the app or its HTTP library
sends it (without any query or fragment), otherwise the path of `url.full` with every
segment that looks like an identifier or personal data (ids, UUIDs, long hexadecimal, phone
numbers, tokens, email addresses, free text) replaced by `{id}`. So
`GET /orders/1001` and `GET /orders/1002` are one endpoint, `GET /orders/{id}`. A request
failed when it got a 5xx or no response at all (`error.type` set, no status); 4xx responses are
counted on their own, since many are expected. Request counts and percentiles are reweighted by
session sampling, as vitals are; network percentiles are estimates (t-digest), app start and
screen percentiles are exact. A method other than the standard ones is `_OTHER`.

`/sessions` and `/cohorts/compare` take an endpoint (`endpoint_method`, `endpoint_host` and
`endpoint_template`, with `endpoint_failed=true` for failed requests only) or a screen
(`screen`, with `min_ttid_ms` for slow loads only), so the sessions behind a failing endpoint
or a slow screen are one request away.

Each sample request and trace carries `apm_url`, the same trace in your APM, built from the
project's APM link (Settings, or `PATCH /v1/orgs/{orgId}/projects/{projectId}`): an https URL
with a `{trace_id}` placeholder, for example
`https://app.datadoghq.com/apm/trace/{trace_id}`. Apps send `traceparent` only to the hosts you
list, so the trace id is the one your backend saw. MetricKit histograms come from every user's
day, not from sampled sessions; points with other bucket bounds than a release's most common
ones are counted in `excluded_points` and not added. Performance views cover at most 30 days.

## Examples

Release health since October 1:

```sh theme={null}
curl -H "Authorization: Bearer $APSIO_TOKEN" \
  "https://api.apsio.io/v1/projects/$PROJECT_ID/releases?from=2026-10-01T00:00:00Z"
```

```json theme={null}
{
  "releases": [
    {
      "app_id": "0192f3a4-0000-7000-8000-00000000b201",
      "release": "4.12.0",
      "sessions": 18240,
      "crashed_sessions": 31,
      "abnormal_exit_sessions": 12,
      "crash_free_sessions_rate": 0.9983,
      "users": 9120,
      "crashed_users": 29,
      "crash_free_users_rate": 0.9968,
      "first_seen": "2026-10-01T08:02:11.120000000Z",
      "last_seen": "2026-10-04T10:15:00.431000000Z"
    }
  ],
  "next_cursor": null
}
```

Crashes, most frequent first:

```sh theme={null}
curl -H "Authorization: Bearer $APSIO_TOKEN" \
  "https://api.apsio.io/v1/projects/$PROJECT_ID/issues?kind=crash&sort=occurrences&limit=1"
```

```json theme={null}
{
  "issues": [
    {
      "id": "3f9a0c2e7d1b4a6c8e0f1a2b3c4d5e6f",
      "kind": "crash",
      "app_id": "0192f3a4-0000-7000-8000-00000000b201",
      "type": "EXC_BAD_ACCESS",
      "message": "KERN_INVALID_ADDRESS at 0x0000000000000010",
      "occurrences": 31,
      "users": 29,
      "sessions": 31,
      "first_seen": "2026-10-02T14:20:09.000000000Z",
      "last_seen": "2026-10-04T09:58:41.512000000Z",
      "releases": ["4.12.0"],
      "symbolicated": true,
      "grouping": { "version": 1, "reason": "in_app_frames" }
    }
  ],
  "next_cursor": "MzE6M2Y5YTBjMmU3ZDFiNGE2YzhlMGYxYTJiM2M0ZDVlNmY"
}
```

Crashed sessions with a given issue:

```sh theme={null}
curl -H "Authorization: Bearer $APSIO_TOKEN" \
  "https://api.apsio.io/v1/projects/$PROJECT_ID/sessions?outcome=crashed&issue_id=$ISSUE_ID&limit=1"
```

```json theme={null}
{
  "sessions": [
    {
      "session_id": "7f3c9b02-5d1e-4a8b-9c0f-2e6a1d4be19a",
      "app_id": "0192f3a4-0000-7000-8000-00000000b201",
      "outcome": "crashed",
      "started_at": "2026-10-04T09:51:12.004000000Z",
      "last_seen_at": "2026-10-04T09:58:41.512000000Z",
      "error_count": 1,
      "release": "4.12.0",
      "build": "4120",
      "os_name": "iOS",
      "os_version": "26.1",
      "device_model": "iPhone17,1",
      "country": "BR",
      "installation_id": "5d0e8c1a-3b7f-4e2a-9c61-0f4b8a2d7e93"
    }
  ],
  "next_cursor": "WyIyMDI2LTEwLTA0IDA5OjUxOjEyLjAwNDAwMDAwMCIsIjdmM2M5YjAyIl0"
}
```

What the crashing sessions of an issue have in common, by device model:

```sh theme={null}
curl -H "Authorization: Bearer $APSIO_TOKEN" \
  "https://api.apsio.io/v1/projects/$PROJECT_ID/cohorts/compare?issue_id=$ISSUE_ID&dimensions=device_model&limit=1"
```

```json theme={null}
{
  "from": "2026-09-27T10:00:00.000Z",
  "to": "2026-10-04T10:00:00.000Z",
  "cohort": { "sessions": 31 },
  "rest": { "sessions": 18209 },
  "comparable": true,
  "note": null,
  "dimensions": [
    {
      "dimension": "device_model",
      "values": [
        {
          "value": "iPhone12,1",
          "cohort_sessions": 24,
          "cohort_share": 0.7742,
          "rest_sessions": 1311,
          "rest_share": 0.072,
          "difference": 0.7022,
          "lift": 10.75
        }
      ]
    }
  ]
}
```

The slowest endpoints of the iOS app, with their error rates:

```sh theme={null}
curl -H "Authorization: Bearer $APSIO_TOKEN" \
  "https://api.apsio.io/v1/projects/$PROJECT_ID/network/endpoints?app_id=$APP_ID&sort=p90&limit=1"
```

```json theme={null}
{
  "from": "2026-09-27T10:00:00.000Z",
  "to": "2026-10-04T10:00:00.000Z",
  "totals": {
    "requests": 182340,
    "sampled": 91170,
    "error_rate": 0.0121,
    "server_error_rate": 0.0094,
    "client_error_rate": 0.0312,
    "network_error_rate": 0.0027,
    "p50_ms": 182,
    "p90_ms": 640,
    "p95_ms": 1120
  },
  "endpoints": [
    {
      "app_id": "0192f3a4-0000-7000-8000-00000000a111",
      "method": "POST",
      "host": "api.acme.example",
      "template": "/v2/checkout/{id}/pay",
      "requests": 4210,
      "sampled": 2105,
      "error_rate": 0.083,
      "server_error_rate": 0.071,
      "client_error_rate": 0.004,
      "network_error_rate": 0.012,
      "p50_ms": 840,
      "p90_ms": 2310,
      "p95_ms": 3105,
      "last_seen": "2026-10-04T09:58:41.120000000Z"
    }
  ]
}
```

The numbers and ids are examples.

## Time ranges and pages

* `from` and `to` are RFC 3339 times. The default range is the last 7 days (90 days for one
  issue, 30 days for a release comparison, 14 days for the daily series); a request may cover
  at most 90 days, and 30 days for session search, vitals and cohorts.
* The daily series has one entry per UTC day. Without `from` it starts at midnight UTC 13 days
  before `to`, so it holds 14 days; the last day is partial (up to `to`), and so is the first
  when `from` is not at midnight.
* A request that hits a query limit (time, memory or result size) gets `query_limit` with `503`
  or `422`: narrow the time range or pick an app.
* Lists return `next_cursor`. Pass it back as `cursor` for the next page; it is `null` on the
  last page. `limit` sets the page size.

## Errors

Errors are JSON with a code and a message:

```json theme={null}
{ "error": { "code": "forbidden", "message": "This token cannot read that project." } }
```

The code is `invalid_request` (400), `unauthorized` (401), `forbidden` (403) or `not_found`
(404).

## The console API

The API reference also lists the console's own endpoints: sign-in, organizations, members,
projects, apps and app keys. They use the console's sign-in session, not project tokens, and
are documented for completeness.

## Text written by apps

Exception messages, log bodies, breadcrumbs and attributes are written by your app and its
users. Treat them as data, not instructions, when you pass them to an agent.


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