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

# Identity and bootstrap

> What a credential resolves to, what the server enforces, and the two documents a client can read before it holds a token.

Three routes let a client learn what it is talking to and who it is. `GET /v1/capabilities` and
`GET /config.json` take no credential; `GET /v1/whoami` reflects whichever credential the Vexlo
server accepted. The UI's boot sequence and the MCP server's startup checks both begin here.

The bodies on this page were captured from three servers: one under bearer authentication on
`127.0.0.1:18200`, seeded with the spec's conformance trace on team `fixture` and holding the
tokens named below; one started with `-no-auth` on `127.0.0.1:18201`; and one started with
`-header-trust -header-trust-secret proxy-shared-secret-7f3a` on `127.0.0.1:18202`. Every token
was minted on a throwaway server storage directory. Minting is covered on
[authentication and tokens](https://docs.vexlo.ai/server/authentication.md).

## GET /v1/whoami

Takes no parameters and returns the caller's own validated credential. Either role is accepted:
an ingest token confirming its pins is as legitimate a caller as the UI learning its team-set.

| Field | Meaning |
| - | - |
| `role` | `read` or `ingest`. |
| `team_ids` | The team-set: one team for an ingest token, one or more for a read token. |
| `service_scope` | The ingest token's service pin, or `null`. Always `null` for a read token. |
| `environment_scope` | The ingest token's environment pin as a list, or `null`. Always `null` for a read token. |

A read token spanning two teams:

```bash theme={null}
curl -s -H "Authorization: Bearer $READ_TOKEN" http://127.0.0.1:18200/v1/whoami
```

```json theme={null}
{"role":"read","team_ids":["demo","fixture"],"service_scope":null,"environment_scope":null}
```

An ingest token for team `fixture` pinned to environment `prod`:

```bash theme={null}
curl -s -H "Authorization: Bearer $FIXTURE_INGEST_TOKEN" http://127.0.0.1:18200/v1/whoami
```

```json theme={null}
{"role":"ingest","team_ids":["fixture"],"service_scope":null,"environment_scope":["prod"]}
```

An unpinned ingest token for team `demo`:

```bash theme={null}
curl -s -H "Authorization: Bearer $DEMO_INGEST_TOKEN" http://127.0.0.1:18200/v1/whoami
```

```json theme={null}
{"role":"ingest","team_ids":["demo"],"service_scope":null,"environment_scope":null}
```

Under no-auth mode no tokens exist. Every call answers with role `read` and the single team the
server was started with (`-team`, default `default`):

```bash theme={null}
curl -s http://127.0.0.1:18201/v1/whoami
```

```json theme={null}
{"role":"read","team_ids":["default"],"service_scope":null,"environment_scope":null}
```

An `Authorization` header sent to that server changes nothing; an unknown token gets the same
body:

```bash theme={null}
curl -s -H "Authorization: Bearer vxlo_read_aaaaaaaaaaaaa_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" \
  http://127.0.0.1:18201/v1/whoami
```

```json theme={null}
{"role":"read","team_ids":["default"],"service_scope":null,"environment_scope":null}
```

Under header-trust mode a reverse proxy in front of the server injects `X-Vexlo-Team` and
`X-Vexlo-Trust-Secret`; the server consults them only when `Authorization` is absent, and the
principal it builds is a read principal for that one team:

```bash theme={null}
curl -s -H "X-Vexlo-Team: fixture" -H "X-Vexlo-Trust-Secret: proxy-shared-secret-7f3a" \
  http://127.0.0.1:18202/v1/whoami
```

```json theme={null}
{"role":"read","team_ids":["fixture"],"service_scope":null,"environment_scope":null}
```

Both headers are required, and the secret must match:

```bash theme={null}
curl -s -i http://127.0.0.1:18202/v1/whoami
```

```http theme={null}
HTTP/1.1 401 Unauthorized
Content-Type: application/json
X-Request-Id: cd99c6d182cbd428
Date: Mon, 14 Sep 2026 09:00:09 GMT
Content-Length: 114

{"error":{"code":"unauthenticated","message":"header-trust mode requires X-Vexlo-Team and X-Vexlo-Trust-Secret"}}
```

```bash theme={null}
curl -s -i -H "X-Vexlo-Team: fixture" -H "X-Vexlo-Trust-Secret: wrong" \
  http://127.0.0.1:18202/v1/whoami
```

```http theme={null}
HTTP/1.1 401 Unauthorized
Content-Type: application/json
X-Request-Id: 1f967525147be5c0
Date: Mon, 14 Sep 2026 09:00:09 GMT
Content-Length: 79

{"error":{"code":"unauthenticated","message":"X-Vexlo-Trust-Secret mismatch"}}
```

## GET /v1/capabilities

Unauthenticated, never rate limited, and free of team or environment data: the route a client
calls before it holds a credential, to learn which authentication mode to use and which bounds
the server enforces. The MCP server reads its own limits from `limits`.

| Field | Meaning |
| - | - |
| `server_version` | Whatever the build reports. The binary these pages were captured from reports `dev`. |
| `api_version` | `v1`. |
| `features` | Names each addition a `/v1` client cannot detect from a response's shape alone. One entry today, `in_flight_attempts`: this server can answer with an attempt whose `status` is `in_flight` ([conventions](https://docs.vexlo.ai/api/conventions.md#the-three-status-fields)). |
| `auth_mode` | `bearer`, `header_trust`, or `none`. One field, because the three modes are mutually exclusive. |
| `limits` | The bounds below, as the server enforces them. |

The bearer server:

```bash theme={null}
curl -s http://127.0.0.1:18200/v1/capabilities | jq .
```

```json theme={null}
{
  "server_version": "dev",
  "api_version": "v1",
  "features": [
    "in_flight_attempts"
  ],
  "auth_mode": "bearer",
  "limits": {
    "record_format_versions": [
      1,
      2
    ],
    "search_limit_default": 50,
    "search_limit_max": 500,
    "trace_steps_limit_default": 500,
    "trace_steps_limit_max": 500,
    "values_max_paths": 8,
    "trace_steps_max": 10000,
    "ingest_body_max_bytes": 4194304,
    "ingest_gzip_expansion_max_bytes": 33554432,
    "read_rate_limit_rps": 20,
    "read_rate_limit_burst": 40,
    "ingest_rate_limit_records_per_s": 4000,
    "ingest_rate_limit_records_burst": 8000,
    "ingest_rate_limit_bytes_per_s": 4194304,
    "ingest_rate_limit_bytes_burst": 8388608
  }
}
```

| `limits` field | Bound |
| - | - |
| `record_format_versions` | The `format_version` window ingest accepts, current version last: 1 and 2. |
| `search_limit_default`, `search_limit_max` | `?limit=` on list routes: 50 and 500. |
| `trace_steps_limit_default`, `trace_steps_limit_max` | Steps per `GET /v1/traces/{id}` response: 500 and 500. |
| `values_max_paths` | Distinct `path` values per `GET /v1/traces/{id}/values` call: 8. |
| `trace_steps_max` | Steps per trace: 10,000. |
| `ingest_body_max_bytes`, `ingest_gzip_expansion_max_bytes` | `POST /v1/records` body: 4 MiB decoded, 32 MiB compressed. |
| `read_rate_limit_rps`, `read_rate_limit_burst` | The read surface: 20 per second, burst 40. |
| `ingest_rate_limit_records_per_s`, `ingest_rate_limit_records_burst` | Records: 4,000 per second, burst 8,000. |
| `ingest_rate_limit_bytes_per_s`, `ingest_rate_limit_bytes_burst` | Bytes: 4 MiB per second, burst 8 MiB. |

`record_format_versions` sits in an unauthenticated response because it answers one operational
question, whether this server accepts what an SDK is about to stamp, and the rollout it belongs to
puts the server before the SDKs. Nothing in the emit path reads it: an emitter stamps the version
it implements, and nothing between the process and the server rewrites that field.

The no-auth server reports `"auth_mode": "none"`. Its `limits` are the same numbers, although
no rate limit is applied in that mode ([conventions](https://docs.vexlo.ai/api/conventions.md#rate-limits)):

```bash theme={null}
curl -s http://127.0.0.1:18201/v1/capabilities | jq .
```

```json theme={null}
{
  "server_version": "dev",
  "api_version": "v1",
  "features": [
    "in_flight_attempts"
  ],
  "auth_mode": "none",
  "limits": {
    "record_format_versions": [
      1,
      2
    ],
    "search_limit_default": 50,
    "search_limit_max": 500,
    "trace_steps_limit_default": 500,
    "trace_steps_limit_max": 500,
    "values_max_paths": 8,
    "trace_steps_max": 10000,
    "ingest_body_max_bytes": 4194304,
    "ingest_gzip_expansion_max_bytes": 33554432,
    "read_rate_limit_rps": 20,
    "read_rate_limit_burst": 40,
    "ingest_rate_limit_records_per_s": 4000,
    "ingest_rate_limit_records_burst": 8000,
    "ingest_rate_limit_bytes_per_s": 4194304,
    "ingest_rate_limit_bytes_burst": 8388608
  }
}
```

The header-trust server reports `"auth_mode": "header_trust"`:

```bash theme={null}
curl -s http://127.0.0.1:18202/v1/capabilities | jq .
```

```json theme={null}
{
  "server_version": "dev",
  "api_version": "v1",
  "features": [
    "in_flight_attempts"
  ],
  "auth_mode": "header_trust",
  "limits": {
    "record_format_versions": [
      1,
      2
    ],
    "search_limit_default": 50,
    "search_limit_max": 500,
    "trace_steps_limit_default": 500,
    "trace_steps_limit_max": 500,
    "values_max_paths": 8,
    "trace_steps_max": 10000,
    "ingest_body_max_bytes": 4194304,
    "ingest_gzip_expansion_max_bytes": 33554432,
    "read_rate_limit_rps": 20,
    "read_rate_limit_burst": 40,
    "ingest_rate_limit_records_per_s": 4000,
    "ingest_rate_limit_records_burst": 8000,
    "ingest_rate_limit_bytes_per_s": 4194304,
    "ingest_rate_limit_bytes_burst": 8388608
  }
}
```

## GET /config.json

The bootstrap document the UI reads first. It sits outside `/v1`, takes no credential, is never
rate limited, and carries one field:

```bash theme={null}
curl -s -i http://127.0.0.1:18200/config.json
```

```http theme={null}
HTTP/1.1 200 OK
Content-Type: application/json
X-Request-Id: f036743c2b4c56af
Date: Mon, 14 Sep 2026 09:00:09 GMT
Content-Length: 20

{"api_base_url":""}
```

`api_base_url` is always the empty string: the UI and the `/v1` API are served from one process
on one port, so the API is same-origin. No flag changes it.


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