Server-Side Testing

Evaluation API

REST reference for server-side testing — authentication, flag config with ETags, evaluate, exposure and conversion, identities, errors and rate limits.

Advanced12 min read

The evaluation API lets any backend read your feature flags, evaluate them for a user, and record exposures and conversions for server-side tests. The Node SDK wraps it; from any other language, call it directly.

Basics

Base URL: https://app.splittestpro.com

Paths: every route exists under both platform prefixes — /api/html/flags/… and /api/shopify/flags/…. Either prefix accepts any valid key: the key, not the path, decides the workspace. Use the one that matches your workspace’s platform.

Authentication: send a server API key in the Authorization header on every request:

Authorization: Bearer stp_<key id>_<secret>

A key in the query string (?api_key= or ?key=) is refused with 401 invalid_key before anything else runs. The API is for servers: it sends no CORS headers, so browsers can’t call it.

Bodies: JSON, at most 64 KB. A malformed body is 400 invalid_request.

Method Path Purpose
GET /api/html/flags/config The workspace’s flags, with an ETag
POST /api/html/flags/evaluate Flag values for one identity
POST /api/html/flags/exposure Record that an identity experienced a variant
POST /api/html/flags/conversion Record a conversion for an identity

Identities

Every evaluate, exposure and conversion names a user with an identity: any stable, opaque id you choose.

  • 1–128 characters of printable ASCII, no whitespace.
  • No @ — an email address is refused. Use an internal user id, a random id stored against the user, or a hash.
  • In a hybrid test, use the pixel’s visitor id (window.SplitTestPro.getVisitorId()) so your server and the pixel place the user in the same variant.

Anything else is 400 invalid_identity. You own the mapping from identity to person — see Privacy & Consent.

GET …/flags/config

Returns every flag in the workspace. Use it to cache flags and to notice changes cheaply.

curl https://app.splittestpro.com/api/html/flags/config \
  -H "Authorization: Bearer $SPLITTESTPRO_API_KEY"
{
  "config_version": "9f2c4e1a7b3d5c60",
  "entitled": true,
  "flags": [
    { "key": "new-checkout", "name": "New checkout", "state": "on", "rollout_pct": 25 },
    { "key": "free-shipping-banner", "name": "Free shipping banner", "state": "killed", "rollout_pct": 100 }
  ]
}
Field Meaning
config_version 16 hex characters. Changes whenever a flag is created, edited, killed, restored or deleted, and whenever an experiment that sets flag values starts, stops or changes.
entitled false when the workspace has no paid plan.
flags[].state on, off or killed.
flags[].rollout_pct 0–100.

The response carries ETag: "<config_version>". Send it back as If-None-Match and an unchanged config answers 304 Not Modified with no body — the cheap way to poll:

curl -i https://app.splittestpro.com/api/html/flags/config \
  -H "Authorization: Bearer $SPLITTESTPRO_API_KEY" \
  -H 'If-None-Match: "9f2c4e1a7b3d5c60"'

POST …/flags/evaluate

Returns each requested flag’s value for one identity. Evaluate is a pure read: it records nothing and costs nothing against your tested sessions.

Request

Field Type
identity string Required. See Identities.
flags string[] Optional. Up to 50 keys; omitted or empty evaluates every flag.
assignments string Optional. The pixel’s assignment string, expId:varId|expId:varId, for hybrid tests. Pairs that don’t belong to this workspace are ignored.
curl https://app.splittestpro.com/api/html/flags/evaluate \
  -H "Authorization: Bearer $SPLITTESTPRO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"identity": "user_8231", "flags": ["new-checkout", "free-shipping-banner", "beta-search"]}'

Response (Cache-Control: no-store)

{
  "config_version": "9f2c4e1a7b3d5c60",
  "entitled": true,
  "frozen": false,
  "flags": {
    "new-checkout": {
      "value": true,
      "reason": "experiment",
      "experiment_id": "3f6c2a8e-5b1d-4c7a-9e2f-0a1b2c3d4e5f",
      "variant_id": "b7e4d1c0-2a9f-4e8b-8c6d-5f4e3d2c1b0a"
    },
    "free-shipping-banner": { "value": false, "reason": "killed" },
    "beta-search": { "value": false, "reason": "unknown" }
  }
}
reason value Meaning
killed false The flag is killed.
experiment the variant’s value A running experiment sets this flag; experiment_id and variant_id name the identity’s variant. A variant that doesn’t set the flag serves false.
rollout true The flag is on and the identity is inside its rollout.
off false The flag is off, or on with the identity outside its rollout.
unknown false No flag has this key.
unentitled false The workspace has no paid plan (entitled: false). Every flag answers this way.

experiment_id and variant_id appear only with the experiment reason — they are what you send to the exposure call.

frozen is true while the workspace is over its monthly sessions limit (the enrollment freeze). No new identities enter experiments: an identity whose assignments names the experiment keeps that variant, and every other one gets the flag’s own state.

POST …/flags/exposure

Records that an identity experienced a variant of a server-side experiment. Call it at the moment the user sees the variant — when the page renders, the email sends, the price is quoted — not on every evaluate. An exposure is what enters a user into the results and counts toward tested sessions (once per distinct identity per month).

Field Type
identity string Required.
experiment_id string Required. From the evaluate response.
variant_id string Required. From the evaluate response.
event_id string Required, at most 128 characters. Your idempotency key — a UUID you generate once per exposure and reuse on retries.
curl https://app.splittestpro.com/api/html/flags/exposure \
  -H "Authorization: Bearer $SPLITTESTPRO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"identity": "user_8231", "experiment_id": "3f6c2a8e-5b1d-4c7a-9e2f-0a1b2c3d4e5f", "variant_id": "b7e4d1c0-2a9f-4e8b-8c6d-5f4e3d2c1b0a", "event_id": "0d9b8f3e-6a1c-4f2e-9d7b-1c2a3b4c5d6e"}'
{ "recorded": true }
Answer Meaning
200 {"recorded": true} Recorded.
200 {"recorded": false, "reason": "duplicate"} This event_id was already recorded — a retry or a replay. Nothing new was written.
200 {"recorded": false, "reason": "frozen"} The workspace is over its monthly sessions limit; nothing was written.
200 {"recorded": false, "reason": "not_running"} The experiment isn’t running (paused, completed or not started).
409 pixel_owned The experiment runs On your site: the pixel records its exposures, and a server exposure would count the visitor twice. Don’t send exposures for hybrid tests.
400 invalid_request A missing field, an event_id over 128 characters, or an experiment or variant that isn’t in this workspace.

POST …/flags/conversion

Records a conversion for an identity — a purchase confirmed by your payment provider, a signup completed on the server.

Field Type
identity string Required.
event_id string Required, at most 128 characters. An order id makes a natural idempotency key.
event_type string Optional, default conversion. The goal’s event key: lowercase letters, digits and _ (not page_view or page_viewed). Use the keys your conversion goals use.
value number Optional. A finite number, magnitude at most 1e15 — revenue for continuous metrics.
currency string Optional. Three-letter ISO 4217 code, e.g. USD.
has_subscription boolean Optional.
assignments string Optional. The pixel’s assignment string. When present, the conversion is attributed to the experiments it names (those in this workspace). When absent, the identity is attributed to every running server-side experiment it is enrolled in — except during the enrollment freeze, when nothing is derived.
curl https://app.splittestpro.com/api/html/flags/conversion \
  -H "Authorization: Bearer $SPLITTESTPRO_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"identity": "user_8231", "event_type": "purchase", "event_id": "order_10482", "value": 49.0, "currency": "USD"}'
Answer Meaning
200 {"recorded": true} Recorded against each attributed experiment.
200 {"recorded": false, "reason": "duplicate"} This event_id was already recorded.
200 {"recorded": false, "reason": "unassigned"} The identity is in no running experiment (or none of the passed assignments belong to this workspace).
400 invalid_request A missing event_id, a bad event_type, value or currency. Unlike the pixel, which drops bad fields silently, the API rejects the whole call.

Server events carry device type Server in Results, no page URL (URL-based goals don’t fire from them) and no new-or-returning tag.

Errors

Every non-2xx answer is JSON with a machine-readable error and a sentence you can log:

{ "error": "invalid_identity", "message": "identity must not contain '@' — send an opaque id, never an email address." }
Status error When
400 invalid_request Malformed JSON, a missing or invalid field, more than 50 flags.
400 invalid_identity The identity breaks the identity rules.
401 invalid_key No key, a malformed, unknown or revoked key, or a key in the query string.
409 pixel_owned An exposure for an experiment served On your site.
429 rate_limited Over the key’s rate limit; retry after Retry-After seconds.
500 internal_error Something failed on our side; retry with backoff (same event_id).

A workspace without a paid plan is not an error: config and evaluate answer 200 with entitled: false, and every flag is false with the reason unentitled.

Rate Limits

Limits are per key, per minute:

Calls Limit
GET …/flags/config 120 / minute
POST …/flags/evaluate 600 / minute
POST …/flags/exposure + POST …/flags/conversion (shared) 1,200 / minute

Over the limit, the API answers 429 rate_limited with a Retry-After header in seconds. Requests with an invalid key are limited to 60 a minute per IP address. To stay well inside the evaluate limit, cache results: poll config with If-None-Match and re-evaluate only when config_version changes — what the Node SDK does.

Next Steps

Ready to ship a winner?
Start free today.

Sign up free in under a minute — no credit card, no installation steps until you're ready.

  • No credit card
  • 14-day free trial