| Server IP : 35.80.110.71 / Your IP : 216.73.216.221 Web Server : Apache/2.4.58 (Ubuntu) System : Linux ip-172-31-21-44 6.17.0-1019-aws #19~24.04.1-Ubuntu SMP Tue Jun 23 18:53:06 UTC 2026 x86_64 User : ubuntu ( 1000) PHP Version : 8.3.31 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : OFF Directory : /var/www/splitstream-web/public/ |
Upload File : |
{
"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"
]
}
}
}
}