# API reference

Base URL `https://api.capless.sh`. Every endpoint is under `/v1` and takes `authorization: Bearer cpl_…` (except signup). The complete, machine-readable contract is the OpenAPI 3.1 spec: [/openapi.yaml](https://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), API key and optional site. No auth. [Details](https://capless.sh/docs/agents.md#signup-api) |

## 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://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://capless.sh/docs/reading.md) · [Time seeking](https://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://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://capless.sh/docs/cel.md)

## Web analytics sites

| Method | Path | |
|---|---|---|
| `POST` | `/v1/web/sites` | Create a site (`name`, `allowedOrigins`, `replaySampleRate`); returns `key` and `snippet` |
| `GET` | `/v1/web/sites` | List sites |
| `PATCH` | `/v1/web/sites/{key}` | Update a site |
| `DELETE` | `/v1/web/sites/{key}` | Delete a site |

[Web SDK](https://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://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_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`, `forbidden`, `query_forbidden` |
| 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` |
| 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.

