ShipEasy
Flags & ExperimentsAPI

API reference

REST endpoints for gates, configs, killswitches, experiments, and universes — with live request samples in cURL, JavaScript, and Python.

The Admin API is the same surface the dashboard, CLI, and MCP server use. Every request:

  • authenticates with Authorization: Bearer sdk_admin_…
  • scopes to a project via X-Project-Id: <projectId>
  • speaks JSON, returns conventional HTTP status codes
curl https://shipeasy.ai/api/admin/gates \
  -H "Authorization: Bearer sdk_admin_..." \
  -H "X-Project-Id: prj_..."

Mint admin keys via POST /api/admin/keys with type: "admin". Keys expire after 90 days; rotate with the revoke action.

Common patterns

Pagination. List endpoints return { data: [...], next_cursor: "..." }. Pass ?cursor=<next_cursor> and ?limit=<n> to page.

Errors. All errors share a single envelope:

{ "error": { "code": "not_found", "message": "Gate 'foo' not found" } }

Idempotency. POST writes that create resources accept Idempotency-Key: <uuid> and replay the same response if you retry within 24h.

Gates

GET/api/admin/gates

List feature gates

Returns a single page of gates ordered by updated_at desc, id desc. Use the cursor query parameter to paginate.

Use caseSnapshot every gate in the project — for example to render an admin overview or to drive a CI check that asserts no gate is left at 100% in staging.

Parameters

NameInTypeDescription
X-Project-IdrequiredheaderstringProject to scope this request to.
limitquerynumberMax results per page (default 50, max 500).
cursorquerystringOpaque pagination cursor from a prior page's `next_cursor`.

Response · 200

NameTypeDescription
datarequiredarray<{ id: string; name: string; enabled: boolean | integer; rolloutPct: integer; rules?: array<{ attr: string; op: "eq" | "neq" | "in" | "not_in" | "gt" | "gte" | "lt" | "lte" | "contains" | "regex"; value: any }>; salt?: string; title?: string | null; description?: string | null; folder?: string | null; groupName?: string | null; ownerEmail?: string | null; stack?: array<{ id: string; type: "condition"; name?: string; fromTemplate?: string | null; pass?: "all" | "any"; rules?: array<object>; locked?: boolean } | { id: string; type: "rollout"; name?: string; fromTemplate?: string | null; rolloutPct: integer; bucketBy?: string; salt?: string; locked?: boolean }> | null; updatedAt: string }>
next_cursorrequiredstring | null
Example · 200
{
  "data": [
    {
      "id": "gat_01j7w7m9q4hxbf6npe6s9zr3vc",
      "name": "checkout_v2",
      "enabled": 1,
      "rolloutPct": 5000,
      "rules": [
        {
          "attr": "country",
          "op": "in",
          "value": [
            "US",
            "CA",
            "GB"
          ]
        },
        {
          "attr": "plan",
          "op": "neq",
          "value": "free"
        }
      ],
      "salt": "9c1f4f1f2c0c4a5fa1c2b6d3e7c8e3a1",
      "title": "Checkout v2",
      "description": "New checkout flow. Pro users in US/CA/GB only.",
      "folder": "checkout",
      "groupName": "growth",
      "ownerEmail": "ana@example.com",
      "stack": null,
      "updatedAt": "2026-05-09T16:01:22.000Z"
    }
  ],
  "next_cursor": null
}
POST/api/admin/gates

Create a feature gate

Creates a new gate. Default enabled: true at the supplied rollout_pct (0 = fully dark).

Only name is required. Request fields use snake_case (owner_email); the GET response returns camelCase (ownerEmail, groupName).

Returns 409 if name already exists in the project (case-sensitive).

Use cases

Use caseDescriptionExample
Dark create + ramp later{ "name": "checkout_v2" } at 0% rollout. Ramp via PATCH after deploy validation.
Targeted rolloutsupply rules to gate the caller (e.g. only plan = pro users) plus a rollout_pct to bucket within that audience.
Gatekeeper stacksupply stack instead of rules/rollout_pct for internal ∪ beta ∪ public fall-through. Stack entries evaluated top-to-bottom; first match wins.
Dashboard metadatapopulate title, description, folder, group, owner_email so the admin UI is self-documenting from day one.
Disabled on createpre-provision with enabled: false for a future launch; flip on with POST /{id}/enable at go-live.

Parameters

NameInTypeDescription
X-Project-IdrequiredheaderstringProject to scope this request to.

Body

NameTypeDescription
namerequiredstringStable gate key used by SDKs (`Shipeasy.checkGate(user, '<name>')`). Lowercase letters, digits, `_` or `-`, must start with a letter/digit, max 64 chars. Immutable after create — rename = delete + recreate.
enabledbooleanMaster switch. Defaults to `true`. Set `false` to create the gate disabled (evaluates to `false` regardless of rules/rollout); flip on via `POST /{id}/enable` or PATCH. default: true
rollout_pctintegerInitial rollout in basis points (0–10000 = 0%–100%). Use `0` to create the gate dark and ramp via PATCH after deploy validation. default: 0
rulesarray<{ attr: string; op: "eq" | "neq" | "in" | "not_in" | "gt" | "gte" | "lt" | "lte" | "contains" | "regex"; value: any }>Targeting predicates. AND-combined. If non-empty, the gate returns `true` only for callers that satisfy every rule **and** fall under `rollout_pct`. default: []
saltstringHash salt for percentage bucketing. Auto-generated if omitted. Provide explicitly to keep a gate's buckets stable across delete/recreate. **Immutable after create** — there is no PATCH for `salt` because changing it would re-bucket every caller.
stackarray<{ id: string; type: "condition"; name?: string; fromTemplate?: string | null; pass?: "all" | "any"; rules?: array<{ attr: string; op: "eq" | "neq" | "in" | "not_in" | "gt" | "gte" | "lt" | "lte" | "contains" | "regex"; value: any }>; locked?: boolean } | { id: string; type: "rollout"; name?: string; fromTemplate?: string | null; rolloutPct: integer; bucketBy?: string; salt?: string; locked?: boolean }> | nullOptional gatekeeper stack. When provided, takes precedence over `rules` + `rollout_pct` at evaluation time. Omit (or pass `null`) for a flat gate.
titlestringHuman-readable title shown in the dashboard. Free-form, no key format constraint.
descriptionstringLong-form description / runbook. Markdown is rendered in the dashboard.
folderstringFolder label for dashboard organisation. Free-form; folders are inferred from the set of values.
groupstringGroup label for dashboard organisation (e.g. team or product area).
owner_emailstringOwner contact. Displayed verbatim; not used for auth.

Response · 201

NameTypeDescription
idrequiredstringNewly assigned gate id (`gat_…`).
namerequiredstringStable gate key used by SDKs (`Shipeasy.checkGate(user, '<name>')`). Lowercase letters, digits, `_` or `-`, must start with a letter/digit, max 64 chars. Immutable after create — rename = delete + recreate.
Example · 201
{
  "id": "gat_01j7w7m9q4hxbf6npe6s9zr3vc",
  "name": "checkout_v2"
}
PATCH/api/admin/gates/{id}

Update a feature gate

Partial update — only supplied fields change. Array fields (rules, stack) replace wholesale; there is no merge or append.

name and the gate id are immutable. The response carries only { id } — re-fetch via GET /api/admin/gates for the new row.

Use cases

Use caseDescriptionExample
Ramp rollout{ "rollout_pct": 5000 } for 50%. Basis points (0–10000); 100 = 1%.
Kill switch{ "enabled": false }. Forces evaluation to false for every caller regardless of rules/rollout. Re-enable with POST /{id}/enable or { "enabled": true }.
Modify a rule's `in` setsend the full new rules array. To add 'GB' to ['US','CA']: { "rules": [{ "attr": "country", "op": "in", "value": ["US","CA","GB"] }] }. No per-rule patch endpoint.
Add targeting from scratch{ "rules": [{ "attr": "email", "op": "regex", "value": "@acme\\.com$" }] }.
Switch to gatekeeper stacksend a non-null stack. To revert to flat eval, send { "stack": null }.
Update metadataany subset of title, description, folder, group, owner_email.

Parameters

NameInTypeDescription
X-Project-IdrequiredheaderstringProject to scope this request to.
idrequiredpathstringStable opaque gate id (`gat_…`).

Body

NameTypeDescription
rollout_pctintegerNew rollout in basis points (0–10000 = 0%–100%). Omit to leave unchanged.
rulesarray<{ attr: string; op: "eq" | "neq" | "in" | "not_in" | "gt" | "gte" | "lt" | "lte" | "contains" | "regex"; value: any }>Replaces the rule list wholesale. To add a value to an `in` rule, send the full new `rules` array with the augmented `value` (e.g. previous `['US','CA']` → `['US','CA','GB']`).
enabledbooleanMaster switch. `false` makes the gate evaluate to `false` for every caller regardless of `rollout_pct`, `rules`, or `stack` — use as kill switch.
stackarray<{ id: string; type: "condition"; name?: string; fromTemplate?: string | null; pass?: "all" | "any"; rules?: array<{ attr: string; op: "eq" | "neq" | "in" | "not_in" | "gt" | "gte" | "lt" | "lte" | "contains" | "regex"; value: any }>; locked?: boolean } | { id: string; type: "rollout"; name?: string; fromTemplate?: string | null; rolloutPct: integer; bucketBy?: string; salt?: string; locked?: boolean }> | nullReplaces the gatekeeper stack wholesale. Send `null` to revert to flat `rules` + `rollout_pct` evaluation.
titlestringHuman-readable title shown in the dashboard. Free-form, no key format constraint.
descriptionstringLong-form description / runbook. Markdown is rendered in the dashboard.
folderstringFolder label for dashboard organisation. Free-form; folders are inferred from the set of values.
groupstringGroup label for dashboard organisation (e.g. team or product area).
owner_emailstringOwner contact. Displayed verbatim; not used for auth.

Response · 200

NameTypeDescription
idrequiredstringGate id that was updated.
Example · 200
{
  "id": "gat_01j7w7m9q4hxbf6npe6s9zr3vc"
}
DELETE/api/admin/gates/{id}

Delete a feature gate

Soft-deletes the gate. Returns 409 if the gate is still referenced by a running experiment as a targeting gate — stop the experiment first.

Use caseTear down a gate after a feature has fully shipped and the rollout flag is no longer needed.

Parameters

NameInTypeDescription
X-Project-IdrequiredheaderstringProject to scope this request to.
idrequiredpathstringStable opaque gate id (`gat_…`).

Response · 200

NameTypeDescription
okrequiredtrue
Example · 200
{
  "ok": true
}
POST/api/admin/gates/{id}/enable

Enable a gate

Sets enabled: true. The current rollout_pct is preserved.

Use caseRe-enable a previously disabled gate without re-issuing a full update.

Parameters

NameInTypeDescription
X-Project-IdrequiredheaderstringProject to scope this request to.
idrequiredpathstringStable opaque gate id.

Response · 201

NameTypeDescription
idrequiredstring
enabledrequiredboolean
Example · 201
{
  "id": "gat_01j7w7m9q4hxbf6npe6s9zr3vc",
  "enabled": true
}
POST/api/admin/gates/{id}/disable

Disable a gate

Sets enabled: false so the gate evaluates to false for every caller, regardless of rollout_pct or rules. Use as a quick kill switch.

Use caseFlip a gate off in production without redeploying — the canonical kill-switch flow.

Parameters

NameInTypeDescription
X-Project-IdrequiredheaderstringProject to scope this request to.
idrequiredpathstringStable opaque gate id.

Response · 201

NameTypeDescription
idrequiredstring
enabledrequiredboolean
Example · 201
{
  "id": "gat_01j7w7m9q4hxbf6npe6s9zr3vc",
  "enabled": false
}

Experiments

GET/api/admin/experiments

List experiments

Returns a single page of non-archived experiments ordered by updated_at desc, id desc. Use the cursor query parameter to paginate.

Use caseSnapshot every active experiment in the project — e.g. render an overview dashboard or drive a CI check that no experiment has been running past its min_runtime_days.

Parameters

NameInTypeDescription
X-Project-IdrequiredheaderstringProject to scope this request to.
limitquerynumberMax results per page (default 50, max 500).
cursorquerystringOpaque pagination cursor from a prior page's `next_cursor`.

Response · 200

NameTypeDescription
datarequiredarray<{ id: string; name: string; description: string | null; tag: string | null; status: "draft" | "running" | "stopped" | "archived"; universe: string; targetingGate: string | null; allocationPct: integer; salt: string; params: object; groups: array<{ name: string; weight: integer; params?: object }>; significanceThreshold: number; minRuntimeDays: integer; minSampleSize: integer; sequentialTesting: boolean; startedAt: string | null; stoppedAt?: string | null; updatedAt: string }>
next_cursorrequiredstring | null
Example · 200
{
  "data": [
    {
      "id": "exp_01j7wb12c3d4e5f6g7h8j9k0l1",
      "name": "checkout_button_color",
      "description": "Test green vs. blue CTA on the checkout page.",
      "tag": "checkout",
      "status": "running",
      "universe": "primary_users",
      "targetingGate": null,
      "allocationPct": 5000,
      "salt": "8d3e9a1f6b7c4a5fa1c2b6d3e7c8e3a1",
      "params": {
        "cta_color": "string"
      },
      "groups": [
        {
          "name": "control",
          "weight": 5000,
          "params": {
            "cta_color": "blue"
          }
        },
        {
          "name": "treatment",
          "weight": 5000,
          "params": {
            "cta_color": "green"
          }
        }
      ],
      "significanceThreshold": 0.05,
      "minRuntimeDays": 7,
      "minSampleSize": 1000,
      "sequentialTesting": false,
      "startedAt": "2026-05-01T12:00:00.000Z",
      "stoppedAt": null,
      "updatedAt": "2026-05-09T18:22:11.000Z"
    }
  ],
  "next_cursor": null
}
POST/api/admin/experiments

Create an experiment

Creates a new experiment in draft status. name, universe, and groups are required; everything else has sensible defaults.

Returns 409 if name already exists, 422 if the named universe doesn't exist, 403 if a plan-gated option is set (sequential_testing, custom significance_threshold) on a plan that doesn't include it.

Use cases

Use caseDescriptionExample
Minimal 50/50name + universe + two equal-weight groups.
Targeted rolloutsupply targeting_gate to restrict the eligible audience and allocation_pct to enrol a slice of it.
Multivariantthree or more groups with weights summing to 10000.
Sequential testingsequential_testing: true for Premium plans.

Parameters

NameInTypeDescription
X-Project-IdrequiredheaderstringProject to scope this request to.

Body

NameTypeDescription
namerequiredstringStable experiment key (a-z, 0-9, `_`/`-`, must start with letter/digit, max 64 chars). Used by SDKs as `Shipeasy.getExperiment(user, '<name>')`. Immutable after create.
descriptionstring | nullFree-form description. Max 2000 chars, markdown rendered in the dashboard. default: null
tagstring | nullOptional tag used to group experiments in the dashboard. default: null
universerequiredstringName of an existing universe in the project. Returns `422` if the universe doesn't exist.
targeting_gatestring | nullOptional gate name. Only callers that pass the gate are enrolled in the experiment. default: null
allocation_pctintegerShare of the (gated) audience allocated to the experiment, in basis points (0–10000 = 0%–100%). `0` = unallocated. Immutable while the experiment is running. default: 0
saltstringHash salt for bucketing. Auto-generated if omitted. Immutable while running.
paramsobjectMap of param-name → scalar type. Defines the shape of `groups[].params`. Example: `{ headline: 'string', show_cta: 'bool' }`. default: {}
groupsrequiredarray<{ name: string; weight: integer; params?: object }>Two or more variants. Weights must sum to exactly 10000 (100%). Immutable while running.
significance_thresholdnumberp-value cutoff used by the analysis pass. Defaults to `0.05`. Values other than 0.05 require Pro plan or higher. default: 0.05
min_runtime_daysintegerMinimum days the experiment must run before results are considered conclusive. default: 0
min_sample_sizeintegerMinimum exposures per group before results are considered conclusive. default: 100
sequential_testingbooleanEnable sequential testing (always-valid p-values). Requires Premium plan or higher. default: false

Response · 201

NameTypeDescription
idrequiredstringNewly assigned experiment id.
namerequiredstringStable experiment key (a-z, 0-9, `_`/`-`, must start with letter/digit, max 64 chars). Used by SDKs as `Shipeasy.getExperiment(user, '<name>')`. Immutable after create.
Example · 201
{
  "id": "exp_01j7wb12c3d4e5f6g7h8j9k0l1",
  "name": "checkout_button_color"
}
GET/api/admin/experiments/{id}

Get one experiment

Returns the full experiment row including groups, params, allocation, and lifecycle timestamps.

Use caseFetch one experiment to render the detail page or to inspect its current allocation and group weights.

Parameters

NameInTypeDescription
X-Project-IdrequiredheaderstringProject to scope this request to.
idrequiredpathstringStable opaque experiment id (`exp_…`) or the experiment's `name`.

Response · 200

NameTypeDescription
idrequiredstringStable opaque experiment id (`exp_…`).
namerequiredstringStable experiment key (a-z, 0-9, `_`/`-`, must start with letter/digit, max 64 chars). Used by SDKs as `Shipeasy.getExperiment(user, '<name>')`. Immutable after create.
descriptionrequiredstring | null
tagrequiredstring | null
statusrequired"draft" | "running" | "stopped" | "archived"
universerequiredstringUniverse name this experiment draws from.
targetingGaterequiredstring | null
allocationPctrequiredintegerAllocation in basis points (0–10000).
saltrequiredstring
paramsrequiredobject
groupsrequiredarray<{ name: string; weight: integer; params?: object }>
significanceThresholdrequirednumber
minRuntimeDaysrequiredinteger
minSampleSizerequiredinteger
sequentialTestingrequiredboolean
startedAtrequiredstring | nullISO-8601 timestamp the experiment last transitioned to `running`, or `null`.
stoppedAtstring | null
updatedAtrequiredstring
Example · 200
{
  "id": "exp_01j7wb12c3d4e5f6g7h8j9k0l1",
  "name": "checkout_button_color",
  "description": "Test green vs. blue CTA on the checkout page.",
  "tag": "checkout",
  "status": "running",
  "universe": "primary_users",
  "targetingGate": null,
  "allocationPct": 5000,
  "salt": "8d3e9a1f6b7c4a5fa1c2b6d3e7c8e3a1",
  "params": {
    "cta_color": "string"
  },
  "groups": [
    {
      "name": "control",
      "weight": 5000,
      "params": {
        "cta_color": "blue"
      }
    },
    {
      "name": "treatment",
      "weight": 5000,
      "params": {
        "cta_color": "green"
      }
    }
  ],
  "significanceThreshold": 0.05,
  "minRuntimeDays": 7,
  "minSampleSize": 1000,
  "sequentialTesting": false,
  "startedAt": "2026-05-01T12:00:00.000Z",
  "stoppedAt": null,
  "updatedAt": "2026-05-09T18:22:11.000Z"
}
PATCH/api/admin/experiments/{id}

Update an experiment

Partial update. allocation_pct, groups, salt, universe, params are immutable while running — returns 409 if you try. Stop the experiment first.

Editing groups while in draft is fine; weights must still sum to 10000.

Use cases

Use caseDescriptionExample
Update metadatadescription, tag, targeting_gate editable any time.
Ramp before launchset allocation_pct while still in draft.
Tighten significancesignificance_threshold (Pro+).
Rewire groupsreplace groups wholesale while in draft; immutable once running.

Parameters

NameInTypeDescription
X-Project-IdrequiredheaderstringProject to scope this request to.
idrequiredpathstringStable opaque experiment id.

Body

NameTypeDescription
namestringStable experiment key (a-z, 0-9, `_`/`-`, must start with letter/digit, max 64 chars). Used by SDKs as `Shipeasy.getExperiment(user, '<name>')`. Immutable after create.
descriptionstring | null
tagstring | null
targeting_gatestring | null
allocation_pctintegerBasis-points allocation (0–10000). Immutable while the experiment is running.
saltstringHash salt. Immutable while running.
universestringNew universe name. Immutable while running. Returns `422` if the universe doesn't exist.
paramsobjectMap of param-name → scalar type. Defines the shape of `groups[].params`. Example: `{ headline: 'string', show_cta: 'bool' }`.
groupsarray<{ name: string; weight: integer; params?: object }>Replacement groups. Weights must sum to 10000. Immutable while running.
significance_thresholdnumber
min_runtime_daysinteger
min_sample_sizeinteger
sequential_testingboolean

Response · 200

NameTypeDescription
idrequiredstringExperiment id that was updated.
Example · 200
{
  "id": "exp_01j7wb12c3d4e5f6g7h8j9k0l1"
}
DELETE/api/admin/experiments/{id}

Delete an experiment

Archives the experiment (soft-delete via status transition). Returns 409 if the experiment is still running — stop it first.

Use caseTear down an experiment after the analysis is signed off.

Parameters

NameInTypeDescription
X-Project-IdrequiredheaderstringProject to scope this request to.
idrequiredpathstringStable opaque experiment id.

Response · 200

NameTypeDescription
okrequiredtrue
Example · 200
{
  "ok": true
}
POST/api/admin/experiments/{id}/status

Transition experiment status

Drives the experiment lifecycle. Allowed transitions:

- draft → running — starts allocation. Bumps the startedAt timestamp. - running → stopped — halts allocation. Existing exposures stay in the dataset. - stopped → archived — soft-delete. - draft → archived — discard an unstarted experiment.

Restarting an archived experiment is not allowed; clone instead. Returns 409 on illegal transitions and 429 if the plan's experiments_running limit is exceeded on → running.

Use cases

Use caseDescriptionExample
Start{ "status": "running" } after wiring up the SDK and verifying targeting on staging.
Stop{ "status": "stopped" } once the experiment hits its min_runtime_days and conclusive results land.
Archive{ "status": "archived" } to soft-delete after sign-off.

Parameters

NameInTypeDescription
X-Project-IdrequiredheaderstringProject to scope this request to.
idrequiredpathstringStable opaque experiment id.

Body

NameTypeDescription
statusrequired"draft" | "running" | "stopped" | "archived"Target status. Allowed transitions: `draft → running`, `running → stopped`, `stopped → archived`, `draft → archived`. Restarting an archived experiment is not allowed.

Response · 201

NameTypeDescription
idrequiredstring
statusrequired"draft" | "running" | "stopped" | "archived"
Example · 201
{
  "id": "exp_01j7wb12c3d4e5f6g7h8j9k0l1",
  "status": "running"
}
POST/api/admin/experiments/{id}/metrics

Attach metrics

Replaces the experiment's metric attachments wholesale. Each entry pairs an existing metric_id with a role (goal / guardrail / secondary).

Returns 422 if any metric_id doesn't exist in the project. Pass { metrics: [] } to detach everything.

Use cases

Use caseDescriptionExample
Standard setupone goal, one or two guardrail, optional secondary metrics for diagnostics.
Detach allsend { "metrics": [] } before archiving.

Parameters

NameInTypeDescription
X-Project-IdrequiredheaderstringProject to scope this request to.
idrequiredpathstringStable opaque experiment id.

Body

NameTypeDescription
metricsrequiredarray<{ metric_id: string; role: "goal" | "guardrail" | "secondary" }>Replacement metrics list — replaces the current attachments wholesale.

Response · 201

NameTypeDescription
idrequiredstring
metricsrequiredarray<{ metric_id: string; role: "goal" | "guardrail" | "secondary" }>
Example · 201
{
  "id": "exp_01j7wb12c3d4e5f6g7h8j9k0l1",
  "metrics": [
    {
      "metric_id": "met_checkout_completed",
      "role": "goal"
    },
    {
      "metric_id": "met_page_errors",
      "role": "guardrail"
    }
  ]
}
GET/api/admin/experiments/{id}/results

Get analysis results

Returns the latest analysis output for the experiment — one row per metric/group/day, including sample size, mean, % delta vs. control, p-value, and a sample-ratio mismatch flag.

Use caseRender the results table on the experiment detail page or drive an automated decision once a goal metric reaches significance.

Parameters

NameInTypeDescription
X-Project-IdrequiredheaderstringProject to scope this request to.
idrequiredpathstringStable opaque experiment id.

Response · 200

NameTypeDescription
experimentrequired{ id: string; name: string; status: "draft" | "running" | "stopped" | "archived" }
resultsrequiredarray<{ metric: string; group_name: string; ds: string; n: number | null; mean: number | null; delta_pct: number | null; p_value: number | null; srm_detected: number | null }>
Example · 200
{
  "experiment": {
    "id": "exp_01j7wb12c3d4e5f6g7h8j9k0l1",
    "name": "checkout_button_color",
    "status": "running"
  },
  "results": [
    {
      "metric": "checkout_completed",
      "group_name": "control",
      "ds": "2026-05-09",
      "n": 12421,
      "mean": 0.1834,
      "delta_pct": null,
      "p_value": null,
      "srm_detected": 0
    },
    {
      "metric": "checkout_completed",
      "group_name": "treatment",
      "ds": "2026-05-09",
      "n": 12519,
      "mean": 0.1922,
      "delta_pct": 4.8,
      "p_value": 0.018,
      "srm_detected": 0
    }
  ]
}
GET/api/admin/experiments/{id}/timeseries

Get analysis timeseries

Same row shape as /results, but returns every daily slice rather than the latest. Filter to a single metric with the metric query parameter.

Use caseDrive a chart of metric movement over the experiment runtime, or sanity-check the lift is monotonic before deciding.

Parameters

NameInTypeDescription
X-Project-IdrequiredheaderstringProject to scope this request to.
idrequiredpathstringStable opaque experiment id.
metricquerystringOptional metric name to filter the series.

Response · 200

NameTypeDescription
experimentrequired{ id: string; name: string; status: "draft" | "running" | "stopped" | "archived" }
seriesrequiredarray<{ metric: string; group_name: string; ds: string; n: number | null; mean: number | null; delta_pct: number | null; p_value: number | null; srm_detected: number | null }>
Example · 200
{
  "experiment": {
    "id": "exp_01j7wb12c3d4e5f6g7h8j9k0l1",
    "name": "checkout_button_color",
    "status": "running"
  },
  "series": [
    {
      "metric": "checkout_completed",
      "group_name": "treatment",
      "ds": "2026-05-08",
      "n": 11200,
      "mean": 0.1903,
      "delta_pct": 3.9,
      "p_value": 0.034,
      "srm_detected": 0
    },
    {
      "metric": "checkout_completed",
      "group_name": "treatment",
      "ds": "2026-05-09",
      "n": 12519,
      "mean": 0.1922,
      "delta_pct": 4.8,
      "p_value": 0.018,
      "srm_detected": 0
    }
  ]
}
POST/api/admin/experiments/{id}/reanalyze

Re-queue analysis

Requeues the daily analysis pass for this experiment outside the normal cron cadence. Useful after attaching a new metric or correcting an event taxonomy. The job runs asynchronously.

Use caseForce-refresh results after wiring up a new metric without waiting for the next nightly cron tick.

Parameters

NameInTypeDescription
X-Project-IdrequiredheaderstringProject to scope this request to.
idrequiredpathstringStable opaque experiment id.

Response · 201

NameTypeDescription
idrequiredstring
queuedrequiredtrue
Example · 201
{
  "id": "exp_01j7wb12c3d4e5f6g7h8j9k0l1",
  "queued": true
}

Configs

GET/api/admin/configs

List dynamic configs

Returns a single page of configs ordered by updated_at desc, id desc. Each row includes the latest published version per env and any active drafts.

Use caseSnapshot every config in the project — e.g. CI check that asserts no env is stuck on a stale default or that every config has a published value on prod.

Parameters

NameInTypeDescription
X-Project-IdrequiredheaderstringProject to scope this request to.
limitquerynumberMax results per page (default 50, max 500).
cursorquerystringOpaque pagination cursor from a prior page's `next_cursor`.

Response · 200

NameTypeDescription
datarequiredarray<{ id: string; name: string; description: string | null; schema: object; updatedAt: string; envs: object; drafts: object; values?: object; draftValues?: object }>
next_cursorrequiredstring | null
Example · 200
{
  "data": [
    {
      "id": "cfg_01j7wae5h6j7k8l9m0n1p2q3r4",
      "name": "pricing.tiers",
      "description": "Pricing tier definitions consumed by the checkout flow.",
      "schema": {
        "type": "object",
        "properties": {
          "tiers": {
            "type": "array",
            "items": {
              "type": "object"
            }
          }
        },
        "required": [
          "tiers"
        ]
      },
      "updatedAt": "2026-05-09T18:22:11.000Z",
      "envs": {
        "dev": {
          "version": 5,
          "publishedAt": "2026-05-09T18:22:11.000Z",
          "publishedBy": "ana@example.com"
        },
        "stage": {
          "version": 4,
          "publishedAt": "2026-05-08T11:05:22.000Z",
          "publishedBy": "ana@example.com"
        },
        "prod": {
          "version": 4,
          "publishedAt": "2026-05-08T11:05:22.000Z",
          "publishedBy": "ana@example.com"
        }
      },
      "drafts": {
        "dev": {
          "updatedAt": "2026-05-10T09:31:00.000Z",
          "authorEmail": "bo@example.com",
          "baseVersion": 5
        }
      }
    }
  ],
  "next_cursor": null
}
POST/api/admin/configs

Create a dynamic config

Creates a new config with the given schema. The initial value (or an empty object) is published as version 1 on every env.

Returns 409 if name already exists in the project, 400 if value doesn't validate against schema.

Use cases

Use caseDescriptionExample
Minimal createname + schema. Initial value defaults to {}.
Seeded createsupply a flat value to publish the same object on every env.
Per-env seedsupply a { env: value } map for different per-env starting values.

Parameters

NameInTypeDescription
X-Project-IdrequiredheaderstringProject to scope this request to.

Body

NameTypeDescription
namerequiredstringStable config key in `folder.name` form (two lowercase segments separated by a dot, e.g. `pricing.tiers`). Used by SDKs as `Shipeasy.getConfig('<name>')`. Immutable after create.
descriptionstringOptional free-form description shown in the dashboard. Max 512 chars.
schemarequiredobjectJSON Schema (draft 2020-12) describing the shape of the config value. Top-level `type` must be `'object'`; every published value is validated against this schema.
valueany | objectInitial config value. Either a single JSON object applied to every env, or a `{ env: value }` map seeding per-env values. Must match `schema`. Defaults to `{}` on every env when omitted.

Response · 201

NameTypeDescription
idrequiredstringNewly assigned config id.
namerequiredstringStable config key in `folder.name` form (two lowercase segments separated by a dot, e.g. `pricing.tiers`). Used by SDKs as `Shipeasy.getConfig('<name>')`. Immutable after create.
Example · 201
{
  "id": "cfg_01j7wae5h6j7k8l9m0n1p2q3r4",
  "name": "pricing.tiers"
}
GET/api/admin/configs/{id}

Get one config

Returns config metadata plus the latest published values per env and any active draft values. Use this to fetch the JSON the editor renders.

Use caseFetch one config's current published values and any in-flight drafts.

Parameters

NameInTypeDescription
X-Project-IdrequiredheaderstringProject to scope this request to.
idrequiredpathstringStable opaque config id (`cfg_…`) or the config's `name`.

Response · 200

NameTypeDescription
idrequiredstringStable opaque config id (`cfg_…`).
namerequiredstringStable config key in `folder.name` form (two lowercase segments separated by a dot, e.g. `pricing.tiers`). Used by SDKs as `Shipeasy.getConfig('<name>')`. Immutable after create.
descriptionrequiredstring | null
schemarequiredobjectJSON Schema (draft 2020-12) describing the shape of the config value. Top-level `type` must be `'object'`; every published value is validated against this schema.
updatedAtrequiredstringISO-8601 timestamp of last mutation.
envsrequiredobjectPer-env latest published version metadata.
draftsrequiredobjectPer-env active drafts (if any).
valuesobjectPer-env latest published values (only returned by `GET /{id}`, not list).
draftValuesobjectPer-env draft values (only returned by `GET /{id}`).
Example · 200
{
  "id": "cfg_01j7wae5h6j7k8l9m0n1p2q3r4",
  "name": "pricing.tiers",
  "description": "Pricing tier definitions consumed by the checkout flow.",
  "schema": {
    "type": "object",
    "properties": {
      "tiers": {
        "type": "array",
        "items": {
          "type": "object"
        }
      }
    },
    "required": [
      "tiers"
    ]
  },
  "updatedAt": "2026-05-09T18:22:11.000Z",
  "envs": {
    "dev": {
      "version": 5,
      "publishedAt": "2026-05-09T18:22:11.000Z",
      "publishedBy": "ana@example.com"
    },
    "stage": {
      "version": 4,
      "publishedAt": "2026-05-08T11:05:22.000Z",
      "publishedBy": "ana@example.com"
    },
    "prod": {
      "version": 4,
      "publishedAt": "2026-05-08T11:05:22.000Z",
      "publishedBy": "ana@example.com"
    }
  },
  "drafts": {
    "dev": {
      "updatedAt": "2026-05-10T09:31:00.000Z",
      "authorEmail": "bo@example.com",
      "baseVersion": 5
    }
  },
  "values": {
    "dev": {
      "tiers": [
        {
          "name": "free"
        },
        {
          "name": "pro"
        }
      ]
    },
    "stage": {
      "tiers": [
        {
          "name": "free"
        },
        {
          "name": "pro"
        }
      ]
    },
    "prod": {
      "tiers": [
        {
          "name": "free"
        },
        {
          "name": "pro"
        }
      ]
    }
  },
  "draftValues": {
    "dev": {
      "tiers": [
        {
          "name": "free"
        },
        {
          "name": "pro"
        },
        {
          "name": "enterprise"
        }
      ]
    }
  }
}
PATCH/api/admin/configs/{id}

Update a dynamic config

Partial update. When value is supplied it is republished on every env (new version per env). When schema is supplied it replaces the current schema; every existing value is re-validated.

For env-scoped edits, use the draft/publish flow (PUT /{id}/drafts then POST /{id}/publish) instead.

Use cases

Use caseDescriptionExample
Republish flat value{ "value": {…} } sets the same value on every env.
Schema migration{ "schema": {…} } replaces the schema; existing values are re-validated.
Env-scoped editsuse PUT /{id}/drafts + POST /{id}/publish instead of PATCH.

Parameters

NameInTypeDescription
X-Project-IdrequiredheaderstringProject to scope this request to.
idrequiredpathstringStable opaque config id.

Body

NameTypeDescription
schemaobjectReplacement schema. When supplied, the new schema is validated against every published value before it lands.
valueanyFlat value applied to **every** env. Publishes a new version per env. To target one env, use `PUT /{id}/drafts` then `POST /{id}/publish`.

Response · 200

NameTypeDescription
idrequiredstringConfig id that was updated.
Example · 200
{
  "id": "cfg_01j7wae5h6j7k8l9m0n1p2q3r4"
}
DELETE/api/admin/configs/{id}

Delete a dynamic config

Soft-deletes the config and rebuilds the project's flags KV blob.

Use caseTear down a config after its consumers have stopped reading it.

Parameters

NameInTypeDescription
X-Project-IdrequiredheaderstringProject to scope this request to.
idrequiredpathstringStable opaque config id.

Response · 200

NameTypeDescription
okrequiredtrue
Example · 200
{
  "ok": true
}
PUT/api/admin/configs/{id}/drafts

Save a draft value

Stages a value for one env without publishing. The draft is validated against the config's current schema and stored alongside the baseVersion it was forked from.

Saving over an existing draft overwrites it. Use POST /{id}/publish to promote it to a new published version.

Use caseIterate on a config value on dev without affecting prod — preview in staging, then publish.

Parameters

NameInTypeDescription
X-Project-IdrequiredheaderstringProject to scope this request to.
idrequiredpathstringStable opaque config id.

Body

NameTypeDescription
envrequired"dev" | "staging" | "prod"Target environment. One of the project's configured envs (`dev`, `stage`, `prod`).
valuerequiredanyDraft value to stage on `env`. Validated against the config's current schema.

Response · 200

NameTypeDescription
idrequiredstring
envrequired"dev" | "staging" | "prod"Target environment. One of the project's configured envs (`dev`, `stage`, `prod`).
baseVersionrequiredintegerPublished version the draft is based on.
updatedAtrequiredstring
Example · 200
{
  "id": "cfg_01j7wae5h6j7k8l9m0n1p2q3r4",
  "env": "dev",
  "baseVersion": 5,
  "updatedAt": "2026-05-10T09:31:00.000Z"
}
DELETE/api/admin/configs/{id}/drafts

Discard a draft

Drops the in-flight draft on one env. Published values are unaffected.

Use caseAbandon an in-progress draft after deciding not to ship it.

Parameters

NameInTypeDescription
X-Project-IdrequiredheaderstringProject to scope this request to.
idrequiredpathstringStable opaque config id.

Body

NameTypeDescription
envrequired"dev" | "staging" | "prod"Target environment. One of the project's configured envs (`dev`, `stage`, `prod`).

Response · 200

NameTypeDescription
okrequiredtrue
Example · 200
{
  "ok": true
}
POST/api/admin/configs/{id}/publish

Publish a draft

Promotes the staged draft on one env to a new published version. The draft must still validate against the current schema.

Returns 404 if there is no draft for the given env.

Use caseShip a staged change once you've validated it on a lower env.

Parameters

NameInTypeDescription
X-Project-IdrequiredheaderstringProject to scope this request to.
idrequiredpathstringStable opaque config id.

Body

NameTypeDescription
envrequired"dev" | "staging" | "prod"Target environment. One of the project's configured envs (`dev`, `stage`, `prod`).

Response · 201

NameTypeDescription
idrequiredstring
envrequired"dev" | "staging" | "prod"Target environment. One of the project's configured envs (`dev`, `stage`, `prod`).
versionrequiredintegerNewly published version on `env`.
Example · 201
{
  "id": "cfg_01j7wae5h6j7k8l9m0n1p2q3r4",
  "env": "dev",
  "version": 6
}
GET/api/admin/configs/{id}/activity

List config activity

Returns recent audit rows for one config (create, update, draft.save, publish, delete) ordered newest first. Use the limit query parameter to cap the result (1–100, default 20).

Use caseRender the activity panel in the config editor or drive a slack notification on publish events.

Parameters

NameInTypeDescription
X-Project-IdrequiredheaderstringProject to scope this request to.
idrequiredpathstringStable opaque config id.
limitqueryintegerMax rows to return (1–100). Defaults to 20.
Example · 200
[
  {
    "id": "act_01j7waf01a2b3c4d5e6f7g8h9i",
    "action": "config.publish",
    "actorEmail": "ana@example.com",
    "actorType": "user",
    "payload": {
      "env": "dev",
      "version": 6
    },
    "createdAt": "2026-05-10T09:31:42.000Z"
  }
]

Killswitches

GET/api/admin/killswitches

List killswitches

Returns a single page of killswitches ordered by updated_at desc, id desc. Each row includes the latest published value/switches/version per env.

Use caseSnapshot every killswitch in the project — e.g. to render an incident-response runbook listing every kill and its current trip state.

Parameters

NameInTypeDescription
X-Project-IdrequiredheaderstringProject to scope this request to.
limitquerynumberMax results per page (default 50, max 500).
cursorquerystringOpaque pagination cursor from a prior page's `next_cursor`.

Response · 200

NameTypeDescription
datarequiredarray<{ id: string; name: string; description: string | null; updatedAt: string; envs: object }>
next_cursorrequiredstring | null
Example · 200
{
  "data": [
    {
      "id": "ksw_01j7w9d8h2k4m6n8p0q2r4s6t8",
      "name": "payments.checkout",
      "description": "Master kill for the checkout flow. Trip to fall back to the legacy provider.",
      "updatedAt": "2026-05-09T18:22:11.000Z",
      "envs": {
        "dev": {
          "value": false,
          "version": 3,
          "publishedAt": "2026-05-09T18:22:11.000Z"
        },
        "stage": {
          "value": false,
          "version": 3,
          "publishedAt": "2026-05-09T18:22:11.000Z"
        },
        "prod": {
          "value": false,
          "switches": {
            "eu_region": true
          },
          "version": 5,
          "publishedAt": "2026-05-09T18:22:11.000Z"
        }
      }
    }
  ],
  "next_cursor": null
}
POST/api/admin/killswitches

Create a killswitch

Creates a new killswitch with value (default false) applied to every env at version 1.

Returns 409 if name already exists in the project.

Use cases

Use caseDescriptionExample
Untripped create{ "name": "payments.checkout" }. Provision the kill ahead of an incident.
Pre-tripped{ "value": true } to ship the killswitch already engaged.
With switchesseed switches to carve out per-region/per-tenant kills from day one.

Parameters

NameInTypeDescription
X-Project-IdrequiredheaderstringProject to scope this request to.

Body

NameTypeDescription
namerequiredstringStable killswitch key in `folder.name` form (two lowercase segments separated by a dot — e.g. `payments.checkout`). Immutable after create.
descriptionstringOptional free-form description shown in the dashboard. Max 512 chars.
valuebooleanDefault value applied to every env at creation. Defaults to `false`. Use `true` to ship the killswitch pre-tripped.
switchesobjectInitial per-switch overrides applied to every env. Empty/omitted leaves the killswitch with only the flat `value`.

Response · 201

NameTypeDescription
idrequiredstringNewly assigned killswitch id.
namerequiredstringStable config key in `folder.name` form (two lowercase segments separated by a dot, e.g. `pricing.tiers`). Used by SDKs as `Shipeasy.getConfig('<name>')`. Immutable after create.
Example · 201
{
  "id": "ksw_01j7w9d8h2k4m6n8p0q2r4s6t8",
  "name": "payments.checkout"
}
GET/api/admin/killswitches/{id}

Get one killswitch

Returns the killswitch metadata plus the latest published value/switches/version per env.

Use caseFetch the current state of one killswitch — e.g. to verify a trip propagated before declaring an incident mitigated.

Parameters

NameInTypeDescription
X-Project-IdrequiredheaderstringProject to scope this request to.
idrequiredpathstringStable opaque killswitch id (`ksw_…`).

Response · 200

NameTypeDescription
idrequiredstringStable opaque killswitch id.
namerequiredstringStable config key in `folder.name` form (two lowercase segments separated by a dot, e.g. `pricing.tiers`). Used by SDKs as `Shipeasy.getConfig('<name>')`. Immutable after create.
descriptionrequiredstring | nullFree-form description or `null`.
updatedAtrequiredstringISO-8601 timestamp of last mutation.
envsrequiredobjectPer-env latest value, switches, version, and publish timestamp.
Example · 200
{
  "id": "ksw_01j7w9d8h2k4m6n8p0q2r4s6t8",
  "name": "payments.checkout",
  "description": "Master kill for the checkout flow. Trip to fall back to the legacy provider.",
  "updatedAt": "2026-05-09T18:22:11.000Z",
  "envs": {
    "dev": {
      "value": false,
      "version": 3,
      "publishedAt": "2026-05-09T18:22:11.000Z"
    },
    "stage": {
      "value": false,
      "version": 3,
      "publishedAt": "2026-05-09T18:22:11.000Z"
    },
    "prod": {
      "value": false,
      "switches": {
        "eu_region": true
      },
      "version": 5,
      "publishedAt": "2026-05-09T18:22:11.000Z"
    }
  }
}
PATCH/api/admin/killswitches/{id}

Update a killswitch

Partial update applied to every env. Setting value/switches publishes a new version per env. Description-only patches don't bump versions.

To change a single switch on a single env, use PUT /{id}/switch instead.

Use cases

Use caseDescriptionExample
Trip everywhere{ "value": true }. Kills the feature across dev/stage/prod in one call.
Untrip everywhere{ "value": false }.
Replace switchessend the full new map; per-key edits use PUT /{id}/switch.
Update descriptionmetadata-only patches don't bump versions.

Parameters

NameInTypeDescription
X-Project-IdrequiredheaderstringProject to scope this request to.
idrequiredpathstringStable opaque killswitch id.

Body

NameTypeDescription
descriptionstring | nullNew description, or `null` to clear it. Max 512 chars.
valuebooleanFlat value applied to every env. Publishes a new version per env when set. Omit to leave values unchanged.
switchesobjectReplace the switches map wholesale on every env. To edit a single entry on a single env use `PUT /{id}/switch` instead.

Response · 200

NameTypeDescription
idrequiredstringKillswitch id that was updated.
Example · 200
{
  "id": "ksw_01j7w9d8h2k4m6n8p0q2r4s6t8"
}
DELETE/api/admin/killswitches/{id}

Delete a killswitch

Soft-deletes the killswitch and rebuilds the project's flags KV blob so SDKs stop seeing it.

Use caseTear down a killswitch after the feature it protected has been removed.

Parameters

NameInTypeDescription
X-Project-IdrequiredheaderstringProject to scope this request to.
idrequiredpathstringStable opaque killswitch id.

Response · 200

NameTypeDescription
okrequiredtrue
Example · 200
{
  "ok": true
}
PUT/api/admin/killswitches/{id}/switch

Set one switch entry

Sets or updates a single switchKey on a single env. Publishes one new version on that env only — other envs untouched.

Use this for surgical per-env, per-key flips during incident response (e.g. trip eu_region on prod without touching the flat value or other envs).

Use cases

Use caseDescriptionExample
Trip a region{ "env": "prod", "switchKey": "eu_region", "value": true }.
Untrip without removingsame payload with value: false. To remove the entry entirely use DELETE /{id}/switch.

Parameters

NameInTypeDescription
X-Project-IdrequiredheaderstringProject to scope this request to.
idrequiredpathstringStable opaque killswitch id.

Body

NameTypeDescription
envrequired"dev" | "staging" | "prod"Target environment (`dev`/`stage`/`prod`).
switchKeyrequiredstringSwitch key to set.
valuerequiredbooleanNew boolean value for this `switchKey` on this `env`.

Response · 200

NameTypeDescription
idrequiredstring
envrequired"dev" | "staging" | "prod"Target environment. One of the project's configured envs (`dev`, `stage`, `prod`).
switchKeyrequiredstringSingle-segment switch key (lowercase letters, digits, `_`/`-`; no dots). Used as the nested switch entry inside a killswitch's `switches` map.
valuerequiredboolean
Example · 200
{
  "id": "ksw_01j7w9d8h2k4m6n8p0q2r4s6t8",
  "env": "prod",
  "switchKey": "eu_region",
  "value": true
}
DELETE/api/admin/killswitches/{id}/switch

Remove one switch entry

Removes a single switchKey from the switches map on a single env. Publishes a new version on that env.

Returns { removed: false } if the entry didn't exist (idempotent no-op).

Use caseClean up a per-region override after the incident is resolved so the flat value governs again.

Parameters

NameInTypeDescription
X-Project-IdrequiredheaderstringProject to scope this request to.
idrequiredpathstringStable opaque killswitch id.

Body

NameTypeDescription
envrequired"dev" | "staging" | "prod"Target environment.
switchKeyrequiredstringSwitch key to remove.

Response · 200

NameTypeDescription
idrequiredstring
envrequired"dev" | "staging" | "prod"Target environment. One of the project's configured envs (`dev`, `stage`, `prod`).
switchKeyrequiredstringSingle-segment switch key (lowercase letters, digits, `_`/`-`; no dots). Used as the nested switch entry inside a killswitch's `switches` map.
removedrequiredboolean`true` if the entry existed and was removed, `false` if no-op.
Example · 200
{
  "id": "ksw_01j7w9d8h2k4m6n8p0q2r4s6t8",
  "env": "prod",
  "switchKey": "eu_region",
  "removed": true
}

Universes

GET/api/admin/universes

List universes

Returns a single page of universes ordered by created_at desc, id desc. The universes table has no updated_at, so this list is keyed on creation time.

Use caseSnapshot every universe in the project — for example to audit which unit_type and holdout_range are in use before launching a new experiment.

Parameters

NameInTypeDescription
X-Project-IdrequiredheaderstringProject to scope this request to.
limitquerynumberMax results per page (default 50, max 500).
cursorquerystringOpaque pagination cursor from a prior page's `next_cursor`.

Response · 200

NameTypeDescription
datarequiredarray<{ id: string; name: string; unitType: string; holdoutRange: array<any> | null; createdAt: string }>
next_cursorrequiredstring | null
Example · 200
{
  "data": [
    {
      "id": "uni_01j7w8a1b2c3d4e5f6g7h8i9j0",
      "name": "primary_users",
      "unitType": "user_id",
      "holdoutRange": [
        9500,
        9999
      ],
      "createdAt": "2026-04-12T10:14:08.000Z"
    }
  ],
  "next_cursor": null
}
POST/api/admin/universes

Create a universe

Creates a new universe. Only name is required — unit_type defaults to user_id and holdout_range defaults to null (no holdout).

Returns 409 if name already exists in the project. Returns 403 if you supply holdout_range on a plan below Pro.

Use cases

Use caseDescriptionExample
Default universe{ "name": "primary_users" }. Per-user randomisation, no holdout.
Reserved holdoutsupply holdout_range to carve out a measurement slice excluded from all experiments.
Account-levelunit_type: 'account_id' so multi-seat accounts see one consistent variant.

Parameters

NameInTypeDescription
X-Project-IdrequiredheaderstringProject to scope this request to.

Body

NameTypeDescription
namerequiredstringStable universe key referenced by experiments via `universe: '<name>'`. Lowercase letters, digits, `_` or `-`, max 64 chars. Immutable after create.
unit_typestringUnit of randomisation. Typically `user_id`. Use `account_id` to keep whole accounts in the same group across an experiment. default: "user_id"
holdout_rangearray<any> | nullInclusive `[lo, hi]` bucket range (0–9999) reserved as the **holdout** — callers hashed into this slice are excluded from every experiment in the universe. `null` disables the holdout. Pro plan or higher required. default: null

Response · 201

NameTypeDescription
idrequiredstringNewly assigned universe id.
namerequiredstringStable universe key referenced by experiments via `universe: '<name>'`. Lowercase letters, digits, `_` or `-`, max 64 chars. Immutable after create.
Example · 201
{
  "id": "uni_01j7w8a1b2c3d4e5f6g7h8i9j0",
  "name": "primary_users"
}
PATCH/api/admin/universes/{id}

Update a universe

Partial update. Only holdout_range is mutable — name and unit_type are immutable after create.

Pass "holdout_range": null to remove an existing holdout.

Use cases

Use caseDescriptionExample
Adjust holdoutchange the reserved measurement slice without recreating experiments.
Remove holdout{ "holdout_range": null }.

Parameters

NameInTypeDescription
X-Project-IdrequiredheaderstringProject to scope this request to.
idrequiredpathstringStable opaque universe id (`uni_…`).

Body

NameTypeDescription
holdout_rangearray<any> | nullInclusive `[lo, hi]` bucket range (0–9999) reserved as the **holdout** — callers hashed into this slice are excluded from every experiment in the universe. `null` disables the holdout. Pro plan or higher required.

Response · 200

NameTypeDescription
idrequiredstringUniverse id that was updated.
Example · 200
{
  "id": "uni_01j7w8a1b2c3d4e5f6g7h8i9j0"
}
DELETE/api/admin/universes/{id}

Delete a universe

Soft-deletes the universe. Returns 409 if any non-archived experiment still references it — archive those experiments first.

Use caseTear down a universe after every experiment that used it has been archived.

Parameters

NameInTypeDescription
X-Project-IdrequiredheaderstringProject to scope this request to.
idrequiredpathstringStable opaque universe id.

Response · 200

NameTypeDescription
okrequiredtrue
Example · 200
{
  "ok": true
}