openapi: 3.2.0 info: title: Shipeasy Admin API version: 2.0.0 description: |- REST API for managing feature gates, experiments, configs, universes, and killswitches in a Shipeasy project. Authenticate with an admin SDK key (`Authorization: Bearer sdk_admin_…`) and scope every request to a project via the `X-Project-Id` header. Mint admin keys via `POST /api/admin/keys` with `type: "admin"`. Keys expire after 90 days; rotate with the `revoke` action. contact: name: Shipeasy url: https://shipeasy.ai license: name: Proprietary identifier: LicenseRef-Proprietary servers: - url: https://shipeasy.ai description: Production - url: http://localhost:3000 description: Local Next.js dev server security: - bearerSdkKey: [] tags: - name: Release summary: Feature delivery — flags, kill switches, dynamic configs, A/B experiments, and the universes they bucket in. kind: nav description: |- Release: the feature-delivery surface. One roof over the five resources that decide which code path a user gets — feature gates (flags), kill switches, dynamic configs, A/B/n experiments, and the universes that bucket them — each exposed as its own subcommand group. **Groups.** - `flags` — boolean feature gates: AND-combined targeting rules + a percentage rollout (basis points), evaluated on the SDK hot path. - `killswitch` (`ks`) — per-env boolean overrides for incident response; no rules, no rollout, versioned per environment. - `configs` — JSON-Schema-validated structured values with a draft → publish workflow, delivered per environment. - `experiments` — randomised A/B/n assignment plus the analysis pipeline (t-test, sequential testing, SRM detection); `universes` hold the shared bucketing space and holdouts the experiments draw from. **Delivery model.** Every write rebuilds the project's KV blob and purges the CDN — SDKs poll at the plan interval and never touch the database on the hot path. The destructive verb is `archive` (soft-delete), never `delete`. Run `shipeasy release --help` for the full contract of each group. - name: Flags parent: Release description: |- Feature gates: boolean flags evaluated at runtime against project rules + a percentage rollout. **Identity.** Each gate is keyed by a stable `name` (a-z, 0-9, `_`/`-`, max 64 chars) which is what SDKs pass to `Shipeasy.checkGate(user, '')`. The `name` is immutable — rename means delete + recreate. **Evaluation model.** A gate returns `true` when (a) `enabled` is true, and (b) the caller satisfies the gate's rules. There are two evaluation shapes: - **Flat** — `rules` (AND-combined predicates) gate the caller, then `rollout_pct` (basis points, 0–10000) hashes them into a bucket. Used for simple `is in X% rollout` gates. - **Gatekeeper stack** — an ordered array of `condition` and `rollout` sub-gates, evaluated top-to-bottom; first match wins. Used to express `internal-only ∪ 1% beta ∪ 50% public` in one gate. When `stack` is present it takes precedence over the flat fields. **Rules.** Each rule is `{ attr, op, value }`. Supported ops: `eq`, `neq`, `in`, `not_in`, `gt`, `gte`, `lt`, `lte`, `contains`, `regex`. Attribute names match the keys on the SDK evaluation context (e.g. `country`, `plan`, `email`). **Rollout basis points.** `rollout_pct` is in **basis points**, not percent. `0` = 0%, `100` = 1%, `5000` = 50%, `10000` = 100%. This allows sub-1% precision (e.g. `7` = 0.07%). **Lifecycle.** Create dark (`rollout_pct: 0`) → attach rules → ramp via PATCH → flip kill-switch via `disable`/`enable` → delete once retired. Deletion is blocked while a running experiment references the gate as a targeting gate. - name: Killswitch parent: Release x-cli: aliases: - ks description: |- Killswitches: per-env boolean overrides for kill-style operational toggles. Optimised for incident response — no rules, no rollout, just a flat boolean (plus optional per-key overrides) versioned per environment. **Identity.** Each killswitch is keyed by `name` in `folder.name` form (e.g. `payments.checkout`). Immutable after create. **Per-env values.** Every killswitch stores one version stream per env (`dev`, `stage`, `prod`). A `PATCH` with `value`/`switches` applies to **every** env in one shot (publishes a new version per env). To touch a single env, use `PUT /{id}/switch` and friends. **Switches map.** Optional `switches: { key: bool }` overrides the flat `value` for specific named call sites — useful for region/feature-scoped kills. **Versioning.** Each publish (create, update, set-switch, unset-switch) bumps the per-env `version` monotonically. SDKs deliver the latest published version. - name: Configs parent: Release description: |- Dynamic configs: JSON-Schema-validated structured values delivered to SDKs and editable per environment with a draft/publish workflow. **Identity.** Each config is keyed by `name` in `folder.name` form (e.g. `pricing.tiers`). Immutable after create. **Schema-first.** Every config carries a JSON Schema (draft 2020-12, top-level `type: 'object'`). Every published value is validated against it. **Drafts → publish.** Per-env edits go through `PUT /{id}/drafts` (stages a value) then `POST /{id}/publish` (promotes to a new version). The flat `PATCH /{id}` republishes on **every** env in one shot — bypassing drafts. **Versioning.** Each publish bumps the per-env `version` monotonically. SDKs deliver the latest published version for each env. - name: Experiments parent: Release description: |- A/B/n experiments: randomised group assignment plus the analysis pipeline (t-test, sequential testing, SRM detection) on top of a universe. **Identity.** Stable `name` (a-z, 0-9, `_`/`-`, max 64 chars). Immutable after create. **Lifecycle.** `draft → running → stopped → archived`. Transition via `POST /{id}/status`. An archived experiment that never started can be restored with `archived → draft`; restarting an archived experiment directly is not allowed. **Allocation.** `allocation_pct` (basis points, 0–10000) is the share of the targeted audience enrolled; `groups[].weight` (must sum to 10000) splits the enrolled audience. `targeting_gate` narrows the eligible audience before allocation. **Immutable while running.** `allocation_pct`, `groups`, `salt`, `universe`, `params` cannot be edited on a running experiment — stop it first. **Metrics.** Attach via `POST /{id}/metrics`. Each metric has a role: `goal` drives the decision, `guardrail` blocks ship on regression, `secondary` is informational. **Analysis.** Daily cron writes results to D1. Read via `GET /{id}/results` (latest per metric/group/day) or `GET /{id}/timeseries` (full history). `POST /{id}/reanalyze` requeues the analysis pass. - name: Universes parent: Experiments description: |- Universes: the shared bucketing space all experiments draw from. **Identity.** Each universe is keyed by a stable `name` (a-z, 0-9, `_`/`-`, max 64 chars). Experiments reference it via `universe: ''`. The name is immutable. **Unit of randomisation.** `unit_type` selects the attribute hashed into a 0–9999 bucket — default `user_id` for per-user randomisation, `account_id` to keep a whole account in the same group. **Holdout.** `holdout_range` is an inclusive `[lo, hi]` bucket range (0–9999) reserved for measurement — callers hashed into the holdout are excluded from every experiment in the universe. Pro plan or higher. **Deletion.** Blocked while any non-archived experiment references the universe — archive those first. - name: Templates parent: Flags description: |- Targeting-rule templates: reusable `{ attr, op, value }` rule definitions (country, email-domain, region presets, …). Read-only built-ins ship with the platform; each project can also save its own. To target by country/plan/region, list templates, copy the matching template's `rules`, substitute the concrete value(s), and pass them to `release flags create`. - name: Attributes parent: Flags description: |- Targeting attributes: the auto-inferred schema of user-context keys the platform has observed in evaluation calls. Read-only — populated by the SDK hot path, surfaced here so you can see which keys (and value types) are available when writing gate/experiment targeting rules. - name: Metrics description: |- Metrics: the event-backed queries that drive tracking dashboards and experiment success / guardrail measurement. **Definition.** Each metric pins one source event (`event_name`), one aggregation, and (for `sum`/`avg`/quantile) a numeric value label. The query is expressed as the DSL string (`query`, e.g. `sum(purchase, amount)`) or its typed IR (`query_ir`) — supply exactly one. **Identity.** Keyed by a stable `name` (single segment or `folder.name`). Resolve endpoints accept the `id` or the `name`. **Deletion.** Archive (soft-delete). Blocked while the metric is attached to a running experiment — stop those first. - name: Events parent: Metrics description: |- Events: the catalog of event names (and their typed properties) that metric queries reference. **Auto-discovery.** The SDK's `/collect` ingest path records any unknown event name it receives as a `pending` row (`pending: 1`) so you can review it. Metrics defined on a pending event fail until it is approved. **Approval.** `POST /{id}/approve` promotes a pending event to usable (`pending: 0`), optionally declaring its folder/description/properties in the same call. Registering a brand-new event via `POST` that matches a pending name approves it instead of failing with a conflict. **Properties.** Each event can declare typed properties (`name`, `type` of `string|number|boolean`, `required`). On update/approve the `properties` array replaces the full set — there is no merge. **Deletion.** Soft-delete (the user-facing verb is `archive`). Blocked while any metric still references the event — delete those metrics first. - name: Ops description: |- Operational queue: the unified table of bug reports, feature requests, and auto-filed error/alert tickets, all over `/api/admin/ops`. One `create` endpoint files either user type (`type: bug | feature_request`); list, get, update, and link-pr are unified across every type. Also exposes the `notify` escalation bell and the read-only Slack-channels list used to resolve alert-rule notification targets. **Handles.** A queue item is addressed by its per-project `number` (e.g. `7`) or its full id — the API resolves either. - name: Alerts parent: Ops x-cli: aliases: - ar description: |- Alert rules: the metric-threshold definitions the analysis cron evaluates each run. **What fires.** Each rule binds a `metricId`, a `comparator` (`gt`/`gte`/`lt`/`lte`), and a `threshold`. On every cron pass the cron aggregates the metric over the trailing `windowHours` and raises an alert at `severity` when `value comparator threshold` holds. **Immutable metric.** The bound metric (and its aggregation) is fixed at create time — there is no update path for `metricId`. Tune `threshold`/`comparator`/`windowHours`/`severity`/`name`/`enabled` instead, or delete + recreate to repoint the rule at a different metric. **Delivery.** `notify` optionally targets a Slack channel and/or email for this rule; `null` falls back to the project's default notification settings. Slack targets require a connected Slack connector. - name: Fired Alerts parent: Ops description: |- Fired alerts: the conditions the platform has actually raised — metric-rule crossings, armed kill switches, experiment SRM/peek guards, and guardrail breaches. Raised only by the platform (UI killswitch handlers plus the worker analysis consumer and alerts cron), never filed by hand; deduped per `(source, dedupeKey)` with auto-resolve when the condition clears. This surface lists them and triages one (status flip + person/agent owner). The *rules* that define metric-threshold alerts are the sibling alert-rules resource. - name: Agents parent: Ops description: |- Connected AI agents — one per authenticated trigger connector (Claude / Cursor / Copilot / Jules). The read-only roster behind agent assignment and the `ops ack` agent types: an AI ack (`ops ack --agent `) requires the type to appear here, so this list is where an `AGENT_NOT_CONNECTED` error sends you. - name: Comments parent: Ops description: |- Comments on a queue item — the discussion thread that hangs off any bug, feature request, or auto-filed error/alert ticket. Append a comment or read the thread; one level of threaded replies via `parentId`. **Mentions.** `@teammate` in a body raises an in-app notification for that person; `@shipeasy` asks Jarvis (the AI agent) to read the item and reply. Comments authored by Jarvis carry `authorType: system`. - name: Investigations parent: Ops description: |- Investigation records on a queue item — the structured, read-only write-ups an AI agent produces while working it (findings / a blocking question / QA notes), plus the `working` run rows an ack or an AI hand-off opens. This is the AI-write seam the cockpit's detail panel renders: `list` reads what a previous run found, `create` appends a new record, and `update` fills in the `working` run record you were handed (findings, PR, confidence) as you work. - name: Projects description: |- Projects: the account-level container every other resource is scoped to. **Account-level, not bound-project-level.** Both operations resolve from the caller's credential rather than the `.shipeasy`-bound project — `current` reads the project the auth header maps to, and `upsert` find-or-creates under the session's owner. Neither touches the local `.shipeasy` binding; recording the result there is a consumer side-effect layered on top. **Idempotent upsert.** A project is keyed by `(owner_email, domain)`. Calling `upsert` again with the same domain returns the existing project with `created: false`, so it is safe to run on every install. - name: i18n description: |- String Manager (i18n): the worker-safe REST surface — locale profiles, the insert-only key push, single-key overwrite, profile publish, and the read-only key/draft listings. The fs/AST parts (scan, validate, install-loader, codemod) are NOT part of this API — they stay in the fs-having CLI/MCP. - name: Profiles parent: i18n description: Locale profiles (e.g. `en:prod`) — create, list, and publish a profile to the CDN. - name: Keys parent: i18n description: Translation keys — the insert-only push, single-key overwrite, and key listing. - name: Drafts parent: i18n description: Machine-translation drafts awaiting review before publish. - name: Errors description: |- Tracked production errors: the deduplicated, fingerprinted error stream the SDK + framework error sinks feed. Read-only list/get plus a status update (`open`/`resolved`/`ignored`), an auto-file-to-GitHub-issue action, and a time-series rollup. Errors are ingestion-only — there is no create route. - name: Trigger parent: Ops description: |- Recurring coding-agent triggers: the scheduled, unattended runs that burn down the ops queue in `--pr` mode (one PR per fixed item; nothing auto-merges). Shipeasy can fire four providers directly — Claude routines, Cursor cloud agents, Copilot cloud agents, and Google Jules (the Gemini path) — registered here as trigger connectors (idempotent per provider key). Other platforms (Codex, Windsurf, Cline, OpenClaw, OpenCode, Continue) schedule on their own surface — typically a GitHub Actions `schedule:` cron running the platform's headless CLI with the shared trigger prompt. - name: Connectors description: |- Outbound integrations: the per-project destinations feedback + trigger events are dispatched to (GitHub, Google Sheets, Slack, and the Claude/Cursor/Copilot/Jules autopilot triggers). CRUD plus a manual `fire` (trigger-only) and a `test` payload. - name: API Keys description: |- Admin + SDK key management: mint, list, and revoke the `sdk_*` keys that authenticate every other endpoint. The plaintext key is returned exactly once, on create. Revoke is the rotation verb. paths: /api/admin/gates: get: operationId: listGates summary: List feature gates description: |- Returns a single page of gates ordered by `updated_at desc, id desc`. Use the `cursor` query parameter to paginate. **Use case:** Snapshot 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. tags: - Flags parameters: - $ref: '#/components/parameters/ProjectId' - $ref: '#/components/parameters/PaginationLimit' - $ref: '#/components/parameters/PaginationCursor' - $ref: '#/components/parameters/Q' responses: '200': description: List feature gates content: application/json: schema: $ref: '#/components/schemas/ListGatesResponse' example: 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 creatorEmail: ana@example.com updaterEmail: bo@example.com version: 4 createdAt: '2026-05-01T10:12:00.000Z' updatedAt: '2026-05-09T16:01:22.000Z' next_cursor: null '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '409': $ref: '#/components/responses/Conflict' '422': $ref: '#/components/responses/UnprocessableEntity' x-error-codes: - BAD_REQUEST - UNAUTHORIZED - FORBIDDEN x-cli: name: list post: operationId: createGate summary: Create a feature gate description: |- 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** - **Dark create + ramp later** — `{ "name": "checkout_v2" }` at 0% rollout. Ramp via PATCH after deploy validation. - **Targeted rollout** — supply `rules` to gate the caller (e.g. only `plan = pro` users) plus a `rollout_pct` to bucket within that audience. - **Gatekeeper stack** — supply `stack` instead of `rules`/`rollout_pct` for `internal ∪ beta ∪ public` fall-through. Stack entries evaluated top-to-bottom; first match wins. - **Dashboard metadata** — populate `title`, `description`, `folder`, `group`, `owner_email` so the admin UI is self-documenting from day one. - **Disabled on create** — pre-provision with `enabled: false` for a future launch; flip on with `POST /{id}/enable` at go-live. tags: - Flags parameters: - $ref: '#/components/parameters/ProjectId' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CreateGateRequest' examples: minimal: summary: Minimal — dark gate, name only description: 'Smallest valid body. The gate is created `enabled: true` at `rollout_pct: 0`, with no rules. Ramp it later via PATCH.' value: name: checkout_v2 targeted: summary: Targeted rollout — rules + percentage description: 'Gate Pro users in US/CA/GB, then hash 10% of that audience into the on-bucket (`rollout_pct: 1000` = 10.00%). Adds dashboard metadata so the gate is self-documenting.' value: name: checkout_v2 rollout_pct: 1000 rules: - attr: country op: in value: - US - CA - GB - attr: plan op: neq value: free title: Checkout v2 description: New checkout flow. Pro users in US/CA/GB only. folder: checkout group: growth owner_email: ana@example.com gatekeeperStack: summary: Gatekeeper stack — internal ∪ beta ∪ public description: 'Three-layer fall-through: employees always on, opted-in beta accounts always on, then 25% of everyone else. First match wins; evaluation stops as soon as one entry returns `true`.' value: name: checkout_v2 title: Checkout v2 owner_email: ana@example.com stack: - id: 01HKJ0EMP type: condition name: Employees pass: any rules: - attr: email op: regex value: '@acme\.com$' - id: 01HKJ0BETA type: condition name: Beta opt-in pass: all rules: - attr: beta_opt_in op: eq value: true - id: 01HKJ0PUB type: rollout name: Public rolloutPct: 2500 bucketBy: userID locked: true disabledOnCreate: summary: Disabled on create — provision then enable later description: 'Create with `enabled: false` so the gate evaluates to `false` for every caller until a follow-up `POST /{id}/enable`. Useful when provisioning ahead of a launch.' value: name: checkout_v2 enabled: false rollout_pct: 0 responses: '201': description: Create a feature gate content: application/json: schema: $ref: '#/components/schemas/CreateGateResponse' example: id: gat_01j7w7m9q4hxbf6npe6s9zr3vc name: checkout_v2 links: UpdateGate: operationId: updateGate parameters: id: $response.body#/id EnableGate: operationId: enableGate parameters: id: $response.body#/id DisableGate: operationId: disableGate parameters: id: $response.body#/id DeleteGate: operationId: deleteGate parameters: id: $response.body#/id '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '409': $ref: '#/components/responses/Conflict' '422': $ref: '#/components/responses/UnprocessableEntity' x-error-codes: - BAD_REQUEST - UNAUTHORIZED - FORBIDDEN - ALREADY_EXISTS - VALIDATION - PLAN_LIMIT x-cli: name: create positional: - name /api/admin/gates/{id}: get: operationId: getGate summary: Get one gate description: |- Returns the full gate row — including the gatekeeper `stack`, resolved creator/last-editor emails, and the edit `version` — for one gate, addressed by id or `name`. **Use case:** Inspect a single gate's current rollout, rules, and stack before editing it, without paging through the whole list. tags: - Flags parameters: - $ref: '#/components/parameters/ProjectId' - name: id in: path required: true description: Stable opaque gate id (`gat_…`) or the gate's `name`. schema: $ref: '#/components/schemas/ResourceId' responses: '200': description: Get one gate content: application/json: schema: $ref: '#/components/schemas/GateApiRow' example: id: gat_01j7w7m9q4hxbf6npe6s9zr3vc name: checkout_v2 enabled: 1 type: targeting rolloutPct: 5000 rules: - attr: country op: in value: - US - CA - GB 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 creatorEmail: ana@example.com updaterEmail: bo@example.com version: 4 createdAt: '2026-05-01T10:12:00.000Z' updatedAt: '2026-05-09T16:01:22.000Z' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '409': $ref: '#/components/responses/Conflict' '422': $ref: '#/components/responses/UnprocessableEntity' x-error-codes: - BAD_REQUEST - UNAUTHORIZED - FORBIDDEN - NOT_FOUND x-cli: name: get positional: - id patch: operationId: updateGate summary: Update a feature gate description: |- 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** - **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` set** — send 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 stack** — send a non-null `stack`. To revert to flat eval, send `{ "stack": null }`. - **Update metadata** — any subset of `title`, `description`, `folder`, `group`, `owner_email`. tags: - Flags parameters: - $ref: '#/components/parameters/ProjectId' - name: id in: path required: true description: Stable opaque gate id (`gat_…`) or the gate's `name`. schema: $ref: '#/components/schemas/ResourceId' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/UpdateGateRequest' examples: rampRollout: summary: Ramp rollout — 0 → 50% description: Single-field patch. `5000` basis points = 50%. value: rollout_pct: 5000 killSwitch: summary: Kill switch — disable the gate description: Forces evaluation to `false` for every caller, regardless of rules or rollout. Same effect as `POST /{id}/disable` but composable with other patches. value: enabled: false extendInRule: summary: Add a value to an `in` rule description: Array fields replace wholesale — there is no per-rule patch. To add `'AU'` to a country whitelist that previously held `['US','CA','GB']`, send the full new `rules` array. Same pattern applies to `not_in`. value: rules: - attr: country op: in value: - US - CA - GB - AU - attr: plan op: neq value: free replaceStack: summary: Replace gatekeeper stack description: 'Stack is replace-wholesale. Send the full new ordered list. Pass `"stack": null` to revert the gate to flat `rules` + `rollout_pct` evaluation.' value: stack: - id: 01HKJ0EMP type: condition name: Employees pass: any rules: - attr: email op: regex value: '@acme\.com$' - id: 01HKJ0PUB type: rollout name: Public rolloutPct: 5000 locked: true updateMetadata: summary: Update dashboard metadata description: Any subset of `title`, `description`, `folder`, `group`, `owner_email`. Snake-case in the request; the GET response returns camelCase (`groupName`, `ownerEmail`). value: title: Checkout v2 (50%) description: Bumped to 50% after passing canary. Added AU. folder: checkout owner_email: ana@example.com responses: '200': description: Update a feature gate content: application/json: schema: $ref: '#/components/schemas/UpdateGateResponse' example: id: gat_01j7w7m9q4hxbf6npe6s9zr3vc '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '409': $ref: '#/components/responses/Conflict' '422': $ref: '#/components/responses/UnprocessableEntity' x-error-codes: - BAD_REQUEST - UNAUTHORIZED - FORBIDDEN - NOT_FOUND - VALIDATION x-cli: name: update positional: - id delete: operationId: deleteGate summary: Delete a feature gate description: |- 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 case:** Tear down a gate after a feature has fully shipped and the rollout flag is no longer needed. tags: - Flags parameters: - $ref: '#/components/parameters/ProjectId' - name: id in: path required: true description: Stable opaque gate id (`gat_…`) or the gate's `name`. schema: $ref: '#/components/schemas/ResourceId' responses: '200': description: Delete a feature gate content: application/json: schema: $ref: '#/components/schemas/DeleteGateResponse' example: ok: true '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '409': $ref: '#/components/responses/Conflict' '422': $ref: '#/components/responses/UnprocessableEntity' x-error-codes: - BAD_REQUEST - UNAUTHORIZED - FORBIDDEN - NOT_FOUND - REFERENCED_IN_USE x-cli: name: archive positional: - id /api/admin/gates/{id}/enable: post: operationId: enableGate summary: Enable a gate description: |- Sets `enabled: true`. The current `rollout_pct` is preserved. **Use case:** Re-enable a previously disabled gate without re-issuing a full update. tags: - Flags parameters: - $ref: '#/components/parameters/ProjectId' - name: id in: path required: true description: Stable opaque gate id (`gat_…`) or the gate's `name`. schema: $ref: '#/components/schemas/ResourceId' responses: '200': description: Enable a gate content: application/json: schema: $ref: '#/components/schemas/EnableGateResponse' example: id: gat_01j7w7m9q4hxbf6npe6s9zr3vc enabled: true '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '409': $ref: '#/components/responses/Conflict' '422': $ref: '#/components/responses/UnprocessableEntity' x-error-codes: - BAD_REQUEST - UNAUTHORIZED - FORBIDDEN - NOT_FOUND - VALIDATION x-cli: name: enable positional: - id /api/admin/gates/{id}/disable: post: operationId: disableGate summary: Disable a gate description: |- 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 case:** Flip a gate off in production without redeploying — the canonical kill-switch flow. tags: - Flags parameters: - $ref: '#/components/parameters/ProjectId' - name: id in: path required: true description: Stable opaque gate id (`gat_…`) or the gate's `name`. schema: $ref: '#/components/schemas/ResourceId' responses: '200': description: Disable a gate content: application/json: schema: $ref: '#/components/schemas/DisableGateResponse' example: id: gat_01j7w7m9q4hxbf6npe6s9zr3vc enabled: false '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '409': $ref: '#/components/responses/Conflict' '422': $ref: '#/components/responses/UnprocessableEntity' x-error-codes: - BAD_REQUEST - UNAUTHORIZED - FORBIDDEN - NOT_FOUND - VALIDATION x-cli: name: disable positional: - id /api/admin/gates/{id}/activity: get: operationId: listGateActivity summary: List gate activity description: |- Returns recent audit rows for one gate (create, update, enable, disable, delete) ordered newest first. Use the `limit` query parameter to cap the result (1–100, default 20). **Use case:** Render the activity feed in the gate detail panel or answer "who ramped this gate, and when?" during an incident review. tags: - Flags parameters: - $ref: '#/components/parameters/ProjectId' - name: id in: path required: true description: Stable opaque gate id (`gat_…`) or the gate's `name`. schema: $ref: '#/components/schemas/ResourceId' - name: limit in: query required: false description: Max rows to return (1–100). Defaults to 20. schema: default: 20 type: integer minimum: 1 maximum: 100 responses: '200': description: List gate activity content: application/json: schema: $ref: '#/components/schemas/ListGateActivityResponse' example: - id: act_01j7waf01a2b3c4d5e6f7g8h9i action: gate.update actorEmail: ana@example.com actorType: user payload: rollout_pct: 5000 createdAt: '2026-05-09T16:01:22.000Z' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '409': $ref: '#/components/responses/Conflict' '422': $ref: '#/components/responses/UnprocessableEntity' x-error-codes: - BAD_REQUEST - UNAUTHORIZED - FORBIDDEN - NOT_FOUND x-cli: name: activity positional: - id /api/admin/gates/{id}/whitelist: get: operationId: getGateWhitelist summary: Read a gate's whitelist description: |- Returns the gate's whitelist — the always-first allowlist that admits the listed identities before any targeting rule or percentage rollout is evaluated. A gate with no whitelist returns `entries: []` (and the default `attr`), never a 404 — so a caller can read-then-write without special-casing the empty gate. **Use case:** Check whether an account is already let through before adding it. tags: - Flags parameters: - $ref: '#/components/parameters/ProjectId' - name: id in: path required: true description: Stable opaque gate id (`gate_…`) or the gate's `name`. schema: $ref: '#/components/schemas/ResourceId' responses: '200': description: Read a gate's whitelist content: application/json: schema: $ref: '#/components/schemas/GateWhitelist' example: id: gate_01j7w8a1b2c3d4e5f6g7h8i9j0 name: new_checkout attr: email entries: - alice@acme.dev - bob@acme.dev '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '409': $ref: '#/components/responses/Conflict' '422': $ref: '#/components/responses/UnprocessableEntity' x-error-codes: - BAD_REQUEST - UNAUTHORIZED - FORBIDDEN - NOT_FOUND x-cli: name: whitelist positional: - id put: operationId: setGateWhitelist summary: Replace a gate's whitelist description: |- Replaces the gate's whole whitelist with `entries`, creating the block if the gate didn't have one. Idempotent — the same call twice leaves the same list. This is the only whitelist call that can switch `attr` (`email` ⇄ `user_id`) or clear the block: `entries: []` removes the whitelist from the gate entirely. **Use cases** - **Pin an exact list** — `{ "entries": ["alice@acme.dev", "bob@acme.dev"] }`. - **Switch to user ids** — `{ "attr": "user_id", "entries": ["usr_123"] }`. - **Drop the whitelist** — `{ "entries": [] }`. tags: - Flags parameters: - $ref: '#/components/parameters/ProjectId' - name: id in: path required: true description: Stable opaque gate id (`gate_…`) or the gate's `name`. schema: $ref: '#/components/schemas/ResourceId' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/SetGateWhitelistRequest' examples: pinList: summary: Pin an exact list of emails value: entries: - alice@acme.dev - bob@acme.dev switchToUserIds: summary: Re-key the whitelist onto user ids description: Switching `attr` replaces the entries wholesale — the old emails are not translated. value: attr: user_id entries: - usr_01j7w8a1b2c3d4e5f6g7h8i9j0 clear: summary: Remove the whitelist description: An empty list drops the pinned block from the gate's stack. value: entries: [] responses: '200': description: Replace a gate's whitelist content: application/json: schema: $ref: '#/components/schemas/GateWhitelist' example: id: gate_01j7w8a1b2c3d4e5f6g7h8i9j0 name: new_checkout attr: email entries: - alice@acme.dev - bob@acme.dev '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '409': $ref: '#/components/responses/Conflict' '422': $ref: '#/components/responses/UnprocessableEntity' x-error-codes: - BAD_REQUEST - UNAUTHORIZED - FORBIDDEN - NOT_FOUND - VALIDATION x-cli: name: whitelist-set positional: - id post: operationId: addToGateWhitelist summary: Add entries to a gate's whitelist description: |- Adds identities to the gate's whitelist, creating the block if the gate doesn't have one yet. Entries already on the list are skipped, so the call is idempotent and safe to retry. Adding to a gate that already has a whitelist keyed on the other attribute is rejected (409) rather than silently re-keying the entries already there — use `PUT` to switch `attr` deliberately. **Use case:** Let one more customer into a private beta without reading the current list first. tags: - Flags parameters: - $ref: '#/components/parameters/ProjectId' - name: id in: path required: true description: Stable opaque gate id (`gate_…`) or the gate's `name`. schema: $ref: '#/components/schemas/ResourceId' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/AddToGateWhitelistRequest' examples: addOne: summary: Admit one more email value: entries: - carol@acme.dev createOnUserIds: summary: Start a whitelist keyed on user ids description: '`attr` is honoured only because this gate has no whitelist yet.' value: attr: user_id entries: - usr_01j7w8a1b2c3d4e5f6g7h8i9j0 responses: '200': description: Add entries to a gate's whitelist content: application/json: schema: $ref: '#/components/schemas/GateWhitelist' example: id: gate_01j7w8a1b2c3d4e5f6g7h8i9j0 name: new_checkout attr: email entries: - alice@acme.dev - bob@acme.dev - carol@acme.dev '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '409': $ref: '#/components/responses/Conflict' '422': $ref: '#/components/responses/UnprocessableEntity' x-error-codes: - BAD_REQUEST - UNAUTHORIZED - FORBIDDEN - NOT_FOUND - IMMUTABLE_FIELD - VALIDATION x-cli: name: whitelist-add positional: - id delete: operationId: removeFromGateWhitelist summary: Remove entries from a gate's whitelist description: |- Removes identities from the gate's whitelist. Entries that aren't on the list are skipped, so the call is idempotent. Removing the last entry leaves an empty whitelist block in place; to drop the block itself use `PUT` with `entries: []`. **Use case:** Revoke one beta tester's access without touching anyone else's. tags: - Flags parameters: - $ref: '#/components/parameters/ProjectId' - name: id in: path required: true description: Stable opaque gate id (`gate_…`) or the gate's `name`. schema: $ref: '#/components/schemas/ResourceId' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/RemoveFromGateWhitelistRequest' examples: removeOne: summary: Revoke one email value: entries: - carol@acme.dev responses: '200': description: Remove entries from a gate's whitelist content: application/json: schema: $ref: '#/components/schemas/GateWhitelist' example: id: gate_01j7w8a1b2c3d4e5f6g7h8i9j0 name: new_checkout attr: email entries: - alice@acme.dev - bob@acme.dev '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '409': $ref: '#/components/responses/Conflict' '422': $ref: '#/components/responses/UnprocessableEntity' x-error-codes: - BAD_REQUEST - UNAUTHORIZED - FORBIDDEN - NOT_FOUND - VALIDATION x-cli: name: whitelist-remove positional: - id /api/admin/experiments: get: operationId: listExperiments summary: List experiments description: |- Returns a single page of non-archived experiments ordered by `updated_at desc, id desc`. Use the `cursor` query parameter to paginate. **Use case:** Snapshot 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`. tags: - Experiments parameters: - $ref: '#/components/parameters/ProjectId' - $ref: '#/components/parameters/PaginationLimit' - $ref: '#/components/parameters/PaginationCursor' - name: status in: query required: false description: Filter by lifecycle status. Pass `archived` to return the archive tab; any other value (or omitting it) returns the non-archived experiments. schema: type: string - $ref: '#/components/parameters/Q' responses: '200': description: List experiments content: application/json: schema: $ref: '#/components/schemas/ListExperimentsResponse' example: 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 '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '409': $ref: '#/components/responses/Conflict' '422': $ref: '#/components/responses/UnprocessableEntity' x-error-codes: - BAD_REQUEST - UNAUTHORIZED - FORBIDDEN x-cli: name: list post: operationId: createExperiment summary: Create an experiment description: |- 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** - **Minimal 50/50** — `name` + `universe` + two equal-weight groups. - **Targeted rollout** — supply `targeting_gate` to restrict the eligible audience and `allocation_pct` to enrol a slice of it. - **Multivariant** — three or more groups with weights summing to 10000. - **Sequential testing** — `sequential_testing: true` for Premium plans. tags: - Experiments parameters: - $ref: '#/components/parameters/ProjectId' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CreateExperimentRequest' examples: minimal: summary: Minimal — two-group 50/50 description: Smallest valid body. Two groups at 50/50, no allocation, no targeting. value: name: checkout_button_color universe: primary_users groups: - name: control weight: 5000 - name: treatment weight: 5000 targeted: summary: Targeted — gate + allocation + params description: Gate the audience with a `targeting_gate`, allocate 50% of it, and parameterise the variants. value: name: checkout_button_color description: Green CTA on checkout for Pro users. universe: primary_users targeting_gate: pro_plan_only allocation_pct: 5000 params: cta_color: string groups: - name: control weight: 5000 params: cta_color: blue - name: treatment weight: 5000 params: cta_color: green multivariant: summary: Multivariant — three groups description: Three variants splitting 40/30/30. Requires the sum to equal 10000. value: name: headline_test universe: primary_users groups: - name: control weight: 4000 - name: alt_a weight: 3000 - name: alt_b weight: 3000 sequential: summary: Sequential testing — Premium plan description: Enable always-valid sequential tests. Requires Premium plan or higher. value: name: checkout_button_color universe: primary_users groups: - name: control weight: 5000 - name: treatment weight: 5000 sequential_testing: true min_runtime_days: 14 responses: '201': description: Create an experiment content: application/json: schema: $ref: '#/components/schemas/CreateExperimentResponse' example: id: exp_01j7wb12c3d4e5f6g7h8j9k0l1 name: checkout_button_color links: GetExperiment: operationId: getExperiment parameters: id: $response.body#/id UpdateExperiment: operationId: updateExperiment parameters: id: $response.body#/id SetExperimentMetrics: operationId: setExperimentMetrics parameters: id: $response.body#/id SetExperimentStatus: operationId: setExperimentStatus parameters: id: $response.body#/id ReanalyzeExperiment: operationId: reanalyzeExperiment parameters: id: $response.body#/id DeleteExperiment: operationId: deleteExperiment parameters: id: $response.body#/id '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '409': $ref: '#/components/responses/Conflict' '422': $ref: '#/components/responses/UnprocessableEntity' x-error-codes: - BAD_REQUEST - UNAUTHORIZED - FORBIDDEN - PLAN_REQUIRED - ALREADY_EXISTS - GROUPS_WEIGHT_SUM - REFERENCED_NOT_FOUND - VALIDATION x-cli: name: create positional: - name /api/admin/experiments/{id}: get: operationId: getExperiment summary: Get one experiment description: |- Returns the full experiment row including groups, params, allocation, and lifecycle timestamps. **Use case:** Fetch one experiment to render the detail page or to inspect its current allocation and group weights. tags: - Experiments parameters: - $ref: '#/components/parameters/ProjectId' - name: id in: path required: true description: Stable opaque experiment id (`exp_…`) or the experiment's `name`. schema: $ref: '#/components/schemas/ResourceId' responses: '200': description: Get one experiment content: application/json: schema: $ref: '#/components/schemas/ExperimentApiRow' example: 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' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '409': $ref: '#/components/responses/Conflict' '422': $ref: '#/components/responses/UnprocessableEntity' x-error-codes: - BAD_REQUEST - UNAUTHORIZED - FORBIDDEN - NOT_FOUND x-cli: name: get positional: - id patch: operationId: updateExperiment summary: Update an experiment description: |- 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** - **Update metadata** — `description`, `tag`, `targeting_gate` editable any time. - **Ramp before launch** — set `allocation_pct` while still in `draft`. - **Tighten significance** — `significance_threshold` (Pro+). - **Rewire groups** — replace `groups` wholesale while in `draft`; immutable once running. tags: - Experiments parameters: - $ref: '#/components/parameters/ProjectId' - name: id in: path required: true description: Stable opaque experiment id (`exp_…`) or the experiment's `name`. schema: $ref: '#/components/schemas/ResourceId' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/UpdateExperimentRequest' examples: rampAllocation: summary: Ramp allocation — 0 → 50% description: Bumps the share of the targeted audience enrolled. Allowed only when the experiment is in `draft`. value: allocation_pct: 5000 changeWeights: summary: Change group weights description: Replace the groups array — weights must still sum to 10000. value: groups: - name: control weight: 2500 - name: treatment weight: 7500 tighterSignificance: summary: Tighter significance threshold description: Drop the p-value cutoff from 0.05 → 0.01. Requires Pro plan or higher. value: significance_threshold: 0.01 updateMetadata: summary: Update metadata description: Change description/tag/targeting_gate freely on running experiments. value: description: Bumped to 50% after passing canary on 25%. tag: checkout responses: '200': description: Update an experiment content: application/json: schema: $ref: '#/components/schemas/UpdateExperimentResponse' example: id: exp_01j7wb12c3d4e5f6g7h8j9k0l1 '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '409': $ref: '#/components/responses/Conflict' '422': $ref: '#/components/responses/UnprocessableEntity' x-error-codes: - BAD_REQUEST - UNAUTHORIZED - FORBIDDEN - PLAN_REQUIRED - NOT_FOUND - IMMUTABLE_FIELD - INVALID_TRANSITION - GROUPS_WEIGHT_SUM - REFERENCED_NOT_FOUND - VALIDATION - EXPERIMENT_IMMUTABLE_FIELD x-cli: name: update positional: - id delete: operationId: deleteExperiment summary: Delete an experiment description: |- Archives the experiment (soft-delete via status transition). Returns `409` if the experiment is still `running` — stop it first. **Use case:** Tear down an experiment after the analysis is signed off. tags: - Experiments parameters: - $ref: '#/components/parameters/ProjectId' - name: id in: path required: true description: Stable opaque experiment id (`exp_…`) or the experiment's `name`. schema: $ref: '#/components/schemas/ResourceId' responses: '200': description: Delete an experiment content: application/json: schema: $ref: '#/components/schemas/DeleteExperimentResponse' example: ok: true '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '409': $ref: '#/components/responses/Conflict' '422': $ref: '#/components/responses/UnprocessableEntity' x-error-codes: - BAD_REQUEST - UNAUTHORIZED - FORBIDDEN - NOT_FOUND - INVALID_TRANSITION x-cli: name: archive positional: - id /api/admin/experiments/{id}/status: post: operationId: setExperimentStatus summary: Transition experiment status description: |- 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. - `archived → draft` — restore a soft-deleted experiment so it can be re-completed and started. Allowed only if it never started; one that already ran must be cloned instead. Restarting an `archived` experiment directly is not allowed — restore it to `draft` first. Returns `409` on illegal transitions and `429` if the plan's `experiments_running` limit is exceeded on `→ running`. **Use cases** - **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. tags: - Experiments parameters: - $ref: '#/components/parameters/ProjectId' - name: id in: path required: true description: Stable opaque experiment id (`exp_…`) or the experiment's `name`. schema: $ref: '#/components/schemas/ResourceId' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/SetExperimentStatusRequest' examples: start: summary: Start — draft → running description: Begins allocation. The experiment counts against the plan's `experiments_running` limit. value: status: running stop: summary: Stop — running → stopped description: Halts allocation. Existing exposures remain analysable. value: status: stopped archive: summary: Archive — stopped → archived description: Soft-deletes the experiment. List endpoints exclude archived rows by default. value: status: archived restore: summary: Restore — archived → draft description: Un-deletes a soft-deleted experiment so it can be re-completed and started. Allowed only if it never ran. value: status: draft responses: '200': description: Transition experiment status content: application/json: schema: $ref: '#/components/schemas/SetExperimentStatusResponse' example: id: exp_01j7wb12c3d4e5f6g7h8j9k0l1 status: running '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '409': $ref: '#/components/responses/Conflict' '422': $ref: '#/components/responses/UnprocessableEntity' x-error-codes: - BAD_REQUEST - UNAUTHORIZED - FORBIDDEN - NOT_FOUND - INVALID_TRANSITION - VALIDATION - PLAN_LIMIT - EXPERIMENT_NO_GOAL_METRIC - EXPERIMENT_ARCHIVED_RESTART - EXPERIMENT_NOT_RUNNING - EXPERIMENT_RUNNING_ARCHIVE - EXPERIMENT_RESTORE_INVALID x-cli: positional: - id commands: - name: start summary: Start an experiment (draft → running) preset: status: running - name: stop summary: Stop a running experiment preset: status: stopped - name: restore summary: Restore an archived experiment (→ draft) preset: status: draft /api/admin/experiments/{id}/metrics: post: operationId: setExperimentMetrics summary: Attach metrics description: |- 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** - **Standard setup** — one `goal`, one or two `guardrail`, optional `secondary` metrics for diagnostics. - **Detach all** — send `{ "metrics": [] }` before archiving. tags: - Experiments parameters: - $ref: '#/components/parameters/ProjectId' - name: id in: path required: true description: Stable opaque experiment id (`exp_…`) or the experiment's `name`. schema: $ref: '#/components/schemas/ResourceId' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/SetExperimentMetricsRequest' examples: standard: summary: Standard — one goal + one guardrail description: Attaches one decision metric and one safety metric. value: metrics: - metric_id: met_checkout_completed role: goal - metric_id: met_page_errors role: guardrail detach: summary: Detach all description: Send an empty array to clear every metric. value: metrics: [] responses: '200': description: Attach metrics content: application/json: schema: $ref: '#/components/schemas/SetExperimentMetricsResponse' example: id: exp_01j7wb12c3d4e5f6g7h8j9k0l1 metrics: - metric_id: met_checkout_completed role: goal - metric_id: met_page_errors role: guardrail '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '409': $ref: '#/components/responses/Conflict' '422': $ref: '#/components/responses/UnprocessableEntity' x-error-codes: - BAD_REQUEST - UNAUTHORIZED - FORBIDDEN - NOT_FOUND - EVENT_PENDING - REFERENCED_NOT_FOUND - VALIDATION - METRIC_UNKNOWN_ID x-cli: name: set-metrics positional: - id /api/admin/experiments/{id}/results: get: operationId: getExperimentResults summary: Get analysis results description: |- 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 case:** Render the results table on the experiment detail page or drive an automated decision once a `goal` metric reaches significance. tags: - Experiments parameters: - $ref: '#/components/parameters/ProjectId' - name: id in: path required: true description: Stable opaque experiment id (`exp_…`) or the experiment's `name`. schema: $ref: '#/components/schemas/ResourceId' responses: '200': description: Get analysis results content: application/json: schema: $ref: '#/components/schemas/GetExperimentResultsResponse' example: experiment: id: exp_01j7wb12c3d4e5f6g7h8j9k0l1 name: checkout_button_color status: running results: - metric: checkout_completed groupName: control ds: '2026-05-09' 'n': 12421 mean: 0.1834 variance: 0.1498 delta: null deltaPct: null ci95Low: null ci95High: null ci99Low: null ci99High: null ciLow: null ciHigh: null pValue: null expectedN: 15000 realizedMde: null srmPValue: 0.82 srmDetected: 0 msprtSignificant: null msprtLambda: null isFinal: 0 peekWarning: 0 - metric: checkout_completed groupName: treatment ds: '2026-05-09' 'n': 12519 mean: 0.1922 variance: 0.1553 delta: 0.0088 deltaPct: 4.8 ci95Low: 0.0015 ci95High: 0.0161 ci99Low: -0.0008 ci99High: 0.0184 ciLow: 0.0015 ciHigh: 0.0161 pValue: 0.018 expectedN: 15000 realizedMde: 0.0079 srmPValue: 0.82 srmDetected: 0 msprtSignificant: null msprtLambda: null isFinal: 0 peekWarning: 0 verdict: ship '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '409': $ref: '#/components/responses/Conflict' '422': $ref: '#/components/responses/UnprocessableEntity' x-error-codes: - BAD_REQUEST - UNAUTHORIZED - FORBIDDEN - NOT_FOUND x-cli: name: results positional: - id /api/admin/experiments/{id}/timeseries: get: operationId: getExperimentTimeseries summary: Get analysis timeseries description: |- 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 case:** Drive a chart of metric movement over the experiment runtime, or sanity-check the lift is monotonic before deciding. tags: - Experiments parameters: - $ref: '#/components/parameters/ProjectId' - name: id in: path required: true description: Stable opaque experiment id (`exp_…`) or the experiment's `name`. schema: $ref: '#/components/schemas/ResourceId' - name: metric in: query required: false description: Optional metric name to filter the series. schema: type: string responses: '200': description: Get analysis timeseries content: application/json: schema: $ref: '#/components/schemas/GetExperimentTimeseriesResponse' example: experiment: id: exp_01j7wb12c3d4e5f6g7h8j9k0l1 name: checkout_button_color status: running series: - metric: checkout_completed groupName: treatment ds: '2026-05-08' 'n': 11200 mean: 0.1903 variance: 0.1541 delta: 0.0071 deltaPct: 3.9 ci95Low: -0.0004 ci95High: 0.0146 ci99Low: -0.0028 ci99High: 0.017 ciLow: -0.0004 ciHigh: 0.0146 pValue: 0.034 expectedN: 15000 realizedMde: 0.0083 srmPValue: 0.79 srmDetected: 0 msprtSignificant: null msprtLambda: null isFinal: 0 peekWarning: 0 - metric: checkout_completed groupName: treatment ds: '2026-05-09' 'n': 12519 mean: 0.1922 variance: 0.1553 delta: 0.0088 deltaPct: 4.8 ci95Low: 0.0015 ci95High: 0.0161 ci99Low: -0.0008 ci99High: 0.0184 ciLow: 0.0015 ciHigh: 0.0161 pValue: 0.018 expectedN: 15000 realizedMde: 0.0079 srmPValue: 0.82 srmDetected: 0 msprtSignificant: null msprtLambda: null isFinal: 0 peekWarning: 0 '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '409': $ref: '#/components/responses/Conflict' '422': $ref: '#/components/responses/UnprocessableEntity' x-error-codes: - BAD_REQUEST - UNAUTHORIZED - FORBIDDEN - NOT_FOUND x-cli: name: timeseries positional: - id /api/admin/experiments/{id}/reanalyze: post: operationId: reanalyzeExperiment summary: Re-queue analysis description: |- 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 case:** Force-refresh results after wiring up a new metric without waiting for the next nightly cron tick. tags: - Experiments parameters: - $ref: '#/components/parameters/ProjectId' - name: id in: path required: true description: Stable opaque experiment id (`exp_…`) or the experiment's `name`. schema: $ref: '#/components/schemas/ResourceId' responses: '200': description: Re-queue analysis content: application/json: schema: $ref: '#/components/schemas/ReanalyzeExperimentResponse' example: id: exp_01j7wb12c3d4e5f6g7h8j9k0l1 queued: true '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '409': $ref: '#/components/responses/Conflict' '422': $ref: '#/components/responses/UnprocessableEntity' x-error-codes: - BAD_REQUEST - UNAUTHORIZED - FORBIDDEN - NOT_FOUND - VALIDATION x-cli: name: reanalyze positional: - id /api/admin/experiments/{id}/readouts: post: operationId: createExperimentReadout summary: Mint a readout snapshot description: |- Freezes the current results view into an immutable, dated readout snapshot — verdict, headline, per-metric numbers, the caveat list with its acknowledgment state, enrollment, and a hash of the assignment-relevant config. Ship/stop flows mint one automatically; "Share readout" mints one on demand. Snapshots are never updated after insert. Pass `requireAllAcknowledged: true` to enforce ship gating server-side — returns `422` while any open caveat is not listed in `acknowledgedCaveatIds`. **Use case:** Capture "what the data said when we decided" before shipping or stopping, so the decision stays auditable even after results move. tags: - Experiments parameters: - $ref: '#/components/parameters/ProjectId' - name: id in: path required: true description: Stable opaque experiment id (`exp_…`) or the experiment's `name`. schema: $ref: '#/components/schemas/ResourceId' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CreateExperimentReadoutRequest' examples: manual: summary: Manual — "Share readout" description: Snapshot the current results view on demand. value: kind: manual shipGated: summary: Ship — with caveat gating description: Mint a ship readout, acknowledging the open caveats; fails with `422` if any open caveat is missing from the list. value: kind: ship acknowledgedCaveatIds: - srm - min_runtime requireAllAcknowledged: true responses: '200': description: Mint a readout snapshot content: application/json: schema: $ref: '#/components/schemas/CreateExperimentReadoutResponse' example: id: 5f0c1f2d-8e90-4c6e-9b1a-3a7c1f2d8e90 links: GetExperimentReadout: operationId: getExperimentReadout parameters: readoutId: $response.body#/id '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '409': $ref: '#/components/responses/Conflict' '422': $ref: '#/components/responses/UnprocessableEntity' x-error-codes: - BAD_REQUEST - UNAUTHORIZED - FORBIDDEN - NOT_FOUND - VALIDATION x-cli: name: readout-create positional: - id /api/admin/experiments/{id}/readouts/{readoutId}: get: operationId: getExperimentReadout summary: Get a readout snapshot description: |- Returns one immutable readout snapshot — the frozen results view (verdict, headline, per-metric numbers, caveats, enrollment) captured when it was minted, plus the `configHash` that tells you whether it is still comparable to the live view. **Use case:** Render a shared, dated readout exactly as it looked at decision time. tags: - Experiments parameters: - $ref: '#/components/parameters/ProjectId' - name: id in: path required: true description: Stable opaque experiment id (`exp_…`) or the experiment's `name`. schema: $ref: '#/components/schemas/ResourceId' - name: readoutId in: path required: true description: Readout snapshot id, as returned by `createExperimentReadout`. schema: $ref: '#/components/schemas/ResourceId' responses: '200': description: Get a readout snapshot content: application/json: schema: $ref: '#/components/schemas/ExperimentReadoutApiRow' example: id: 5f0c1f2d-8e90-4c6e-9b1a-3a7c1f2d8e90 experimentId: exp_01j7wb12c3d4e5f6g7h8j9k0l1 experimentName: checkout_button_color kind: ship createdBy: pm@example.com createdAt: '2026-05-09T18:22:11.000Z' verdict: ship title: Treatment wins on checkout conversion why: Goal metric significant with guardrails flat after 8 days. configHash: 9c1f4f1f2c0c4a5fa1c2b6d3e7c8e3a19c1f4f1f2c0c4a5fa1c2b6d3e7c8e3a1 metricsJson: - name: checkout_completed role: goal delta: 0.0088 deltaPct: 4.8 ci: - 0.0015 - 0.0161 p: 0.018 sig: true caveatsJson: - id: min_runtime severity: warn text: Below the 14-day minimum runtime. acknowledged: true usersJson: groups: - name: control 'n': 12421 - name: treatment 'n': 12519 days: 8 '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '409': $ref: '#/components/responses/Conflict' '422': $ref: '#/components/responses/UnprocessableEntity' x-error-codes: - BAD_REQUEST - UNAUTHORIZED - FORBIDDEN - NOT_FOUND x-cli: name: readout-get positional: - id - readoutId /api/admin/configs: get: operationId: listConfigs summary: List dynamic configs description: |- 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 case:** Snapshot 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. tags: - Configs parameters: - $ref: '#/components/parameters/ProjectId' - $ref: '#/components/parameters/PaginationLimit' - $ref: '#/components/parameters/PaginationCursor' - $ref: '#/components/parameters/Q' responses: '200': description: List dynamic configs content: application/json: schema: $ref: '#/components/schemas/ListConfigsResponse' example: data: - id: cfg_01j7wae5h6j7k8l9m0n1p2q3r4 name: pricing.tiers description: Pricing tier definitions consumed by the checkout flow. folder: pricing creatorEmail: ana@example.com createdAt: '2026-05-01T10:12:00.000Z' 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 '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '409': $ref: '#/components/responses/Conflict' '422': $ref: '#/components/responses/UnprocessableEntity' x-error-codes: - BAD_REQUEST - UNAUTHORIZED - FORBIDDEN x-cli: name: list post: operationId: createConfig summary: Create a dynamic config description: |- 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** - **Minimal create** — `name` + `schema`. Initial value defaults to `{}`. - **Seeded create** — supply a flat `value` to publish the same object on every env. - **Per-env seed** — supply a `{ env: value }` map under `value`, or pass the env keys `dev`/`staging`/`prod` directly (each overrides `value` for that env and is published at version 1). tags: - Configs parameters: - $ref: '#/components/parameters/ProjectId' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CreateConfigRequest' examples: minimal: summary: Minimal — name + schema description: Smallest valid body. Initial value defaults to `{}` on every env. value: name: pricing.tiers schema: type: object properties: tiers: type: array required: - tiers seededValue: summary: Seeded — schema + initial value description: Seed the same JSON object on every env at version 1. value: name: pricing.tiers description: Pricing tier definitions consumed by checkout. schema: type: object properties: tiers: type: array required: - tiers value: tiers: - name: free - name: pro perEnvSeed: summary: Per-env seed (env keys) description: Pass the env keys directly to seed different initial values per env. Each overrides `value` for that env and is published at version 1. value: name: pricing.tiers schema: type: object properties: tiers: type: array required: - tiers dev: tiers: - name: free - name: pro - name: enterprise prod: tiers: - name: free - name: pro responses: '201': description: Create a dynamic config content: application/json: schema: $ref: '#/components/schemas/CreateConfigResponse' example: id: cfg_01j7wae5h6j7k8l9m0n1p2q3r4 name: pricing.tiers links: GetConfig: operationId: getConfig parameters: id: $response.body#/id UpdateConfig: operationId: updateConfig parameters: id: $response.body#/id PublishConfigDraft: operationId: publishConfigDraft parameters: id: $response.body#/id DeleteConfig: operationId: deleteConfig parameters: id: $response.body#/id '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '409': $ref: '#/components/responses/Conflict' '422': $ref: '#/components/responses/UnprocessableEntity' x-error-codes: - BAD_REQUEST - UNAUTHORIZED - FORBIDDEN - ALREADY_EXISTS - VALIDATION - PLAN_LIMIT x-cli: name: create positional: - name /api/admin/configs/{id}: get: operationId: getConfig summary: Get one config description: |- 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 case:** Fetch one config's current published values and any in-flight drafts. tags: - Configs parameters: - $ref: '#/components/parameters/ProjectId' - name: id in: path required: true description: Stable opaque config id (`cfg_…`) or the config's `name`. schema: $ref: '#/components/schemas/ResourceId' responses: '200': description: Get one config content: application/json: schema: $ref: '#/components/schemas/GetConfigResponse' example: id: cfg_01j7wae5h6j7k8l9m0n1p2q3r4 name: pricing.tiers description: Pricing tier definitions consumed by the checkout flow. folder: pricing 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 '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '409': $ref: '#/components/responses/Conflict' '422': $ref: '#/components/responses/UnprocessableEntity' x-error-codes: - BAD_REQUEST - UNAUTHORIZED - FORBIDDEN - NOT_FOUND x-cli: name: get positional: - id patch: operationId: updateConfig summary: Update a dynamic config description: |- Partial update. When `value` is supplied it is **republished on every env** (new version per env). A per-env key (`dev`/`staging`/`prod`) publishes a new version to **only that env**, immediately, overriding `value` for it. When `schema` is supplied it replaces the current schema; every existing value is re-validated. **Use cases** - **Republish flat value** — `{ "value": {…} }` sets the same value on every env. - **Publish one env** — `{ "prod": {…} }` publishes a new version to prod only, instantly. - **Schema migration** — `{ "schema": {…} }` replaces the schema; existing values are re-validated. tags: - Configs parameters: - $ref: '#/components/parameters/ProjectId' - name: id in: path required: true description: Stable opaque config id (`cfg_…`) or the config's `name`. schema: $ref: '#/components/schemas/ResourceId' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/UpdateConfigRequest' examples: flatValue: summary: Flat value — republish on every env description: Sets the same JSON object as a new version on dev/stage/prod. value: value: tiers: - name: free - name: pro prodOnly: summary: Per-env — publish prod only description: Publishes a new version to prod only, immediately. dev/staging untouched. value: prod: tiers: - name: free - name: pro - name: enterprise schemaOnly: summary: Schema-only update description: Replaces the schema. Existing values must still validate or the update fails. value: schema: type: object properties: tiers: type: array trial_days: type: integer required: - tiers responses: '200': description: Update a dynamic config content: application/json: schema: $ref: '#/components/schemas/UpdateConfigResponse' example: id: cfg_01j7wae5h6j7k8l9m0n1p2q3r4 '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '409': $ref: '#/components/responses/Conflict' '422': $ref: '#/components/responses/UnprocessableEntity' x-error-codes: - BAD_REQUEST - UNAUTHORIZED - FORBIDDEN - NOT_FOUND - VALIDATION x-cli: name: update positional: - id delete: operationId: deleteConfig summary: Delete a dynamic config description: |- Soft-deletes the config and rebuilds the project's flags KV blob. **Use case:** Tear down a config after its consumers have stopped reading it. tags: - Configs parameters: - $ref: '#/components/parameters/ProjectId' - name: id in: path required: true description: Stable opaque config id (`cfg_…`) or the config's `name`. schema: $ref: '#/components/schemas/ResourceId' responses: '200': description: Delete a dynamic config content: application/json: schema: $ref: '#/components/schemas/DeleteConfigResponse' example: ok: true '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '409': $ref: '#/components/responses/Conflict' '422': $ref: '#/components/responses/UnprocessableEntity' x-error-codes: - BAD_REQUEST - UNAUTHORIZED - FORBIDDEN - NOT_FOUND x-cli: name: archive positional: - id /api/admin/configs/{id}/drafts: put: operationId: saveConfigDraft summary: Save a draft value description: |- 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 case:** Iterate on a config value on dev without affecting prod — preview in staging, then publish. tags: - Configs parameters: - $ref: '#/components/parameters/ProjectId' - name: id in: path required: true description: Stable opaque config id (`cfg_…`) or the config's `name`. schema: $ref: '#/components/schemas/ResourceId' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/SaveConfigDraftRequest' examples: stageDev: summary: Stage a dev draft description: Stages a new value on dev without touching stage/prod. value: env: dev value: tiers: - name: free - name: pro - name: enterprise responses: '200': description: Save a draft value content: application/json: schema: $ref: '#/components/schemas/SaveConfigDraftResponse' example: id: cfg_01j7wae5h6j7k8l9m0n1p2q3r4 env: dev baseVersion: 5 updatedAt: '2026-05-10T09:31:00.000Z' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '409': $ref: '#/components/responses/Conflict' '422': $ref: '#/components/responses/UnprocessableEntity' x-error-codes: - BAD_REQUEST - UNAUTHORIZED - FORBIDDEN - NOT_FOUND - VALIDATION x-cli: name: draft hidden: true positional: - id delete: operationId: discardConfigDraft summary: Discard a draft description: |- Drops the in-flight draft on one env. Published values are unaffected. **Use case:** Abandon an in-progress draft after deciding not to ship it. tags: - Configs parameters: - $ref: '#/components/parameters/ProjectId' - name: id in: path required: true description: Stable opaque config id (`cfg_…`) or the config's `name`. schema: $ref: '#/components/schemas/ResourceId' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/DiscardConfigDraftRequest' examples: discardDev: summary: Discard the dev draft description: Drops the staged dev draft without publishing. value: env: dev responses: '200': description: Discard a draft content: application/json: schema: $ref: '#/components/schemas/DiscardConfigDraftResponse' example: ok: true '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '409': $ref: '#/components/responses/Conflict' '422': $ref: '#/components/responses/UnprocessableEntity' x-error-codes: - BAD_REQUEST - UNAUTHORIZED - FORBIDDEN - NOT_FOUND x-cli: name: discard-draft hidden: true positional: - id /api/admin/configs/{id}/publish: post: operationId: publishConfigDraft summary: Publish a draft description: |- 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 case:** Ship a staged change once you've validated it on a lower env. tags: - Configs parameters: - $ref: '#/components/parameters/ProjectId' - name: id in: path required: true description: Stable opaque config id (`cfg_…`) or the config's `name`. schema: $ref: '#/components/schemas/ResourceId' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/PublishConfigDraftRequest' examples: publishDev: summary: Publish the dev draft description: Promotes the dev draft to a new published version on dev. value: env: dev publishProd: summary: Publish to prod description: Promotes the prod draft after staging signed off. value: env: prod responses: '200': description: Publish a draft content: application/json: schema: $ref: '#/components/schemas/PublishConfigDraftResponse' example: id: cfg_01j7wae5h6j7k8l9m0n1p2q3r4 env: dev version: 6 '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '409': $ref: '#/components/responses/Conflict' '422': $ref: '#/components/responses/UnprocessableEntity' x-error-codes: - BAD_REQUEST - UNAUTHORIZED - FORBIDDEN - NOT_FOUND - INVALID_TRANSITION - VALIDATION x-cli: name: publish hidden: true positional: - id /api/admin/configs/{id}/activity: get: operationId: listConfigActivity summary: List config activity description: |- 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 case:** Render the activity panel in the config editor or drive a slack notification on publish events. tags: - Configs parameters: - $ref: '#/components/parameters/ProjectId' - name: id in: path required: true description: Stable opaque config id (`cfg_…`) or the config's `name`. schema: $ref: '#/components/schemas/ResourceId' - name: limit in: query required: false description: Max rows to return (1–100). Defaults to 20. schema: default: 20 type: integer minimum: 1 maximum: 100 responses: '200': description: List config activity content: application/json: schema: $ref: '#/components/schemas/ListConfigActivityResponse' example: - id: act_01j7waf01a2b3c4d5e6f7g8h9i action: config.publish actorEmail: ana@example.com actorType: user payload: env: dev version: 6 createdAt: '2026-05-10T09:31:42.000Z' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '409': $ref: '#/components/responses/Conflict' '422': $ref: '#/components/responses/UnprocessableEntity' x-error-codes: - BAD_REQUEST - UNAUTHORIZED - FORBIDDEN - NOT_FOUND x-cli: name: activity hidden: true positional: - id /api/admin/configs/{id}/schema: patch: operationId: updateConfigSchema summary: Update a config schema description: |- Replaces a config's JSON Schema in place. Every existing published value is re-validated against the new schema before it lands; the update fails if any value no longer validates. **Use case:** Evolve a config's shape (add/remove a field) without republishing values. tags: - Configs parameters: - $ref: '#/components/parameters/ProjectId' - name: id in: path required: true description: Stable opaque config id (`cfg_…`) or the config's `name`. schema: $ref: '#/components/schemas/ResourceId' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/UpdateConfigSchemaRequest' examples: addField: summary: Add a field to the schema description: Replaces the schema; existing values must still validate or the update fails. value: schema: type: object properties: tiers: type: array trial_days: type: integer required: - tiers responses: '200': description: Update a config schema content: application/json: schema: $ref: '#/components/schemas/UpdateConfigSchemaResponse' example: id: cfg_01j7wae5h6j7k8l9m0n1p2q3r4 '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '409': $ref: '#/components/responses/Conflict' '422': $ref: '#/components/responses/UnprocessableEntity' x-error-codes: - BAD_REQUEST - UNAUTHORIZED - FORBIDDEN - NOT_FOUND - VALIDATION x-cli: name: update-schema positional: - id /api/admin/configs/{id}/versions: get: operationId: listConfigVersions summary: List config version history description: |- Returns every published version of the config's value on one env, newest first. The `env` query parameter picks the environment (`dev`, `staging`, or `prod`) and defaults to `prod`; an unknown env returns `400`. The config's JSON Schema is config-level and not versioned — this is value history only. **Use case:** Render the History timeline in the config detail pane (value diff + restore), or audit which value was live on prod at a given version. tags: - Configs parameters: - $ref: '#/components/parameters/ProjectId' - name: id in: path required: true description: Stable opaque config id (`cfg_…`) or the config's `name`. schema: $ref: '#/components/schemas/ResourceId' - name: env in: query required: false description: Environment to list history for (`dev`, `staging`, or `prod`). Defaults to `prod` when omitted. schema: $ref: '#/components/schemas/Env' responses: '200': description: List config version history content: application/json: schema: $ref: '#/components/schemas/ListConfigVersionsResponse' example: - version: 5 value: tiers: - name: free - name: pro publishedAt: '2026-05-09T18:22:11.000Z' publishedBy: ana@example.com - version: 4 value: tiers: - name: free publishedAt: '2026-05-08T11:05:22.000Z' publishedBy: bo@example.com '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '409': $ref: '#/components/responses/Conflict' '422': $ref: '#/components/responses/UnprocessableEntity' x-error-codes: - BAD_REQUEST - UNAUTHORIZED - FORBIDDEN - NOT_FOUND x-cli: name: versions hidden: true positional: - id /api/admin/killswitches: get: operationId: listKillswitches summary: List killswitches description: |- 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 case:** Snapshot every killswitch in the project — e.g. to render an incident-response runbook listing every kill and its current trip state. tags: - Killswitch parameters: - $ref: '#/components/parameters/ProjectId' - $ref: '#/components/parameters/PaginationLimit' - $ref: '#/components/parameters/PaginationCursor' - $ref: '#/components/parameters/Q' responses: '200': description: List killswitches content: application/json: schema: $ref: '#/components/schemas/ListKillswitchesResponse' example: data: - id: ksw_01j7w9d8h2k4m6n8p0q2r4s6t8 name: payments.checkout description: Master kill for the checkout flow. Trip to fall back to the legacy provider. folder: payments creatorEmail: ana@example.com createdAt: '2026-05-01T10:12:00.000Z' 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 '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '409': $ref: '#/components/responses/Conflict' '422': $ref: '#/components/responses/UnprocessableEntity' x-error-codes: - BAD_REQUEST - UNAUTHORIZED - FORBIDDEN x-cli: name: list post: operationId: createKillswitch summary: Create a killswitch description: |- 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** - **Untripped create** — `{ "name": "payments.checkout" }`. Provision the kill ahead of an incident. - **Pre-tripped** — `{ "value": true }` to ship the killswitch already engaged. - **With switches** — seed `switches` to carve out per-region/per-tenant kills from day one. tags: - Killswitch parameters: - $ref: '#/components/parameters/ProjectId' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CreateKillswitchRequest' examples: minimal: summary: Minimal — name only, untripped description: 'Smallest valid body. Creates a killswitch with `value: false` on every env.' value: name: payments.checkout preTripped: summary: Pre-tripped on create description: 'Ship the killswitch already tripped (`value: true`) — useful when provisioning a kill ahead of a deploy you want to ship with the feature off.' value: name: payments.checkout description: Master kill for the new checkout flow. value: true withSwitches: summary: With per-region switches description: Seed a `switches` map so specific named call sites can be tripped independently of the flat `value`. value: name: payments.checkout value: false switches: eu_region: true apac_region: false responses: '201': description: Create a killswitch content: application/json: schema: $ref: '#/components/schemas/CreateKillswitchResponse' example: id: ksw_01j7w9d8h2k4m6n8p0q2r4s6t8 name: payments.checkout links: GetKillswitch: operationId: getKillswitch parameters: id: $response.body#/id UpdateKillswitch: operationId: updateKillswitch parameters: id: $response.body#/id SetKillswitchSwitch: operationId: setKillswitchSwitch parameters: id: $response.body#/id SetKillswitchValue: operationId: setKillswitchValue parameters: id: $response.body#/id DeleteKillswitch: operationId: deleteKillswitch parameters: id: $response.body#/id '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '409': $ref: '#/components/responses/Conflict' '422': $ref: '#/components/responses/UnprocessableEntity' x-error-codes: - BAD_REQUEST - UNAUTHORIZED - FORBIDDEN - ALREADY_EXISTS - VALIDATION - PLAN_LIMIT x-cli: name: create positional: - name /api/admin/killswitches/{id}: get: operationId: getKillswitch summary: Get one killswitch description: |- Returns the killswitch metadata plus the latest published `value`/`switches`/`version` per env. **Use case:** Fetch the current state of one killswitch — e.g. to verify a trip propagated before declaring an incident mitigated. tags: - Killswitch parameters: - $ref: '#/components/parameters/ProjectId' - name: id in: path required: true description: Stable opaque killswitch id (`ksw_…`) or the killswitch's `name`. schema: $ref: '#/components/schemas/ResourceId' responses: '200': description: Get one killswitch content: application/json: schema: $ref: '#/components/schemas/GetKillswitchResponse' example: id: ksw_01j7w9d8h2k4m6n8p0q2r4s6t8 name: payments.checkout description: Master kill for the checkout flow. Trip to fall back to the legacy provider. folder: payments creatorEmail: ana@example.com createdAt: '2026-05-01T10:12:00.000Z' 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' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '409': $ref: '#/components/responses/Conflict' '422': $ref: '#/components/responses/UnprocessableEntity' x-error-codes: - BAD_REQUEST - UNAUTHORIZED - FORBIDDEN - NOT_FOUND x-cli: name: get positional: - id patch: operationId: updateKillswitch summary: Update a killswitch description: |- 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** - **Trip everywhere** — `{ "value": true }`. Kills the feature across dev/stage/prod in one call. - **Untrip everywhere** — `{ "value": false }`. - **Replace switches** — send the full new map; per-key edits use `PUT /{id}/switch`. - **Update description** — metadata-only patches don't bump versions. tags: - Killswitch parameters: - $ref: '#/components/parameters/ProjectId' - name: id in: path required: true description: Stable opaque killswitch id (`ksw_…`) or the killswitch's `name`. schema: $ref: '#/components/schemas/ResourceId' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/UpdateKillswitchRequest' examples: trip: summary: Trip the killswitch on every env description: 'Sets `value: true` on dev/stage/prod, bumping each env''s version by one.' value: value: true untrip: summary: Untrip on every env description: 'Sets `value: false` on every env.' value: value: false replaceSwitches: summary: Replace switches map description: Replaces the per-key overrides wholesale on every env. Pass `{}` to clear. value: switches: eu_region: true apac_region: true updateDescription: summary: Description-only update description: Doesn't bump versions or trigger a republish — only metadata changes. value: description: 'Master kill — owner: payments-platform' responses: '200': description: Update a killswitch content: application/json: schema: $ref: '#/components/schemas/UpdateKillswitchResponse' example: id: ksw_01j7w9d8h2k4m6n8p0q2r4s6t8 '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '409': $ref: '#/components/responses/Conflict' '422': $ref: '#/components/responses/UnprocessableEntity' x-error-codes: - BAD_REQUEST - UNAUTHORIZED - FORBIDDEN - NOT_FOUND - VALIDATION x-cli: name: update positional: - id delete: operationId: deleteKillswitch summary: Delete a killswitch description: |- Soft-deletes the killswitch and rebuilds the project's flags KV blob so SDKs stop seeing it. **Use case:** Tear down a killswitch after the feature it protected has been removed. tags: - Killswitch parameters: - $ref: '#/components/parameters/ProjectId' - name: id in: path required: true description: Stable opaque killswitch id (`ksw_…`) or the killswitch's `name`. schema: $ref: '#/components/schemas/ResourceId' responses: '200': description: Delete a killswitch content: application/json: schema: $ref: '#/components/schemas/DeleteKillswitchResponse' example: ok: true '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '409': $ref: '#/components/responses/Conflict' '422': $ref: '#/components/responses/UnprocessableEntity' x-error-codes: - BAD_REQUEST - UNAUTHORIZED - FORBIDDEN - NOT_FOUND x-cli: name: archive positional: - id /api/admin/killswitches/{id}/switch: put: operationId: setKillswitchSwitch summary: Set one switch entry description: |- 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** - **Trip a region** — `{ "env": "prod", "switchKey": "eu_region", "value": true }`. - **Untrip without removing** — same payload with `value: false`. To remove the entry entirely use `DELETE /{id}/switch`. tags: - Killswitch parameters: - $ref: '#/components/parameters/ProjectId' - name: id in: path required: true description: Stable opaque killswitch id (`ksw_…`) or the killswitch's `name`. schema: $ref: '#/components/schemas/ResourceId' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/SetKillswitchSwitchRequest' examples: tripRegion: summary: Trip eu_region on prod description: Sets `switches.eu_region = true` on prod only. Other envs and the flat `value` are unchanged. value: env: prod switchKey: eu_region value: true untripRegion: summary: Untrip eu_region on prod description: Flip the same entry back off without removing it from the map. value: env: prod switchKey: eu_region value: false responses: '200': description: Set one switch entry content: application/json: schema: $ref: '#/components/schemas/SetKillswitchSwitchResponse' example: id: ksw_01j7w9d8h2k4m6n8p0q2r4s6t8 env: prod switchKey: eu_region value: true '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '409': $ref: '#/components/responses/Conflict' '422': $ref: '#/components/responses/UnprocessableEntity' x-error-codes: - BAD_REQUEST - UNAUTHORIZED - FORBIDDEN - NOT_FOUND - VALIDATION x-cli: name: set positional: - id delete: operationId: unsetKillswitchSwitch summary: Remove one switch entry description: |- 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 case:** Clean up a per-region override after the incident is resolved so the flat `value` governs again. tags: - Killswitch parameters: - $ref: '#/components/parameters/ProjectId' - name: id in: path required: true description: Stable opaque killswitch id (`ksw_…`) or the killswitch's `name`. schema: $ref: '#/components/schemas/ResourceId' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/UnsetKillswitchSwitchRequest' examples: removeRegion: summary: Remove eu_region from prod description: Drops the `eu_region` override on prod so the flat `value` applies again. value: env: prod switchKey: eu_region responses: '200': description: Remove one switch entry content: application/json: schema: $ref: '#/components/schemas/UnsetKillswitchSwitchResponse' example: id: ksw_01j7w9d8h2k4m6n8p0q2r4s6t8 env: prod switchKey: eu_region removed: true '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '409': $ref: '#/components/responses/Conflict' '422': $ref: '#/components/responses/UnprocessableEntity' x-error-codes: - BAD_REQUEST - UNAUTHORIZED - FORBIDDEN - NOT_FOUND x-cli: name: unset positional: - id /api/admin/killswitches/{id}/value: put: operationId: setKillswitchValue summary: Set the flat value on one env description: |- Sets the flat `value` on a single `env`, publishing one new version on that env only. `switches` and other envs are untouched. Use this to trip (or untrip) a killswitch on one environment without replacing its per-key overrides. **Use cases** - **Trip on prod** — `{ "env": "prod", "value": true }`. - **Untrip on prod** — `{ "env": "prod", "value": false }`. tags: - Killswitch parameters: - $ref: '#/components/parameters/ProjectId' - name: id in: path required: true description: Stable opaque killswitch id (`ksw_…`) or the killswitch's `name`. schema: $ref: '#/components/schemas/ResourceId' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/SetKillswitchValueRequest' examples: tripProd: summary: Trip on prod description: 'Publishes `value: true` on prod only, bumping that env''s version.' value: env: prod value: true untripProd: summary: Untrip on prod description: 'Publishes `value: false` on prod only.' value: env: prod value: false responses: '200': description: Set the flat value on one env content: application/json: schema: $ref: '#/components/schemas/SetKillswitchValueResponse' example: id: ksw_01j7w9d8h2k4m6n8p0q2r4s6t8 env: prod version: 6 published: value: true switches: eu_region: true '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '409': $ref: '#/components/responses/Conflict' '422': $ref: '#/components/responses/UnprocessableEntity' x-error-codes: - BAD_REQUEST - UNAUTHORIZED - FORBIDDEN - NOT_FOUND - VALIDATION x-cli: name: set-value positional: - id /api/admin/killswitches/{id}/toggle: post: operationId: toggleKillswitch summary: Toggle a killswitch or one of its switches description: |- Flips a killswitch on one environment and publishes a new version there. This is the one-call incident verb: it reads the current value, flips it, and publishes, so you don't have to fetch the killswitch first. Every body field is optional, which is what makes the call widen cleanly: - **Flip the killswitch** — `{}`. Flips the flat `value` on `prod`. - **Flip one sub-switch** — `{ "switchKey": "eu_region" }`. Flips that entry on `prod`, creating it (from `false`) if it isn't in the map yet. - **Set it idempotently** — `{ "switchKey": "eu_region", "value": true }`. Publishes exactly that value, so a retried call can't undo the first one. A `null` `value` means "flip", not "set to null". - **Choose the environment** — add `"env": "staging"`. Omitted, `env` is `prod`. The response reports both `previous` and `value`, so a caller that asked for a flip can see what it actually changed. Prefer this over `PUT /{id}/value` and `PUT /{id}/switch` unless you specifically need those endpoints' unconditional set semantics. tags: - Killswitch parameters: - $ref: '#/components/parameters/ProjectId' - name: id in: path required: true description: Stable opaque killswitch id (`ksw_…`) or the killswitch's `name`. schema: $ref: '#/components/schemas/ResourceId' requestBody: required: false content: application/json: schema: $ref: '#/components/schemas/ToggleKillswitchRequest' examples: flipOnProd: summary: Flip the killswitch on prod description: No body fields — reads the flat `value` on prod and publishes its opposite. value: {} flipOneSwitch: summary: Flip one sub-switch on prod description: Flips `switches.eu_region` on prod, leaving the flat `value` and every other key alone. value: switchKey: eu_region setOneSwitch: summary: Trip one sub-switch idempotently description: Publishes `switches.eu_region = true` on prod whatever it was before — safe to retry. value: switchKey: eu_region value: true setOneSwitchOnStaging: summary: Trip one sub-switch on a chosen env description: The same idempotent set, published on staging instead of prod. value: switchKey: eu_region value: true env: staging responses: '200': description: Toggle a killswitch or one of its switches content: application/json: schema: $ref: '#/components/schemas/ToggleKillswitchResponse' example: id: ksw_01j7w9d8h2k4m6n8p0q2r4s6t8 env: prod switchKey: eu_region previous: false value: true version: 7 published: value: false switches: eu_region: true '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '409': $ref: '#/components/responses/Conflict' '422': $ref: '#/components/responses/UnprocessableEntity' x-error-codes: - BAD_REQUEST - UNAUTHORIZED - FORBIDDEN - NOT_FOUND - VALIDATION x-cli: name: toggle positional: - id /api/admin/universes: get: operationId: listUniverses summary: List universes description: |- 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 case:** Snapshot every universe in the project — for example to audit which `unit_type` and `holdout_range` are in use before launching a new experiment. tags: - Universes parameters: - $ref: '#/components/parameters/ProjectId' - $ref: '#/components/parameters/PaginationLimit' - $ref: '#/components/parameters/PaginationCursor' - $ref: '#/components/parameters/Q' responses: '200': description: List universes content: application/json: schema: $ref: '#/components/schemas/ListUniversesResponse' example: data: - id: uni_01j7w8a1b2c3d4e5f6g7h8i9j0 name: primary_users unitType: user_id holdoutRange: - 9500 - 9999 createdAt: '2026-04-12T10:14:08.000Z' next_cursor: null '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '409': $ref: '#/components/responses/Conflict' '422': $ref: '#/components/responses/UnprocessableEntity' x-error-codes: - BAD_REQUEST - UNAUTHORIZED - FORBIDDEN x-cli: name: list post: operationId: createUniverse summary: Create a universe description: |- 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** - **Default universe** — `{ "name": "primary_users" }`. Per-user randomisation, no holdout. - **Reserved holdout** — supply `holdout_range` to carve out a measurement slice excluded from all experiments. - **Account-level** — `unit_type: 'account_id'` so multi-seat accounts see one consistent variant. tags: - Universes parameters: - $ref: '#/components/parameters/ProjectId' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CreateUniverseRequest' examples: minimal: summary: Minimal — name only description: Smallest valid body. Creates a `user_id`-keyed universe with no holdout. value: name: primary_users holdout: summary: Holdout — reserve 5% for measurement description: Reserves buckets 9500–9999 (5% of callers) as the holdout. Pro plan or higher. value: name: primary_users holdout_range: - 9500 - 9999 accountLevel: summary: Account-level randomisation description: 'Bucket whole accounts together via `unit_type: ''account_id''`.' value: name: saas_accounts unit_type: account_id responses: '201': description: Create a universe content: application/json: schema: $ref: '#/components/schemas/CreateUniverseResponse' example: id: uni_01j7w8a1b2c3d4e5f6g7h8i9j0 name: primary_users links: UpdateUniverse: operationId: updateUniverse parameters: id: $response.body#/id DeleteUniverse: operationId: deleteUniverse parameters: id: $response.body#/id '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '409': $ref: '#/components/responses/Conflict' '422': $ref: '#/components/responses/UnprocessableEntity' x-error-codes: - BAD_REQUEST - UNAUTHORIZED - FORBIDDEN - PLAN_REQUIRED - ALREADY_EXISTS - VALIDATION - PLAN_LIMIT x-cli: name: create positional: - name /api/admin/universes/{id}: patch: operationId: updateUniverse summary: Update a universe description: |- 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** - **Adjust holdout** — change the reserved measurement slice without recreating experiments. - **Remove holdout** — `{ "holdout_range": null }`. tags: - Universes parameters: - $ref: '#/components/parameters/ProjectId' - name: id in: path required: true description: Stable opaque universe id (`uni_…`) or the universe's `name`. schema: $ref: '#/components/schemas/ResourceId' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/UpdateUniverseRequest' examples: setHoldout: summary: Set holdout — reserve 5% description: Reserve buckets 9500–9999 as the holdout. Pro plan or higher. value: holdout_range: - 9500 - 9999 removeHoldout: summary: Remove holdout description: Disable the holdout entirely. value: holdout_range: null responses: '200': description: Update a universe content: application/json: schema: $ref: '#/components/schemas/UpdateUniverseResponse' example: id: uni_01j7w8a1b2c3d4e5f6g7h8i9j0 '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '409': $ref: '#/components/responses/Conflict' '422': $ref: '#/components/responses/UnprocessableEntity' x-error-codes: - BAD_REQUEST - UNAUTHORIZED - FORBIDDEN - PLAN_REQUIRED - NOT_FOUND - VALIDATION x-cli: name: update positional: - id delete: operationId: deleteUniverse summary: Delete a universe description: |- Soft-deletes the universe. Returns `409` if any non-archived experiment still references it — archive those experiments first. **Use case:** Tear down a universe after every experiment that used it has been archived. tags: - Universes parameters: - $ref: '#/components/parameters/ProjectId' - name: id in: path required: true description: Stable opaque universe id (`uni_…`) or the universe's `name`. schema: $ref: '#/components/schemas/ResourceId' responses: '200': description: Delete a universe content: application/json: schema: $ref: '#/components/schemas/DeleteUniverseResponse' example: ok: true '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '409': $ref: '#/components/responses/Conflict' '422': $ref: '#/components/responses/UnprocessableEntity' x-error-codes: - BAD_REQUEST - UNAUTHORIZED - FORBIDDEN - NOT_FOUND - REFERENCED_IN_USE x-cli: name: archive positional: - id /api/admin/gates/templates: get: operationId: listGateTemplates summary: List gate templates description: |- Returns the merged targeting-rule template catalog — read-only **built-ins first** (the `@team`/`@owner` audience aliases, country, email-domain, region presets, …), then this project's **customer** templates. Each item carries a `rules: [{ attr, op, value }]` definition; the alias templates carry a symbol value (`email in ["@team"]`) that the SDK expands to the resolved email list at rebuild. **Turn "launch X in country Y" into a gated flag:** list templates (optionally with `q`), take the matching template's `rules`, substitute the concrete value(s), and pass them as the `rules` arg of `release_flags_create`. The catalog is small and bounded, so the full merged set is returned in one page — this endpoint is **not** paginated (`next_cursor` is always `null`; there are no `limit`/`cursor` params). tags: - Templates parameters: - $ref: '#/components/parameters/ProjectId' - $ref: '#/components/parameters/Q' - name: query in: query required: false deprecated: true description: Deprecated alias for `q`, kept working for one release. Prefer `q`. schema: type: string responses: '200': description: List gate templates content: application/json: schema: $ref: '#/components/schemas/ListGateTemplatesResponse' example: data: - id: country name: Country is description: Target by ISO country code (resolve from GeoIP at the SDK edge before evaluating). category: condition auto: true builtin: true iconKey: globe rules: - attr: country op: in value: - DE - FR - IT createdAt: null updatedAt: null next_cursor: null '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '422': $ref: '#/components/responses/UnprocessableEntity' x-error-codes: - BAD_REQUEST - UNAUTHORIZED - FORBIDDEN x-cli: name: list post: operationId: createGateTemplate summary: Create a gate template description: Creates a per-project (customer) targeting-rule template. Built-ins are read-only and cannot be created here. Returns `409` if a template with this `name` already exists in the project. tags: - Templates parameters: - $ref: '#/components/parameters/ProjectId' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CreateGateTemplateRequest' examples: enterpriseBeta: summary: Save an "enterprise plan" condition value: name: Enterprise beta description: Our enterprise plan on the beta ring. category: condition rules: - attr: plan op: eq value: enterprise responses: '201': description: Create a gate template content: application/json: schema: $ref: '#/components/schemas/CreateGateTemplateResponse' example: id: gtpl_01j7w8a1b2c3d4e5f6g7h8i9j0 name: Enterprise beta '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '409': $ref: '#/components/responses/Conflict' '422': $ref: '#/components/responses/UnprocessableEntity' x-error-codes: - BAD_REQUEST - UNAUTHORIZED - FORBIDDEN - ALREADY_EXISTS - VALIDATION x-cli: name: create positional: - name /api/admin/gates/templates/{id}: get: operationId: getGateTemplate summary: Get one gate template description: Returns a single template by its `id` — a built-in slug (`country`) or a customer `gtpl_…` id. tags: - Templates parameters: - $ref: '#/components/parameters/ProjectId' - name: id in: path required: true description: Built-in slug (`country`) or customer template id (`gtpl_…`). schema: $ref: '#/components/schemas/ResourceId' responses: '200': description: Get one gate template content: application/json: schema: $ref: '#/components/schemas/GateTemplate' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' x-error-codes: - BAD_REQUEST - UNAUTHORIZED - FORBIDDEN - NOT_FOUND x-cli: name: get positional: - id patch: operationId: updateGateTemplate summary: Update a gate template description: Partial update of a **customer** template. `rules` replaces the list wholesale. Returns `409` if `id` names a read-only built-in template. tags: - Templates parameters: - $ref: '#/components/parameters/ProjectId' - name: id in: path required: true description: Customer template id (`gtpl_…`) or its `name`. schema: $ref: '#/components/schemas/ResourceId' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/UpdateGateTemplateRequest' examples: rename: summary: Rename + retarget value: name: Enterprise GA rules: - attr: plan op: in value: - enterprise - enterprise_trial responses: '200': description: Update a gate template content: application/json: schema: $ref: '#/components/schemas/UpdateGateTemplateResponse' example: id: gtpl_01j7w8a1b2c3d4e5f6g7h8i9j0 '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '409': $ref: '#/components/responses/Conflict' '422': $ref: '#/components/responses/UnprocessableEntity' x-error-codes: - BAD_REQUEST - UNAUTHORIZED - FORBIDDEN - NOT_FOUND - READ_ONLY - VALIDATION x-cli: name: update positional: - id delete: operationId: deleteGateTemplate summary: Delete a gate template description: Soft-deletes (archives) a **customer** template. Returns `409` if `id` names a read-only built-in template. tags: - Templates parameters: - $ref: '#/components/parameters/ProjectId' - name: id in: path required: true description: Customer template id (`gtpl_…`) or its `name`. schema: $ref: '#/components/schemas/ResourceId' responses: '200': description: Delete a gate template content: application/json: schema: $ref: '#/components/schemas/DeleteGateTemplateResponse' example: ok: true '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '409': $ref: '#/components/responses/Conflict' '422': $ref: '#/components/responses/UnprocessableEntity' x-error-codes: - BAD_REQUEST - UNAUTHORIZED - FORBIDDEN - NOT_FOUND - READ_ONLY x-cli: name: archive positional: - id /api/admin/attributes: get: operationId: listAttributes summary: List targeting attributes description: |- Returns every auto-inferred targeting attribute in the project — the `name` and (when known) the value `type` — for building gate/experiment targeting rules. **Use case:** Discover which user-context keys are available before authoring a targeting rule, instead of guessing attribute names. tags: - Attributes parameters: - $ref: '#/components/parameters/ProjectId' - $ref: '#/components/parameters/Q' responses: '200': description: List targeting attributes content: application/json: schema: $ref: '#/components/schemas/ListAttributesResponse' example: - id: attr_01j7w8a1b2c3 name: plan type: enum enumValues: - free - paid required: 1 description: Billing plan of the calling user. sdkPath: user.plan createdAt: '2026-06-20T09:14:08.000Z' source: manual lastSeenAt: '2026-07-16T22:04:11.000Z' deprecated: 0 - id: attr_01j7w8a1b2c4 name: country type: string enumValues: null required: 0 description: null sdkPath: null createdAt: '2026-06-20T09:14:08.000Z' source: auto lastSeenAt: '2026-07-16T22:04:11.000Z' deprecated: 0 '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '409': $ref: '#/components/responses/Conflict' '422': $ref: '#/components/responses/UnprocessableEntity' x-error-codes: - BAD_REQUEST - UNAUTHORIZED - FORBIDDEN x-cli: name: list post: operationId: createAttribute summary: Declare a targeting attribute description: |- Declare a targeting attribute the SDK reports and gates/experiments can target. `type: enum` requires `enum_values`. **Use case:** Register a `plan` or `country` attribute so targeting rules can reference it. tags: - Attributes parameters: - $ref: '#/components/parameters/ProjectId' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/CreateAttributeRequest' example: name: plan type: enum enum_values: - free - paid required: true responses: '201': description: Declare a targeting attribute content: application/json: schema: $ref: '#/components/schemas/CreateAttributeResponse' example: id: attr_01j7w8a1b2c3 name: plan links: GetAttribute: operationId: getAttribute parameters: id: $response.body#/id UpdateAttribute: operationId: updateAttribute parameters: id: $response.body#/id DeleteAttribute: operationId: deleteAttribute parameters: id: $response.body#/id '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '409': $ref: '#/components/responses/Conflict' '422': $ref: '#/components/responses/UnprocessableEntity' x-error-codes: - BAD_REQUEST - UNAUTHORIZED - FORBIDDEN - ALREADY_EXISTS - VALIDATION x-cli: name: create positional: - name /api/admin/attributes/{id}: get: operationId: getAttribute summary: Get a targeting attribute description: |- Fetch one targeting attribute by id. **Use case:** Inspect an attribute's declared type + allowed values before editing it. tags: - Attributes parameters: - $ref: '#/components/parameters/ProjectId' - name: id in: path required: true description: The attribute id. schema: $ref: '#/components/schemas/ResourceId' responses: '200': description: Get a targeting attribute content: application/json: schema: $ref: '#/components/schemas/GetAttributeResponse' example: id: attr_01j7w8a1b2c3 name: plan type: enum enumValues: - free - paid required: 1 createdAt: '2026-06-20T09:14:08.000Z' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '409': $ref: '#/components/responses/Conflict' '422': $ref: '#/components/responses/UnprocessableEntity' x-error-codes: - BAD_REQUEST - UNAUTHORIZED - FORBIDDEN - NOT_FOUND x-cli: name: get positional: - id patch: operationId: updateAttribute summary: Update a targeting attribute description: |- Update a targeting attribute's type, allowed values, required flag, description, or SDK path. `name` is immutable. **Use case:** Add an allowed value to an `enum` attribute, or flip its required flag. tags: - Attributes parameters: - $ref: '#/components/parameters/ProjectId' - name: id in: path required: true description: The attribute id. schema: $ref: '#/components/schemas/ResourceId' requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/UpdateAttributeRequest' example: enum_values: - free - paid - enterprise responses: '200': description: Update a targeting attribute content: application/json: schema: $ref: '#/components/schemas/UpdateAttributeResponse' example: id: attr_01j7w8a1b2c3 '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '409': $ref: '#/components/responses/Conflict' '422': $ref: '#/components/responses/UnprocessableEntity' x-error-codes: - BAD_REQUEST - UNAUTHORIZED - FORBIDDEN - NOT_FOUND - VALIDATION x-cli: name: update positional: - id delete: operationId: deleteAttribute summary: Archive a targeting attribute description: |- Soft-deletes (archives) a targeting attribute. **Use case:** Retire an attribute no targeting rule references anymore (the user-facing verb is `archive`). tags: - Attributes parameters: - $ref: '#/components/parameters/ProjectId' - name: id in: path required: true description: The attribute id. schema: $ref: '#/components/schemas/ResourceId' responses: '200': description: Archive a targeting attribute content: application/json: schema: $ref: '#/components/schemas/DeleteAttributeResponse' example: ok: true '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '409': $ref: '#/components/responses/Conflict' '422': $ref: '#/components/responses/UnprocessableEntity' x-error-codes: - BAD_REQUEST - UNAUTHORIZED - FORBIDDEN - NOT_FOUND x-cli: name: archive positional: - id /api/admin/metrics: get: operationId: listMetrics summary: List metrics description: |- Returns every metric in the project (not paginated) — name, folder, source event, the typed `queryIr`, and the rendered query. **Use case:** Audit every metric defined in the project — for example to find the metric id to attach as an experiment's success metric. tags: - Metrics parameters: - $ref: '#/components/parameters/ProjectId' - $ref: '#/components/parameters/Q' responses: '200': description: List metrics content: application/json: schema: $ref: '#/components/schemas/ListMetricsResponse' example: - id: met_01j7w8a1b2c3d4e5f6g7h8i9j0 name: checkouts folder: null eventName: checkout_completed query: count(checkout_completed) queryIr: v: 2 expr: node: agg event: checkout_completed agg: kind: count filters: [] direction: higher_better version: 1 '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '409': $ref: '#/components/responses/Conflict' '422': $ref: '#/components/responses/UnprocessableEntity' x-error-codes: - BAD_REQUEST - UNAUTHORIZED - FORBIDDEN x-cli: name: list post: operationId: createMetric summary: Create a metric description: |- Creates an event-backed metric. Pass the query as the DSL string (`query`) **or** the typed IR (`query_ir`) — exactly one. `event_name` must equal the event the query references. Returns `409` if a metric with the same `name` already exists, and `422` if the query is invalid or references an unregistered event / label. **Use cases** - **Track an event** — `count()` for event counts. - **Sum a value** — `sum(,