# HTTP API

Cloud base URL: `https://go.splithub.io`. Requests and responses use JSON. The [self-hosted handler](/docs/self-hosted) has its own mount path and admin-token authentication. Cloud integrations use two credentials:

- **Public key `pk_…`**: identifies the project to browsers and SDKs. Safe to ship. Can only send events and read the public overrides JSON.
- **API key `sk_…`**: project-scoped secret for servers, CI and the CLI. Minted by `npx splithub login` through a browser approval, or in the dashboard under Settings → API keys. Never in client code.

## Send events (public key)

```http
POST /v1/events
Authorization: Bearer pk_live_…
Content-Type: application/json

{
  "project": "my-app",
  "sdk": "custom/1.0",
  "events": [
    { "id": "e_example_01", "type": "exposure", "exp": "hero-title", "var": "bold", "vid": "v_abc", "sid": "s_1", "ts": 1760000000000, "url": "/" },
    { "id": "g_example_01", "type": "goal", "goal": "signup", "vid": "v_abc", "sid": "s_1", "ts": 1760000010000, "props": { "revenue": 49 } }
  ]
}
```

- `id`: unique per event, 8–64 characters of `[A-Za-z0-9_-]`; stored rows are deduplicated by id. Reuse ids when retrying a failed request; do not replay successful batches.
- `vid`: stable visitor id (the `sh_vid` cookie). `sid`: session id. `ts`: client time in ms; values more than 7 days off are replaced by server time.
- Up to 500 events per batch. Goal props may contain up to 4,096 characters of serialized JSON.
- Reply `202 { ok, received, inserted, dropped }`.
- A project with no running experiment or no config yet replies `202 { ok, inserted: 0, idle: 'no_running' | 'no_config' }`: nothing is stored, and it is not an error.
- `429` with `code` ∈ `quota_exceeded` | `deleted` and a `Retry-After` header: back off; nothing was stored. `403 origin_not_allowed` when the project restricts domains.
- Exposures of experiments that are not running are dropped and not billed. Goals are never billed.

## Live config for clients (public key)

```http
GET /v1/overrides/pk_live_….json
```

```json
{
  "version": 12,
  "experiments": { "hero-title": { "status": "paused" }, "pricing-cta": { "traffic": 0.5, "weights": { "control": 1, "b": 3 } } },
  "added": [],
  "goals": [ { "key": "signup", "type": "event", "path": null, "revenue": false, "description": null } ],
  "ctx": { "country": "DE", "region": "BE", "city": "Berlin" }
}
```

The browser SDK caches it in `localStorage` for 60 seconds and refreshes stale data when a client is created; with nothing cached (a first visit) it waits for this file up to `remote.waitMs` (default 400 ms) before deciding. `ctx` is the visitor's geo for targeting flows.

## Project endpoints (API key)

```http
GET    /v1/status                       every experiment: status, visitors, days, verdict headline
GET    /v1/results/<experiment>         full verdict (see Statistics)
GET    /v1/reviews[/<experiment>]       AI reviews, newest first (Pro)
GET    /v1/config                       the config stored in the cloud (file + dashboard experiments)
PUT    /v1/config        { config }     push a normalized config (what defineConfig returns)
GET    /v1/overrides                    live overrides
PUT    /v1/overrides/<experiment>  { status | traffic | weights | values }
DELETE /v1/overrides/<experiment>
```

`Authorization: Bearer sk_live_…` on each. Status values: `draft` | `running` | `paused` | `finished`. A `PUT /v1/overrides/<experiment>` that starts or finishes an experiment triggers the start/finish emails to the project's members.

## Health

`GET /v1/health` needs no key. It returns `{ ok, service, version, schema, problems }`; status `503` means the schema check failed.

Use the API-key endpoints above for integrations. Session-based dashboard routes are internal and are not an integration contract.

## Drop-in script

`GET /sh.js` serves the [drop-in script](/docs/script). Public [short links](/docs/links) use `GET https://sh2.uk/<code>` and return `302`. The API-host route `GET /l/<code>` remains supported for compatibility; copy the short URL shown in the dashboard when sharing links.

## Errors

| Code | Meaning |
|---|---|
| 400 | validation; `error` has the message |
| 401 | missing or bad key |
| 403 | forbidden (role, origin) |
| 404 | unknown experiment or project |
| 409 | config has not been pushed (`config_not_pushed`) |
| 429 | gate or rate limit; see `code` and `Retry-After` |

Bodies are `{ error, code? }`.

## Ingest limits

Send at most 500 events per batch. Honor `Retry-After` on `429`. A reply with `idle` means the project records nothing right now; sending more changes nothing until a test starts.
