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: 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: 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, chunk 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's chunks 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 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}: 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/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/GetExperimentResponse' 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 group_name: control ds: '2026-05-09' 'n': 12421 mean: 0.1834 delta_pct: null p_value: null srm_detected: 0 - metric: checkout_completed group_name: treatment ds: '2026-05-09' 'n': 12519 mean: 0.1922 delta_pct: 4.8 p_value: 0.018 srm_detected: 0 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 group_name: treatment ds: '2026-05-08' 'n': 11200 mean: 0.1903 delta_pct: 3.9 p_value: 0.034 srm_detected: 0 - metric: checkout_completed group_name: treatment ds: '2026-05-09' 'n': 12519 mean: 0.1922 delta_pct: 4.8 p_value: 0.018 srm_detected: 0 '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/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. 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 for different per-env starting values. 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 description: 'Pass a `{ env: value }` map to seed different initial values per env.' value: name: pricing.tiers schema: type: object properties: tiers: type: array required: - tiers value: dev: tiers: - name: free - name: pro - name: enterprise stage: tiers: - name: free - name: pro 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. 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). When `schema` is supplied it replaces the current schema; every existing value is re-validated. For env-scoped edits, use the draft/publish flow (`PUT /{id}/drafts` then `POST /{id}/publish`) instead. **Use cases** - **Republish flat value** — `{ "value": {…} }` sets the same value on every env. - **Schema migration** — `{ "schema": {…} }` replaces the schema; existing values are re-validated. - **Env-scoped edits** — use `PUT /{id}/drafts` + `POST /{id}/publish` instead of PATCH. 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 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 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 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 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 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/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. 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. 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/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: - name: plan type: string - name: country type: string - name: seats type: number '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_users(checkout_completed) queryIr: agg: kind: count_users metric: checkout_completed filters: [] direction: higher_better '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_users()` for unique-user counts. - **Sum a value** — `sum(,