HTTP API
Event ingest with the public key, project endpoints with the API key, live overrides JSON, errors.
Cloud base URL: https://go.splithub.io. Requests and responses use JSON. The self-hosted handler 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 bynpx splithub loginthrough a browser approval, or in the dashboard under Settings → API keys. Never in client code.
Send events (public key)
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 (thesh_vidcookie).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. 429withcode∈quota_exceeded|deletedand aRetry-Afterheader: back off; nothing was stored.403 origin_not_allowedwhen 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)
GET /v1/overrides/pk_live_….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)
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. Public short 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.