Skip to main content
This doc walks through the full lifecycle of a Contextual Bandit driven entirely from the REST API. Every step assumes you have a valid Personal Access Token or Secret Key with the relevant Contextual Bandit permissions on the target project, and that the general prerequisites are already in place. See Setting up a Contextual Bandit for that checklist. Contextual Bandits use two REST resources:
  • /v1/contextual-bandit-queries — the assignment SQL plus the targeting (context) columns the bandit splits on. This is the bandit-specific replacement for the experiment assignment query.
  • /v1/contextual-bandits — the bandit itself, which references a query by id.
See the REST API reference for the full schema of each resource.

1. Create a Contextual Bandit Query

A Contextual Bandit needs a query that defines the assignment SQL and the targeting-attribute columns it splits on. targetingAttributeColumns is required and must be non-empty — a bandit with no context to split on is not a contextual bandit. Each column name must be a valid SQL identifier and match a non-archived targeting attribute in your organization (Settings → Attributes), otherwise the request fails with a 400. Each column must also be selected by the query; this is not validated on create, but the snapshot will fail at run time if it is missing. The query should also select the leaf_id, bandit_version, and variation_weights columns logged by your tracking callback — without them the SRM health check is skipped. Create the query with POST /v1/contextual-bandit-queries:
The response includes the new query id (cbq_…). You can manage queries with these endpoints:

2. Create the Contextual Bandit

Reference the query from the previous step via contextualBanditQueryId. The full request schema is in the create Contextual Bandit reference.
Required fields are name, trackingKey, datasource, contextualBanditQueryId, variations, decisionMetric, and contextualAttributes. Like targetingAttributeColumns, every contextualAttributes entry must match an organization targeting attribute; entries not present in the query’s targetingAttributeColumns are dropped at run time rather than rejected on create. Another crucial field is hashAttribute, which is the attribute used to randomize users into variations, which should map to the user identifier in your assignment query. For short-lived bandits, we suggest a session_id or device_id that will re-generate on future visits to ensure you get the most power while managing when a session ends yourself. The response includes the new Contextual Bandit id (cb_…). The CB is provisioned in status: "draft" with an empty currentLeafWeights array at the document root; it stays empty until the first successful refresh. A Contextual Bandit serves its variations through a contextual-bandit-ref rule on a Feature Flag, and it needs at least one linked Feature Flag before it can start. Create the flag first with POST /v2/features, then link it to the contextual bandit:
variations must cover every Contextual Bandit variation exactly once — variationId is the id of each entry in the CB’s variations array, not its key. Targeting (condition, saved groups, prerequisites, coverage) lives on the Contextual Bandit and is inherited by the rule, so those fields are not accepted here. The rule is appended to the bottom of the flag’s rule list in a new draft revision, which auto-publishes when you start the Contextual Bandit. Pass "autoPublish": true to publish it right away, or "draftVersion": 7 to add the rule to an existing draft instead of starting a new one. By default the rule applies to every environment; pass "allEnvironments": false with "environments": ["production"] to narrow it. Response:
Two companion endpoints round this out:

4. Start the Contextual Bandit

Move the CB out of draft (to status: "running") so it is eligible for refresh with POST /v1/contextual-bandits/:id/start:
The response returns the updated CB: { "contextualBandit": { … } }.

5. Refresh — run a snapshot

POST /v1/contextual-bandits/:id/refresh triggers the contextual bandit update. Each update runs the warehouse query and the stats engine and updates weights. Each successful update that changes weights increments the CB’s banditVersion.
Response shape:

6. Read the latest results

GET /v1/contextual-bandits/:id/results returns the same payload the GrowthBook UI uses to render the CB results table — the latest stats engine output plus a snapshot-status summary.
Response (abbreviated; contextualBanditSnapshot and latest are each null until a run exists):
latest.status is one of running, success, or error, and latest.runStarted may be null if the run hasn’t started. Use the companion endpoints for finer-grained inspection: The /current response looks like:

7. Stop the Contextual Bandit

When you’re done, stop the CB with POST /v1/contextual-bandits/:id/stop:
Like start, this returns the updated CB under contextualBandit.

Standard CRUD

Both resources also expose the default CRUD endpoints:

Permissions cheat sheet

Permissions are checked directly against the Contextual Bandit doc (CBs no longer delegate RBAC to a paired experiment):
  • Read (GET results, GET current, GET snapshot(s), GET event(s), GET /:id, GET /) — canReadSingleProjectResource(cb.project).
  • Create (POST /) — canCreateContextualBandit(cb).
  • Update (PUT /:id) — canUpdateContextualBandit(existing, updated).
  • Run (POST refresh, POST start, POST stop) — canRunContextualBandit(cb, environments).
  • Delete (DELETE /:id) — canDeleteContextualBandit(cb).
  • Link / unlink a Feature Flag (POST / DELETE /:id/linked-feature/:featureId) — canUpdateContextualBandit on the bandit, plus canEditFeatureDrafts on the Feature Flag. When autoPublish is set, canPublishFeature (scoped to the rule’s environments) is also required.
Contextual Bandit Query create/update/delete are gated by the datasource: you need canUpdateDataSourceSettings on the query’s datasource. Plan gate (hasPremiumFeature("contextual-bandits")) is checked first on every endpoint and short-circuits with 402 before any data access.