# Collection API

The public endpoints the SDK and the hook use. You rarely call them yourself, but they are stable, small and documented here in full.

## Conventions

- Request bodies are JSON objects of at most 16 KB, sent with `Content-Type: application/json`.
- Errors answer `{"error":"code"}` with an HTTP status. See [Error codes](https://clitrail.com/docs/errors).
- Browsers may call the `/v1/` endpoints only from a website's registered origins: its primary origin, additional domains and registered pages. Credentials are never sent (`credentials: 'omit'`).
- Requests are rate limited. See [Limits](https://clitrail.com/docs/limits).
- When a Free organisation has used its [monthly quota](https://clitrail.com/docs/plans#tracked-events), visits and installs still get their normal answer, with `"recorded": false`, and nothing is stored.
- Every request is recorded in [Logs](https://clitrail.com/docs/logs) with its exact body, rejected ones included; receipts and tokens appear there only as keyed hashes.

| Endpoint | Called by | Purpose |
| --- | --- | --- |
| `GET /browser.js` | Website | The SDK. |
| `GET /v1/projects/{website}/browser-config` | SDK | GA4 measurement IDs and providers. |
| `POST /v1/visits` | SDK | Records a visit and issues a receipt. |
| `POST /v1/visits/activate` | SDK | Confirms the marker was written. |
| `POST /v1/handoffs` | SDK | Issues a handoff token. |
| `GET /v1/projects/{website}/paths/{path}/report.sh` | Installer | An install path’s hook, with the path ID written in. A path the website’s organisation does not have answers `404` `install_path_not_found`. |
| `GET /v1/projects/{website}/report.sh` | Installer | The website’s hook without a path ID; its installs count toward the first install path. |
| `POST /v1/events` | Hook | Reports an install. |
| `GET /report.sh` | Anyone | The hook's source as plain text, for review. |
| `GET /api/limits` | Anyone | Every plan’s limits, the prices and the service limits as JSON, cacheable for 5 minutes. See [Limits](https://clitrail.com/docs/limits). |
| `GET /skills/clitrail-setup/…` | Coding agents | The [setup skill](https://clitrail.com/docs/agent-setup): `SKILL.md`, its references, `manifest.json` and `install.sh`. |
| `GET /health` | Monitoring | Returns `{"ok":true,"ga_mode","delivery",…}`: `delivery` is `live` once any destination sends live. |

## GET /v1/projects/{website}/browser-config

```json
{
  "measurementIds": ["G-XXXXXXXXXX"],
  "providers": ["ga4", "google_ads", "meta", "tiktok", "x"]
}
```

The measurement IDs of the website's GA4 destinations, and its providers setting.

## POST /v1/visits

Must come from a registered origin; the page URL must be on that origin.

| Field | Type | Description |
| --- | --- | --- |
| `project` | string | Website ID. |
| `url_id` | string | `url_` plus the first 16 hex characters of SHA-256(origin + path), or a registered page ID. |
| `page_url` | string | The page's URL. Stored as origin and path. |
| `referrer` | string | `document.referrer`. Stored as origin and path. |
| `identities` | object | `{ ga4: { "G-…": { client_id, session_id } }, google_ads: { gclid, wbraid, gbraid, gcl_aw, session_attributes }, meta: { fbp, fbc, fbclid }, tiktok: { ttp, ttclid, ttclid_at }, x: { twclid, twclid_at }, custom: { … } }`. Google Ads, Meta, TikTok and X fields for providers the website does not use are dropped; GA4 IDs are kept whenever sent. |
| `previous_receipt` | string or null | The receipt this page's marker held before. It is revoked when the new one is activated. |
| `source` | object | `{ referrer, utm: { source, medium, campaign, term, content, id }, landed_at }` |
| `gpc` | boolean | `true` when the browser sends Global Privacy Control. |
| `client` | object | Coarse hints: the user-agent client hints, `architecture`, `bitness`, `brave`, `touch`, `timezone`, `language`. See [Browsers and systems](https://clitrail.com/docs/distributions). |

```json
{
  "marker": {
    "format": "opfs-install-attribution",
    "version": 1,
    "project": "p_04de14934e8005ed97dd",
    "url_id": "url_96f86d390b35cfa7",
    "origin": "https://acme.dev",
    "receipt": "5GodFohsqck6lAXWFydmiZTBXvtQnU2WzbpsB8tq-_k",
    "created_at": 1790171920120,
    "expires_at": 1792763920120
  }
}
```

## POST /v1/visits/activate

Body `{ "project", "receipt" }`, from the same origin as the visit. Answers `{"ok":true}`. Until activated, a receipt never matches.

## POST /v1/handoffs

Either `{ "project", "receipt" }` for a visit that already has a receipt, or the fields of `/v1/visits` (without `previous_receipt`) to record a visit that exists only for the handoff. Both accept `gpc`. For a handoff-only visit the answer also carries its `receipt`, which the SDK keeps in memory to link further installation scopes of the same page view; such a receipt can never be activated or matched as a browser receipt.

```json
{
  "ok": true,
  "token": "hV0c3t8G2mIqzW1b6Xy9kQ4rLpE7uN5sJ0aTfDgC2vY",
  "expires_at": 1792763920120
}
```

## POST /v1/events

Called by the hook, never by a browser: a request with an `Origin` header is refused with `native_endpoint`.

| Field | Type | Description |
| --- | --- | --- |
| `project` | string | Website ID. |
| `event_id` | string | 8 to 128 letters, digits, `_` or `-`. Idempotency key. |
| `installation_id` | string | 8 to 128 letters, digits, `_` or `-`. |
| `event_type` | string | `install_started`, `install_completed` or `first_run`. |
| `receipts` | string[] | Up to 64 receipts of 43 characters. |
| `handoff` | string | Optional handoff token. `receipts` must then be empty. |
| `platform` | string | Optional: `darwin` or `linux`. |
| `install_path` | string | Optional: the [install path](https://clitrail.com/docs/install-paths) ID. Missing or unknown: the organisation’s first path. |
| `arch`, `browsers`, `default_browser`, `timezone`, `language` | string, string[] | Optional facts about the machine; see [What it sends](https://clitrail.com/docs/hook#what-it-sends). |

```json
{
  "ok": true,
  "event_id": "6f2da9610246dec311f2b793f7c32902",
  "duplicate": false,
  "resolution": "matched",
  "candidate_count": 1,
  "selection_policy": "latest_visit",
  "ga": {
    "status": "pending",
    "deliveries": [{
      "destination": "d_2f7c7e7eac2f8f79c5e2",
      "status": "pending",
      "session_mode": "original_session_window"
    }]
  }
}
```

On Free, and for a paused install path, `resolution` is `counted`: receipts and tokens are not read. When Free has used its monthly quota, the answer is the same shape with `"recorded": false`, and nothing is stored. The response never contains identifiers or referring pages. `ga` is kept for older clients: it lists the delivery queued for each destination, with the first one's status as `status` (`not_queued` when there is none).

## Dashboard API

The dashboard uses JSON endpoints under `/api/`, authenticated by the session cookie and accepted only from the service's own origin. They serve the dashboard and may change; build integrations on webhooks and the endpoints above.
