Docs / Tools View as Markdown

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 by npx splithub login through 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 (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)

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.

SplitHub
Test ideas. Ship winners.