{
  "openapi": "3.1.0",
  "info": {
    "title": "Splitstream API",
    "version": "0.1.0",
    "summary": "A/B testing and experimentation API with sticky bucketing, Bayesian credible intervals, and SRM detection.",
    "description": "Splitstream is the sibling to Pennant. Where Pennant decides *whether* a\nfeature ships, Splitstream decides *which variant* a user sees and\n*whether the variant moved the metric*. Define experiments in a\nworkspace, configure variants + audience + metrics, let SDKs call\n`/v1/assign` for sticky bucketing, and stream conversion events into\n`/v1/events`. The analysis worker computes Bayesian credible intervals\nevery 4 hours; the results endpoint returns the latest snapshot.\n\nThis spec is the source of truth. Laravel controllers conform to it; SDKs\nare generated from it. Run `npm run spectral` to validate locally.\n\nCalibrated decision-rule defaults (Phase 0):\n\n| Parameter | Default |\n|---|---|\n| posterior_threshold | 0.995 |\n| min_sample_per_variant | 20000 |\n| snapshot_cadence_minutes | 240 |\n\nPlan's draft defaults (0.95 / 1000 / 15min) delivered 66% empirical false-\npositive rate under continuous peeking. See `/docs/concepts/bayesian-inference`.\n",
    "contact": {
      "name": "Philip Rehberger",
      "url": "https://splitstream.philiprehberger.com"
    },
    "license": {
      "name": "MIT",
      "identifier": "MIT"
    }
  },
  "servers": [
    {
      "url": "https://api.splitstream.philiprehberger.com",
      "description": "Production"
    },
    {
      "url": "http://localhost:8000",
      "description": "Local development"
    }
  ],
  "security": [
    {
      "ApiKeyAuth": []
    }
  ],
  "tags": [
    {
      "name": "Health",
      "description": "Liveness check."
    },
    {
      "name": "Workspaces",
      "description": "Tenant boundary; one workspace per customer."
    },
    {
      "name": "Environments",
      "description": "Per-workspace environments (e.g. dev / staging / prod)."
    },
    {
      "name": "Experiments",
      "description": "Experiment definitions and lifecycle."
    },
    {
      "name": "Metrics",
      "description": "Reusable metric definitions referenced by experiments."
    },
    {
      "name": "Mutex Groups",
      "description": "Mutually exclusive experiment cohorts \u2014 a unit assigned to one experiment in the group cannot be assigned to another."
    },
    {
      "name": "Segments",
      "description": "Reusable audience filters (shared expression-tree format with Pennant)."
    },
    {
      "name": "Assignment",
      "description": "SDK-facing sticky bucketing."
    },
    {
      "name": "Events",
      "description": "Conversion and exposure event ingest."
    },
    {
      "name": "Results",
      "description": "Posterior + decision-rule snapshots per experiment."
    },
    {
      "name": "API Keys",
      "description": "Server and client keys for SDK authentication."
    },
    {
      "name": "Audit",
      "description": "Read-only mutation + peek-event history."
    }
  ],
  "paths": {
    "/v1/healthz": {
      "get": {
        "summary": "Liveness check",
        "description": "Returns 200 if the API is up. Does not require auth.",
        "operationId": "healthz",
        "tags": [
          "Health"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "Service is up.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Health"
                }
              }
            }
          }
        }
      }
    },
    "/v1/workspaces/current": {
      "get": {
        "summary": "Get the workspace the current API key belongs to",
        "operationId": "getCurrentWorkspace",
        "tags": [
          "Workspaces"
        ],
        "responses": {
          "200": {
            "description": "The workspace.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Workspace"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/v1/environments": {
      "get": {
        "summary": "List environments",
        "operationId": "listEnvironments",
        "tags": [
          "Environments"
        ],
        "responses": {
          "200": {
            "description": "Page of environments.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EnvironmentList"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      },
      "post": {
        "summary": "Create an environment",
        "operationId": "createEnvironment",
        "tags": [
          "Environments"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EnvironmentInput"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Environment created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Environment"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          }
        }
      }
    },
    "/v1/experiments": {
      "get": {
        "summary": "List experiments",
        "operationId": "listExperiments",
        "tags": [
          "Experiments"
        ],
        "parameters": [
          {
            "in": "query",
            "name": "status",
            "schema": {
              "type": "string",
              "enum": [
                "draft",
                "running",
                "stopped",
                "archived"
              ]
            }
          },
          {
            "in": "query",
            "name": "cursor",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Page of experiments.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ExperimentList"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      },
      "post": {
        "summary": "Create an experiment",
        "operationId": "createExperiment",
        "tags": [
          "Experiments"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ExperimentInput"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Experiment created (status `draft`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Experiment"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          }
        }
      }
    },
    "/v1/experiments/{id}": {
      "parameters": [
        {
          "in": "path",
          "name": "id",
          "required": true,
          "schema": {
            "type": "string"
          },
          "description": "Experiment ULID"
        }
      ],
      "get": {
        "summary": "Get an experiment",
        "operationId": "getExperiment",
        "tags": [
          "Experiments"
        ],
        "responses": {
          "200": {
            "description": "The experiment.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Experiment"
                }
              }
            }
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          }
        }
      },
      "patch": {
        "summary": "Update an experiment",
        "operationId": "updateExperiment",
        "tags": [
          "Experiments"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ExperimentInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Experiment"
                }
              }
            }
          },
          "422": {
            "$ref": "#/components/responses/ValidationError"
          }
        }
      },
      "delete": {
        "summary": "Archive an experiment",
        "operationId": "archiveExperiment",
        "tags": [
          "Experiments"
        ],
        "responses": {
          "204": {
            "description": "Archived (no body)."
          }
        }
      }
    },
    "/v1/experiments/{id}/start": {
      "parameters": [
        {
          "in": "path",
          "name": "id",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "post": {
        "summary": "Start an experiment (draft \u2192 running)",
        "operationId": "startExperiment",
        "tags": [
          "Experiments"
        ],
        "responses": {
          "200": {
            "description": "Started.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Experiment"
                }
              }
            }
          },
          "409": {
            "description": "Experiment not in `draft` status.",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          }
        }
      }
    },
    "/v1/experiments/{id}/stop": {
      "parameters": [
        {
          "in": "path",
          "name": "id",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "post": {
        "summary": "Stop an experiment",
        "operationId": "stopExperiment",
        "tags": [
          "Experiments"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/StopExperimentInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Stopped.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Experiment"
                }
              }
            }
          }
        }
      }
    },
    "/v1/experiments/{id}/results": {
      "parameters": [
        {
          "in": "path",
          "name": "id",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "get": {
        "summary": "Latest analysis snapshot",
        "description": "Returns the most recent snapshot computed by the analysis worker.\nSnapshots run every 4 hours by default; the production cadence is\ncontrolled by `snapshot_cadence_minutes` per experiment.\n\nOpening this endpoint via the admin UI logs a peek event; SDK access\nvia API key does not (the SDK reads this for SRM monitoring, not for\nthe decision).\n",
        "operationId": "getResults",
        "tags": [
          "Results"
        ],
        "responses": {
          "200": {
            "description": "Latest snapshot.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AnalysisSnapshot"
                }
              }
            }
          },
          "404": {
            "description": "No snapshots yet."
          }
        }
      }
    },
    "/v1/experiments/{id}/results/history": {
      "parameters": [
        {
          "in": "path",
          "name": "id",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "get": {
        "summary": "Snapshot history",
        "description": "Time series of snapshots \u2014 for the \"watch the credible interval tighten\" chart.",
        "operationId": "getResultsHistory",
        "tags": [
          "Results"
        ],
        "responses": {
          "200": {
            "description": "Snapshot time series.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AnalysisSnapshotHistory"
                }
              }
            }
          }
        }
      }
    },
    "/v1/metrics": {
      "get": {
        "summary": "List metric definitions",
        "operationId": "listMetrics",
        "tags": [
          "Metrics"
        ],
        "responses": {
          "200": {
            "description": "Page of metrics.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MetricList"
                }
              }
            }
          }
        }
      },
      "post": {
        "summary": "Create a metric definition",
        "operationId": "createMetric",
        "tags": [
          "Metrics"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MetricInput"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Metric"
                }
              }
            }
          }
        }
      }
    },
    "/v1/mutex-groups": {
      "get": {
        "summary": "List mutex groups",
        "operationId": "listMutexGroups",
        "tags": [
          "Mutex Groups"
        ],
        "responses": {
          "200": {
            "description": "Page of mutex groups.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MutexGroupList"
                }
              }
            }
          }
        }
      },
      "post": {
        "summary": "Create a mutex group",
        "operationId": "createMutexGroup",
        "tags": [
          "Mutex Groups"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/MutexGroupInput"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/MutexGroup"
                }
              }
            }
          }
        }
      }
    },
    "/v1/assign": {
      "post": {
        "summary": "Sticky bucket assignment for one experiment",
        "description": "Returns the variant assigned to `unit_id` for `experiment_key`.\nFirst call writes to the `assignments` table (sticky-forever); later\ncalls read the row.\n\nSide effect: the first call logs an exposure event for the variant.\nSubsequent calls do not double-log.\n\n**Latency target:** 20ms p99 cache-hit, 50ms p99 cache-miss. Endpoint\nruns on Laravel Octane (RoadRunner) \u2014 Apache mod_proxy carves out the\npath to `127.0.0.1:8000`.\n\n**Holdout responses:** if the experiment is in a mutex group and\n`unit_id` is already claimed by another experiment in the group, the\nresponse is `{ variant: null, reason: \"mutex_holdout\" }`.\n",
        "operationId": "assign",
        "tags": [
          "Assignment"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AssignInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Assignment.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Assignment"
                }
              }
            }
          },
          "404": {
            "description": "Experiment not found or not in `running` status."
          }
        }
      }
    },
    "/v1/assign/batch": {
      "post": {
        "summary": "Sticky bucket assignment across many experiments in one round-trip",
        "description": "Returns a map of `experiment_key \u2192 assignment`. Max 50 experiments per\nrequest.\n",
        "operationId": "assignBatch",
        "tags": [
          "Assignment"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AssignBatchInput"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Per-experiment assignments.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AssignBatchResult"
                }
              }
            }
          }
        }
      }
    },
    "/v1/assignments/{unit_id}": {
      "parameters": [
        {
          "in": "path",
          "name": "unit_id",
          "required": true,
          "schema": {
            "type": "string"
          },
          "description": "Bucketed unit identifier (1\u2013256 bytes UTF-8)"
        }
      ],
      "get": {
        "summary": "All assignments for a unit",
        "description": "SDK reconciliation endpoint \u2014 returns every assignment the unit currently has.",
        "operationId": "getAssignmentsForUnit",
        "tags": [
          "Assignment"
        ],
        "responses": {
          "200": {
            "description": "All assignments.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AssignmentList"
                }
              }
            }
          }
        }
      }
    },
    "/v1/assignments/{experiment}/{unit_id}": {
      "parameters": [
        {
          "in": "path",
          "name": "experiment",
          "required": true,
          "schema": {
            "type": "string"
          }
        },
        {
          "in": "path",
          "name": "unit_id",
          "required": true,
          "schema": {
            "type": "string"
          }
        }
      ],
      "delete": {
        "summary": "Force re-bucket (atomic, audit-logged)",
        "description": "Deletes the assignment row **and** sets `superseded_at = now()` on\nevery `exposure_events` row for the unit-experiment pair. The\nanalysis worker filters tombstoned rows. Use sparingly \u2014 breaks the\nexperiment's data for that unit.\n",
        "operationId": "forceRebucket",
        "tags": [
          "Assignment"
        ],
        "responses": {
          "204": {
            "description": "Rebucketed."
          }
        }
      }
    },
    "/v1/events": {
      "post": {
        "summary": "Track a conversion or arbitrary metric event",
        "description": "Synchronous endpoint, asynchronous persistence. Body is validated,\ndeduped via Redis SETNX on `client_event_id`, written to a Redis\nlist, and `202 Accepted` returned. The ingest worker drains the list\nevery second via `COPY ... FROM STDIN`.\n\n**Latency target:** 30ms p99 (Octane).\n\n**Late-event policy:** events with `occurred_at` 7\u201330 days old are\naccepted and counted into `late_event_count`; older than 30 days are\nrejected with `412 Precondition Failed`.\n",
        "operationId": "trackEvent",
        "tags": [
          "Events"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EventInput"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted (or idempotent replay).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EventAck"
                }
              }
            }
          },
          "412": {
            "description": "Event too late (occurred_at > 30 days ago).",
            "content": {
              "application/problem+json": {
                "schema": {
                  "$ref": "#/components/schemas/Problem"
                }
              }
            }
          },
          "429": {
            "description": "Backpressure \u2014 Redis buffer exceeded 100k entries.",
            "headers": {
              "Retry-After": {
                "schema": {
                  "type": "integer"
                },
                "description": "Seconds to wait"
              }
            }
          },
          "503": {
            "description": "Redis unreachable."
          }
        }
      }
    },
    "/v1/events/batch": {
      "post": {
        "summary": "Track up to 500 events in one round-trip",
        "operationId": "trackEventBatch",
        "tags": [
          "Events"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/EventBatchInput"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/EventBatchAck"
                }
              }
            }
          }
        }
      }
    },
    "/v1/exposures": {
      "post": {
        "summary": "Log an exposure (for SDKs that decouple assignment from exposure)",
        "description": "Server-side and mobile SDKs call this when the variant is actually\nshown to the user. Browser SDKs typically don't need it \u2014 the\n`/v1/assign` first-call already logged exposure.\n",
        "operationId": "logExposure",
        "tags": [
          "Events"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ExposureInput"
              }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Accepted."
          }
        }
      }
    },
    "/v1/api-keys": {
      "get": {
        "summary": "List API keys",
        "operationId": "listApiKeys",
        "tags": [
          "API Keys"
        ],
        "responses": {
          "200": {
            "description": "Page of keys (secrets redacted; only `last_four` returned).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiKeyList"
                }
              }
            }
          }
        }
      },
      "post": {
        "summary": "Issue an API key",
        "description": "Returns the full key value exactly once \u2014 the server stores\n`HMAC-SHA256(key, APP_KEY_PEPPER)` for fast lookup and an Argon2id\nhash as defense-in-depth.\n",
        "operationId": "createApiKey",
        "tags": [
          "API Keys"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ApiKeyInput"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Key created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ApiKeyWithSecret"
                }
              }
            }
          }
        }
      }
    },
    "/v1/audit": {
      "get": {
        "summary": "Audit log",
        "operationId": "listAudit",
        "tags": [
          "Audit"
        ],
        "responses": {
          "200": {
            "description": "Audit entries.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/AuditList"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "ApiKeyAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "ss_{srv|clt}_{env}_{secret}"
      }
    },
    "responses": {
      "Unauthorized": {
        "description": "Missing or invalid API key.",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "NotFound": {
        "description": "Resource not found.",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      },
      "ValidationError": {
        "description": "Body validation failed.",
        "content": {
          "application/problem+json": {
            "schema": {
              "$ref": "#/components/schemas/Problem"
            }
          }
        }
      }
    },
    "schemas": {
      "Health": {
        "type": "object",
        "properties": {
          "status": {
            "type": "string",
            "const": "ok"
          },
          "version": {
            "type": "string"
          }
        },
        "required": [
          "status"
        ]
      },
      "Problem": {
        "type": "object",
        "description": "RFC 7807 problem details.",
        "properties": {
          "type": {
            "type": "string",
            "format": "uri"
          },
          "title": {
            "type": "string"
          },
          "status": {
            "type": "integer"
          },
          "detail": {
            "type": "string"
          },
          "instance": {
            "type": "string"
          }
        },
        "required": [
          "type",
          "title",
          "status"
        ]
      },
      "Workspace": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "description": "ULID"
          },
          "name": {
            "type": "string"
          },
          "slug": {
            "type": "string"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "id",
          "name",
          "slug",
          "created_at"
        ]
      },
      "Environment": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "workspace_id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "slug": {
            "type": "string"
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "id",
          "workspace_id",
          "name",
          "slug",
          "created_at"
        ]
      },
      "EnvironmentInput": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string"
          },
          "slug": {
            "type": "string"
          }
        },
        "required": [
          "name",
          "slug"
        ]
      },
      "EnvironmentList": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Environment"
            }
          },
          "next_cursor": {
            "type": "string",
            "nullable": true
          }
        },
        "required": [
          "data"
        ]
      },
      "Experiment": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "workspace_id": {
            "type": "string"
          },
          "environment_id": {
            "type": "string"
          },
          "key": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "hypothesis": {
            "type": "string"
          },
          "unit_type": {
            "type": "string",
            "enum": [
              "user",
              "account",
              "session",
              "device",
              "custom_attribute"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "draft",
              "running",
              "stopped",
              "archived"
            ]
          },
          "variants": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Variant"
            }
          },
          "audience": {
            "$ref": "#/components/schemas/AudienceExpression"
          },
          "decision_rule": {
            "$ref": "#/components/schemas/DecisionRule"
          },
          "primary_metric_id": {
            "type": "string",
            "nullable": true
          },
          "guardrail_metric_ids": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "mutex_group_id": {
            "type": "string",
            "nullable": true
          },
          "started_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "stopped_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "stop_reason": {
            "type": "string",
            "enum": [
              "won",
              "lost",
              "inconclusive",
              "bug",
              "business"
            ],
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "id",
          "workspace_id",
          "environment_id",
          "key",
          "name",
          "hypothesis",
          "unit_type",
          "status",
          "variants",
          "decision_rule",
          "created_at"
        ]
      },
      "ExperimentInput": {
        "type": "object",
        "properties": {
          "environment_id": {
            "type": "string"
          },
          "key": {
            "type": "string",
            "pattern": "^[a-z0-9._-]{1,128}$"
          },
          "name": {
            "type": "string"
          },
          "hypothesis": {
            "type": "string",
            "minLength": 1
          },
          "unit_type": {
            "type": "string",
            "enum": [
              "user",
              "account",
              "session",
              "device",
              "custom_attribute"
            ]
          },
          "variants": {
            "type": "array",
            "minItems": 2,
            "items": {
              "$ref": "#/components/schemas/VariantInput"
            }
          },
          "audience": {
            "$ref": "#/components/schemas/AudienceExpression"
          },
          "decision_rule": {
            "$ref": "#/components/schemas/DecisionRule"
          },
          "primary_metric_id": {
            "type": "string",
            "nullable": true
          },
          "guardrail_metric_ids": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "mutex_group_id": {
            "type": "string",
            "nullable": true
          }
        },
        "required": [
          "environment_id",
          "key",
          "name",
          "hypothesis",
          "unit_type",
          "variants"
        ]
      },
      "ExperimentList": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Experiment"
            }
          },
          "next_cursor": {
            "type": "string",
            "nullable": true
          }
        },
        "required": [
          "data"
        ]
      },
      "StopExperimentInput": {
        "type": "object",
        "properties": {
          "reason": {
            "type": "string",
            "enum": [
              "won",
              "lost",
              "inconclusive",
              "bug",
              "business"
            ]
          },
          "outcome_notes": {
            "type": "string"
          }
        },
        "required": [
          "reason"
        ]
      },
      "Variant": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "key": {
            "type": "string"
          },
          "weight": {
            "type": "integer",
            "minimum": 0,
            "maximum": 100
          },
          "description": {
            "type": "string"
          },
          "is_control": {
            "type": "boolean"
          }
        },
        "required": [
          "id",
          "key",
          "weight",
          "is_control"
        ]
      },
      "VariantInput": {
        "type": "object",
        "properties": {
          "key": {
            "type": "string",
            "pattern": "^[a-z0-9._-]{1,64}$"
          },
          "weight": {
            "type": "integer",
            "minimum": 0,
            "maximum": 100
          },
          "description": {
            "type": "string"
          },
          "is_control": {
            "type": "boolean",
            "default": false
          }
        },
        "required": [
          "key",
          "weight"
        ]
      },
      "AudienceExpression": {
        "type": "object",
        "description": "JSON expression tree \u2014 reuses the Pennant rule format. Either\n`{operator, operands}` for compound, or `{attribute, op, value}` for\nleaf.\n",
        "nullable": true
      },
      "DecisionRule": {
        "type": "object",
        "properties": {
          "method": {
            "type": "string",
            "enum": [
              "bayesian.posterior_threshold",
              "bayesian.always_valid_evalue",
              "frequentist.sequential_msprt"
            ],
            "default": "bayesian.posterior_threshold"
          },
          "posterior_threshold": {
            "type": "number",
            "minimum": 0.5,
            "maximum": 0.9999,
            "default": 0.995
          },
          "minimum_detectable_effect": {
            "type": "number",
            "default": 0.02
          },
          "min_sample_per_variant": {
            "type": "integer",
            "minimum": 1,
            "default": 20000
          },
          "max_duration_days": {
            "type": "integer",
            "default": 28
          },
          "snapshot_cadence_minutes": {
            "type": "integer",
            "default": 240
          }
        }
      },
      "Metric": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "workspace_id": {
            "type": "string"
          },
          "key": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "event_key": {
            "type": "string"
          },
          "kind": {
            "type": "string",
            "enum": [
              "binary",
              "count",
              "revenue",
              "duration"
            ]
          },
          "aggregation": {
            "type": "string",
            "enum": [
              "sum",
              "mean",
              "count"
            ]
          },
          "unit_dedup_window": {
            "type": "string",
            "description": "ISO-8601 duration; default P7D"
          },
          "property_path": {
            "type": "string",
            "nullable": true,
            "description": "Strict JSON-Path subset (dot-chain, bracketed string keys) \u2014 no recursive descent, no filters."
          }
        },
        "required": [
          "id",
          "key",
          "name",
          "event_key",
          "kind"
        ]
      },
      "MetricInput": {
        "type": "object",
        "properties": {
          "key": {
            "type": "string",
            "pattern": "^[a-z0-9._-]{1,128}$"
          },
          "name": {
            "type": "string"
          },
          "event_key": {
            "type": "string"
          },
          "kind": {
            "type": "string",
            "enum": [
              "binary",
              "count",
              "revenue",
              "duration"
            ]
          },
          "aggregation": {
            "type": "string",
            "enum": [
              "sum",
              "mean",
              "count"
            ],
            "default": "count"
          },
          "unit_dedup_window": {
            "type": "string",
            "default": "P7D"
          },
          "property_path": {
            "type": "string",
            "nullable": true
          }
        },
        "required": [
          "key",
          "name",
          "event_key",
          "kind"
        ]
      },
      "MetricList": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Metric"
            }
          },
          "next_cursor": {
            "type": "string",
            "nullable": true
          }
        },
        "required": [
          "data"
        ]
      },
      "MutexGroup": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "key": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "experiment_count": {
            "type": "integer"
          }
        },
        "required": [
          "id",
          "key",
          "name"
        ]
      },
      "MutexGroupInput": {
        "type": "object",
        "properties": {
          "key": {
            "type": "string"
          },
          "name": {
            "type": "string"
          }
        },
        "required": [
          "key",
          "name"
        ]
      },
      "MutexGroupList": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/MutexGroup"
            }
          },
          "next_cursor": {
            "type": "string",
            "nullable": true
          }
        },
        "required": [
          "data"
        ]
      },
      "AssignInput": {
        "type": "object",
        "properties": {
          "experiment_key": {
            "type": "string"
          },
          "unit_id": {
            "type": "string",
            "minLength": 1,
            "maxLength": 256
          },
          "context": {
            "type": "object",
            "additionalProperties": true,
            "description": "User attributes for audience targeting; \u22644KB JSON."
          }
        },
        "required": [
          "experiment_key",
          "unit_id"
        ]
      },
      "Assignment": {
        "type": "object",
        "properties": {
          "experiment_key": {
            "type": "string"
          },
          "unit_id": {
            "type": "string"
          },
          "variant": {
            "type": "string",
            "nullable": true,
            "description": "null when mutex_holdout"
          },
          "assignment_id": {
            "type": "string"
          },
          "reason": {
            "type": "string",
            "enum": [
              "bucketed",
              "forced",
              "mutex_holdout",
              "audience_excluded"
            ]
          },
          "exposure_logged_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          }
        },
        "required": [
          "experiment_key",
          "unit_id",
          "variant",
          "reason"
        ]
      },
      "AssignBatchInput": {
        "type": "object",
        "properties": {
          "experiments": {
            "type": "array",
            "minItems": 1,
            "maxItems": 50,
            "items": {
              "type": "string"
            }
          },
          "unit_id": {
            "type": "string"
          },
          "context": {
            "type": "object",
            "additionalProperties": true
          }
        },
        "required": [
          "experiments",
          "unit_id"
        ]
      },
      "AssignBatchResult": {
        "type": "object",
        "properties": {
          "assignments": {
            "type": "object",
            "additionalProperties": {
              "$ref": "#/components/schemas/Assignment"
            }
          }
        },
        "required": [
          "assignments"
        ]
      },
      "AssignmentList": {
        "type": "object",
        "properties": {
          "unit_id": {
            "type": "string"
          },
          "assignments": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/Assignment"
            }
          }
        },
        "required": [
          "unit_id",
          "assignments"
        ]
      },
      "EventInput": {
        "type": "object",
        "properties": {
          "event_key": {
            "type": "string",
            "pattern": "^[a-z0-9._-]{1,128}$"
          },
          "unit_id": {
            "type": "string",
            "minLength": 1,
            "maxLength": 256
          },
          "properties": {
            "type": "object",
            "additionalProperties": true,
            "description": "\u226416KB JSON, max nesting depth 8."
          },
          "occurred_at": {
            "type": "string",
            "format": "date-time"
          },
          "client_event_id": {
            "type": "string",
            "maxLength": 128
          }
        },
        "required": [
          "event_key",
          "unit_id"
        ]
      },
      "EventAck": {
        "type": "object",
        "properties": {
          "accepted": {
            "type": "boolean"
          },
          "idempotent_replay": {
            "type": "boolean",
            "description": "true if client_event_id was already seen within 30d"
          }
        },
        "required": [
          "accepted"
        ]
      },
      "EventBatchInput": {
        "type": "object",
        "properties": {
          "events": {
            "type": "array",
            "minItems": 1,
            "maxItems": 500,
            "items": {
              "$ref": "#/components/schemas/EventInput"
            }
          }
        },
        "required": [
          "events"
        ]
      },
      "EventBatchAck": {
        "type": "object",
        "properties": {
          "accepted_count": {
            "type": "integer"
          },
          "rejected": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "index": {
                  "type": "integer"
                },
                "reason": {
                  "type": "string"
                }
              }
            }
          }
        },
        "required": [
          "accepted_count"
        ]
      },
      "ExposureInput": {
        "type": "object",
        "properties": {
          "experiment_key": {
            "type": "string"
          },
          "unit_id": {
            "type": "string"
          },
          "variant": {
            "type": "string"
          }
        },
        "required": [
          "experiment_key",
          "unit_id",
          "variant"
        ]
      },
      "AnalysisSnapshot": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "experiment_id": {
            "type": "string"
          },
          "computed_at": {
            "type": "string",
            "format": "date-time"
          },
          "per_variant": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/VariantPosterior"
            }
          },
          "srm_chi_squared_p": {
            "type": "number",
            "nullable": true
          },
          "srm_warning": {
            "type": "boolean"
          },
          "decision_rule_satisfied": {
            "type": "boolean"
          },
          "peek_count_at_computation": {
            "type": "integer"
          },
          "late_event_count": {
            "type": "integer"
          },
          "weights_changed_since_start": {
            "type": "boolean"
          }
        },
        "required": [
          "id",
          "experiment_id",
          "computed_at",
          "per_variant",
          "decision_rule_satisfied"
        ]
      },
      "AnalysisSnapshotHistory": {
        "type": "object",
        "properties": {
          "experiment_id": {
            "type": "string"
          },
          "snapshots": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AnalysisSnapshot"
            }
          }
        },
        "required": [
          "experiment_id",
          "snapshots"
        ]
      },
      "VariantPosterior": {
        "type": "object",
        "properties": {
          "variant_key": {
            "type": "string"
          },
          "is_control": {
            "type": "boolean"
          },
          "sample_size": {
            "type": "integer"
          },
          "conversions": {
            "type": "integer"
          },
          "observed_rate": {
            "type": "number"
          },
          "posterior": {
            "type": "object",
            "properties": {
              "mean": {
                "type": "number"
              },
              "credible_interval_95": {
                "type": "array",
                "minItems": 2,
                "maxItems": 2,
                "items": {
                  "type": "number"
                }
              }
            },
            "required": [
              "mean",
              "credible_interval_95"
            ]
          },
          "prob_best": {
            "type": "number",
            "description": "P(this variant is best)"
          },
          "expected_loss_if_stop_now": {
            "type": "number",
            "description": "Expected lift forgone if we stop now and this variant turns out to be inferior"
          }
        },
        "required": [
          "variant_key",
          "sample_size",
          "conversions",
          "posterior"
        ]
      },
      "ApiKey": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "workspace_id": {
            "type": "string"
          },
          "environment_id": {
            "type": "string",
            "nullable": true
          },
          "name": {
            "type": "string"
          },
          "kind": {
            "type": "string",
            "enum": [
              "server",
              "client"
            ]
          },
          "prefix": {
            "type": "string",
            "description": "e.g. ss_srv_live"
          },
          "last_four": {
            "type": "string"
          },
          "last_used_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "id",
          "kind",
          "prefix",
          "last_four",
          "created_at"
        ]
      },
      "ApiKeyInput": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string"
          },
          "kind": {
            "type": "string",
            "enum": [
              "server",
              "client"
            ]
          },
          "environment_id": {
            "type": "string",
            "nullable": true
          }
        },
        "required": [
          "name",
          "kind"
        ]
      },
      "ApiKeyWithSecret": {
        "allOf": [
          {
            "$ref": "#/components/schemas/ApiKey"
          },
          {
            "type": "object",
            "properties": {
              "key": {
                "type": "string",
                "description": "Full key \u2014 shown once at creation only."
              }
            },
            "required": [
              "key"
            ]
          }
        ]
      },
      "ApiKeyList": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ApiKey"
            }
          },
          "next_cursor": {
            "type": "string",
            "nullable": true
          }
        },
        "required": [
          "data"
        ]
      },
      "AuditEntry": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "actor_id": {
            "type": "string",
            "nullable": true
          },
          "action": {
            "type": "string"
          },
          "target_type": {
            "type": "string"
          },
          "target_id": {
            "type": "string"
          },
          "payload": {
            "type": "object",
            "additionalProperties": true
          },
          "created_at": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "id",
          "action",
          "target_type",
          "target_id",
          "created_at"
        ]
      },
      "AuditList": {
        "type": "object",
        "properties": {
          "data": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/AuditEntry"
            }
          },
          "next_cursor": {
            "type": "string",
            "nullable": true
          }
        },
        "required": [
          "data"
        ]
      }
    }
  }
}