Graine AI
API referenceAgent versions

Update an A/B experiment

Move an experiment through its lifecycle, or change its traffic split.

Move an experiment through its lifecycle, or change its traffic split.

Supported transitions::

draft -> active start splitting live traffic active -> paused stop splitting, keep the record active | paused -> concluded close the experiment

Three state rules, all answered with 409 (code 1201):

  • Only one experiment per control agent may be ACTIVE. Conclude or pause the running one before activating another.
  • winner is only accepted once the experiment is concluded. Setting the winner on an active experiment is a 409 — but {"status": "concluded", "winner": "variant"} in a single request is legal and is the normal way to close an experiment.
  • An ACTIVE experiment cannot be deleted (see DELETE).

Promoting the variant: set variant_traffic_pct to 100. Every call against the control agent then routes to the variant, and nothing else has to change — campaigns, batches and scheduled calls keep naming the control agent, so there is no re-targeting step and no window in which some calls use the old agent. Conversely 0 parks the variant without ending the experiment.

Nothing can be cleared. The update is applied with null-exclusion upstream, so a field sent as null would be discarded and the response would look like a successful no-op. This API rejects an explicit null with 400 instead; in particular a winner cannot be un-set once recorded.

Rate limit: 1000 requests per minute per organization (bucket default). Exceeding it returns 429 with Retry-After; the X-RateLimit-* response headers report your remaining allowance on every call.

PATCH
/v2/agents/{agent_id}/versions/{version_id}
/v2/agents/6c6f4cc3-f56c-479d-8422-7f76694daa29/versions/9f1c0f7e-1f2a-4c3b-8a11-2c6a0a5f8d10

The Authorization access token

Authorization

Authorization
Required
Bearer <token>

Your Graine API key. Create one in the dashboard under Developers, or via POST /v2/api-keys. Send it as Authorization: Bearer <key>.

In: header

Request Body

application/jsonRequired

version_nameVersion Name | null

New experiment name.

Maximum length: 100

descriptionDescription | null

New notes.

Maximum length: 500

variable_testedVariable Tested | null

New label for what is being tested.

Maximum length: 200

variant_traffic_pctVariant Traffic Pct | null

New traffic split. 0 sends every call to the control agent; 100 sends every call to the variant, which is how a variant is promoted.

Minimum: 0Maximum: 100

statusStatus | null

draft | active | paused | concluded

winnerWinner | null

control | variant. Only accepted when the experiment is, or is becoming in this same request, concluded.

start_dateStart Date | null

ISO-8601 timestamp with a timezone offset. Informational.

end_dateEnd Date | null

ISO-8601 timestamp with a timezone offset. Informational.

Path Parameters

agent_id
Required
Agent Id

The CONTROL agent.

version_id
Required
Version Id

Experiment identifier.

Query Parameters

organization_idOrganization Id

Must match the organisation the API key belongs to.

Response Body

200

The updated experiment.

version_id
Required
Version Id

Experiment identifier.

main_agent_id
Required
Main Agent Id

The CONTROL agent. Always equal to the agent_id in the path.

variant_agent_id
Required
Variant Agent Id

The CHALLENGER agent that receives the split traffic.

version_name
Required
Version Name

Experiment name.

descriptionDescription | null

Free-text notes about the experiment.

variable_testedVariable Tested | null

What is being tested, e.g. 'voice', 'script', 'cadence'.

variant_traffic_pct
Required
Variant Traffic Pct

Percentage of calls routed to the variant (0-100). The remainder goes to the control agent. 0 pauses the variant in place; 100 promotes it.

status
Required
Status

draft | active | paused | concluded

winnerWinner | null

control | variant. Only ever set on a concluded experiment.

start_dateStart Date | null

ISO-8601 UTC. Informational; it does not gate routing.

end_dateEnd Date | null

ISO-8601 UTC. Informational; it does not gate routing.

created_byCreated By | null

Identifier of whoever created the experiment.

created_atCreated At | null

ISO-8601 UTC.

updated_atUpdated At | null

ISO-8601 UTC.

400

1000 — malformed or contradictory input: an unknown status token, an empty patch body, an explicit null, or a variant agent that does not exist in this organisation.

error
Required
Error

Stable integer code from the /v2 error table. Branch on this.

message
Required
Message

One human-readable sentence. Wording may change; the code will not.

401

1100 — missing, unknown or inactive API key. 1101 — a browser session token was presented.

error
Required
Error

Stable integer code from the /v2 error table. Branch on this.

message
Required
Message

One human-readable sentence. Wording may change; the code will not.

403

1102 — the key is valid but lacks the scope this endpoint requires, or names another organisation. GET /v2/scopes reports which scopes the key holds.

error
Required
Error

Stable integer code from the /v2 error table. Branch on this.

message
Required
Message

One human-readable sentence. Wording may change; the code will not.

404

1200 — no such agent, or no such version for that agent, in this organisation. Cross-tenant reads answer 404, never 403, so ids cannot be probed.

error
Required
Error

Stable integer code from the /v2 error table. Branch on this.

message
Required
Message

One human-readable sentence. Wording may change; the code will not.

409

1201 — the experiment's state does not allow this action: a second ACTIVE version for the same control agent, a DELETE against an ACTIVE version, or a winner set before the experiment is concluded.

error
Required
Error

Stable integer code from the /v2 error table. Branch on this.

message
Required
Message

One human-readable sentence. Wording may change; the code will not.

422

1001 — the body or query failed validation; the message names the first offending field.

error
Required
integer

Stable integer code from the error table. Branch on this, not on the message.

message
Required
string

One human-readable sentence. Wording may change; the code will not.

429

1300 — per-organisation rate limit exceeded. Carries Retry-After.

error
Required
Error

Stable integer code from the /v2 error table. Branch on this.

message
Required
Message

One human-readable sentence. Wording may change; the code will not.

503

1501 — the agent service is unreachable or returned a 5xx.

error
Required
Error

Stable integer code from the /v2 error table. Branch on this.

message
Required
Message

One human-readable sentence. Wording may change; the code will not.

504

1502 — the agent service did not respond in time.

error
Required
Error

Stable integer code from the /v2 error table. Branch on this.

message
Required
Message

One human-readable sentence. Wording may change; the code will not.

curl -X PATCH "https://api.graine.ai/v2/agents/6c6f4cc3-f56c-479d-8422-7f76694daa29/versions/9f1c0f7e-1f2a-4c3b-8a11-2c6a0a5f8d10?organization_id=string" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "version_name": "Warmer opening line",
    "description": "Testing a warmer opening line against the current script.",
    "variable_tested": "script",
    "variant_traffic_pct": 30,
    "status": "active",
    "winner": "variant",
    "start_date": "2026-08-20T18:36:01+05:30",
    "end_date": "2026-08-27T15:00:00+05:30"
  }'

The updated experiment.

{
  "version_id": "9f1c0f7e-1f2a-4c3b-8a11-2c6a0a5f8d10",
  "main_agent_id": "6c6f4cc3-f56c-479d-8422-7f76694daa29",
  "variant_agent_id": "85598652-87ce-4585-93e3-e03a42259796",
  "version_name": "Warmer opening line",
  "description": "Testing a warmer opening line against the current script.",
  "variable_tested": "script",
  "variant_traffic_pct": 50,
  "status": "active",
  "winner": "variant",
  "start_date": "2026-08-20T13:06:01.724Z",
  "end_date": "2026-08-27T09:30:00.000Z",
  "created_by": "dev_1a2b3c4d5e6f7a8b",
  "created_at": "2026-08-20T13:06:01.724Z",
  "updated_at": "2026-08-20T13:27:49.073Z"
}