# API reference

Base URL `https://api.capless.sh`. Every endpoint is under `/v1` and takes `authorization: Bearer cpl_…` (except signup). The claim secret of an unclaimed project (`Bearer csk_…`) is accepted for the read-only endpoints listed under [Claiming](#claiming). The complete, machine-readable contract is the OpenAPI 3.1 spec: [/openapi.yaml](https://analytics.capless.sh/openapi.yaml) (also served at [api.capless.sh/v1/openapi.yaml](https://api.capless.sh/v1/openapi.yaml)).

## Account

| Method | Path | |
|---|---|---|
| `POST` | `/v1/signup` | Create user, project (trial) and API key; with `claimSecret`, take over that unclaimed project. No auth. [Details](https://analytics.capless.sh/docs/agents.md#signup-api) |
| `GET` | `/v1/project` | The project: plan, billing (including `spendCap`), `claimed`, `role`, allowance, usage, `web` |
| `PATCH` | `/v1/billing` | Set or clear the monthly spend cap: `{"spendCap": 50}` or `{"spendCap": null}` (owners and admins) |

## Claiming

A snippet from analytics.capless.sh creates an **unclaimed** project with its first event. Its claim secret (`csk_…`) is a bearer token with read-only access to that project until it is claimed:

| Method | Path |
|---|---|
| `GET` | `/v1/project`, `/v1/usage`, `/v1/web`, `/v1/streams`, `/v1/streams/{stream}`, `/v1/replays`, `/v1/replays/{session}/{tab}` |
| `POST` | `/v1/streams/{stream}/read`, `/v1/streams/{stream}/locate` |

Anything else returns `403 claim_required`. Once the project is claimed, the secret returns `401 project_claimed`.

| Method | Path | |
|---|---|---|
| `POST` | `/v1/claim` | `{"secret": "csk_…"}` with a dashboard session: claim the project for the signed-in account |
| `POST` | `/v1/signup` | `{"email": "…", "claimSecret": "csk_…"}`: create an account and claim the project in one call |

Both return `409 already_claimed` if another account owns the project. [Quickstart](https://analytics.capless.sh/docs/quickstart.md)

## Streams

| Method | Path | |
|---|---|---|
| `POST` | `/v1/streams` | Create a stream (`name`, optional `validator`) |
| `GET` | `/v1/streams` | List streams, ordered by name |
| `GET` | `/v1/streams/{stream}` | Metadata: head `cursor`, `entryCount`, `lastCommittedAt`, status |
| `PATCH` | `/v1/streams/{stream}` | Rename and/or change the validator |
| `DELETE` | `/v1/streams/{stream}` | Delete (asynchronous, `202`) |

`{stream}` is the ID (`str_…`) or the name. [Streams & cursors](https://analytics.capless.sh/docs/streams.md)

## Entries

| Method | Path | |
|---|---|---|
| `POST` | `/v1/streams/{stream}/entries` | Insert a batch atomically, idempotently |
| `POST` | `/v1/streams/{stream}/read` | Read in cursor order with CEL `filter`/`map` |
| `POST` | `/v1/streams/{stream}/changes` | Ordered feed of inserts and erasures |
| `POST` | `/v1/streams/{stream}/locate` | Cursor for a commit time (index seek) |

[Reading & change feed](https://analytics.capless.sh/docs/reading.md) · [Time seeking](https://analytics.capless.sh/docs/time-seeking.md)

## Erasures

| Method | Path | |
|---|---|---|
| `POST` | `/v1/streams/{stream}/erasures` | Erase entries matching a CEL predicate (`202`) |
| `GET` | `/v1/streams/{stream}/erasures` | List erasures in stream order |
| `GET` | `/v1/streams/{stream}/erasures/{erasure}` | Status, progress, downstream propagation |
| `GET` | `/v1/streams/{stream}/erasures/{erasure}/entries` | Exact affected entry IDs (once `logically_applied`) |

[Erasures & GDPR](https://analytics.capless.sh/docs/erasures.md)

## Functions

| Method | Path | |
|---|---|---|
| `GET` | `/v1/functions` | Describe the CEL environment |
| `POST` | `/v1/functions/evaluate` | Evaluate an expression against a sample entry |

[CEL functions](https://analytics.capless.sh/docs/cel.md)

## Web analytics

| Method | Path | |
|---|---|---|
| `GET` | `/v1/web` | The project's ingest `key`, `snippet`, `replaySampleRate`, `allowedOrigins`, and `hosts` (hostnames that sent events) |
| `PATCH` | `/v1/web` | Change `replaySampleRate` and/or `allowedOrigins` (owners and admins) |
| `GET` | `/v1/replays` | Session recordings from the last 7 days |
| `GET` | `/v1/replays/{session}/{tab}` | Frames of one recording |

[Web SDK](https://analytics.capless.sh/docs/web-sdk.md)

## Materializations

| Method | Path | |
|---|---|---|
| `POST` | `/v1/materializations` | Create (`name`, `type: "clickhouse"`) |
| `GET` | `/v1/materializations` | List |
| `GET` | `/v1/materializations/{id}` | Get |
| `DELETE` | `/v1/materializations/{id}` | Delete with all its data (streams untouched) |
| `POST` | `/v1/materializations/{id}/query` | Run one ClickHouse SQL statement |
| `POST` | `/v1/materializations/{id}/sources` | Connect a stream to a table |
| `GET` | `/v1/materializations/{id}/sources` | List sources with checkpoints |
| `DELETE` | `/v1/materializations/{id}/sources/{sourceId}` | Detach a source |
| `GET` | `/v1/materializations/{id}/status` | Insert and deletion freshness |

[ClickHouse materializations](https://analytics.capless.sh/docs/materializations.md)

## Conventions

* Bodies are JSON, ≤ 8 MiB. Timestamps are UTC ISO 8601 with millisecond precision.
* Lists: `?limit=` (1–1000, default 100) and `?pageToken=` → `{"data": [...], "nextPageToken": "…" | null}`.
* Writes accept `idempotencyKey` (kept 7 days).

## Errors

```json
{ "error": { "code": "validation_failed", "message": "1 of 2 entries failed validation; nothing was committed.",
  "details": { "failedCount": 1, "failures": [{ "index": 1, "message": "validator returned false" }] }, "requestId": "1b4e…" } }
```

| HTTP | Codes |
|---|---|
| 400 | `invalid_json`, `invalid_request`, `invalid_name`, `invalid_claim_secret`, `invalid_spend_cap`, `invalid_expression`, `invalid_cursor`, `invalid_limit`, `invalid_timestamp`, `invalid_page_token`, `cursor_out_of_range`, `query_failed`, `unsupported_output_format`, `unsupported_type`, `project_required` |
| 401 / 403 | `unauthenticated`, `project_claimed`, `forbidden`, `claim_required`, `built_in_stream`, `origin_not_allowed`, `query_forbidden` |
| 402 | `trial_ended`, `no_active_plan`, `event_limit_reached`, `storage_limit_reached`, `spend_cap_reached`, `plan_required`, `plan_limit` |
| 404 | `stream_not_found`, `erasure_not_found`, `materialization_not_found`, `source_not_found`, `site_not_found`, `route_not_found` |
| 409 | `stream_name_taken`, `materialization_name_taken`, `idempotency_key_reused`, `erasure_not_resolved`, `email_taken`, `already_claimed` |
| 410 | `stream_deleted` |
| 413 | `request_too_large`, `batch_too_large`, `entry_too_large` |
| 422 | `validation_failed`, `expression_evaluation_failed`, `result_too_large`, `limit_too_small`, `table_not_found`, `missing_lineage_column`, `unsupported_table_engine` |
| 429 | `rate_limited` |
| 502 / 503 | `engine_error`, `engine_unavailable`, `engine_unreachable` |

Branch on `code`; messages are for humans and may change.

