Reference

Experiments

Run two versions of a policy against each other on real signups, and score them on what those signups went on to do — not on how many Sortance let in.

Endpoints

MethodPathDoes
GET/v1/experimentsEvery experiment.
POST/v1/experimentsCreate one as a draft. Nothing changes until you start it.
POST/v1/experiments/compile{ text } — English in, a draft configuration out. Saves nothing.
GET/v1/experiments/:slugOne experiment, with status, started_at and stopped_at.
POST/v1/experiments/:slug/startDraft to running. Within about a minute, /v1/decide starts assigning requests to variants.
POST/v1/experiments/:slug/stopRunning to stopped. Within about a minute, /v1/decide goes back to the active version alone.
GET/v1/experiments/:slug/reportThe report. 404 while the experiment is still a draft.

The configuration

POST /v1/experiments
{
  "slug": "block-freemail-q4",
  "hypothesis": "Reviewing consumer mailboxes cuts support load without cutting paid conversion.",
  "policy": "default",
  "variants": [
    { "name": "control",   "version": 14, "weight": 80 },
    { "name": "treatment", "version": 15, "weight": 20 }
  ],
  "mode": "enforce",
  "metric": { "event": "subscribed", "window_days": 14 },
  "guardrails": [ { "event": "support_ticket", "max_rate_delta": 0.02 } ],
  "min_sample": 400
}
FieldMeaning
slugLowercase letters, digits and -, up to 63 characters. Unique per account.
hypothesisOptional, up to 500 characters. What you expect, in a sentence.
policyThe policy slug whose versions are being compared. It must exist.
variantsOne or two of { name, version, weight }. The first is the control. Weights are integers that sum to 100; every version must exist.
modeenforce or shadow.
metric{ event, window_days } — the funnel event that counts as success, and how long after the decision it may arrive (1–365 days).
guardrailsOptional, up to 5 of { event, max_rate_delta } — an event that must not rise by more than this between variants.
min_sampleInteger, at least 30. No rate and no probability is shown until every variant has this many exposures.

Enforce and shadow

  • Assignment is per person, not per request. A request’s bucket is a hash of the experiment’s own salt and the hashed address, mod 100, cut by the weights. Someone who retries your signup form lands in the same variant every time.
  • enforce returns the assigned variant’s decision. The experiment block on the decide response names the slug, the variant and the mode.
  • shadow evaluates every variant and records each decision, but the active version answers. The response’s experiment.shadow_decision is what the assigned variant would have done. A one-variant shadow experiment is how you try a new version with zero risk to signups before enforcing it.
  • One running experiment per policy at a time. A shadow variant that reads enrichment buys it, so starting one can change cost even though it cannot change a decision.

GET /v1/experiments/:slug/report

FieldMeaning
statusdraft, running, stopped or concluded.
started_atISO 8601, or null.
exposuresDecisions per variant.
decisionsThe decision mix per variant.
metric.control, metric.treatment{ events, rate, ci } — rate is the posterior mean and ci the 90% credible interval. Both null below min_sample.
metric.p_treatment_betterThe probability the treatment's rate is higher. Null below min_sample, with one variant, or until the nightly run has computed it.
metric.expected_loss{ choose_control, choose_treatment } — the expected cost of being wrong either way, in rate units. Null under the same conditions.
metric.verdictinsufficient_data, inconclusive, treatment_better or control_better.
metric.saysOne plain sentence, e.g. "Not enough data yet — 212 of 400 minimum exposures in treatment."
guardrails{ event, delta, ok } per guardrail. Null values while there is not enough data.
shadowIn shadow mode, each variant's decision mix side by side. Otherwise null.

How it is analysed

  • Each variant’s metric rate is a Beta posterior over its events and exposures, starting from a uniform Beta(1, 1) prior.
  • The probability that treatment beats control comes from 10,000 Monte Carlo draws from the two posteriors. A verdict is called at 90%: treatment_better at 0.9 or above, control_better at 0.1 or below, otherwise inconclusive.
  • The draws run in the nightly job, never on a request. Before the first nightly run, the report shows exposures, decision mix and per-variant rates with the probability left null and a sentence saying it is computed nightly.
  • Below min_sample in any variant the verdict is insufficient_data and no rate or probability is shown. A three-exposure experiment at 100% is the most common way a dashboard lies.
  • A shadow experiment’s verdict is always inconclusive: every request received the same decision, so the metric cannot separate the variants. Compare the decision mix, then run it in enforce mode to measure the outcome.
  • Metrics come only from events — ones you send to /v1/events or ones Stripe sends. Sortance’s own decision counts are never the metric.

Errors

StatusCodeWhen
400invalid_requestThe configuration fails validation (the first five problems are in the message), or starting or stopping from the wrong status.
404not_foundNo such experiment, or a report requested for a draft.
409policy_conflictAn experiment with that slug exists, or another experiment is already running on the same policy.
422compile_failedThe compiler could not produce a valid configuration, or is not configured on this deployment.