# REST API

Base URL: `https://vaka.example.com/api/v1`. The OpenAPI document is at `https://vaka.example.com/api/v1/openapi.json`.
The [MCP server](mcp.md) uses the same query layer, so both return the same data.

## Authentication

```
Authorization: Bearer vt_...
```

API tokens are created by admins in the admin UI (**Tokens**) or with `POST /api/v1/tokens`.
Each token is either scoped to one project or to all projects, and is either `read` or `write`.
Signed-in admin UI sessions also work (cookie), which is handy for quick browser checks.

Errors are `{"error": "explanation"}` with a 4xx/5xx status. `401` means a missing or bad token,
`403` a token without access to that project or without write scope.

## Conventions

- `{p}` is a project slug or id. `{id}` for issues is the id or short id (`WEB-12`).
- Time parameters (`since`, `until`, `window`, `baseline`) take a relative duration (`30m`,
  `24h`, `7d`), an RFC 3339 timestamp, or unix milliseconds.
- Timestamps in responses are RFC 3339 UTC.
- Stack traces are crash site first: `frames[0]` is where the error was raised.

## Projects and keys

| Method & path | Description |
| --- | --- |
| `GET /me` | The current user or token and its access |
| `GET /projects` | Projects you can access |
| `POST /projects` | Create: `{"name", "platform", "slug"?}`. Platforms: javascript, android, ios, rust, node, jvm, go, dotnet, python, other. Returns `ingest_key` and `install_instructions_url` |
| `GET /projects/{p}` | One project |
| `PATCH /projects/{p}` | Admin: `{"name"?, "retention_days"?, "scrub_enabled"?, "scrub_fields"?}` (`scrub_fields`: comma-separated extra keys) |
| `POST /projects/{p}/keys` | Create another ingest key |
| `DELETE /projects/{p}/keys/{key}` | Revoke an ingest key |
| `GET /projects/{p}/install/{platform}` | Install guide as Markdown with key and endpoint filled in (`javascript`, `android`, `ios`, `rust`, `http`) |

## Issues and events

| Method & path | Description |
| --- | --- |
| `GET /projects/{p}/issues` | `status` (open\|resolved\|ignored\|all, default open), `query` (title/culprit text), `release`, `environment`, `since` (count window, default 24h), `sort` (last_seen\|first_seen\|times_seen\|events\|users), `limit` |
| `GET /issues/{id}` | Issue with latest event (symbolicated frames, source context, breadcrumbs, tags) and breakdowns by release, OS, device, browser |
| `PATCH /issues/{id}` | `{"status"?: "open"\|"resolved"\|"ignored", "resolved_in_release"?, "ignore_for"?: "7d", "assignee"?: "email" or ""}` |
| `GET /issues/{id}/events` | Recent events (`limit`) |
| `POST /issues/{id}/comments` | `{"body": "markdown"}` |
| `GET /projects/{p}/events` | Search error events: `query`, `level`, `release`, `environment`, `user_id`, `exception_type`, `since`, `until`, `limit` |
| `GET /projects/{p}/events/{event_id}` | One event in full |

A resolved issue that receives a new event is reopened and marked as a regression (and fires
`regression` alerts). With `resolved_in_release`, only events from that release or newer count.

## Releases

| Method & path | Description |
| --- | --- |
| `GET /projects/{p}/releases` | Known releases |
| `GET /projects/{p}/releases/health` | Crash-free sessions and users, sessions, errors and new issues per release; `release`, `since` |
| `GET /projects/{p}/releases/compare?base=1.2.0&target=1.3.0` | Health side by side, new issues, gone issues, issues that increased |
| `GET /projects/{p}/explain-spike` | Why errors went up: `window` (default `1h`) vs `baseline` (default the 24h before), ranked by issue, release, OS, device, country, exception type |

## Analytics

| Method & path | Description |
| --- | --- |
| `GET /projects/{p}/analytics/summary` | Visitors, pageviews, screen views, custom events, identified users, with the previous period |
| `GET /projects/{p}/analytics/timeseries` | Same metrics per `interval` (hour\|day\|week\|month) |
| `GET /projects/{p}/analytics/breakdown` | Top values of `dimension`, ranked by visitors; `limit` |

Common parameters: `since`, `until`, `type` (pageview\|screen_view\|custom), and repeated
`filter=dimension:value` (prefix the value with `~` for contains or `!` for not equal).

Dimensions: `path`, `url`, `hostname`, `referrer` (referrer host or UTM source), `referrer_url`,
`utm_source`, `utm_medium`, `utm_campaign`, `utm_term`, `utm_content`, `country`, `device`,
`device_model`, `browser`, `browser_version`, `os`, `os_version`, `app_version`, `release`,
`environment`, `platform`, `event` (event name), `type`, `screen`, and `prop:<name>` for custom
event properties.

```sh
curl -H "Authorization: Bearer $VAKA_TOKEN" \
  "https://vaka.example.com/api/v1/projects/web/analytics/breakdown?dimension=prop:plan&filter=event:signup&since=30d"
```

## Debug files

| Method & path | Description |
| --- | --- |
| `GET /projects/{p}/artifacts` | Uploaded files (`release` filter) |
| `POST /projects/{p}/artifacts` | Multipart, fields in this order: `kind` (sourcemap\|proguard\|native; dSYMs, ELF and PDB are `native`), `release` (required except for native, which is matched by debug id), `name` (for source maps: URL path of the minified file, e.g. `/assets/app.js`), then `file`. Max 1 GB |
| `DELETE /projects/{p}/artifacts/{id}` | Delete |

The [CLI](cli.md) wraps these; prefer `vaka upload ...` in build scripts.

## Privacy

| Method & path | Description |
| --- | --- |
| `DELETE /projects/{p}/users/{user_id}` | Delete all errors, events and sessions for a user id (write scope). Returns counts. Cannot be undone |

## Alerts

| Method & path | Description |
| --- | --- |
| `GET /alert-rules` | Rules, plus the valid triggers and channels |
| `POST /alert-rules` | See [alerts](alerts.md) |
| `PATCH /alert-rules/{id}` / `DELETE /alert-rules/{id}` | Update or delete |
| `POST /alert-rules/{id}/test` | Send a test notification |
| `GET /alert-rules/{id}/deliveries` | Recent deliveries and their status |

## Tokens and users (admins)

| Method & path | Description |
| --- | --- |
| `GET /tokens`, `POST /tokens` | Create: `{"name", "project"?, "scope": "read"\|"write", "expires_in_days"?}`. The token is returned once |
| `DELETE /tokens/{id}` | Revoke |
| `GET /users` | Users |
| `POST /users/invite` | `{"email", "role": "viewer"\|"member"\|"admin"\|"owner"}`; returns an invite link (also emailed if Resend is set up) |
| `PATCH /users/{id}` / `DELETE /users/{id}` | `{"role"?, "disabled"?}` |

Roles: **viewer** reads everything, **member** can also triage issues and upload debug files,
**admin** manages projects, tokens, users and alerts, **owner** is an admin who can manage other owners.
