Docs
APIAPI referenceCommand center

Experiments

Record the product experiments an agent runs on a project, and keep their status and result current.

View as Markdown
POST/v1/projects/:projectId/experiments
Phase 1stable
Auth
Bearer
Scope
experiments:write
GET/v1/projects/:projectId/experiments
Phase 1stable
Auth
Bearer
Scope
experiments:read
GET/v1/projects/:projectId/experiments/:experimentId
Phase 1stable
Auth
Bearer
Scope
experiments:read
PATCH/v1/projects/:projectId/experiments/:experimentId
Phase 1stable
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.

Query
  • status
    stringoptional
    Only experiments in this status: planned, running, paused, reading, decided or stopped.
  • cursor
    stringoptional
    Opaque cursor from a prior response's nextCursor; omit for the first page. Up to 512 characters.
  • limit
    integeroptional
    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

Body
  • key
    stringrequired
    Your stable handle, unique per project. 1 to 80 lowercase letters, digits, dots, dashes or underscores.
  • title
    stringrequired
    Short name shown in MMA.
  • hypothesis
    stringoptional
    Up to 4,000 characters.
  • primaryMetric
    stringoptional
    The metric the decision rule reads.
  • decisionRule
    stringoptional
    How the result is decided.
  • decisionRuleDetail
    stringoptional
    The full decision rule and its dated amendments, up to 10,000 characters. decisionRule holds the short rule.
  • baselineValue
    numberoptional
    The primary metric's value before the test, in its own unit (a rate as a proportion, 0.18 for 18%).
  • baselineQuery
    stringoptional
    The query or report that produced baselineValue, up to 4,000 characters.
  • baselineDate
    stringoptional
    The day baselineValue was read, YYYY-MM-DD.
  • mde
    numberoptional
    The minimum detectable effect, absolute, in the primary metric's unit (0.03 is 3 points on a rate).
  • alpha
    numberoptional
    The two-sided significance level, between 0 and 1 (for example 0.05).
  • power
    numberoptional
    The planned statistical power, between 0 and 1 (for example 0.8).
  • nPerArm
    integeroptional
    The planned units per arm, a positive integer.
  • primarySource
    stringoptional
    Where the primary metric is read, for example MMA or PostHog.
  • analysisMethod
    stringoptional
    The test, and the multiplicity control across arms or metrics.
  • sequentialPlan
    stringoptional
    A pre-declared sequential plan with its looks and boundaries. Null means one read at the planned n.
  • recheckMethod
    stringoptional
    The independent re-check planned before a decision.
  • guardrails
    arrayoptional
    Up to 20 `{ metric, rule }` objects.
  • arms
    arrayoptional
    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.
  • status
    stringoptional
    `planned` (default), `running`, `paused`, `reading`, `decided` or `stopped`.
  • startedAt
    stringoptional
    ISO-8601 UTC. Set to now when the status first becomes `running` and you omit it.
  • readAt
    stringoptional
    The planned read date, ISO-8601 UTC.
  • decidedAt
    stringoptional
    ISO-8601 UTC. Set to now when the status first becomes `decided` and you omit it.
  • result
    objectoptional
    `{ winner, effect, ciLow, ciHigh, nPerArm, note }`, each nullable.
  • ownerLabel
    stringoptional
    Who runs it, e.g. the agent or session name.
  • sourceUrl
    stringoptional
    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.

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"
  }'
201Created
{
  "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." } }'
statusMeaning
plannedDesigned, not started.
runningCollecting data.
pausedEnrollment is halted for now; set running to resume.
readingCollection ended; the result is being read.
decidedA result is recorded.
stoppedEnded without a decision.

Errors

StatusCodeWhen
404NOT_FOUNDThe project or experiment is not in your organization, or the experiment belongs to another project.
409CONFLICTThe project already has an experiment with this key.
422VALIDATIONA malformed field, an extra field, an empty PATCH body, or a malformed experiment id.

On this page