Shipeasy
Flags & ExperimentsAPI

API reference

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

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

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

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

Common patterns

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

Errors. All errors share a single envelope:

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

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

Flags

GET/api/admin/gates

List feature gates

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

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

Parameters

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

Response · 200

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

Create a feature gate

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

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

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

Use cases

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

Parameters

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

Body

NameTypeDescription
namerequiredstringStable gate key used by SDKs (`Shipeasy.checkGate(user, '<name>')`). Single segment or `folder.name`. Lowercase letters, digits, `_` or `-`; max 128 chars. Immutable after create — rename = delete + recreate.
type"targeting" | "holdout"Gate kind. `targeting` (default) is a normal flag with the full builder. `holdout` is a **restricted** flag — only a public rollout % and a whitelist are allowed; attribute rules and a gatekeeper stack are rejected. Used as an experiment's `holdout_gate`. default: "targeting"
enabledbooleanMaster switch. Defaults to `true`. Set `false` to create the gate disabled (evaluates to `false` regardless of rules/rollout); flip on via `POST /{id}/enable` or PATCH. default: true
rollout_pctintegerInitial rollout in **basis points** (0–10000 = 0%–100%) — `100` here means **1%**, not 100%. Use `rollout_percent` (0–100) below if you'd rather think in percent. Use `0` to create the gate dark and ramp via PATCH after deploy validation. default: 0
rollout_percentnumberInitial rollout as a **percentage** (0–100, fractional ok). Friendlier alias for `rollout_pct`; converted internally to basis points (e.g. `100` here = 10000 bp = 100%). If both `rollout_pct` and `rollout_percent` are set, `rollout_percent` wins.
rulesarray<{ attr: string; op: string; value: any }>Targeting predicates. AND-combined. If non-empty, the gate returns `true` only for callers that satisfy every rule **and** fall under `rollout_pct`. default: []
saltstringHash salt for percentage bucketing. Auto-generated if omitted. Provide explicitly to keep a gate's buckets stable across delete/recreate. **Immutable after create** — there is no PATCH for `salt` because changing it would re-bucket every caller.
stackarray<{ id: string; type: "condition"; name?: string; fromTemplate?: string | null; pass?: "all" | "any"; rules?: array<{ attr: string; op: string; value: any }>; rolloutPct?: integer; bucketBy?: string; salt?: string; ramp?: { from: integer; to: integer; startAt: integer; durationMs: integer }; locked?: boolean } | { id: string; type: "rollout"; name?: string; fromTemplate?: string | null; rolloutPct: integer; bucketBy?: string; salt?: string; ramp?: { from: integer; to: integer; startAt: integer; durationMs: integer }; locked?: boolean }> | nullOptional gatekeeper stack. When provided, takes precedence over `rules` + `rollout_pct` at evaluation time. Omit (or pass `null`) for a flat gate.
titlestringHuman-readable title shown in the dashboard. Free-form, no key format constraint.
descriptionstringLong-form description / runbook. Markdown is rendered in the dashboard.
folderstring | null
groupstringGroup label for dashboard organisation (e.g. team or product area).
owner_emailstringOwner contact. Displayed verbatim; not used for auth.

Response · 201

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

Update a feature gate

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

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

Use cases

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

Parameters

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

Body

NameTypeDescription
type"targeting" | "holdout"Gate kind. Switching to `holdout` requires the gate carry only a public rollout % + whitelist (attribute rules / stack are rejected).
rollout_pctintegerNew rollout in **basis points** (0–10000 = 0%–100%) — `100` here means **1%**. Use `rollout_percent` (0–100) below for percent. Omit both to leave unchanged.
rollout_percentnumberNew rollout as a **percentage** (0–100). Friendlier alias for `rollout_pct`; converted internally. Wins over `rollout_pct` if both are supplied. Omit both to leave unchanged.
rulesarray<{ attr: string; op: string; value: any }>Replaces the rule list wholesale. To add a value to an `in` rule, send the full new `rules` array with the augmented `value` (e.g. previous `['US','CA']` → `['US','CA','GB']`).
enabledbooleanMaster switch. `false` makes the gate evaluate to `false` for every caller regardless of `rollout_pct`, `rules`, or `stack` — use as kill switch.
stackarray<{ id: string; type: "condition"; name?: string; fromTemplate?: string | null; pass?: "all" | "any"; rules?: array<{ attr: string; op: string; value: any }>; rolloutPct?: integer; bucketBy?: string; salt?: string; ramp?: { from: integer; to: integer; startAt: integer; durationMs: integer }; locked?: boolean } | { id: string; type: "rollout"; name?: string; fromTemplate?: string | null; rolloutPct: integer; bucketBy?: string; salt?: string; ramp?: { from: integer; to: integer; startAt: integer; durationMs: integer }; locked?: boolean }> | nullReplaces the gatekeeper stack wholesale. Send `null` to revert to flat `rules` + `rollout_pct` evaluation.
titlestringHuman-readable title shown in the dashboard. Free-form, no key format constraint.
descriptionstringLong-form description / runbook. Markdown is rendered in the dashboard.
folderstring | null
groupstringGroup label for dashboard organisation (e.g. team or product area).
owner_emailstringOwner contact. Displayed verbatim; not used for auth.

Response · 200

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

Delete a feature gate

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

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

Parameters

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

Response · 200

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

Enable a gate

Sets enabled: true. The current rollout_pct is preserved.

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

Parameters

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

Response · 200

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

Disable a gate

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

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

Parameters

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

Response · 200

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

Experiments

GET/api/admin/experiments

List experiments

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

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

Parameters

NameInTypeDescription
X-Project-IdrequiredheaderstringProject to scope this request to.
limitquerynumberMax results per page (default 50, max 500).
cursorquerystringOpaque pagination cursor from a prior page's `next_cursor`.
statusquerystringFilter by lifecycle status. Pass `archived` to return the archive tab; any other value (or omitting it) returns the non-archived experiments.

Response · 200

NameTypeDescription
datarequiredarray<{ id: string; name: string; description: string | null; hypothesis: string | null; tag: string | null; ownerEmail: string | null; audience: string | null; bucketBy: string | null; folder: string | null; status: "draft" | "running" | "stopped" | "archived"; universe: string; targetingGate: string | null; holdoutGate: string | null; allocationPct: integer; reservedHeadroom: integer; hashVersion: integer; poolOffsetBp: integer | null; poolSizeBp: integer | null; salt: string; params: object; groups: array<{ name: string; weight: integer; params?: object }>; significanceThreshold: number; minRuntimeDays: integer; minSampleSize: integer; sequentialTesting: boolean; startedAt: string | null; stoppedAt: string | null; updatedAt: string; version: integer | null; creatorEmail?: string | null; updaterEmail?: string | null; verdict?: "ship" | "hold" | "wait" | "invalid" | "draft"; verdictTitle?: string; verdictWhy?: string; goalMetric?: { id: string; name: string } | null; guardrails?: array<{ id: string; name: string; eventName?: string | null }>; guardrailCount?: integer; primaryLiftPct?: number | null; significancePct?: number | null; sampleSize?: integer | null; exposure?: array<{ ds: string; value: number }>; exposureTotal?: integer | null }>
next_cursorrequiredstring | null
Example · 200
{
  "data": [
    {
      "id": "exp_01j7wb12c3d4e5f6g7h8j9k0l1",
      "name": "checkout_button_color",
      "description": "Test green vs. blue CTA on the checkout page.",
      "tag": "checkout",
      "status": "running",
      "universe": "primary_users",
      "targetingGate": null,
      "allocationPct": 5000,
      "salt": "8d3e9a1f6b7c4a5fa1c2b6d3e7c8e3a1",
      "params": {
        "cta_color": "string"
      },
      "groups": [
        {
          "name": "control",
          "weight": 5000,
          "params": {
            "cta_color": "blue"
          }
        },
        {
          "name": "treatment",
          "weight": 5000,
          "params": {
            "cta_color": "green"
          }
        }
      ],
      "significanceThreshold": 0.05,
      "minRuntimeDays": 7,
      "minSampleSize": 1000,
      "sequentialTesting": false,
      "startedAt": "2026-05-01T12:00:00.000Z",
      "stoppedAt": null,
      "updatedAt": "2026-05-09T18:22:11.000Z"
    }
  ],
  "next_cursor": null
}
POST/api/admin/experiments

Create an experiment

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

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

Use cases

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

Parameters

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

Body

NameTypeDescription
namerequiredstringStable experiment key. Single segment or `folder.name` (a-z, 0-9, `_`/`-`; max 128 chars). Used by SDKs as `Shipeasy.getExperiment(user, '<name>')`. Immutable after create.
descriptionstring | nullFree-form description. Max 2000 chars, markdown rendered in the dashboard. default: null
hypothesisstring | nullHypothesis statement shown in the editor. Display-only. default: null
tagstring | nullShort tag chip rendered next to the name. Display-only. default: null
owner_emailstring | nullOwner email. Display-only. default: null
audiencestring | nullAudience label shown in the editor. Display-only. default: null
bucket_bystring | null default: null
folderstring | null
universerequiredstringName of an existing universe in the project. Returns `422` if the universe doesn't exist.
targeting_gatestring | nullOptional gate name (a `targeting`-type flag). Only callers that pass the gate are enrolled in the experiment. default: null
holdout_gatestring | nullOptional per-experiment holdout gate — the name of a `holdout`-type flag (public % + whitelist). A caller the flag passes is *held out* (never assigned, sees the universe defaults). Distinct from the universe-level holdout. default: null
allocation_pctintegerShare of the (gated) audience allocated to the experiment, in basis points (0–10000 = 0%–100%). `0` = unallocated. Under pooled assignment this is the size of the universe-pool slice claimed. Use `allocation_percent` (0–100) below to think in percent. Immutable while the experiment is running. default: 0
allocation_percentnumberAllocation as a **percentage** (0–100, fractional ok). Friendlier alias for `allocation_pct`; converted to basis points server-side (e.g. `50` = 5000 bp). If both are set, `allocation_percent` wins.
reserved_headroomintegerBasis points of this experiment's split kept empty (0–10000) so a new variant can be appended into it while running without reshuffling. Group weights must sum to `10000 − reserved_headroom`. Defaults to the universe's `recommended_headroom` when omitted.
saltstringHash salt for bucketing. Auto-generated if omitted. Immutable while running.
paramsobject**Deprecated** — the universe now owns the config schema (`param_schema`). Retained for back-compat; new experiments should leave this empty and declare params on the universe. Map of param-name → scalar type. default: {}
groupsrequiredarray<{ name: string; weight: integer; params?: object }>Two or more variants. Weights must sum to `10000 − reserved_headroom`. Existing weights are immutable while running, but a new variant may be appended into the reserved tail.
significance_thresholdnumberp-value cutoff used by the analysis pass. Defaults to `0.05`. Values other than 0.05 require Pro plan or higher. default: 0.05
min_runtime_daysintegerMinimum days the experiment must run before results are considered conclusive. default: 0
min_sample_sizeintegerMinimum exposures per group before results are considered conclusive. default: 100
sequential_testingbooleanEnable sequential testing (always-valid p-values). Requires Premium plan or higher. default: false
goal_metric{ name?: string; query?: string; event?: string; aggregation?: "count_users" | "count_events" | "retention_7d" | "retention_30d" | "sum" | "avg"; value?: string; min_effect_of_interest?: number | null }Single goal metric defined inline — either a DSL `query` or an `event` (+`aggregation`/`value`) the server compiles. Attaching one is required before the experiment can be started. The underlying event is auto-created if missing.
guardrail_metricsarray<{ name?: string; query?: string; event?: string; aggregation?: "count_users" | "count_events" | "retention_7d" | "retention_30d" | "sum" | "avg"; value?: string; min_effect_of_interest?: number | null }>Up to 10 guardrail metrics defined inline. Each is upserted (event + metric) and attached with role=guardrail. default: []

Response · 201

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

Get one experiment

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

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

Parameters

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

Response · 200

NameTypeDescription
idrequiredstringStable opaque experiment id (`exp_…`).
namerequiredstringStable experiment key. Single segment or `folder.name` (a-z, 0-9, `_`/`-`; max 128 chars). Used by SDKs as `Shipeasy.getExperiment(user, '<name>')`. Immutable after create.
descriptionrequiredstring | null
hypothesisrequiredstring | null
tagrequiredstring | null
ownerEmailrequiredstring | null
audiencerequiredstring | null
bucketByrequiredstring | null
folderrequiredstring | null
statusrequired"draft" | "running" | "stopped" | "archived"
universerequiredstringUniverse name this experiment draws from.
targetingGaterequiredstring | null
holdoutGaterequiredstring | nullPer-experiment holdout gate name (a `holdout`-type flag), or `null`.
allocationPctrequiredintegerAllocation in basis points (0–10000).
reservedHeadroomrequiredintegerBasis points of the split reserved for appended variants (group weights sum to 10000 − this).
hashVersionrequiredintegerBucketing hash algorithm version for the experiment's pool slice (§B4). Defaults to 1.
poolOffsetBprequiredinteger | nullBasis-point offset of the experiment's contiguous slice in the universe pool, or `null` before a slice is allocated.
poolSizeBprequiredinteger | nullBasis-point width of the experiment's pool slice (equal to its allocation), or `null` before a slice is allocated.
saltrequiredstring
paramsrequiredobject
groupsrequiredarray<{ name: string; weight: integer; params?: object }>
significanceThresholdrequirednumber
minRuntimeDaysrequiredinteger
minSampleSizerequiredinteger
sequentialTestingrequiredboolean
startedAtrequiredstring | nullISO-8601 timestamp the experiment last transitioned to `running`, or `null`.
stoppedAtrequiredstring | null
updatedAtrequiredstring
versionrequiredinteger | nullSave counter (number of published edits), or `null` on pre-versioning rows.
creatorEmailstring | nullResolved creator email (`created_by` → users). Enriched field: present on list rows, omitted from the by-id detail.
updaterEmailstring | nullResolved last-editor email (`updated_by` → users). Enriched field: present on list rows, omitted from the by-id detail.
verdict"ship" | "hold" | "wait" | "invalid" | "draft"Answer-first decision from the goal metric + guardrails + SRM vs. the significance threshold and min runtime: `ship`, `hold`, `wait`, `invalid`, `draft`. Enriched field: list rows only.
verdictTitlestringExpanded verdict headline the results hero renders. Enriched field: list rows only.
verdictWhystringVerdict rationale the results hero renders. Enriched field: list rows only.
goalMetric{ id: string; name: string } | nullThe experiment's goal metric `{ id, name }`, or `null` when none is set. Enriched field: list rows only.
guardrailsarray<{ id: string; name: string; eventName?: string | null }>Guardrail metrics `{ id, name, eventName? }`. Enriched field: list rows only.
guardrailCountintegerNumber of guardrail metrics. Enriched field: list rows only.
primaryLiftPctnumber | nullGoal-metric lift of the leading variant vs. control, in percent, or `null` when not yet computed. Enriched field: list rows only.
significancePctnumber | nullStatistical significance (1 − p) as a percent, or `null` when not yet computed. Enriched field: list rows only.
sampleSizeinteger | nullTotal distinct units analysed, or `null` when not yet computed. Enriched field: list rows only.
exposurearray<{ ds: string; value: number }>Cumulative unique-users-enrolled series for the exposure sparkline. Enriched field: list rows only.
exposureTotalinteger | nullTotal distinct units ever exposed (sparkline headline), or `null`. Enriched field: list rows only.
Example · 200
{
  "id": "exp_01j7wb12c3d4e5f6g7h8j9k0l1",
  "name": "checkout_button_color",
  "description": "Test green vs. blue CTA on the checkout page.",
  "tag": "checkout",
  "status": "running",
  "universe": "primary_users",
  "targetingGate": null,
  "allocationPct": 5000,
  "salt": "8d3e9a1f6b7c4a5fa1c2b6d3e7c8e3a1",
  "params": {
    "cta_color": "string"
  },
  "groups": [
    {
      "name": "control",
      "weight": 5000,
      "params": {
        "cta_color": "blue"
      }
    },
    {
      "name": "treatment",
      "weight": 5000,
      "params": {
        "cta_color": "green"
      }
    }
  ],
  "significanceThreshold": 0.05,
  "minRuntimeDays": 7,
  "minSampleSize": 1000,
  "sequentialTesting": false,
  "startedAt": "2026-05-01T12:00:00.000Z",
  "stoppedAt": null,
  "updatedAt": "2026-05-09T18:22:11.000Z"
}
PATCH/api/admin/experiments/{id}

Update an experiment

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

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

Use cases

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

Parameters

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

Body

NameTypeDescription
namestringStable experiment key. Single segment or `folder.name` (a-z, 0-9, `_`/`-`; max 128 chars). Used by SDKs as `Shipeasy.getExperiment(user, '<name>')`. Immutable after create.
descriptionstring | null
hypothesisstring | null
tagstring | null
owner_emailstring | null
audiencestring | null
bucket_bystring | null
folderstring | null
targeting_gatestring | null
holdout_gatestring | nullPer-experiment holdout gate — the name of a `holdout`-type flag, or `null` to clear. A caller the flag passes is held out.
allocation_pctintegerBasis-points allocation (0–10000). Use `allocation_percent` (0–100) for percent. Immutable while the experiment is running.
reserved_headroomintegerBasis points of the split kept empty for appended variants. Group weights must sum to `10000 − reserved_headroom`. May be shrunk (never grown into existing weights) while running when appending a variant.
allocation_percentnumberAllocation as a **percentage** (0–100). Friendlier alias for `allocation_pct`; converted to basis points server-side. Wins over `allocation_pct` if both are supplied. Immutable while running.
saltstringHash salt. Immutable while running.
universestringNew universe name. Immutable while running. Returns `422` if the universe doesn't exist.
paramsobject**Deprecated** — the universe owns the config schema (`param_schema`). Retained for back-compat. Map of param-name → scalar type.
groupsarray<{ name: string; weight: integer; params?: object }>Replacement groups. Weights must sum to `10000 − reserved_headroom`. Existing weights/values are immutable while running; a new variant may be appended into the reserved tail.
significance_thresholdnumber
min_runtime_daysinteger
min_sample_sizeinteger
sequential_testingboolean
goal_metric{ name?: string; query?: string; event?: string; aggregation?: "count_users" | "count_events" | "retention_7d" | "retention_30d" | "sum" | "avg"; value?: string; min_effect_of_interest?: number | null }Replaces the goal metric — DSL `query` or `event` (+`aggregation`/`value`) the server compiles (event auto-upserted).
guardrail_metricsarray<{ name?: string; query?: string; event?: string; aggregation?: "count_users" | "count_events" | "retention_7d" | "retention_30d" | "sum" | "avg"; value?: string; min_effect_of_interest?: number | null }>Replaces the guardrail set wholesale (event auto-upserted per entry).

Response · 200

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

Delete an experiment

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

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

Parameters

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

Response · 200

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

Transition experiment status

Drives the experiment lifecycle. Allowed transitions:

- draft → running — starts allocation. Bumps the startedAt timestamp. - running → stopped — halts allocation. Existing exposures stay in the dataset. - stopped → archived — soft-delete. - draft → archived — discard an unstarted experiment. - 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

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

Parameters

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

Body

NameTypeDescription
statusrequired"draft" | "running" | "stopped" | "archived"Target status. Allowed transitions: `draft → running`, `running → stopped`, `stopped → archived`, `draft → archived`, and `archived → draft` (restore — only if the experiment never started). Restarting an archived experiment directly is not allowed; restore it to draft first, then start.

Response · 200

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

Attach metrics

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

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

Use cases

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

Parameters

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

Body

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

Response · 200

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

Get analysis results

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

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

Parameters

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

Response · 200

NameTypeDescription
experimentrequired{ id: string; name: string; status: "draft" | "running" | "stopped" | "archived" }
resultsrequiredarray<{ metric: string; group_name: string; ds: string; n: number | null; mean: number | null; delta_pct: number | null; p_value: number | null; srm_detected: number | null; realized_mde: number | null }>
verdictrequired"ship" | "hold" | "wait" | "invalid" | "draft"Server-computed decision from the goal metric + guardrails + SRM vs. the significance threshold and min runtime: `ship` (goal significant + guardrails pass), `hold` (a guardrail regressed), `wait` (inconclusive / under-powered), `invalid` (sample-ratio mismatch), `draft` (never started).
Example · 200
{
  "experiment": {
    "id": "exp_01j7wb12c3d4e5f6g7h8j9k0l1",
    "name": "checkout_button_color",
    "status": "running"
  },
  "results": [
    {
      "metric": "checkout_completed",
      "group_name": "control",
      "ds": "2026-05-09",
      "n": 12421,
      "mean": 0.1834,
      "delta_pct": null,
      "p_value": null,
      "srm_detected": 0
    },
    {
      "metric": "checkout_completed",
      "group_name": "treatment",
      "ds": "2026-05-09",
      "n": 12519,
      "mean": 0.1922,
      "delta_pct": 4.8,
      "p_value": 0.018,
      "srm_detected": 0
    }
  ],
  "verdict": "ship"
}
GET/api/admin/experiments/{id}/timeseries

Get analysis timeseries

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

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

Parameters

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

Response · 200

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

Re-queue analysis

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

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

Parameters

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

Response · 200

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

Configs

GET/api/admin/configs

List dynamic configs

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

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

Parameters

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

Response · 200

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

Create a dynamic config

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

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

Use cases

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

Parameters

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

Body

NameTypeDescription
namerequiredstring
descriptionstringOptional free-form description shown in the dashboard. Max 512 chars.
folderstring | null
schemarequiredobjectJSON Schema (draft 2020-12) describing the shape of the config value. Top-level `type` must be `'object'`; every published value is validated against this schema.
valueanyInitial config value. Either a single JSON object applied to every env, or a `{ env: value }` map seeding per-env values. Must match `schema`. Defaults to `{}` on every env when omitted.

Response · 201

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

Get one config

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

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

Parameters

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

Response · 200

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

Update a dynamic config

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

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

Use cases

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

Parameters

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

Body

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

Response · 200

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

Delete a dynamic config

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

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

Parameters

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

Response · 200

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

Save a draft value

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

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

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

Parameters

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

Body

NameTypeDescription
envrequired"dev" | "staging" | "prod"
valuerequiredanyDraft value to stage on `env`. Validated against the config's current schema.

Response · 200

NameTypeDescription
idrequiredstring
envrequired"dev" | "staging" | "prod"
baseVersionrequiredintegerPublished version the draft is based on.
updatedAtrequiredstring
Example · 200
{
  "id": "cfg_01j7wae5h6j7k8l9m0n1p2q3r4",
  "env": "dev",
  "baseVersion": 5,
  "updatedAt": "2026-05-10T09:31:00.000Z"
}
DELETE/api/admin/configs/{id}/drafts

Discard a draft

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

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

Parameters

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

Body

NameTypeDescription
envrequired"dev" | "staging" | "prod"

Response · 200

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

Publish a draft

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

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

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

Parameters

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

Body

NameTypeDescription
envrequired"dev" | "staging" | "prod"

Response · 200

NameTypeDescription
idrequiredstring
envrequired"dev" | "staging" | "prod"
versionrequiredintegerNewly published version on `env`.
Example · 200
{
  "id": "cfg_01j7wae5h6j7k8l9m0n1p2q3r4",
  "env": "dev",
  "version": 6
}
GET/api/admin/configs/{id}/activity

List config activity

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

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

Parameters

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

Update a config schema

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 caseEvolve a config's shape (add/remove a field) without republishing values.

Parameters

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

Body

NameTypeDescription
schemarequiredobjectReplacement JSON Schema (draft 2020-12). Validated against every published value before it lands.

Response · 200

NameTypeDescription
idrequiredstringConfig id whose schema was updated.
Example · 200
{
  "id": "cfg_01j7wae5h6j7k8l9m0n1p2q3r4"
}

Killswitch

GET/api/admin/killswitches

List killswitches

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

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

Parameters

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

Response · 200

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

Create a killswitch

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

Returns 409 if name already exists in the project.

Use cases

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

Parameters

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

Body

NameTypeDescription
namerequiredstring
descriptionstringOptional free-form description shown in the dashboard. Max 512 chars.
folderstring | null
valuebooleanDefault value applied to every env at creation. Defaults to `false`. Use `true` to ship the killswitch pre-tripped.
switchesobjectInitial per-switch overrides applied to every env. Empty/omitted leaves the killswitch with only the flat `value`.

Response · 201

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

Get one killswitch

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

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

Parameters

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

Response · 200

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

Update a killswitch

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

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

Use cases

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

Parameters

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

Body

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

Response · 200

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

Delete a killswitch

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

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

Parameters

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

Response · 200

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

Set one switch entry

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

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

Use cases

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

Parameters

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

Body

NameTypeDescription
envrequired"dev" | "staging" | "prod"
switchKeyrequiredstringSwitch key to set.
valuerequiredbooleanNew boolean value for this `switchKey` on this `env`.

Response · 200

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

Remove one switch entry

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

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

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

Parameters

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

Body

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

Response · 200

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

Set the flat value on one env

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

Use caseDescriptionExample
Trip on prod{ "env": "prod", "value": true }.
Untrip on prod{ "env": "prod", "value": false }.

Parameters

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

Body

NameTypeDescription
envrequired"dev" | "staging" | "prod"
valuerequiredbooleanFlat boolean to publish on `env`. Publishes a new version on that env only.

Response · 200

NameTypeDescription
idrequiredstring
envrequired"dev" | "staging" | "prod"
versionrequiredintegerNewly published version on `env`.
publishedrequired{ value: boolean; switches?: object }
Example · 200
{
  "id": "ksw_01j7w9d8h2k4m6n8p0q2r4s6t8",
  "env": "prod",
  "version": 6,
  "published": {
    "value": true,
    "switches": {
      "eu_region": true
    }
  }
}

Universes

GET/api/admin/universes

List universes

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

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

Parameters

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

Response · 200

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

Create a universe

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

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

Use cases

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

Parameters

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

Body

NameTypeDescription
namerequiredstringStable universe key. Single segment or `folder.name`. Lowercase letters, digits, `_` or `-`; max 128 chars. Immutable after create.
folderstring | null
descriptionstring | nullHuman-readable blurb shown in the universe picker/hovercard. default: null
unit_typestringUnit of randomisation. Typically `user_id`. Use `account_id` to keep whole accounts in the same group across an experiment. default: "user_id"
holdout_rangearray<integer> | nullInclusive `[lo, hi]` bucket range (0–9999) reserved as the **holdout** — callers hashed into this slice are excluded from every experiment in the universe. `null` disables the holdout. Pro plan or higher required. default: null
recommended_headroomintegerBasis points of reserved headroom seeded into each new experiment created in this universe (0 = none). Lets variants be appended into a running experiment without reshuffling. default: 0
param_schemaarray<{ name: string; type: "bool" | "string" | "int" | "number"; default: any }> | nullThe universe-owned config schema — an ordered `{ name, type, default }[]`. Experiments may only override values per variant, never add fields. `null` starts an empty schema. default: null

Response · 201

NameTypeDescription
idrequiredstringNewly assigned universe id.
namerequiredstringStable universe key. Single segment or `folder.name`. Lowercase letters, digits, `_` or `-`; max 128 chars. Immutable after create.
Example · 201
{
  "id": "uni_01j7w8a1b2c3d4e5f6g7h8i9j0",
  "name": "primary_users"
}
PATCH/api/admin/universes/{id}

Update a universe

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

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

Use cases

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

Parameters

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

Body

NameTypeDescription
folderstring | null
descriptionstring | nullHuman-readable blurb shown in the universe picker/hovercard.
holdout_rangearray<integer> | nullInclusive `[lo, hi]` bucket range (0–9999) reserved as the **holdout** — callers hashed into this slice are excluded from every experiment in the universe. `null` disables the holdout. Pro plan or higher required.
recommended_headroomintegerBasis points of reserved headroom seeded into new experiments in this universe.
param_schemaarray<{ name: string; type: "bool" | "string" | "int" | "number"; default: any }> | nullReplace the universe config schema. Additive changes + default edits are always allowed; removing a param a running experiment overrides is rejected (deprecate-only).

Response · 200

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

Delete a universe

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

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

Parameters

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

Response · 200

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

Templates

GET/api/admin/gates/templates

List gate templates

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).

Parameters

NameInTypeDescription
X-Project-IdrequiredheaderstringProject to scope this request to.
queryquerystringDeprecated alias for `q`, kept working for one release. Prefer `q`.

Response · 200

NameTypeDescription
datarequiredarray<{ id: string; name: string; description: string; category: "condition" | "rollout"; auto: boolean; builtin: boolean; iconKey?: string | null; rules: array<{ attr: string; op: string; value?: any }>; createdAt?: string | null; updatedAt?: string | null }>
next_cursorrequiredstring | null
Example · 200
{
  "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
}
POST/api/admin/gates/templates

Create a gate template

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.

Parameters

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

Body

NameTypeDescription
namerequiredstringHuman label. Unique per project.
descriptionstringOne-liner shown in pickers and matched by the list `query` filter. default: ""
category"condition" | "rollout" default: "condition"
icon_keystringDisplay-only icon hint.
autobooleanMark the attribute as request-derived (resolved at the SDK edge). default: false
rulesrequiredarray<{ attr: string; op: string; value: string | number | boolean | array<any> }>The rule definition captured by the template.

Response · 201

NameTypeDescription
idrequiredstringNewly assigned template id (`gtpl_…`).
namerequiredstring
Example · 201
{
  "id": "gtpl_01j7w8a1b2c3d4e5f6g7h8i9j0",
  "name": "Enterprise beta"
}
GET/api/admin/gates/templates/{id}

Get one gate template

Returns a single template by its id — a built-in slug (country) or a customer gtpl_… id.

Parameters

NameInTypeDescription
X-Project-IdrequiredheaderstringProject to scope this request to.
idrequiredpathstringBuilt-in slug (`country`) or customer template id (`gtpl_…`).

Response · 200

NameTypeDescription
idrequiredstringStable slug for built-ins (`country`), `gtpl_…` for customer templates.
namerequiredstringHuman label shown in pickers (`Country is`).
descriptionrequiredstringOne-liner — feeds the list `query` filter.
categoryrequired"condition" | "rollout"`condition` = rule-based predicate, `rollout` = percentage bucket.
autorequiredbooleanTrue when the attribute is request-derived (country/browser/…) and resolved at the SDK edge, so the caller need not pass it.
builtinrequiredbooleanRead-only built-in (`true`) vs editable customer template (`false`).
iconKeystring | nullDisplay-only icon hint.
rulesrequiredarray<{ attr: string; op: string; value?: any }>The rule definition — copy, substitute the value(s), pass as `rules`.
createdAtstring | nullISO-8601 creation timestamp; `null` for built-ins.
updatedAtstring | nullISO-8601 last-mutation timestamp; `null` for built-ins.
PATCH/api/admin/gates/templates/{id}

Update a gate template

Partial update of a customer template. rules replaces the list wholesale. Returns 409 if id names a read-only built-in template.

Parameters

NameInTypeDescription
X-Project-IdrequiredheaderstringProject to scope this request to.
idrequiredpathstringCustomer template id (`gtpl_…`) or its `name`.

Body

NameTypeDescription
namestring
descriptionstring
category"condition" | "rollout"
icon_keystring | null
autoboolean
rulesarray<{ attr: string; op: string; value: string | number | boolean | array<any> }>

Response · 200

NameTypeDescription
idrequiredstring
Example · 200
{
  "id": "gtpl_01j7w8a1b2c3d4e5f6g7h8i9j0"
}
DELETE/api/admin/gates/templates/{id}

Delete a gate template

Soft-deletes (archives) a customer template. Returns 409 if id names a read-only built-in template.

Parameters

NameInTypeDescription
X-Project-IdrequiredheaderstringProject to scope this request to.
idrequiredpathstringCustomer template id (`gtpl_…`) or its `name`.

Response · 200

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

Attributes

GET/api/admin/attributes

List targeting attributes

Returns every auto-inferred targeting attribute in the project — the name and (when known) the value type — for building gate/experiment targeting rules.

Use caseDiscover which user-context keys are available before authoring a targeting rule, instead of guessing attribute names.

Parameters

NameInTypeDescription
X-Project-IdrequiredheaderstringProject to scope this request to.
Example · 200
[
  {
    "name": "plan",
    "type": "string"
  },
  {
    "name": "country",
    "type": "string"
  },
  {
    "name": "seats",
    "type": "number"
  }
]
POST/api/admin/attributes

Declare a targeting attribute

Declare a targeting attribute the SDK reports and gates/experiments can target. type: enum requires enum_values.

Use caseRegister a plan or country attribute so targeting rules can reference it.

Parameters

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

Body

NameTypeDescription
namerequiredstringAttribute key (lowercase alphanumeric start, then letters/digits/`_`/`-`; max 64 chars). Immutable after create.
typerequired"string" | "number" | "boolean" | "enum" | "date"
enum_valuesarray<string> | nullAllowed values when `type` is `enum` (required in that case — 422 otherwise); `null` for non-enum types. default: null
requiredbooleanWhether the attribute must be present on the evaluation context. default: false
descriptionstringOptional human note shown in the dashboard.
sdk_pathstringOptional dotted path the SDK reads the value from.

Response · 201

NameTypeDescription
idrequiredstringNewly assigned attribute id.
namerequiredstringThe attribute key that was created.
Example · 201
{
  "id": "attr_01j7w8a1b2c3",
  "name": "plan"
}
GET/api/admin/attributes/{id}

Get a targeting attribute

Fetch one targeting attribute by id.

Use caseInspect an attribute's declared type + allowed values before editing it.

Parameters

NameInTypeDescription
X-Project-IdrequiredheaderstringProject to scope this request to.
idrequiredpathstringThe attribute id.

Response · 200

NameTypeDescription
idrequiredstringStable opaque attribute id.
namerequiredstringAttribute key.
typerequired"string" | "number" | "boolean" | "enum" | "date"
enumValuesarray<string> | nullAllowed values for `enum` attributes, else `null`.
required0 | 1Whether the attribute is required (D1 stores the flag as `0`/`1`).
descriptionstring | nullHuman note, or `null`.
sdkPathstring | nullDotted SDK path, or `null`.
createdAtstringISO-8601 creation timestamp.
Example · 200
{
  "id": "attr_01j7w8a1b2c3",
  "name": "plan",
  "type": "enum",
  "enumValues": [
    "free",
    "paid"
  ],
  "required": 1,
  "createdAt": "2026-06-20T09:14:08.000Z"
}
PATCH/api/admin/attributes/{id}

Update a targeting attribute

Update a targeting attribute's type, allowed values, required flag, description, or SDK path. name is immutable.

Use caseAdd an allowed value to an enum attribute, or flip its required flag.

Parameters

NameInTypeDescription
X-Project-IdrequiredheaderstringProject to scope this request to.
idrequiredpathstringThe attribute id.

Body

NameTypeDescription
type"string" | "number" | "boolean" | "enum" | "date"
enum_valuesarray<string> | nullReplacement allowed values (for `enum`), or `null` to clear.
requiredbooleanWhether the attribute must be present on the evaluation context.
descriptionstringOptional human note shown in the dashboard.
sdk_pathstringOptional dotted path the SDK reads the value from.

Response · 200

NameTypeDescription
idrequiredstringId of the attribute that was updated.
Example · 200
{
  "id": "attr_01j7w8a1b2c3"
}
DELETE/api/admin/attributes/{id}

Archive a targeting attribute

Soft-deletes (archives) a targeting attribute.

Use caseRetire an attribute no targeting rule references anymore (the user-facing verb is archive).

Parameters

NameInTypeDescription
X-Project-IdrequiredheaderstringProject to scope this request to.
idrequiredpathstringThe attribute id.

Response · 200

NameTypeDescription
okrequiredbooleanTrue when the attribute was archived.
Example · 200
{
  "ok": true
}

Metrics

GET/api/admin/metrics

List metrics

Returns every metric in the project (not paginated) — name, folder, source event, the typed queryIr, and the rendered query.

Use caseAudit every metric defined in the project — for example to find the metric id to attach as an experiment's success metric.

Parameters

NameInTypeDescription
X-Project-IdrequiredheaderstringProject to scope this request to.
Example · 200
[
  {
    "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"
  }
]
POST/api/admin/metrics

Create a metric

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

Use caseDescriptionExample
Track an eventcount_users(<event>) for unique-user counts.
Sum a valuesum(<event>, <label>) for revenue / quantity metrics.
Experiment success metriccreate the metric, then attach its id to an experiment.

Parameters

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

Response · 201

NameTypeDescription
idrequiredstringNewly assigned metric id.
namerequiredstringMetric name that was created.
Example · 201
{
  "id": "met_01j7w8a1b2c3d4e5f6g7h8i9j0",
  "name": "checkouts"
}
GET/api/admin/metrics/{id}

Get a metric

Fetch one metric by its id or name, including the rendered DSL query and the typed IR.

Use caseInspect a single metric's full definition before reusing it in an experiment or alert rule.

Parameters

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

Response · 200

NameTypeDescription
idrequiredstringStable opaque metric id.
namerequiredstringMetric key.
folderrequiredstring | nullFolder grouping the metric, or `null`.
eventNamerequiredstringSource event name (camelCase in response).
queryrequiredstring | nullRendered DSL text form of the query, or `null` if it could not be rendered.
queryIrrequired{ agg: { kind: "count_users" } | { kind: "count_events" } | { kind: "sum" } | { kind: "avg" } | { kind: "min" } | { kind: "max" } | { kind: "unique" } | { kind: "quantile"; p: 0.5 | 0.75 | 0.9 | 0.95 | 0.99 | 0.999 } | { kind: "retention_Nd"; n: integer } | { kind: "ratio"; numerator: object; denominator: object }; metric: string; valueLabel?: string; filters?: array<{ label: string; op: "=" | "!=" | "=~" | "!~"; value: string }>; groupBy?: { op: "by" | "without"; labels: array<string> } }
direction"higher_better" | "lower_better" | "neutral"Desired direction of movement. `higher_better` (default), `lower_better`, or `neutral` (guardrail).
winsorizePctnumberWinsorise percentile applied to the metric.
defaultMinEffectOfInterestnumber | nullMetric-level default minimum effect of interest (relative, 0–1), or `null`.
createdAtstringISO-8601 creation timestamp.
updatedAtstringISO-8601 last-update timestamp.
Example · 200
{
  "id": "met_01j7w8a1b2c3d4e5f6g7h8i9j0",
  "name": "revenue",
  "folder": "checkout",
  "eventName": "purchase",
  "query": "sum(purchase, amount)",
  "queryIr": {
    "agg": {
      "kind": "sum"
    },
    "metric": "purchase",
    "valueLabel": "amount",
    "filters": []
  },
  "direction": "higher_better",
  "winsorizePct": 99
}
PATCH/api/admin/metrics/{id}

Update a metric

Update a metric's definition — folder, source event, query (query DSL or typed query_ir), winsorisation, minimum detectable effect, or direction. name is immutable. Provide at most one of query / query_ir.

Use caseRefine a metric's query or guardrail direction without recreating it.

Parameters

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

Response · 200

NameTypeDescription
idrequiredstringStable opaque metric id.
namerequiredstringMetric key.
folderrequiredstring | nullFolder grouping the metric, or `null`.
eventNamerequiredstringSource event name (camelCase in response).
queryrequiredstring | nullRendered DSL text form of the query, or `null` if it could not be rendered.
queryIrrequired{ agg: { kind: "count_users" } | { kind: "count_events" } | { kind: "sum" } | { kind: "avg" } | { kind: "min" } | { kind: "max" } | { kind: "unique" } | { kind: "quantile"; p: 0.5 | 0.75 | 0.9 | 0.95 | 0.99 | 0.999 } | { kind: "retention_Nd"; n: integer } | { kind: "ratio"; numerator: object; denominator: object }; metric: string; valueLabel?: string; filters?: array<{ label: string; op: "=" | "!=" | "=~" | "!~"; value: string }>; groupBy?: { op: "by" | "without"; labels: array<string> } }
direction"higher_better" | "lower_better" | "neutral"Desired direction of movement. `higher_better` (default), `lower_better`, or `neutral` (guardrail).
winsorizePctnumberWinsorise percentile applied to the metric.
defaultMinEffectOfInterestnumber | nullMetric-level default minimum effect of interest (relative, 0–1), or `null`.
createdAtstringISO-8601 creation timestamp.
updatedAtstringISO-8601 last-update timestamp.
Example · 200
{
  "id": "met_01j7w8a1b2c3d4e5f6g7h8i9j0",
  "name": "revenue",
  "direction": "lower_better"
}
DELETE/api/admin/metrics/{id}

Archive a metric

Soft-deletes (archives) the metric. Returns 409 if it is attached to a running experiment — stop those experiments first.

Use caseRetire a metric once no running experiment depends on it (the user-facing verb is archive).

Parameters

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

Response · 200

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

Events

GET/api/admin/events

List events

Returns every catalogued event in the project, including pending auto-discovered names. Each row carries its own pending flag, so the unapproved queue can be filtered client-side.

Use caseSnapshot the event catalog — for example to review the pending auto-discovery queue before approving names, or to confirm which events your metrics can reference.

Parameters

NameInTypeDescription
X-Project-IdrequiredheaderstringProject to scope this request to.
Example · 200
[
  {
    "id": "evt_01j7w8a1b2c3d4e5f6g7h8i9j0",
    "name": "checkout_completed",
    "folder": "checkout",
    "description": "Fired when a customer finishes checkout.",
    "properties": [
      {
        "name": "amount",
        "type": "number",
        "required": true,
        "description": ""
      }
    ],
    "pending": 0,
    "createdAt": "2026-04-12T10:14:08.000Z"
  }
]
POST/api/admin/events

Register an event

Registers a new event name and (optionally) its typed properties. Only name is required.

If the name matches an existing pending (auto-discovered) row, this approves that row instead of returning a conflict. Otherwise an already-registered name returns 409.

Use cases

Use caseDescriptionExample
Register a known event{ "name": "checkout_completed" } so metrics can reference it.
Declare typed propertiessupply properties to document the event's payload shape.

Parameters

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

Body

NameTypeDescription
namerequiredstringEvent name. Starts with a letter, digit, or `_`; letters, digits, `_`, `-`, `.`; max 128 chars. Immutable after create — this is the handle metric queries reference.
folderstring | null
descriptionstringOptional human-readable description of the event.
propertiesarray<{ name: string; type: "string" | "number" | "boolean"; required?: boolean; description?: string }>Typed properties declared on the event. Defaults to an empty list. default: []

Response · 201

NameTypeDescription
idrequiredstringNewly assigned event id.
namerequiredstringThe event name that was registered.
Example · 201
{
  "id": "evt_01j7w8a1b2c3d4e5f6g7h8i9j0",
  "name": "checkout_completed"
}
GET/api/admin/events/{id}

Get an event

Returns one event's full detail. Resolves by exact id, unique id-prefix, or exact (unique) name.

Use caseInspect one event's declared properties and pending state by id or name.

Parameters

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

Response · 200

NameTypeDescription
idrequiredstringStable opaque event id.
namerequiredstringEvent name — the handle metric queries reference.
folderstring | null | nullFolder the event is filed under, or `null` if at the root.
descriptionrequiredstring | nullHuman-readable description, or `null` if none set.
propertiesrequiredarray<{ name: string; type: "string" | "number" | "boolean"; required?: boolean; description?: string }>Typed properties declared on the event.
pendingrequiredinteger`1` if this is an auto-discovered name awaiting approval (metrics on it fail until approved), `0` if approved/usable.
createdAtrequiredstringISO-8601 timestamp of creation.
Example · 200
{
  "id": "evt_01j7w8a1b2c3d4e5f6g7h8i9j0",
  "name": "checkout_completed",
  "folder": "checkout",
  "description": "Fired when a customer finishes checkout.",
  "properties": [
    {
      "name": "amount",
      "type": "number",
      "required": true,
      "description": ""
    }
  ],
  "pending": 0,
  "createdAt": "2026-04-12T10:14:08.000Z"
}
PATCH/api/admin/events/{id}

Update an event

Partial update of an event's folder, description, or properties. name is immutable.

properties replaces the full set (no merge) — omit it to leave properties unchanged.

Use caseRefile an event, update its description, or redeclare its typed properties.

Parameters

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

Body

NameTypeDescription
folderstring | null
descriptionstringNew description for the event.
propertiesarray<{ name: string; type: "string" | "number" | "boolean"; required?: boolean; description?: string }>Replaces the full property set (no merge). Omit to leave properties unchanged.

Response · 200

NameTypeDescription
idrequiredstringEvent id that was updated.
Example · 200
{
  "id": "evt_01j7w8a1b2c3d4e5f6g7h8i9j0"
}
DELETE/api/admin/events/{id}

Archive an event

Soft-deletes (archives) the event. Returns 409 if any metric still references it — delete those metrics first.

Use caseRetire an event from the catalog once no metric depends on it.

Parameters

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

Response · 200

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

Approve a pending event

Promotes a pending (auto-discovered) event to usable so metrics can query it (pending0).

You may optionally declare the event's folder, description, or properties in the same call — the body is the same shape as update, and may be empty.

Use caseClear an auto-discovered event out of the pending queue so metrics defined on it start resolving.

Parameters

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

Body

NameTypeDescription
folderstring | null
descriptionstringNew description for the event.
propertiesarray<{ name: string; type: "string" | "number" | "boolean"; required?: boolean; description?: string }>Replaces the full property set (no merge). Omit to leave properties unchanged.

Response · 200

NameTypeDescription
idrequiredstringEvent id that was updated.
Example · 200
{
  "id": "evt_01j7w8a1b2c3d4e5f6g7h8i9j0"
}

Ops

GET/api/admin/ops

List the operational queue

Returns the unified ops queue (bugs, feature requests, errors, alerts) in work order — highest priority first, oldest first within a priority — so consumers work it top-down. Filter by type and/or status, and cap with limit. Human-gated holding states (items awaiting human sign-off in the dashboard) are never returned by all/default status.

Use casePull the open queue to triage — e.g. every bug still open — before working items down one by one.

Parameters

NameInTypeDescription
X-Project-IdrequiredheaderstringProject to scope this request to.
typequerystringFilter by item type (`bug`/`feature_request`/`error`/`alert`), or `all`.
statusquerystringFilter by lifecycle status, or `all`. The human-gated holding states (`pending_approval`, `triage`) are excluded from `all`/default and returned only when requested as the exact status.
limitqueryintegerMax items to return (1–500).
Example · 200
[
  {
    "id": "fb_01j7w8a1b2c3d4e5f6g7h8i9j0",
    "number": 7,
    "type": "bug",
    "title": "Checkout button misaligned on mobile",
    "status": "open",
    "priority": "high",
    "createdAt": "2026-06-20T09:14:08.000Z"
  }
]
POST/api/admin/ops

File a queue item

Files one queue item — a bug report or a feature request — and fires the project's connectors (GitHub issue / Slack). type selects which; only the two user-fileable types are accepted (error/alert tickets are auto-filed by the platform). Returns the new id and per-project number.

Use cases

Use caseDescriptionExample
File a bug{ "type": "bug", "title": "Checkout 500s on Safari", "stepsToReproduce": "…" }.
File a feature request{ "type": "feature_request", "title": "Dark mode", "priority": "nice_to_have" }.

Parameters

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

Response · 201

NameTypeDescription
idrequiredstringNewly created item id.
numbernumber | nullPer-project item number assigned to the new item.
Example · 201
{
  "id": "fb_01j7w8a1b2c3d4e5f6g7h8i9j0",
  "number": 7
}
GET/api/admin/ops/{handle}

Get one queue item

Fetch a single queue item by its per-project number or full id.

Use caseInspect one item's full detail before updating its status or linking a PR.

Parameters

NameInTypeDescription
X-Project-IdrequiredheaderstringProject to scope this request to.
handlerequiredpathstringPer-project item number (e.g. `7`) or the full ops item id.

Response · 200

NameTypeDescription
idrequiredstringStable opaque item id.
numberrequirednumber | nullPer-project item number (the `#7` handle), or `null` if unnumbered.
typerequired"bug" | "feature_request" | "error" | "alert" | "measure_plan"Queue item type. `bug` and `feature_request` are user-fileable; `error`, `alert`, and `measure_plan` tickets are auto-filed by the platform (a tracked production error, a metric-threshold alert, and an assistant-proposed measurement plan respectively).
titlerequiredstringOne-line item title (all types).
statusrequired"open" | "pending_approval" | "triage" | "triaged" | "in_progress" | "ready_for_qa" | "resolved" | "wont_fix"
priorityrequired"nice_to_have" | "medium" | "high" | "critical" | nullTriage priority, or `null` if not yet set (all types).
source"team" | "system"How the item was filed: `team` (a human admin/teammate — bug/feature) or `system` (auto-filed — error/alert/measure_plan).
sourceRefstring | nullStable key of the originating record for auto-filed tickets (error fingerprint, `<alert source>:<dedupeKey>`, or measurement-plan ref); `null` for human-filed bugs/features.
reporterEmailstring | nullReporter email (bug/feature), or `null`.
stepsToReproducestringReproduction steps — populated for `bug`, empty string otherwise.
actualResultstringWhat actually happened — `bug` only, empty string otherwise.
expectedResultstringWhat was expected — `bug` only, empty string otherwise.
descriptionstringFeature description — populated for `feature_request`, empty string otherwise.
useCasestringFeature use case — `feature_request` only, empty string otherwise.
contextrequired{ browser?: { pageUrl?: string | null; userAgent?: string | null; viewport?: string | null }; error?: { id: string; fingerprint: string; causedByFingerprint?: string | null; errorType?: string | null; message: string; subject?: string | null; outcome?: string | null; kind?: string | null; side?: string | null; env?: string | null; count: number; firstSeenAt: string; lastSeenAt: string; seenUrls: array<string>; stackTail?: string | null; sdkVersion?: string | null }; alert?: { source: "metric_rule" | "experiment_srm" | "experiment_peek" | "guardrail"; dedupeKey: string; severity: "danger" | "warn" | "info"; observedValue?: number | null; href?: string | null; status?: "active" | "resolved" | "dismissed" | null; activeSince?: string | null; resolvedAt?: string | null; rule?: object | null; metric?: object | null }; measurePlan?: { goal?: string | null; created: array<object>; pending?: array<object>; instrumentation: array<object> } } | nullType-specific capture context. For `bug`/`feature_request` it carries `browser` (the auto-collected page URL / user-agent / viewport). For auto-filed tickets it carries the hydrated `error`, `alert`, or `measurePlan` block. `null` only for legacy rows with nothing captured.
attachmentsrequiredarray<{ id: string; kind: string; filename: string; mimeType: string; sizeBytes: number; fetchUrl: string }>Files attached to the report — screenshots, recordings, logs. Populated for `bug`/`feature_request`; always an empty array for auto-filed `error`/`alert`/`measure_plan` tickets. Fetch each via its `fetchUrl` with the same admin/ops key (the download route is ops-key allow-listed).
relatedrequired{ dashboard?: string; error?: string; alertRule?: string; metric?: string; githubIssue?: string; githubPr?: string }
connectorData{ github?: { issue?: object; pr?: object }; slack?: { message?: object } } | nullPer-connector linkage recorded for this item, keyed by connector provider. Populated as connectors act on the item (e.g. GitHub opens an issue, Slack posts a message); more than one provider can be present at once. `null` if no connector has touched the item.
notify{ slackChannel?: { id: string; name: string } | null; email?: string | null } | null | nullPer-item completion-notification target, or `null` (falls back to the project default).
createdAtrequiredstringISO-8601 creation timestamp.
updatedAtstringISO-8601 last-update timestamp.
Example · 200
{
  "id": "fb_01j7w8a1b2c3d4e5f6g7h8i9j0",
  "number": 7,
  "type": "bug",
  "title": "Checkout button misaligned on mobile",
  "status": "open",
  "priority": "high",
  "createdAt": "2026-06-20T09:14:08.000Z"
}
PATCH/api/admin/ops/{handle}

Update a queue item

Update a queue item. The body is validated against the item's stored type: a bug accepts its content fields (title, steps-to-reproduce, actual/expected result) plus status/priority/notify and a GitHub PR link; a feature_request its content (title, description, use-case) plus the same triage fields; error/alert/measure_plan accept status/priority/notify only (their content is platform-owned). Pass at least one field.

Completing an error ticket (status resolved or ready_for_qa) also resolves the tracked error it links to; the error reopens automatically if it recurs — so completing is safe pre-deploy.

Use cases

Use caseDescriptionExample
Start working an item{ "status": "in_progress" }.
Hand off for review{ "status": "ready_for_qa" } once the fix landed (the mode PR-based loops use).
Triage{ "priority": "high" }, content edits on bug/feature items.

Parameters

NameInTypeDescription
X-Project-IdrequiredheaderstringProject to scope this request to.
handlerequiredpathstringPer-project item number (e.g. `7`) or the full ops item id.

Response · 200

NameTypeDescription
idrequiredstringItem id that was updated.
Example · 200
{
  "id": "fb_01j7w8a1b2c3d4e5f6g7h8i9j0"
}
POST/api/admin/ops/{handle}/link-pr

Link a fixing PR

Record the pull request that fixes a queue item (and clears the link with prNumber: null).

Use caseTie the fixing PR to the item so closing the PR can flip it to ready_for_qa.

Parameters

NameInTypeDescription
X-Project-IdrequiredheaderstringProject to scope this request to.
handlerequiredpathstringPer-project item number (e.g. `7`) or the full ops item id.

Body

NameTypeDescription
prNumberrequiredinteger | nullPR number to record on the item. `null` unlinks the PR.
prUrlstringExplicit PR URL. Required for error/alert tickets (no GitHub issue to derive the URL from).

Response · 200

NameTypeDescription
idrequiredstringItem id that was updated.
Example · 200
{
  "id": "fb_01j7w8a1b2c3d4e5f6g7h8i9j0"
}
POST/api/admin/notifications

Raise an attention notification

Raise a 'needs your attention' bell notification. Create-only and idempotent on dedupeKey — re-raising with the same key updates the one card instead of stacking duplicates. It never reads, marks read, or deletes the feed, so it is safe for restricted ops keys.

Use caseEscalate work that can't land in code (a product decision, a credential only a human has, a resource only a human may edit), deduped so repeats don't spam the bell.

Parameters

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

Body

NameTypeDescription
titlerequiredstringOne-line headline of what's blocked.
summaryrequiredstringOne sentence: why it can't be fixed in code. Renders markdown.
stepsarray<string>Ordered steps the human should take to unblock — self-contained (the human reads only this card, not the agent's transcript), 3–6 steps, each naming the exact file, command, env var, or dashboard page. Renders markdown.
hrefstring | nullDashboard-relative deep link to the related item. `null` is accepted and treated as "no link".
dedupeKeystringStable per-escalation key (e.g. `feedback:7`) so re-runs dedupe to one row.

Response · 201

NameTypeDescription
dedupeKeyrequiredstringThe dedupe key the escalation was recorded under.
dispatchedrequiredboolean`true` if a new escalation was dispatched; `false` on an idempotent repeat.
Example · 201
{
  "dedupeKey": "error:checkout:5xx",
  "dispatched": true
}

Comments

GET/api/admin/ops/{handle}/comments

List an item's comments

List the comment thread on a queue item, oldest first. Each comment carries its author (a teammate email, or system for a comment authored by Jarvis — the AI agent), its markdown body, and parentId for the single level of threaded replies. Removed comments are omitted.

Use caseRead the discussion on an item before replying or acting on it.

Parameters

NameInTypeDescription
X-Project-IdrequiredheaderstringProject to scope this request to.
handlerequiredpathstringPer-project item number (e.g. `7`) or the full ops item id.

Response · 200

NameTypeDescription
commentsrequiredarray<{ id: string; feedbackId: string; parentId: string | null; authorType: "user" | "system"; authorEmail: string | null; body: string; createdAt: string; updatedAt: string }>
POST/api/admin/ops/{handle}/comments

Comment on an item

Append a comment to a queue item's thread. The body is markdown (mentions like @teammate notify that person; @shipeasy asks Jarvis, the AI agent, to reply). Pass parentId to reply under an existing top-level comment (one level of threading — a reply to a reply attaches to the same parent).

Create-only and append-only, so it is safe for restricted ops keys — the same channel the ops loop and Jarvis use to leave a note on an item.

Parameters

NameInTypeDescription
X-Project-IdrequiredheaderstringProject to scope this request to.
handlerequiredpathstringPer-project item number (e.g. `7`) or the full ops item id.

Body

NameTypeDescription
bodyrequiredstringThe comment body as markdown. Mentions (`@teammate`, `@shipeasy`) are parsed from it.
parentIdstring | nullReply under this top-level comment. Omit / `null` for a top-level comment. Replying to a reply attaches to the same top-level parent (threading is one level deep).

Response · 201

NameTypeDescription
idrequiredstringNewly created comment id.
Example · 201
{
  "id": "cm_01j7w8a1b2c3d4e5f6g7h8i9j0"
}

Alerts

GET/api/admin/slack/channels

List Slack channels

List the project's connected Slack channels — used to resolve an alert rule's notification target.

Use casePopulate a channel picker, or validate an alert rule's --slack-channel before saving.

Parameters

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

Response · 200

NameTypeDescription
connectedrequiredbooleanWhether a Slack connector is connected and authenticated.
channelsrequiredarray<{ id: string; name: string; isPrivate?: boolean }>The project's Slack channels (empty when no Slack is connected).
Example · 200
{
  "connected": true,
  "channels": [
    {
      "id": "C0123",
      "name": "alerts"
    },
    {
      "id": "C0456",
      "name": "general"
    }
  ]
}
GET/api/admin/alert-rules

List alert rules

Returns every alert rule in the project (not paginated). Each rule carries its bound metricId, the denormalised metricName (or null if the metric was removed), the comparator/threshold/window, severity, enabled flag, and delivery target.

Use caseAudit which metrics have alerting configured — for example to confirm an on-call threshold is set before a launch.

Parameters

NameInTypeDescription
X-Project-IdrequiredheaderstringProject to scope this request to.
Example · 200
[
  {
    "id": "ar_01j7w8a1b2c3d4e5f6g7h8i9j0",
    "name": "Checkout error rate",
    "metricId": "met_01j6abc2d3e4f5g6h7i8j9k0l1",
    "metricName": "checkout_errors",
    "comparator": "gt",
    "threshold": 50,
    "windowHours": 24,
    "severity": "warn",
    "enabled": true,
    "notify": null,
    "createdAt": "2026-04-12T10:14:08.000Z",
    "updatedAt": "2026-04-12T10:14:08.000Z"
  }
]
POST/api/admin/alert-rules

Create an alert rule

Creates a metric-threshold alert rule. name, metricId, comparator, and threshold are required; windowHours defaults to 24, severity to warn, and enabled to true.

Returns 404 if metricId does not resolve, and 400 for a metric with no scalar form over a window (e.g. retention metrics) — the cron can't evaluate those.

Use cases

Use caseDescriptionExample
Threshold alertwarn when an error/latency metric crosses a value over a rolling window.
Routed alertset notify to page a specific Slack channel or on-call email instead of the project default.

Parameters

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

Body

NameTypeDescription
namerequiredstringHuman label for the rule, shown on the alert and the rules list.
metricIdrequiredstringId of the metric to evaluate.
comparatorrequired"gt" | "gte" | "lt" | "lte"How the metric value is compared to the threshold (gt/gte/lt/lte).
thresholdrequirednumberThreshold the metric value is compared against.
windowHoursintegerLookback window (hours) the metric is aggregated over. 1–720. default: 24
severity"danger" | "warn" | "info"Severity of the raised alert. default: "warn"
enabledbooleanWhether the rule is evaluated by the cron. default: true
notify{ slackChannel?: { id: string; name: string } | null; email?: string | null } | null

Response · 201

NameTypeDescription
idrequiredstringNewly assigned alert-rule id.
Example · 201
{
  "id": "ar_01j7w8a1b2c3d4e5f6g7h8i9j0"
}
PATCH/api/admin/alert-rules/{id}

Update an alert rule

Partial update of a rule's tunable knobs. metricId is immutable — the metric also pins the aggregation, so a body carrying metricId is rejected with 409 IMMUTABLE_FIELD; create a new rule bound to the other metric instead (rule deletion is dashboard-only).

Pass "notify": null to revert the rule's delivery target back to the project default.

Use cases

Use caseDescriptionExample
Tune sensitivitychange threshold/comparator/windowHours as the metric's baseline shifts.
Pause without losing config{ "enabled": false } instead of deleting the rule.

Parameters

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

Body

NameTypeDescription
namestring
comparator"gt" | "gte" | "lt" | "lte"
thresholdnumber
windowHoursinteger
severity"danger" | "warn" | "info"
enabledboolean
notify{ slackChannel?: { id: string; name: string } | null; email?: string | null } | null

Response · 200

NameTypeDescription
idrequiredstringNewly assigned alert-rule id.
Example · 200
{
  "id": "ar_01j7w8a1b2c3d4e5f6g7h8i9j0"
}
DELETE/api/admin/alert-rules/{id}

Delete an alert rule

Deletes the alert rule. The cron stops evaluating it immediately. Use this (then create a new rule) to repoint alerting at a different metric, since metricId is immutable.

Use caseRemove an alert rule that is no longer needed, or as the first half of repointing a rule at a different metric.

Parameters

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

Response · 200

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

Projects

GET/api/admin/projects/current

Show the current project

Returns the project the caller's auth header resolves to — plan, status, billing, and which modules are enabled. The server reads the project from the credential, so there is no id parameter. Powers whoami.

Use caseResolve who you are — the project, plan, and enabled modules tied to the current credential — without passing an id. Backs a registry-driven whoami.

Parameters

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

Response · 200

NameTypeDescription
idrequiredstringStable opaque project id.
namerequiredstringProject name.
domainrequiredstring | nullProject domain, or `null` if unset.
ownerEmailrequiredstringEmail of the account that owns the project.
planrequired"free" | "pro" | "business" | "enterprise"Billing plan tier.
statusrequired"active" | "inactive"Project lifecycle status.
subscriptionStatusrequiredstringStripe subscription status (`none`, `active`, `trialing`, `past_due`, …).
billingIntervalrequired"monthly" | "annual"Billing cadence.
currentPeriodEndrequiredstring | nullISO-8601 end of the current billing period, or `null`.
trialEndsAtrequiredstring | nullISO-8601 trial end, or `null` if not trialing.
cancelAtPeriodEndrequirednumber`1` if the subscription is set to cancel at period end, else `0`.
moduleTranslationsrequiredboolean | numberWhether the i18n/translations module is enabled.
moduleConfigsrequiredboolean | numberWhether the dynamic-configs module is enabled.
moduleGatesrequiredboolean | numberWhether the feature-gates module is enabled.
moduleExperimentsrequiredboolean | numberWhether the experiments module is enabled.
moduleFeedbackrequiredboolean | numberWhether the feedback/ops module is enabled.
minSampleSizeintegerVerdict power guard — minimum users per arm before a ship/hold verdict.
minRuntimeDaysintegerMinimum days an experiment must run before a verdict (peeking guard).
defaultPowernumberTarget statistical power (1−β) feeding the realized-MDE calculation.
ciConfidencenumberConfidence level for the interval surfaced on results.
defaultAllocationPctintegerDefault traffic allocation (basis points) new experiments start with.
defaultHoldoutintegerDefault holdout carve-out (basis points) that seeds each new universe's holdout.
defaultWinsorizePctintegerDefault winsorization percentile new metrics start with.
defaultMeinumber | nullDefault minimum effect of interest (relative, 0–1) new metrics start with, or null.
cupedBaselineDaysintegerCUPED baseline window — days of pre-experiment history, frozen at start.
cupedMinOverlapnumberCUPED selection-bias guard — min share of users with a baseline, else skip.
cupedMinBaselineUsersintegerCUPED — minimum users with a baseline before it runs at all.
msprtTauMeiFactornumbermSPRT prior width — τ = minimum effect of interest × this factor.
msprtTauSdFactornumbermSPRT fallback prior width — τ = this × control SD when no MEI is set.
srmThresholdnumberSRM chi-square p-value below which the run is called invalid.
createdAtrequiredstringISO-8601 timestamp of project creation.
updatedAtrequiredstringISO-8601 timestamp of last update.
Example · 200
{
  "id": "f81d4fae-7dec-11d0-a765-00a0c91e6bf6",
  "name": "Acme",
  "domain": "acme.com",
  "ownerEmail": "owner@acme.com",
  "plan": "pro",
  "status": "active",
  "subscriptionStatus": "active",
  "billingInterval": "monthly",
  "currentPeriodEnd": "2026-07-12T00:00:00.000Z",
  "trialEndsAt": null,
  "cancelAtPeriodEnd": 0,
  "moduleTranslations": true,
  "moduleConfigs": true,
  "moduleGates": true,
  "moduleExperiments": true,
  "moduleFeedback": true,
  "createdAt": "2026-04-12T10:14:08.000Z",
  "updatedAt": "2026-06-12T08:01:55.000Z"
}
POST/api/admin/projects/upsert

Find-or-create a project by domain

Find-or-creates a project keyed by (owner_email, domain) under the session's owner, and returns it. Idempotent: a second call with the same domain returns the existing project with created: false.

Only domain is required — name defaults to the domain on first create. Recording the result in a local .shipeasy binding is a consumer side-effect; this endpoint never performs it.

Use cases

Use caseDescriptionExample
Install flowprovision a per-app project without a trip to the dashboard. Run it on every install; the idempotent key means a re-run returns the existing project rather than duplicating it.
Name explicitlypass name to label the project distinctly from its domain.

Parameters

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

Body

NameTypeDescription
domainrequiredstringLowercase bare hostname (e.g. `acme.com`, `app.acme.com`, `*.acme.com`), or `*` to allow any origin. Full URLs with `https://` are not accepted. The project is keyed by `(owner_email, domain)`, so a second call with the same domain returns the existing project.
namestringHuman-readable project name. Defaults to the domain on first create.

Response · 200

NameTypeDescription
idrequiredstringStable opaque project id.
namerequiredstringProject name (the supplied `name`, or the domain on first create).
domainrequiredstring | nullProject domain, or `null` if unset.
owner_emailrequiredstringEmail of the account that owns the project.
createdrequiredboolean`true` if this call created the project, `false` if it returned an existing one.
Example · 200
{
  "id": "f81d4fae-7dec-11d0-a765-00a0c91e6bf6",
  "name": "Acme",
  "domain": "acme.com",
  "owner_email": "owner@acme.com",
  "created": true
}
PATCH/api/admin/projects/{id}

Update the current project

Update the current project's settings — name, domain, slug, default environment, timezone, experiment-analysis knobs (statistical method, significance threshold, auto-rollback, minimum sample days), and the per-module enable flags. Partial: only the fields you send change.

The project id in the path must match the project the caller's credential resolves to (a credential can only edit its own project). Changing domain re-stamps the allowed origin into every live SDK key.

Use caseRename a project, move its domain, toggle a module on/off, or tune the experiment-analysis defaults without leaving the CLI.

Parameters

NameInTypeDescription
X-Project-IdrequiredheaderstringProject to scope this request to.
idrequiredpathstringStable opaque project id. Must match the caller's own project.

Body

NameTypeDescription
namestringNew project name.
domainstring
slugstringURL-safe identifier used in app URLs and SDK config. Lowercase letters, numbers, and hyphens; 2–48 chars; cannot start or end with a hyphen. The caller lowercases the raw slug before sending.
defaultEnv"dev" | "staging" | "prod"Default environment new resources are scoped to.
timezonestringIANA timezone the project's daily analysis runs in.
statMethod"sequential" | "fixed" | "bayesian"Statistical method the experiment analyzer uses.
sigThreshold"0.01" | "0.05" | "0.10"Significance threshold (alpha) for experiment analysis.
autoRollbackbooleanWhether a failing guardrail auto-rolls back the experiment.
minSampleDaysintegerMinimum number of days an experiment must run before it can be called.
moduleTranslationsbooleanEnable/disable the i18n/translations module.
moduleConfigsbooleanEnable/disable the dynamic-configs module.
moduleGatesbooleanEnable/disable the feature-gates module.
moduleExperimentsbooleanEnable/disable the experiments module.
moduleFeedbackbooleanEnable/disable the feedback/ops module.
moduleUserbooleanEnable/disable the user-management module.
moduleEventsbooleanEnable/disable the events module.
minSampleSizeintegerVerdict power guard — minimum users per arm before a ship/hold verdict.
minRuntimeDaysintegerMinimum days an experiment must run before a verdict (peeking guard).
defaultPowernumberTarget statistical power (1−β) feeding the realized-MDE calculation.
ciConfidencenumberConfidence level for the interval surfaced on results (any value in [0.5, 0.999], e.g. 0.90, 0.95, 0.975, 0.99).
defaultAllocationPctintegerDefault traffic allocation (basis points, 1000 = 10%) new experiments start with; overridable per experiment.
defaultHoldoutintegerDefault holdout carve-out (basis points) that seeds each new universe's holdout (0 = none).
defaultWinsorizePctintegerDefault winsorization percentile new metrics start with; overridable per metric.
defaultMeinumber | nullDefault minimum effect of interest (relative, 0–1) new metrics start with; overridable per metric and per experiment. Null clears it.
cupedBaselineDaysintegerCUPED baseline window — days of pre-experiment history, frozen at start.
cupedMinOverlapnumberCUPED selection-bias guard — min share of users with a baseline, else skip.
cupedMinBaselineUsersintegerCUPED — minimum users with a baseline before it runs at all.
msprtTauMeiFactornumbermSPRT prior width — τ = minimum effect of interest × this factor.
msprtTauSdFactornumbermSPRT fallback prior width — τ = this × control SD when no MEI is set.
srmThresholdnumberSRM chi-square p-value below which the run is called invalid.

Response · 200

NameTypeDescription
idrequiredstringStable opaque project id.
namerequiredstringProject name.
domainrequiredstring | nullProject domain, or `null` if unset.
ownerEmailrequiredstringEmail of the account that owns the project.
planrequired"free" | "pro" | "business" | "enterprise"Billing plan tier.
statusrequired"active" | "inactive"Project lifecycle status.
subscriptionStatusrequiredstringStripe subscription status (`none`, `active`, `trialing`, `past_due`, …).
billingIntervalrequired"monthly" | "annual"Billing cadence.
currentPeriodEndrequiredstring | nullISO-8601 end of the current billing period, or `null`.
trialEndsAtrequiredstring | nullISO-8601 trial end, or `null` if not trialing.
cancelAtPeriodEndrequirednumber`1` if the subscription is set to cancel at period end, else `0`.
moduleTranslationsrequiredboolean | numberWhether the i18n/translations module is enabled.
moduleConfigsrequiredboolean | numberWhether the dynamic-configs module is enabled.
moduleGatesrequiredboolean | numberWhether the feature-gates module is enabled.
moduleExperimentsrequiredboolean | numberWhether the experiments module is enabled.
moduleFeedbackrequiredboolean | numberWhether the feedback/ops module is enabled.
minSampleSizeintegerVerdict power guard — minimum users per arm before a ship/hold verdict.
minRuntimeDaysintegerMinimum days an experiment must run before a verdict (peeking guard).
defaultPowernumberTarget statistical power (1−β) feeding the realized-MDE calculation.
ciConfidencenumberConfidence level for the interval surfaced on results.
defaultAllocationPctintegerDefault traffic allocation (basis points) new experiments start with.
defaultHoldoutintegerDefault holdout carve-out (basis points) that seeds each new universe's holdout.
defaultWinsorizePctintegerDefault winsorization percentile new metrics start with.
defaultMeinumber | nullDefault minimum effect of interest (relative, 0–1) new metrics start with, or null.
cupedBaselineDaysintegerCUPED baseline window — days of pre-experiment history, frozen at start.
cupedMinOverlapnumberCUPED selection-bias guard — min share of users with a baseline, else skip.
cupedMinBaselineUsersintegerCUPED — minimum users with a baseline before it runs at all.
msprtTauMeiFactornumbermSPRT prior width — τ = minimum effect of interest × this factor.
msprtTauSdFactornumbermSPRT fallback prior width — τ = this × control SD when no MEI is set.
srmThresholdnumberSRM chi-square p-value below which the run is called invalid.
createdAtrequiredstringISO-8601 timestamp of project creation.
updatedAtrequiredstringISO-8601 timestamp of last update.
Example · 200
{
  "id": "f81d4fae-7dec-11d0-a765-00a0c91e6bf6",
  "name": "Acme Rockets",
  "domain": "acme.com",
  "ownerEmail": "owner@acme.com",
  "plan": "pro",
  "status": "active",
  "subscriptionStatus": "active",
  "billingInterval": "monthly",
  "currentPeriodEnd": "2026-07-12T00:00:00.000Z",
  "trialEndsAt": null,
  "cancelAtPeriodEnd": 0,
  "moduleTranslations": true,
  "moduleConfigs": true,
  "moduleGates": true,
  "moduleExperiments": false,
  "moduleFeedback": true,
  "createdAt": "2026-04-12T10:14:08.000Z",
  "updatedAt": "2026-06-12T08:01:55.000Z"
}
GET/api/admin/search

Search project resources

Universal substring search across the project's primary resources — feature gates, experiments, dynamic configs, killswitches, metrics, and team members. Matches the trimmed q against each resource's immutable name and its human title/display name (members match on display name or email), newest-first, capped per resource family so one noisy type can't crowd out the rest.

Returns a flat, ranked-by-type list of hits (members first); an empty or whitespace-only q returns no hits. Each hit carries a project-relative href that deep-links to the row.

Use caseBack a command-palette / quick-switcher in the dashboard, or look up the id of a resource by a fragment of its name before driving another API call.

Parameters

NameInTypeDescription
X-Project-IdrequiredheaderstringProject to scope this request to.
qquerystringSearch query. Matched as a case-insensitive substring against each resource's `name` and title/display name. Trimmed; empty returns no hits.

Response · 200

NameTypeDescription
hitsrequiredarray<{ type: "member" | "gate" | "experiment" | "config" | "killswitch" | "metric"; id: string; name: string; title: string | null; href: string }>
Example · 200
{
  "hits": [
    {
      "type": "member",
      "id": "ana@example.com",
      "name": "Ana Ng",
      "title": "ana@example.com",
      "href": "/team"
    },
    {
      "type": "gate",
      "id": "gat_01j7w7m9q4hxbf6npe6s9zr3vc",
      "name": "checkout_v2",
      "title": "Checkout v2",
      "href": "/gates?open=gat_01j7w7m9q4hxbf6npe6s9zr3vc"
    },
    {
      "type": "experiment",
      "id": "exp_01j7w7m9q4hxbf6npe6s9zr3vd",
      "name": "checkout_button_color",
      "title": "Checkout button colour test",
      "href": "/experiments?open=exp_01j7w7m9q4hxbf6npe6s9zr3vd"
    }
  ]
}

Profiles

GET/api/admin/i18n/profiles

List i18n profiles

Returns every locale profile in the project (e.g. en:prod, fr:prod).

Use caseDiscover which locale profiles exist before pushing keys or publishing a chunk.

Parameters

NameInTypeDescription
X-Project-IdrequiredheaderstringProject to scope this request to.
Example · 200
[
  {
    "id": "i18n_01j7w8a1b2c3",
    "name": "en:prod",
    "locales": [
      "en"
    ],
    "default_locale": "en"
  }
]
POST/api/admin/i18n/profiles

Create an i18n profile

Create a locale profile. name is the stable handle (e.g. fr:prod).

Use caseStand up a new locale before seeding its keys.

Parameters

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

Body

NameTypeDescription
namerequiredstringProfile handle to create, e.g. `en:prod` or `fr:prod`. Lowercase alphanumeric start, then letters/digits/`_`/`:`/`.`/`-`; max 64 chars. The locale is encoded in the handle, so no separate locale fields are accepted.

Response · 201

NameTypeDescription
idrequiredstringNewly assigned profile id.
namerequiredstringProfile handle that was created.
Example · 201
{
  "id": "i18n_01j7w8a1b2c4",
  "name": "fr:prod"
}
POST/api/admin/i18n/profiles/{profileId}/publish

Publish a profile live

Publish a profile to the CDN — rebuild its KV snapshot + purge the edge. Publishing is PROFILE-WIDE: the whole profile is snapshotted into one KV blob, so the optional chunk in the body is an audit label only (it does not scope what ships).

Use caseShip the latest translations live after pushing/updating keys.

Parameters

NameInTypeDescription
X-Project-IdrequiredheaderstringProject to scope this request to.
profileIdrequiredpathstringThe profile id to publish.

Body

NameTypeDescription
chunkstringOptional chunk label to stamp on the audit log. Publishing is profile-wide regardless — the whole profile is snapshotted into one KV blob.

Response · 200

NameTypeDescription
okrequiredtrueAlways `true` on success.
profile_idrequiredstringProfile that was published.
chunkrequiredstring | nullAudit chunk label, or `null` when none was given.
published_atrequiredstringISO-8601 timestamp of the publish.
versionrequiredstringNew KV snapshot version that was shipped.
key_countrequirednumberNumber of keys in the published snapshot.
changedrequiredbooleanWhether the snapshot's contents actually changed since the last publish.
purgedrequired"purged" | "skipped" | "failed"CDN purge outcome: `purged` ok, `skipped` (no creds), or `failed` (edge still stale).
kv_verifiedrequiredbooleanWhether a KV read-back confirmed the new version persisted.
warningstringHuman-readable caveat when the publish landed but is not fully live.
Example · 200
{
  "ok": true
}

Keys

GET/api/admin/i18n/keys

List i18n keys

List keys for a profile, optionally filtered to a name prefix.

Use caseRead the current keys (and values) for a profile — e.g. to diff before a push.

Parameters

NameInTypeDescription
X-Project-IdrequiredheaderstringProject to scope this request to.
profile_idquerystringProfile id to list keys for.
prefixquerystringOnly keys whose name starts with this.
qquerystringFree-text search — matches keys whose name, value, OR description contains this substring (case-insensitive). Use it to find the key behind a piece of on-screen copy.
limitqueryintegerMax keys to return (1–500).
offsetqueryintegerNumber of keys to skip before returning `limit` rows (offset pagination).

Response · 200

NameTypeDescription
keysrequiredarray<{ id: string; key: string; value: string; description?: string | null; variables?: array<string> | null; profileId?: string; chunkId?: string; updatedAt?: string; updatedBy?: string }>The page of matching keys.
totalrequirednumberTotal matching keys across all pages (ignores `limit`/`offset`).
Example · 200
{
  "keys": [
    {
      "id": "key_01j7",
      "key": "checkout.cta",
      "value": "Buy now"
    }
  ]
}
POST/api/admin/i18n/keys

Push new i18n keys (insert-only)

Add NEW keys to a profile. Insert-only — existing keys are left untouched (overwrite one with updateI18nKey).

Use caseSeed newly-extracted keys without clobbering translations already in the profile.

Parameters

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

Body

NameTypeDescription
profile_idrequiredstringTarget profile id to add keys to.
chunkstringLogical grouping the new keys are filed under. Defaults to `default`. default: "default"
keysrequiredarray<{ key: string; value: string; description?: string; variables?: array<string> }>Keys to add. Insert-only — existing keys are reported back as `skipped`.

Response · 201

NameTypeDescription
addedrequiredarray<string>Key names that were newly inserted.
skippedrequiredarray<string>Key names that already existed and were left untouched.
pushed_countrequirednumberNumber of keys inserted (== `added.length`).
skipped_countrequirednumberNumber of keys skipped (== `skipped.length`).
chunkstringThe chunk the keys were filed under.
Example · 201
{
  "added": [
    "checkout.cta"
  ],
  "skipped": [],
  "pushed_count": 1,
  "skipped_count": 0
}
PUT/api/admin/i18n/keys/{id}

Update one i18n key

Overwrite a single existing key's value — the only overwrite path.

The change goes live on its own: the profile's KV snapshot is rebuilt and the CDN purged as part of this call, so no separate profile publish is needed.

Use caseCorrect or re-translate a single string in place.

Parameters

NameInTypeDescription
X-Project-IdrequiredheaderstringProject to scope this request to.
idrequiredpathstringThe key's id.

Body

NameTypeDescription
valuerequiredstringNew value for the key (the only overwrite path).
descriptionstringOptional human note to store with the key.
variablesarray<string>Explicit `{{var}}` placeholder names in the value. Omit to auto-derive them from the value.

Response · 200

NameTypeDescription
idrequiredstringId of the key that was updated.
Example · 200
{
  "id": "key_01j7"
}
POST/api/admin/i18n/set

Set a key's value and publish it live

Upsert a single key's value into a profile and immediately publish the whole profile (KV rebuild + CDN purge) so the new value is live in one call. The key is inserted when new and overwritten when it already exists. profile is a profile name — omit it to target the project's default-marked profile, or pass a name to target another locale.

Use caseCorrect or replace one live string and ship it without a separate push + publish.

Parameters

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

Body

NameTypeDescription
keyrequiredstringDotted key path to set, e.g. `home.cta`.
valuerequiredstringNew value for the key. Inserted when the key is new, overwritten when it exists.
profilestringProfile name to target, e.g. `en:prod`. Omit to target the project's default-marked profile.
descriptionstringOptional human note to store with the key.

Response · 200

NameTypeDescription
okrequiredtrueAlways `true` on success.
profilerequiredstringName of the profile that was published.
profile_idrequiredstringId of the profile that was published.
keyrequiredstringThe key that was set.
valuerequiredstringThe value that was stored.
published_atrequiredstringISO-8601 timestamp of the publish.
versionrequiredstringNew KV snapshot version that was shipped.
key_countrequirednumberNumber of keys in the published snapshot.
changedrequiredbooleanWhether the snapshot's contents actually changed since the last publish.
purgedrequired"purged" | "skipped" | "failed"CDN purge outcome: `purged` ok, `skipped` (no creds), or `failed` (edge still stale).
kv_verifiedrequiredbooleanWhether a KV read-back confirmed the new version persisted.
warningstringHuman-readable caveat when the publish landed but is not fully live.
Example · 200
{
  "ok": true,
  "profile": "en:prod",
  "key": "home.cta",
  "value": "Get started",
  "version": "7",
  "key_count": 42,
  "changed": true,
  "purged": "purged",
  "kv_verified": true
}

Drafts

GET/api/admin/i18n/drafts

List translation drafts

List staged translation drafts awaiting review/publish.

Use caseReview machine-translation drafts before publishing them to a locale.

Parameters

NameInTypeDescription
X-Project-IdrequiredheaderstringProject to scope this request to.
Example · 200
[
  {
    "id": "draft_01j7"
  }
]
POST/api/admin/i18n/drafts

Create a translation draft

Stage a new translation draft against a target profile, optionally seeding its keys from a source profile.

Use caseOpen a machine-translation draft for review before publishing it to a locale.

Parameters

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

Body

NameTypeDescription
namerequiredstringDraft name, e.g. the target locale being staged.
profile_idrequiredstringProfile the draft targets.
source_profile_idstringOptional profile to seed the draft's keys from.

Response · 200

NameTypeDescription
idrequiredstringStable opaque draft id.
namestringDraft name, e.g. the target locale being staged.
profileIdstringProfile the draft targets.
sourceProfileIdstring | nullProfile the draft was seeded from, or `null`.
status"open" | "merged" | "abandoned"Lifecycle state of the draft.
createdBystringActor email that created the draft.
createdAtstringISO-8601 timestamp of creation.
publishedAtstring | nullISO-8601 merge/publish timestamp, or `null`.
Example · 200
{
  "id": "draft_01j7",
  "name": "fr:prod",
  "profileId": "3f1a6c2e-1b2c-4d5e-8f90-0a1b2c3d4e5f",
  "status": "open"
}
PATCH/api/admin/i18n/drafts/{draftId}

Update a translation draft

Transition a draft's lifecycle state (open / merged / abandoned).

Use caseMark a reviewed draft as merged, or abandon one that should not ship.

Parameters

NameInTypeDescription
X-Project-IdrequiredheaderstringProject to scope this request to.
draftIdrequiredpathstringThe draft id to update.

Body

NameTypeDescription
status"open" | "merged" | "abandoned"New lifecycle state for the draft.

Response · 200

NameTypeDescription
idrequiredstringStable opaque draft id.
namestringDraft name, e.g. the target locale being staged.
profileIdstringProfile the draft targets.
sourceProfileIdstring | nullProfile the draft was seeded from, or `null`.
status"open" | "merged" | "abandoned"Lifecycle state of the draft.
createdBystringActor email that created the draft.
createdAtstringISO-8601 timestamp of creation.
publishedAtstring | nullISO-8601 merge/publish timestamp, or `null`.
Example · 200
{
  "id": "draft_01j7",
  "status": "merged"
}

Errors

GET/api/admin/errors

List tracked errors

Returns a single page of tracked production errors as a bare JSON array (no pagination envelope), ordered by lastSeenAt desc. Filter with status, free-text-search with q, and cap the page with limit.

Tracked errors are never filed by hand — an ingestion path (worker log drain / the see() SDK reporter) folds each occurrence into a row keyed by fingerprint, bumping count and lastSeenAt. This surface only reads them and (via PATCH) flips their status.

Use caseSnapshot the project's open issues for a triage dashboard, or drive a CI gate that fails the build when any open error of kind: uncaught exists in prod.

Parameters

NameInTypeDescription
X-Project-IdrequiredheaderstringProject to scope this request to.
statusquerystringFilter by triage state. `all` (the default) returns every status.
qquerystringCase-insensitive substring match against `message`, `errorType`, and `subject`.
limitqueryintegerMaximum number of rows to return (1–500). Defaults to 200.
Example · 200
[
  {
    "id": "err_01j7w7m9q4hxbf6npe6s9zr3vc",
    "projectId": "e976b15e-2f0d-4c6e-9b1a-3a7c1f2d8e90",
    "fingerprint": "9c1f4f1f2c0c4a5fa1c2b6d3e7c8e3a1",
    "causedByFingerprint": null,
    "message": "Cannot read properties of undefined (reading 'id')",
    "errorType": "TypeError",
    "stack": "TypeError: Cannot read properties of undefined (reading 'id')\n    at GateEditor (/app/gates/[id]/page.tsx:42:18)",
    "source": "shipeasy",
    "url": "https://app.example.com/dashboard/gat_01j7/edit",
    "seenUrls": "[\"https://app.example.com/dashboard/#/edit\"]",
    "subject": "gate editor",
    "outcome": "crash on load",
    "side": "client",
    "env": "prod",
    "kind": "uncaught",
    "lastExtrasJson": "{\"gateId\":\"gat_01j7\"}",
    "sdkVersion": "6.0.0",
    "count": 137,
    "status": "open",
    "firstSeenAt": "2026-06-20T08:11:02.000Z",
    "lastSeenAt": "2026-06-28T14:55:31.000Z",
    "createdAt": "2026-06-20T08:11:02.000Z",
    "updatedAt": "2026-06-28T14:55:31.000Z"
  }
]
GET/api/admin/errors/{id}

Get a tracked error

Returns a single tracked error by its id, including the latest occurrence's stack, extras, and consequence, plus occurrences — the sampled per-instance detail rows behind the issue (newest first; exhaustive while the issue is small, thinned at volume, capped at 100). Returns 404 if no such error exists in the project.

Use caseDrill into one issue — fetch its full stack and seenUrls to investigate, or walk occurrences to see how the failing message/stack varies across instances.

Parameters

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

Response · 200

NameTypeDescription
idrequiredstringStable opaque error id.
projectIdrequiredstringProject this issue belongs to.
fingerprintrequiredstringStable dedupe key — the issue TITLE (hash of `errorType` + `subject` + `outcome`, plus the violation name for violations). Unique per project — every occurrence with the same title folds into this one row; per-instance variety (message/stack/extras) lives in `occurrences`.
causedByFingerprintstring | nullFingerprint of the issue this one descends from — set when `see()` reported the same error (re-throw) or its `{ cause }` (wrap) at an inner boundary first. Points at another error's `fingerprint` in the same project (soft reference, no FK). `null` for a root issue.
messagerequiredstringError message text.
errorTypestring | nullError class/name, e.g. `TypeError`. `null` when the source didn't supply one.
stackstring | nullStack trace of the latest occurrence, or `null` if none was captured.
sourcestring | nullWhere it surfaced — Worker name (`shipeasy`, `shipeasy-worker`) or `sdk-client` / `sdk-server`. `null` if unknown.
urlstring | nullLatest occurrence's raw URL (with ids intact), or `null`.
seenUrlsstring | nullDistinct, id-normalized route templates this issue has surfaced on, as a JSON-encoded string array (e.g. `["https://app/dashboard/#/gates"]`). UUIDs / numeric ids / opaque tokens are collapsed to `#` so the same route under different ids counts once. `null` if none recorded.
subjectstring | nullConsequence subject — `<errorType> causes the <subject> to <outcome>`. `null` if no consequence was reported.
outcomestring | nullConsequence outcome — see `subject`. `null` if no consequence was reported.
sidestring | nullWhich SDK side reported it — `client` or `server`. `null` if unknown.
envstring | nullPublished env the reporting SDK ran against (e.g. `dev`, `staging`, `prod`). `null` if unknown.
kind"caught" | "uncaught" | "unhandled_rejection" | "network" | "violation" | null`see()` error kind. `null` when the source didn't classify the occurrence.
lastExtrasJsonstring | nullLatest occurrence's sanitized extras, JSON-encoded. `null` if none.
sdkVersionstring | null`@shipeasy/sdk` version of the latest occurrence, or `null`.
countrequiredintegerEXACT number of folded occurrences for this fingerprint — every occurrence increments it, regardless of detail-row sampling.
occurrencesarray<{ id: string; message: string; stack?: string | null; url?: string | null; env?: string | null; side?: string | null; sdkVersion?: string | null; extrasJson?: string | null; sampleRate: integer; seenAt: string }>Sampled per-instance detail rows behind this issue, newest first. Returned only by `GET /api/admin/errors/{id}` (never in list responses). The parent row's `count` / `firstSeenAt` / `lastSeenAt` are exact; these rows are a *sampled sketch* of the instances — exhaustive while the issue is small, thinning to roughly 1-in-10 past 10 occurrences, 1-in-100 past 100, and 1-in-1000 past 1000 (each row's `sampleRate` records the rate in force when it was kept), capped at the newest 100 rows.
statusrequired"open" | "resolved" | "ignored"Triage state. `open` is the default; a `resolved` error reopens automatically (ingestion-side) if it recurs; `ignored` is sticky until flipped back here.
firstSeenAtrequiredstringISO-8601 timestamp of the first folded occurrence.
lastSeenAtrequiredstringISO-8601 timestamp of the most recent folded occurrence. Rows are ordered by this, descending.
createdAtrequiredstringISO-8601 timestamp the row was created.
updatedAtrequiredstringISO-8601 timestamp of the last mutation (e.g. a status flip).
Example · 200
{
  "id": "err_01j7w7m9q4hxbf6npe6s9zr3vc",
  "projectId": "e976b15e-2f0d-4c6e-9b1a-3a7c1f2d8e90",
  "fingerprint": "9c1f4f1f2c0c4a5fa1c2b6d3e7c8e3a1",
  "causedByFingerprint": null,
  "message": "Cannot read properties of undefined (reading 'id')",
  "errorType": "TypeError",
  "stack": "TypeError: Cannot read properties of undefined (reading 'id')\n    at GateEditor (/app/gates/[id]/page.tsx:42:18)",
  "source": "shipeasy",
  "url": "https://app.example.com/dashboard/gat_01j7/edit",
  "seenUrls": "[\"https://app.example.com/dashboard/#/edit\"]",
  "subject": "gate editor",
  "outcome": "crash on load",
  "side": "client",
  "env": "prod",
  "kind": "uncaught",
  "lastExtrasJson": "{\"gateId\":\"gat_01j7\"}",
  "sdkVersion": "6.0.0",
  "count": 137,
  "status": "open",
  "firstSeenAt": "2026-06-20T08:11:02.000Z",
  "lastSeenAt": "2026-06-28T14:55:31.000Z",
  "createdAt": "2026-06-20T08:11:02.000Z",
  "updatedAt": "2026-06-28T14:55:31.000Z",
  "occurrences": [
    {
      "id": "occ_01j7w7qq0v3k9d2m5rc8t1xh4n",
      "message": "Cannot read properties of undefined (reading 'id')",
      "stack": "TypeError: Cannot read properties of undefined (reading 'id')\n    at GateEditor (/app/gates/[id]/page.tsx:42:18)",
      "url": "https://app.example.com/dashboard/gat_01j7/edit",
      "env": "prod",
      "side": "client",
      "sdkVersion": "6.0.0",
      "extrasJson": "{\"gateId\":\"gat_01j7\"}",
      "sampleRate": 100,
      "seenAt": "2026-06-28T14:55:31.000Z"
    },
    {
      "id": "occ_01j7w7pk8s6e3f9b0zad7y5w2q",
      "message": "Cannot read properties of undefined (reading 'id')",
      "stack": "TypeError: Cannot read properties of undefined (reading 'id')\n    at GateEditor (/app/gates/[id]/page.tsx:42:18)",
      "url": "https://app.example.com/dashboard/gat_02c4/edit",
      "env": "prod",
      "side": "client",
      "sdkVersion": "6.0.0",
      "extrasJson": "{\"gateId\":\"gat_02c4\"}",
      "sampleRate": 10,
      "seenAt": "2026-06-24T09:12:05.000Z"
    }
  ]
}
PATCH/api/admin/errors/{id}

Update a tracked error's status

Flips the triage state of one error — the only mutation this surface allows. Valid transitions are between open, resolved, and ignored; the body must carry exactly { "status": … }.

A resolved error reopens automatically (ingestion-side) if it recurs; ignored is sticky until flipped back here. Returns the updated row.

Use cases

Use caseDescriptionExample
Triage{ "status": "ignored" } to suppress a known-benign issue from the open list.
Close out{ "status": "resolved" } once the fix lands; it reopens on its own if the error recurs.

Parameters

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

Body

NameTypeDescription
statusrequired"open" | "resolved" | "ignored"New triage state. `resolved` reopens automatically on recurrence; `ignored` is sticky.

Response · 200

NameTypeDescription
idrequiredstringStable opaque error id.
projectIdrequiredstringProject this issue belongs to.
fingerprintrequiredstringStable dedupe key — the issue TITLE (hash of `errorType` + `subject` + `outcome`, plus the violation name for violations). Unique per project — every occurrence with the same title folds into this one row; per-instance variety (message/stack/extras) lives in `occurrences`.
causedByFingerprintstring | nullFingerprint of the issue this one descends from — set when `see()` reported the same error (re-throw) or its `{ cause }` (wrap) at an inner boundary first. Points at another error's `fingerprint` in the same project (soft reference, no FK). `null` for a root issue.
messagerequiredstringError message text.
errorTypestring | nullError class/name, e.g. `TypeError`. `null` when the source didn't supply one.
stackstring | nullStack trace of the latest occurrence, or `null` if none was captured.
sourcestring | nullWhere it surfaced — Worker name (`shipeasy`, `shipeasy-worker`) or `sdk-client` / `sdk-server`. `null` if unknown.
urlstring | nullLatest occurrence's raw URL (with ids intact), or `null`.
seenUrlsstring | nullDistinct, id-normalized route templates this issue has surfaced on, as a JSON-encoded string array (e.g. `["https://app/dashboard/#/gates"]`). UUIDs / numeric ids / opaque tokens are collapsed to `#` so the same route under different ids counts once. `null` if none recorded.
subjectstring | nullConsequence subject — `<errorType> causes the <subject> to <outcome>`. `null` if no consequence was reported.
outcomestring | nullConsequence outcome — see `subject`. `null` if no consequence was reported.
sidestring | nullWhich SDK side reported it — `client` or `server`. `null` if unknown.
envstring | nullPublished env the reporting SDK ran against (e.g. `dev`, `staging`, `prod`). `null` if unknown.
kind"caught" | "uncaught" | "unhandled_rejection" | "network" | "violation" | null`see()` error kind. `null` when the source didn't classify the occurrence.
lastExtrasJsonstring | nullLatest occurrence's sanitized extras, JSON-encoded. `null` if none.
sdkVersionstring | null`@shipeasy/sdk` version of the latest occurrence, or `null`.
countrequiredintegerEXACT number of folded occurrences for this fingerprint — every occurrence increments it, regardless of detail-row sampling.
occurrencesarray<{ id: string; message: string; stack?: string | null; url?: string | null; env?: string | null; side?: string | null; sdkVersion?: string | null; extrasJson?: string | null; sampleRate: integer; seenAt: string }>Sampled per-instance detail rows behind this issue, newest first. Returned only by `GET /api/admin/errors/{id}` (never in list responses). The parent row's `count` / `firstSeenAt` / `lastSeenAt` are exact; these rows are a *sampled sketch* of the instances — exhaustive while the issue is small, thinning to roughly 1-in-10 past 10 occurrences, 1-in-100 past 100, and 1-in-1000 past 1000 (each row's `sampleRate` records the rate in force when it was kept), capped at the newest 100 rows.
statusrequired"open" | "resolved" | "ignored"Triage state. `open` is the default; a `resolved` error reopens automatically (ingestion-side) if it recurs; `ignored` is sticky until flipped back here.
firstSeenAtrequiredstringISO-8601 timestamp of the first folded occurrence.
lastSeenAtrequiredstringISO-8601 timestamp of the most recent folded occurrence. Rows are ordered by this, descending.
createdAtrequiredstringISO-8601 timestamp the row was created.
updatedAtrequiredstringISO-8601 timestamp of the last mutation (e.g. a status flip).
Example · 200
{
  "id": "err_01j7w7m9q4hxbf6npe6s9zr3vc",
  "projectId": "e976b15e-2f0d-4c6e-9b1a-3a7c1f2d8e90",
  "fingerprint": "9c1f4f1f2c0c4a5fa1c2b6d3e7c8e3a1",
  "causedByFingerprint": null,
  "message": "Cannot read properties of undefined (reading 'id')",
  "errorType": "TypeError",
  "stack": null,
  "source": "shipeasy",
  "url": "https://app.example.com/dashboard/gat_01j7/edit",
  "seenUrls": null,
  "subject": "gate editor",
  "outcome": "crash on load",
  "side": "client",
  "env": "prod",
  "kind": "uncaught",
  "lastExtrasJson": null,
  "sdkVersion": "6.0.0",
  "count": 137,
  "status": "resolved",
  "firstSeenAt": "2026-06-20T08:11:02.000Z",
  "lastSeenAt": "2026-06-28T14:55:31.000Z",
  "createdAt": "2026-06-20T08:11:02.000Z",
  "updatedAt": "2026-06-28T15:02:10.000Z"
}
POST/api/admin/errors/{id}/file

File a feedback ticket for an error

Files a feedback ticket (type: "error") for a tracked production error — the "File an issue" action on the errors dashboard. The ticket carries the error's fingerprint as its sourceRef so it dedupes against, and joins back to, the tracked error. Takes no body.

Idempotent: if an open error ticket already tracks this fingerprint (hand- or auto-filed), that existing ticket is returned instead of creating a duplicate. Returns 404 if the error does not exist.

Use casePromote a noisy tracked error into an actionable ticket in the ops queue (the same item the worker auto-files once an error crosses its occurrence threshold), so it can be triaged, assigned, and burned down via the shipeasy-ops-work skill.

Parameters

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

Response · 201

NameTypeDescription
idrequiredstringFeedback ticket id.
numberrequiredintegerHuman-facing per-project ticket number.
Example · 201
{
  "id": "3f2a9b1c-7d4e-4a8f-9c2b-1e5d6f7a8b90",
  "number": 42
}
POST/api/admin/errors/{id}/resolve

Resolve a tracked error

Marks one tracked error resolved — the single-purpose "close out" action. Takes no body; it is PATCH /api/admin/errors/{id} pinned to { "status": "resolved" }, exposed so tooling can close an error without being handed the full open/resolved/ignored status machine. A resolved error reopens automatically (ingestion-side) if it recurs, so resolving is always safe: a premature resolve un-does itself on the next occurrence. Returns the updated row; 404 if the error does not exist.

Use caseClose out a tracked error from an agent or script once its fix has shipped — e.g. after a deploy, resolve every open issue the change addressed and let recurrence reopen anything that wasn't actually fixed.

Parameters

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

Response · 200

NameTypeDescription
idrequiredstringStable opaque error id.
projectIdrequiredstringProject this issue belongs to.
fingerprintrequiredstringStable dedupe key — the issue TITLE (hash of `errorType` + `subject` + `outcome`, plus the violation name for violations). Unique per project — every occurrence with the same title folds into this one row; per-instance variety (message/stack/extras) lives in `occurrences`.
causedByFingerprintstring | nullFingerprint of the issue this one descends from — set when `see()` reported the same error (re-throw) or its `{ cause }` (wrap) at an inner boundary first. Points at another error's `fingerprint` in the same project (soft reference, no FK). `null` for a root issue.
messagerequiredstringError message text.
errorTypestring | nullError class/name, e.g. `TypeError`. `null` when the source didn't supply one.
stackstring | nullStack trace of the latest occurrence, or `null` if none was captured.
sourcestring | nullWhere it surfaced — Worker name (`shipeasy`, `shipeasy-worker`) or `sdk-client` / `sdk-server`. `null` if unknown.
urlstring | nullLatest occurrence's raw URL (with ids intact), or `null`.
seenUrlsstring | nullDistinct, id-normalized route templates this issue has surfaced on, as a JSON-encoded string array (e.g. `["https://app/dashboard/#/gates"]`). UUIDs / numeric ids / opaque tokens are collapsed to `#` so the same route under different ids counts once. `null` if none recorded.
subjectstring | nullConsequence subject — `<errorType> causes the <subject> to <outcome>`. `null` if no consequence was reported.
outcomestring | nullConsequence outcome — see `subject`. `null` if no consequence was reported.
sidestring | nullWhich SDK side reported it — `client` or `server`. `null` if unknown.
envstring | nullPublished env the reporting SDK ran against (e.g. `dev`, `staging`, `prod`). `null` if unknown.
kind"caught" | "uncaught" | "unhandled_rejection" | "network" | "violation" | null`see()` error kind. `null` when the source didn't classify the occurrence.
lastExtrasJsonstring | nullLatest occurrence's sanitized extras, JSON-encoded. `null` if none.
sdkVersionstring | null`@shipeasy/sdk` version of the latest occurrence, or `null`.
countrequiredintegerEXACT number of folded occurrences for this fingerprint — every occurrence increments it, regardless of detail-row sampling.
occurrencesarray<{ id: string; message: string; stack?: string | null; url?: string | null; env?: string | null; side?: string | null; sdkVersion?: string | null; extrasJson?: string | null; sampleRate: integer; seenAt: string }>Sampled per-instance detail rows behind this issue, newest first. Returned only by `GET /api/admin/errors/{id}` (never in list responses). The parent row's `count` / `firstSeenAt` / `lastSeenAt` are exact; these rows are a *sampled sketch* of the instances — exhaustive while the issue is small, thinning to roughly 1-in-10 past 10 occurrences, 1-in-100 past 100, and 1-in-1000 past 1000 (each row's `sampleRate` records the rate in force when it was kept), capped at the newest 100 rows.
statusrequired"open" | "resolved" | "ignored"Triage state. `open` is the default; a `resolved` error reopens automatically (ingestion-side) if it recurs; `ignored` is sticky until flipped back here.
firstSeenAtrequiredstringISO-8601 timestamp of the first folded occurrence.
lastSeenAtrequiredstringISO-8601 timestamp of the most recent folded occurrence. Rows are ordered by this, descending.
createdAtrequiredstringISO-8601 timestamp the row was created.
updatedAtrequiredstringISO-8601 timestamp of the last mutation (e.g. a status flip).
Example · 200
{
  "id": "err_01j7w7m9q4hxbf6npe6s9zr3vc",
  "projectId": "e976b15e-2f0d-4c6e-9b1a-3a7c1f2d8e90",
  "fingerprint": "9c1f4f1f2c0c4a5fa1c2b6d3e7c8e3a1",
  "causedByFingerprint": null,
  "message": "Cannot read properties of undefined (reading 'id')",
  "errorType": "TypeError",
  "stack": null,
  "source": "shipeasy",
  "url": "https://app.example.com/dashboard/gat_01j7/edit",
  "seenUrls": null,
  "subject": "gate editor",
  "outcome": "crash on load",
  "side": "client",
  "env": "prod",
  "kind": "uncaught",
  "lastExtrasJson": null,
  "sdkVersion": "6.0.0",
  "count": 137,
  "status": "resolved",
  "firstSeenAt": "2026-06-20T08:11:02.000Z",
  "lastSeenAt": "2026-06-28T14:55:31.000Z",
  "createdAt": "2026-06-20T08:11:02.000Z",
  "updatedAt": "2026-06-28T15:02:10.000Z"
}
POST/api/admin/errors/{id}/series

Get an error's occurrence series

Returns a bucketed occurrence timeseries for one tracked error (by its fingerprint), read from the shipeasy_errors Analytics Engine dataset (near-real-time; ingest lag is seconds). The window bounds are epoch seconds; to must be strictly greater than from. The response echoes the SQL that produced the rows.

Use caseRender the trend sparkline / occurrence chart on the error detail panel, or pull the raw bucketed counts to alert when an issue's rate spikes.

Parameters

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

Body

NameTypeDescription
fromrequiredintegerWindow start, epoch seconds (inclusive).
torequiredintegerWindow end, epoch seconds (exclusive). Must be greater than `from`.
bucketintegerBucket width in seconds (60s–86400s/1d). Defaults to `3600` (hourly). Each returned point is floor-aligned to this width. default: 3600

Response · 200

NameTypeDescription
sqlrequiredstringThe Analytics Engine SQL executed to produce `rows` (echoed for transparency / debugging).
rowsrequiredarray<{ t: integer; v: number }>Bucketed occurrence series, ordered by `t` ascending.
Example · 200
{
  "sql": "SELECT intDiv(toUInt32(double2), 3600) * 3600 AS t,\n  sum(double1 * _sample_interval) AS v\nFROM shipeasy_errors\nWHERE index1 = 'e976b15e-2f0d-4c6e-9b1a-3a7c1f2d8e90'\n  AND blob1 = '9c1f4f1f2c0c4a5fa1c2b6d3e7c8e3a1'\n  AND double2 >= 1751000400 AND double2 < 1751086800\nGROUP BY t\nORDER BY t",
  "rows": [
    {
      "t": 1751000400,
      "v": 12
    },
    {
      "t": 1751004000,
      "v": 31
    },
    {
      "t": 1751007600,
      "v": 27
    }
  ]
}

Connectors

GET/api/admin/connectors

List connectors

Returns every connector in the project as a bare array (no pagination envelope).

The encrypted credentials backing each connector are never serialised — only the connector's non-secret config, accountLabel, and last-attempt health (lastError, lastAttemptAt, lastSuccessAt) are returned.

Use caseRender the integrations/triggers settings page, or drive a CI check that asserts every github connector is enabled and last dispatched without error.

Parameters

NameInTypeDescription
X-Project-IdrequiredheaderstringProject to scope this request to.
Example · 200
[
  {
    "id": "9b0e7c2a-1f3d-4a8e-bf21-0c6a2e5d7f10",
    "projectId": "prj_01j7w7m9q4hxbf6npe6s9zr3vc",
    "provider": "github",
    "name": "Bugs → acme/app issues",
    "enabled": true,
    "events": [
      "bug.created"
    ],
    "config": {
      "owner": "acme",
      "repo": "app"
    },
    "accountLabel": "acme/app",
    "lastError": null,
    "lastAttemptAt": "2026-06-27T18:04:11.000Z",
    "lastSuccessAt": "2026-06-27T18:04:11.000Z",
    "createdAt": "2026-06-01T09:12:00.000Z",
    "updatedAt": "2026-06-27T18:04:11.000Z"
  },
  {
    "id": "2d44f0aa-9c81-4e6b-8a17-7b3e0f9c1a22",
    "projectId": "prj_01j7w7m9q4hxbf6npe6s9zr3vc",
    "provider": "claude_trigger",
    "name": "Nightly ops sweep",
    "enabled": true,
    "events": [],
    "config": {
      "routineId": "rtn_8fk20"
    },
    "accountLabel": "rtn_8fk20",
    "lastError": null,
    "lastAttemptAt": null,
    "lastSuccessAt": null,
    "createdAt": "2026-06-20T22:00:00.000Z",
    "updatedAt": "2026-06-20T22:00:00.000Z"
  }
]
POST/api/admin/connectors

Create a connector

Creates a connector. The request body is discriminated on provider.

- OAuth/app providers (google_sheets, github, slack) — supply { provider, name, events }. The connector is created enabled: false with empty config and no credentials; the provider's OAuth flow then attaches credentials and enables it. - Trigger providers (claude_trigger, cursor_trigger, copilot_trigger, jules_trigger) — supply config plus the provider's credential field(s) and the connector is fireable immediately. Trigger creates are idempotent by their natural key (config.routineId / config.repoUrl / config.owner+config.repo / config.source): re-creating updates the existing row rather than duplicating it.

Use cases

Use caseDescriptionExample
File bugs as GitHub Issues{ "provider": "github", "name": "Bugs → acme/app", "events": ["bug.created"] }, then finish the GitHub App install.
Nightly ops sweepregister a claude_trigger with its routineId and (optionally) a fire token; subscribe events later to auto-fire on new bugs.
Cold cloud-agent runregister a cursor_trigger/jules_trigger with the repo coordinates plus both keys, or a copilot_trigger with the repo + user PAT.

Parameters

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

Response · 201

NameTypeDescription
idrequiredstringNewly assigned connector id.
Example · 201
{
  "id": "9b0e7c2a-1f3d-4a8e-bf21-0c6a2e5d7f10"
}
GET/api/admin/connectors/{id}

Get a connector

Returns a single connector by id. The encrypted credentials are never serialised.

Use caseInspect one connector's health — its enabled state, subscribed events, and last-attempt outcome (lastError/lastSuccessAt).

Parameters

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

Response · 200

NameTypeDescription
idrequiredstringStable opaque connector id (a UUID).
projectIdrequiredstringId of the project the connector belongs to.
providerrequired"google_sheets" | "github" | "slack" | "claude_trigger" | "cursor_trigger" | "copilot_trigger" | "jules_trigger"
namerequiredstringHuman-readable connector label shown in the dashboard.
enabledrequiredbooleanWhether the connector is active. OAuth providers are created `false` and flip to `true` only after the OAuth/config flow completes; trigger providers default to `true` on create. A disabled connector never dispatches and never auto-fires.
eventsrequiredarray<"bug.created" | "feature_request.created">Events this connector is subscribed to. Empty array = no auto-dispatch / no auto-fire (the connector can still be fired/tested manually).
configrequiredobjectProvider-specific, non-secret configuration (e.g. a Google Sheets `spreadsheetId`/`sheetTitle`, a Slack channel ref, or a trigger's repo coordinates + routine id). Secrets are never stored here — they live in an encrypted credentials cipher that is never returned by the API.
accountLabelrequiredstring | nullDisplay label for the connected account / target (e.g. the OAuth account email, or a trigger's idempotency key such as its repo url or routine id). `null` until the connector is authenticated/configured.
lastErrorrequiredstring | nullError message from the most recent failed dispatch/fire attempt, or `null` if the last attempt succeeded (or none has run).
lastAttemptAtrequiredstring | nullISO-8601 timestamp of the most recent dispatch/fire attempt, or `null` if none has run.
lastSuccessAtrequiredstring | nullISO-8601 timestamp of the most recent successful dispatch/fire, or `null` if none has succeeded. Preserved across later failures.
createdAtrequiredstringISO-8601 timestamp of creation.
updatedAtrequiredstringISO-8601 timestamp of last mutation.
Example · 200
{
  "id": "9b0e7c2a-1f3d-4a8e-bf21-0c6a2e5d7f10",
  "projectId": "prj_01j7w7m9q4hxbf6npe6s9zr3vc",
  "provider": "github",
  "name": "Bugs → acme/app issues",
  "enabled": true,
  "events": [
    "bug.created"
  ],
  "config": {
    "owner": "acme",
    "repo": "app"
  },
  "accountLabel": "acme/app",
  "lastError": null,
  "lastAttemptAt": "2026-06-27T18:04:11.000Z",
  "lastSuccessAt": "2026-06-27T18:04:11.000Z",
  "createdAt": "2026-06-01T09:12:00.000Z",
  "updatedAt": "2026-06-27T18:04:11.000Z"
}
PATCH/api/admin/connectors/{id}

Update a connector

Partial update — only supplied fields change. events and config replace wholesale; there is no merge or append. Secrets cannot be set through this endpoint (credential rotation goes through the provider-specific re-register flow).

The response carries only { id } — re-fetch via GET /api/admin/connectors/{id} for the new row.

Use cases

Use caseDescriptionExample
Pause a connector{ "enabled": false }. Stops all dispatch / auto-fire without deleting it.
Change subscribed eventssend the full new events array. An empty array unsubscribes the connector from every event.
Rename{ "name": "Bugs → acme/app issues" }.
Retargetsend a new config (e.g. a different Sheets sheetTitle); it replaces the stored config wholesale.

Parameters

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

Body

NameTypeDescription
namestringNew connector label.
enabledbooleanToggle the connector on/off.
eventsarray<"bug.created" | "feature_request.created">Replaces the subscribed-events list wholesale. An empty array unsubscribes the connector from every event (e.g. turning off a trigger's auto-fire).
configobjectReplaces the non-secret config wholesale. Secrets are never set through this endpoint.

Response · 200

NameTypeDescription
idrequiredstringConnector id that was updated.
Example · 200
{
  "id": "9b0e7c2a-1f3d-4a8e-bf21-0c6a2e5d7f10"
}
DELETE/api/admin/connectors/{id}

Delete a connector

Permanently removes the connector and its stored credentials.

Use caseDisconnect an integration for good — e.g. tear down a github connector after migrating bug routing elsewhere.

Parameters

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

Response · 200

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

Fire a trigger connector

Manually kicks a trigger connector's run — Claude (kicks its preconfigured routine) or Cursor / Copilot / Jules (launches a cold cloud-agent run). Firing is event-less: it kicks the run with an optional caller-supplied prompt override rather than dispatching a single lifecycle payload.

Only trigger providers can be fired, and only once authenticated (a tokenless trigger cannot fire). The attempt's outcome is recorded on the connector's lastAttemptAt / lastError / lastSuccessAt.

Use caseKick a one-off ops sweep on demand from the dashboard's "Fire now" button, optionally overriding the routine's default prompt.

Parameters

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

Body

NameTypeDescription
textstringOptional prompt override for the run. When omitted, each provider falls back to its own default (Claude → the configured `fireText`; the others → their built cold-start prompt).

Response · 200

NameTypeDescription
okrequiredbooleanAlways `true` — a successful fire returns HTTP 200. A dispatch failure returns HTTP 502 with the `Error` envelope, not this body.
Example · 200
{
  "ok": true
}
POST/api/admin/connectors/{id}/test

Test a connector

Dispatches a single synthetic bug.created payload to the connector's destination so you can verify the integration end-to-end. Unlike fire, this runs the real dispatch path (posts a Slack message / appends a Sheets row / files a GitHub Issue) with throwaway test content. The attempt's outcome is recorded on the connector's lastAttemptAt / lastError / lastSuccessAt.

When the provider produces a linkable artifact (e.g. a GitHub Issue), its URL is returned as issueUrl; otherwise issueUrl is null.

Use caseClick "Send test" after wiring up a connector to confirm credentials and config are correct before relying on it for real events.

Parameters

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

Response · 200

NameTypeDescription
okrequiredbooleanAlways `true` — a successful test returns HTTP 200. A dispatch failure returns HTTP 502 with the `Error` envelope, not this body.
issueUrlrequiredstring | nullURL of the artifact the test produced (e.g. the GitHub Issue created by a `github` connector), or `null` when the provider produces no linkable artifact.
Example · 200
{
  "ok": true,
  "issueUrl": "https://github.com/acme/app/issues/4217"
}
PATCH/api/admin/connectors/{id}/trigger

Update a trigger connector

Edit an existing coding-agent trigger connector (claude_trigger / cursor_trigger / copilot_trigger / jules_trigger) — replace its non-secret config and, optionally, rotate its credential secret(s). Discriminated on provider; the id in the path selects the connector and its provider must match the body.

Unlike the generic PATCH /api/admin/connectors/{id} (which cannot touch secrets), this endpoint always replaces the non-secret config wholesale and merges any supplied secret over the stored credential cipher — so a single half of a two-key pair (e.g. just the ops key) can be rotated on its own. A blank/omitted secret leaves the stored cipher untouched.

The response carries only { id } — re-fetch via GET /api/admin/connectors/{id} for the new row.

Use caseRe-point a Cursor trigger at a new repo ref, or rotate a Claude trigger's fire token, without deleting and re-creating the connector.

Parameters

NameInTypeDescription
X-Project-IdrequiredheaderstringProject to scope this request to.
idrequiredpathstringStable opaque connector id. Its stored `provider` must match the body's `provider`.

Response · 200

NameTypeDescription
idrequiredstringConnector id that was updated.
Example · 200
{
  "id": "9b0e7c2a-1f3d-4a8e-bf21-0c6a2e5d7f10"
}

Trigger

POST/api/admin/connectors/trigger

Create a recurring coding-agent trigger

Creates (or idempotently updates) a coding-agent trigger connector — the recurring, unattended run that burns down the ops queue in --pr mode. Discriminated on provider; only the four Shipeasy-fireable providers are accepted (claude_trigger, cursor_trigger, copilot_trigger, jules_trigger). Config + credential(s) arrive together; creates are idempotent by the provider's natural key, so re-creating updates the existing row.

Platforms without a fire endpoint (Codex, Windsurf, Cline, OpenClaw, OpenCode, Continue) cannot be created here — they are scheduled on their own platform (typically a GitHub Actions schedule: cron running the platform's headless CLI with the trigger prompt).

Use cases

Use caseDescriptionExample
Register a Claude routineafter RemoteTrigger {action:"create"} returns trig_…, { "provider": "claude_trigger", "config": { "routineId": "trig_…" } } (tokenless is fine; add the fire token later).
Cold Cursor/Jules runrepo coordinates + both keys; Shipeasy launches the run and the PR opens via the provider's GitHub App.
Copilot cloud agentrepo + a Copilot-licensed user PAT with the "Agent tasks" permission.

Parameters

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

Response · 201

NameTypeDescription
idrequiredstringNewly assigned connector id.
Example · 201
{
  "id": "9b0e7c2a-1f3d-4a8e-bf21-0c6a2e5d7f10"
}

API Keys

GET/api/admin/keys

List API keys

Returns a single page of the project's API keys ordered by created_at desc, id desc, in the standard { data, next_cursor } envelope. Use the cursor query parameter to paginate.

Response fields are snake_case (created_at, revoked_at, created_by_email, last4). The raw token is never returned — only its last4 tail, so a held key can be matched against the masked row. Revoked keys are included (with a non-null revoked_at).

Use caseAudit which keys exist for a project — surface stale or never-expiring keys, or drive a CI check that asserts no client key is still active in prod after a rotation.

Parameters

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

Response · 200

NameTypeDescription
datarequiredarray<{ id: string; type: "server" | "client" | "admin" | "ops"; env: "dev" | "staging" | "prod"; created_at: string; revoked_at: string | null; expires_at: string | null; created_by_email: string | null; name: string | null; scopes: array<string> | null; last4: string | null }>
next_cursorrequiredstring | nullOpaque cursor for the next page, or `null` on the last page. Pass it back as the `cursor` query parameter.
Example · 200
{
  "data": [
    {
      "id": "3f2a9b1c-7d4e-4a8f-9c2b-1e5d6f7a8b90",
      "type": "server",
      "env": "prod",
      "created_at": "2026-05-09T16:01:22.000Z",
      "revoked_at": null,
      "expires_at": null,
      "created_by_email": "ana@example.com",
      "name": "production back-end",
      "scopes": [
        "gates:evaluate",
        "events:write"
      ],
      "last4": "a1b2"
    }
  ],
  "next_cursor": null
}
POST/api/admin/keys

Create an API key

Mints a new API key and returns the plaintext token once — it is stored hashed and can never be retrieved again, so capture it on creation.

Only type is required. env is required for server and client keys (the key is bound to one environment, which is the read-env isolation boundary); for admin and ops keys env is ignored and the key is pinned to prod. Expiry is fixed for some types: admin keys always get a 90-day expiry and ops keys a short sliding window, regardless of expiresInDays. Only server/client keys count toward the plan key limit.

Use cases

Use caseDescriptionExample
Back-end key{ "type": "server", "env": "prod" } for the production server SDK.
Public browser key{ "type": "client", "env": "prod", "name": "marketing site" } to embed in the browser SDK.
Scoped, expiring key{ "type": "server", "env": "staging", "scopes": ["gates:evaluate"], "expiresInDays": 30 } for a time-boxed integration.

Parameters

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

Body

NameTypeDescription
typerequired"server" | "client" | "admin" | "ops"Key kind to mint. `server` (back-end), `client` (public, browser), `admin` (CLI/devtools token), `ops` (restricted unattended-trigger credential). `admin`/`ops` keys don't count toward the plan key limit.
namestringOptional human label. Programmatic (API) mints that omit it get an auto-generated descriptive name; dashboard mints may leave it blank.
scopesarray<"experiments:read" | "gates:evaluate" | "events:write" | "configs:write" | "experiments:write" | "tickets:public_create">Optional permission strings recorded on the key. `tickets:public_create` is enforced: a client key needs it (plus the project's `allowPublicTickets` setting) to file a `pending_approval` bug via the public `POST /cli/report` endpoint. The rest are audit/display only.
expiresInDaysinteger | nullDays until the key expires (1–3650), or `null`/omitted for a key that never expires. Ignored for `admin` keys (fixed 90-day expiry) and `ops` keys (short sliding window).
envrequired"dev" | "staging" | "prod"Environment to bind the key to. Always required. For `server`/`client` keys it is the isolation boundary the worker reads from; `admin`/`ops` keys are env-agnostic and pinned to `prod` by the handler regardless of the value sent.

Response · 201

NameTypeDescription
idrequiredstringStable opaque key id (UUID). Use it to revoke the key.
typerequired"server" | "client" | "admin" | "ops"The kind of key that was minted.
envrequired"dev" | "staging" | "prod"Environment the key is bound to (`prod` for `admin`/`ops`).
keyrequiredstringThe plaintext API token (e.g. `sdk_server_…`). Returned once — store it now; it cannot be recovered.
expires_atrequiredstring | nullISO-8601 expiry, or `null` if the key never expires.
Example · 201
{
  "id": "3f2a9b1c-7d4e-4a8f-9c2b-1e5d6f7a8b90",
  "type": "server",
  "env": "prod",
  "key": "sdk_server_9c1f4f1f2c0c4a5fa1c2b6d3e7c8e3a1",
  "expires_at": null
}
POST/api/admin/keys/{id}/revoke

Revoke an API key

Revokes a key by id — stamps its revoked_at and deletes the hot-path KV entry so the token stops authenticating immediately. Takes no body.

Idempotent: revoking an already-revoked key is a no-op and returns the same { id, revoked: true }. Returns 404 if no such key exists in the project.

Use caseRotate a leaked or stale credential — mint the replacement, then revoke the old key.

Parameters

NameInTypeDescription
X-Project-IdrequiredheaderstringProject to scope this request to.
idrequiredpathstringStable opaque key id (UUID) returned by `create` / `list`.

Response · 200

NameTypeDescription
idrequiredstringId of the key that was revoked.
revokedrequiredtrue
Example · 200
{
  "id": "3f2a9b1c-7d4e-4a8f-9c2b-1e5d6f7a8b90",
  "revoked": true
}
Was this page helpful?
✎ Edit this page