# Experiments (/docs/api/reference/command-center/experiments)



<Endpoint method="POST" path="/v1/projects/:projectId/experiments" auth="Bearer" scope="experiments:write" phase="1" />

<Endpoint method="GET" path="/v1/projects/:projectId/experiments" auth="Bearer" scope="experiments:read" phase="1" />

<Endpoint method="GET" path="/v1/projects/:projectId/experiments/:experimentId" auth="Bearer" scope="experiments:read" phase="1" />

<Endpoint method="PATCH" path="/v1/projects/:projectId/experiments/:experimentId" auth="Bearer" scope="experiments:write" phase="1" />

An experiment is a record of a product test on one project: the hypothesis, the arms and their traffic split, the metric and rule that decide it, the planned read date and, once read, the result. Studio members see every running experiment and its next read date in MMA.

Layers stores the record. Your agent runs the test and reports its state.

## List experiments [#list-experiments]

`GET /v1/projects/:projectId/experiments` returns the project's experiments, newest first.

<Parameters
  title="Query"
  rows="[
  { name: 'status', type: 'string', description: 'Only experiments in this status: planned, running, paused, reading, decided or stopped.' },
  { name: 'cursor', type: 'string', description: &#x22;Opaque cursor from a prior response's nextCursor; omit for the first page. Up to 512 characters.&#x22; },
  { name: 'limit', type: 'integer', description: 'Experiments per page, 1 to 50, default 20.' },
]"
/>

The response is `{ items, nextCursor }`. Each item is an experiment with the fields shown below. Send `nextCursor` as `cursor` to read the next page; `nextCursor` is `null` on the last page.

## Record an experiment [#record-an-experiment]

<Parameters
  title="Body"
  rows="[
  { name: 'key', type: 'string', required: true, description: 'Your stable handle, unique per project. 1 to 80 lowercase letters, digits, dots, dashes or underscores.' },
  { name: 'title', type: 'string', required: true, description: 'Short name shown in MMA.' },
  { name: 'hypothesis', type: 'string', description: 'Up to 4,000 characters.' },
  { name: 'primaryMetric', type: 'string', description: 'The metric the decision rule reads.' },
  { name: 'decisionRule', type: 'string', description: 'How the result is decided.' },
  { name: 'decisionRuleDetail', type: 'string', description: 'The full decision rule and its dated amendments, up to 10,000 characters. decisionRule holds the short rule.' },
  { name: 'baselineValue', type: 'number', description: &#x22;The primary metric's value before the test, in its own unit (a rate as a proportion, 0.18 for 18%).&#x22; },
  { name: 'baselineQuery', type: 'string', description: 'The query or report that produced baselineValue, up to 4,000 characters.' },
  { name: 'baselineDate', type: 'string', description: 'The day baselineValue was read, YYYY-MM-DD.' },
  { name: 'mde', type: 'number', description: &#x22;The minimum detectable effect, absolute, in the primary metric's unit (0.03 is 3 points on a rate).&#x22; },
  { name: 'alpha', type: 'number', description: 'The two-sided significance level, between 0 and 1 (for example 0.05).' },
  { name: 'power', type: 'number', description: 'The planned statistical power, between 0 and 1 (for example 0.8).' },
  { name: 'nPerArm', type: 'integer', description: 'The planned units per arm, a positive integer.' },
  { name: 'primarySource', type: 'string', description: 'Where the primary metric is read, for example MMA or PostHog.' },
  { name: 'analysisMethod', type: 'string', description: 'The test, and the multiplicity control across arms or metrics.' },
  { name: 'sequentialPlan', type: 'string', description: 'A pre-declared sequential plan with its looks and boundaries. Null means one read at the planned n.' },
  { name: 'recheckMethod', type: 'string', description: 'The independent re-check planned before a decision.' },
  { name: 'guardrails', type: 'array', description: 'Up to 20 `{ metric, rule }` objects.' },
  { name: 'arms', type: 'array', description: 'Up to 20 `{ name, allocation }` objects. Names are unique; allocations are 0 to 1 and sum to at most 1. Each arm can also say how MMA reads it: `match`, up to 3 `{ property, value }` SDK event property values its users carry (all must hold; a dotted property such as `ab_test.variant` reads a nested key); `landingUrls`, up to 5 page addresses its web visitors land on, without a query string; `exposureEvent`, the one event that counts as exposure; and `campaignIds` and `adSetIds`, the ads whose spend counts toward the arm. For a before and after test of ad structures, an arm takes `installNetworks` (installs Layers proves to those networks join it), `includeCountries` and `excludeCountries` (ISO alpha-2 codes: only installs whose first day of events comes from an included country join the arm, then excluded countries are left out, so two arms on one network that run at the same time split by country), and its own `windowStart` and `windowEnd` (ISO-8601): only installs, conversions and spend inside the window count, and arms with a window are left out of the allocation sum. All arms together fit in 20,000 characters. Sending `arms` replaces every arm.' },
  { name: 'status', type: 'string', description: '`planned` (default), `running`, `paused`, `reading`, `decided` or `stopped`.' },
  { name: 'startedAt', type: 'string', description: 'ISO-8601 UTC. Set to now when the status first becomes `running` and you omit it.' },
  { name: 'readAt', type: 'string', description: 'The planned read date, ISO-8601 UTC.' },
  { name: 'decidedAt', type: 'string', description: 'ISO-8601 UTC. Set to now when the status first becomes `decided` and you omit it.' },
  { name: 'result', type: 'object', description: '`{ winner, effect, ciLow, ciHigh, nPerArm, note }`, each nullable.' },
  { name: 'ownerLabel', type: 'string', description: 'Who runs it, e.g. the agent or session name.' },
  { name: 'sourceUrl', type: 'string', description: 'A link to the test configuration or analysis.' },
]"
/>

Send an `Idempotency-Key` header. A second create with a `key` the project already has returns `409 CONFLICT` with the existing id in `details.experimentId`; update that one instead.

```bash
curl -X POST "https://api.layers.com/v1/projects/prj_254a4ce1-f4ca-42b1-9e36-17ca45ef3d39/experiments" \
  -H "Authorization: Bearer lp_..." \
  -H "Idempotency-Key: paywall-annual-first-create" \
  -H "Content-Type: application/json" \
  -d '{
    "key": "paywall-annual-first",
    "title": "Annual plan first on the paywall",
    "primaryMetric": "d7_revenue_per_install",
    "decisionRule": "Ship B if the 95% interval on D7 revenue per install is above zero.",
    "decisionRuleDetail": "2026-09-25: Read once at 2,700 installs per arm. Ship B if the 95% interval on D7 revenue per install is above zero and the guardrails hold.",
    "baselineValue": 0.18,
    "baselineQuery": "MMA D7 revenue per install for installs from 2026-09-11 through 2026-09-17.",
    "baselineDate": "2026-09-25",
    "mde": 0.03,
    "alpha": 0.05,
    "power": 0.8,
    "nPerArm": 2700,
    "primarySource": "MMA",
    "analysisMethod": "Welch two-sample t-test on D7 revenue per install; one primary metric and two arms.",
    "sequentialPlan": null,
    "recheckMethod": "Recompute each arm from the exported install and revenue report before deciding.",
    "arms": [{ "name": "A", "allocation": 0.5 }, { "name": "B", "allocation": 0.5 }],
    "status": "running",
    "readAt": "2026-10-10T00:00:00Z",
    "ownerLabel": "catch-master-growth-agent"
  }'
```

<Response status="201" description="Created">
  ```json
  {
    "experiment": {
      "id": "exp_4c0f2b1e-7d3a-4e8b-9a61-2f5d8c3b7e10",
      "projectId": "prj_254a4ce1-f4ca-42b1-9e36-17ca45ef3d39",
      "key": "paywall-annual-first",
      "title": "Annual plan first on the paywall",
      "hypothesis": null,
      "primaryMetric": "d7_revenue_per_install",
      "decisionRule": "Ship B if the 95% interval on D7 revenue per install is above zero.",
      "decisionRuleDetail": "2026-09-25: Read once at 2,700 installs per arm. Ship B if the 95% interval on D7 revenue per install is above zero and the guardrails hold.",
      "baselineValue": 0.18,
      "baselineQuery": "MMA D7 revenue per install for installs from 2026-09-11 through 2026-09-17.",
      "baselineDate": "2026-09-25",
      "mde": 0.03,
      "alpha": 0.05,
      "power": 0.8,
      "nPerArm": 2700,
      "primarySource": "MMA",
      "analysisMethod": "Welch two-sample t-test on D7 revenue per install; one primary metric and two arms.",
      "sequentialPlan": null,
      "recheckMethod": "Recompute each arm from the exported install and revenue report before deciding.",
      "guardrails": [],
      "arms": [{ "name": "A", "allocation": 0.5 }, { "name": "B", "allocation": 0.5 }],
      "status": "running",
      "startedAt": "2026-09-26T14:02:11Z",
      "readAt": "2026-10-10T00:00:00Z",
      "decidedAt": null,
      "result": null,
      "ownerLabel": "catch-master-growth-agent",
      "sourceUrl": null,
      "createdAt": "2026-09-26T14:02:11Z",
      "updatedAt": "2026-09-26T14:02:11Z"
    }
  }
  ```
</Response>

## Update an experiment [#update-an-experiment]

`PATCH` takes any body field except `key`. Leaving a field out keeps its stored value; sending `null` clears a nullable field. Record the read by moving the status to `decided` with a `result`:

```bash
curl -X PATCH "https://api.layers.com/v1/projects/prj_254a4ce1-f4ca-42b1-9e36-17ca45ef3d39/experiments/exp_4c0f2b1e-7d3a-4e8b-9a61-2f5d8c3b7e10" \
  -H "Authorization: Bearer lp_..." \
  -H "Idempotency-Key: paywall-annual-first-decided" \
  -H "Content-Type: application/json" \
  -d '{ "status": "decided", "result": { "winner": "B", "effect": 0.12, "ciLow": 0.03, "ciHigh": 0.21, "nPerArm": 4120, "note": "Ship B." } }'
```

| `status`  | Meaning                                                |
| --------- | ------------------------------------------------------ |
| `planned` | Designed, not started.                                 |
| `running` | Collecting data.                                       |
| `paused`  | Enrollment is halted for now; set `running` to resume. |
| `reading` | Collection ended; the result is being read.            |
| `decided` | A result is recorded.                                  |
| `stopped` | Ended without a decision.                              |

## Errors [#errors]

| Status | Code         | When                                                                                                 |
| ------ | ------------ | ---------------------------------------------------------------------------------------------------- |
| `404`  | `NOT_FOUND`  | The project or experiment is not in your organization, or the experiment belongs to another project. |
| `409`  | `CONFLICT`   | The project already has an experiment with this `key`.                                               |
| `422`  | `VALIDATION` | A malformed field, an extra field, an empty `PATCH` body, or a malformed experiment id.              |
