Experiments
Record the product experiments an agent runs on a project, and keep their status and result current.
/v1/projects/:projectId/experiments- Auth
- Bearer
- Scope
- experiments:write
/v1/projects/:projectId/experiments- Auth
- Bearer
- Scope
- experiments:read
/v1/projects/:projectId/experiments/:experimentId- Auth
- Bearer
- Scope
- experiments:read
/v1/projects/:projectId/experiments/:experimentId- Auth
- Bearer
- Scope
- experiments:write
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
GET /v1/projects/:projectId/experiments returns the project's experiments, newest first.
statusstringoptionalOnly experiments in this status: planned, running, paused, reading, decided or stopped.cursorstringoptionalOpaque cursor from a prior response's nextCursor; omit for the first page. Up to 512 characters.limitintegeroptionalExperiments 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
keystringrequiredYour stable handle, unique per project. 1 to 80 lowercase letters, digits, dots, dashes or underscores.titlestringrequiredShort name shown in MMA.hypothesisstringoptionalUp to 4,000 characters.primaryMetricstringoptionalThe metric the decision rule reads.decisionRulestringoptionalHow the result is decided.decisionRuleDetailstringoptionalThe full decision rule and its dated amendments, up to 10,000 characters. decisionRule holds the short rule.baselineValuenumberoptionalThe primary metric's value before the test, in its own unit (a rate as a proportion, 0.18 for 18%).baselineQuerystringoptionalThe query or report that produced baselineValue, up to 4,000 characters.baselineDatestringoptionalThe day baselineValue was read, YYYY-MM-DD.mdenumberoptionalThe minimum detectable effect, absolute, in the primary metric's unit (0.03 is 3 points on a rate).alphanumberoptionalThe two-sided significance level, between 0 and 1 (for example 0.05).powernumberoptionalThe planned statistical power, between 0 and 1 (for example 0.8).nPerArmintegeroptionalThe planned units per arm, a positive integer.primarySourcestringoptionalWhere the primary metric is read, for example MMA or PostHog.analysisMethodstringoptionalThe test, and the multiplicity control across arms or metrics.sequentialPlanstringoptionalA pre-declared sequential plan with its looks and boundaries. Null means one read at the planned n.recheckMethodstringoptionalThe independent re-check planned before a decision.guardrailsarrayoptionalUp to 20 `{ metric, rule }` objects.armsarrayoptionalUp 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.statusstringoptional`planned` (default), `running`, `paused`, `reading`, `decided` or `stopped`.startedAtstringoptionalISO-8601 UTC. Set to now when the status first becomes `running` and you omit it.readAtstringoptionalThe planned read date, ISO-8601 UTC.decidedAtstringoptionalISO-8601 UTC. Set to now when the status first becomes `decided` and you omit it.resultobjectoptional`{ winner, effect, ciLow, ciHigh, nPerArm, note }`, each nullable.ownerLabelstringoptionalWho runs it, e.g. the agent or session name.sourceUrlstringoptionalA 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.
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"
}'{
"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"
}
}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:
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
| 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. |