> ## Documentation Index
> Fetch the complete documentation index at: https://docs2.growthbook.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Driving a Contextual Bandit via API

> Run a contextual bandit entirely from the GrowthBook REST API, from assignment queries through results.

export const CommercialFeature = ({feature, description}) => {
  const commercialFeatures = {
    "adv-presentations": {
      plan: "enterprise",
      displayName: "Adv Presentations"
    },
    "advanced-permissions": {
      plan: "pro",
      displayName: "Advanced Permissions"
    },
    "ai-byok": {
      plan: "enterprise",
      displayName: "Ai Byok"
    },
    "ai-suggestions": {
      plan: "enterprise",
      displayName: "AI Suggestions"
    },
    archetypes: {
      plan: "pro",
      displayName: "Archetypes"
    },
    "audit-logging": {
      plan: "enterprise",
      displayName: "Audit Logging"
    },
    "cloud-proxy": {
      plan: "pro",
      displayName: "Cloud Proxy"
    },
    "code-references": {
      plan: "pro",
      displayName: "Code References"
    },
    "contextual-bandits": {
      plan: "enterprise",
      displayName: "Contextual Bandits"
    },
    "custom-hooks": {
      plan: "enterprise",
      displayName: "Custom Hooks"
    },
    "custom-launch-checklist": {
      plan: "enterprise",
      displayName: "Custom Launch Checklist"
    },
    "custom-markdown": {
      plan: "enterprise",
      displayName: "Custom Markdown"
    },
    "custom-metadata": {
      plan: "enterprise",
      displayName: "Custom Metadata"
    },
    "custom-roles": {
      plan: "enterprise",
      displayName: "Custom Roles"
    },
    dashboards: {
      plan: "enterprise",
      displayName: "Dashboards"
    },
    "decision-framework": {
      plan: "pro",
      displayName: "Decision Framework"
    },
    "encrypt-features-endpoint": {
      plan: "pro",
      displayName: "Encrypt Features Endpoint"
    },
    "environment-inheritance": {
      plan: "enterprise",
      displayName: "Environment Inheritance"
    },
    "events-forwarder": {
      plan: "pro",
      displayName: "Events Forwarder"
    },
    "experiment-impact": {
      plan: "enterprise",
      displayName: "Experiment Impact"
    },
    "feature-configs": {
      plan: "enterprise",
      displayName: "Feature Configs"
    },
    "funnel-metrics": {
      plan: "pro",
      displayName: "Funnel Metrics"
    },
    "hash-secure-attributes": {
      plan: "pro",
      displayName: "Hash Secure Attributes"
    },
    "historical-power": {
      plan: "pro",
      displayName: "Historical Power"
    },
    holdouts: {
      plan: "enterprise",
      displayName: "Holdouts"
    },
    "incremental-refresh": {
      plan: "enterprise",
      displayName: "Incremental Refresh"
    },
    "json-validation": {
      plan: "enterprise",
      displayName: "JSON Validation"
    },
    "large-saved-groups": {
      plan: "enterprise",
      displayName: "Large Saved Groups"
    },
    learnings: {
      plan: "enterprise",
      displayName: "Learnings"
    },
    livechat: {
      plan: "pro",
      displayName: "Livechat"
    },
    "manage-official-resources": {
      plan: "enterprise",
      displayName: "Manage Official Resources"
    },
    "metric-correlations": {
      plan: "enterprise",
      displayName: "Metric Correlations"
    },
    "metric-effects": {
      plan: "enterprise",
      displayName: "Metric Effects"
    },
    "metric-groups": {
      plan: "enterprise",
      displayName: "Metric Groups"
    },
    "metric-populations": {
      plan: "pro",
      displayName: "Metric Populations"
    },
    "metric-slices": {
      plan: "enterprise",
      displayName: "Metric Slices"
    },
    "multi-armed-bandits": {
      plan: "pro",
      displayName: "Multi Armed Bandits"
    },
    "multi-metric-queries": {
      plan: "enterprise",
      displayName: "Multi Metric Queries"
    },
    "multi-org": {
      plan: "enterprise",
      displayName: "Multi Org"
    },
    "multiple-sdk-webhooks": {
      plan: "pro",
      displayName: "Multiple Sdk Webhooks"
    },
    "no-access-role": {
      plan: "enterprise",
      displayName: "No Access Role"
    },
    "override-metrics": {
      plan: "pro",
      displayName: "Override Metrics"
    },
    "pipeline-mode": {
      plan: "enterprise",
      displayName: "Pipeline Mode"
    },
    "post-stratification": {
      plan: "enterprise",
      displayName: "Post Stratification"
    },
    "precomputed-dimensions": {
      plan: "pro",
      displayName: "Precomputed Dimensions"
    },
    "prerequisite-targeting": {
      plan: "enterprise",
      displayName: "Prerequisite Targeting"
    },
    prerequisites: {
      plan: "pro",
      displayName: "Prerequisites"
    },
    "product-analytics-dashboards": {
      plan: "pro",
      displayName: "Product Analytics Dashboards"
    },
    "project-admin-role": {
      plan: "enterprise",
      displayName: "Project Admin Role"
    },
    "quantile-metrics": {
      plan: "pro",
      displayName: "Quantile Metrics"
    },
    "ramp-schedules": {
      plan: "pro",
      displayName: "Ramp Schedules"
    },
    redirects: {
      plan: "pro",
      displayName: "Redirects"
    },
    "regression-adjustment": {
      plan: "pro",
      displayName: "CUPED"
    },
    releases: {
      plan: "enterprise",
      displayName: "Releases"
    },
    "remote-evaluation": {
      plan: "pro",
      displayName: "Remote Evaluation"
    },
    "require-approvals": {
      plan: "enterprise",
      displayName: "Require Approvals"
    },
    "require-project-for-features-setting": {
      plan: "enterprise",
      displayName: "Require Project For Features Setting"
    },
    "require-project-for-sdk-connections-setting": {
      plan: "enterprise",
      displayName: "Require Project For Sdk Connections Setting"
    },
    "retention-metrics": {
      plan: "pro",
      displayName: "Retention Metrics"
    },
    "safe-rollout": {
      plan: "pro",
      displayName: "Safe Rollout"
    },
    saveSqlExplorerQueries: {
      plan: "pro",
      displayName: "Save SQL Explorer Queries"
    },
    "schedule-feature-flag": {
      plan: "pro",
      displayName: "Schedule Feature Flag"
    },
    "scheduled-revisions": {
      plan: "enterprise",
      displayName: "Scheduled Revisions"
    },
    scim: {
      plan: "enterprise",
      displayName: "SCIM"
    },
    "sequential-testing": {
      plan: "pro",
      displayName: "Sequential Testing"
    },
    "share-product-analytics-dashboards": {
      plan: "enterprise",
      displayName: "Share Product Analytics Dashboards"
    },
    simulate: {
      plan: "pro",
      displayName: "Simulate"
    },
    sso: {
      plan: "enterprise",
      displayName: "SSO"
    },
    "sticky-bucketing": {
      plan: "pro",
      displayName: "Sticky Bucketing"
    },
    teams: {
      plan: "enterprise",
      displayName: "Teams"
    },
    templates: {
      plan: "enterprise",
      displayName: "Templates"
    },
    "unlimited-managed-warehouse-usage": {
      plan: "pro",
      displayName: "Unlimited Managed Warehouse Usage"
    },
    "visual-editor": {
      plan: "pro",
      displayName: "Visual Editor"
    }
  };
  const {plan, displayName} = commercialFeatures[feature];
  const isEnterprise = plan === "enterprise";
  const defaultDescription = isEnterprise ? "is available on Enterprise plans." : "is available on Pro and Enterprise plans.";
  const planLabel = isEnterprise ? "Enterprise" : "Pro";
  const containerStyle = isEnterprise ? {
    backgroundColor: "color-mix(in srgb, var(--indigo-a3) 60%, transparent)"
  } : {
    backgroundColor: "color-mix(in srgb, var(--amber-a3) 60%, transparent)"
  };
  const badgeStyle = isEnterprise ? {
    boxShadow: "inset 0 0 0 1px var(--indigo-a8)",
    color: "var(--indigo-a11)"
  } : {
    boxShadow: "inset 0 0 0 1px var(--amber-a8)",
    color: "var(--amber-a11)"
  };
  return <div className="flex items-start gap-2 mb-4 p-3 text-sm leading-[1.4] rounded-lg" style={containerStyle} role="note">
      <span className="inline-flex items-center justify-center px-1.5 h-5 text-xs font-medium rounded-full shrink-0 leading-none" style={badgeStyle}>
        {planLabel}
      </span>
      <div className="flex-1 leading-[1.3]">
        <strong className="font-semibold">{displayName}</strong>{" "}
        {defaultDescription} {description}
      </div>
    </div>;
};

<CommercialFeature feature="contextual-bandits" />

<Badge color="purple">beta</Badge>

This runbook 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 — a 1.7+ SDK, an updated tracking callback, and a connected data source — are already in place. See [Setting up a Contextual Bandit](/bandits/contextual-config) 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 borrowing an Experiment Assignment Query off the datasource.
* `/v1/contextual-bandits` — the bandit itself, which references a query by id.

See the [REST API reference](https://docs.growthbook.io/api) for the full schema.

<Info>
  Contextual Bandits are an Enterprise-only feature. Every endpoint below returns `402 Plan Does Not Allow` on Pro / Free plans.
</Info>

## 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 named column must also appear in the query's `SELECT`. The query should also select the `leaf_id`, `bandit_version`, and `variation_weights` columns logged by your [tracking callback](/bandits/contextual-config#2-update-your-tracking-callback) — without them the SRM health check is skipped.

```bash theme={null}
curl -X POST 'https://api.growthbook.io/api/v1/contextual-bandit-queries' \
  -H 'Authorization: Bearer YOUR_API_KEY' \
  -H 'Content-Type: application/json' \
  -d '{
    "datasourceId": "ds_abc",
    "name": "Logged-in users",
    "userIdType": "user_id",
    "query": "SELECT user_id, timestamp, experiment_id, variation_id, leaf_id, bandit_version, variation_weights, country, device FROM experiment_viewed",
    "targetingAttributeColumns": ["country", "device"]
  }'
```

The response includes the new query id (`cbq_…`). You can manage queries with the standard CRUD endpoints (`GET /v1/contextual-bandit-queries`, `GET/PUT/DELETE /v1/contextual-bandit-queries/:id`); pass `?datasourceId=ds_abc` on the list endpoint to scope by datasource.

## 2. Create the Contextual Bandit

Reference the query from the previous step via `contextualBanditQueryId`.

```bash theme={null}
curl -X POST 'https://api.growthbook.io/api/v1/contextual-bandits' \
  -H 'Authorization: Bearer YOUR_API_KEY' \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "Homepage CTA bandit",
    "trackingKey": "homepage-cta-bandit",
    "datasource": "ds_abc",
    "contextualBanditQueryId": "cbq_abc",
    "decisionMetric": "metric_signup",
    "variations": [
      { "key": "control",   "name": "Control"  },
      { "key": "treatment", "name": "Treatment" }
    ],
    "contextualAttributes": ["country", "device"],
    "project": "proj_a"
  }'
```

Required fields are `name`, `trackingKey`, `datasource`, `contextualBanditQueryId`, `variations`, `decisionMetric`, and `contextualAttributes`. Other fields (`hashAttribute`, `queryFilter`, `activationMetric`, `minUsersPerLeaf`, `maxLeaves`, etc.) are optional and fall back to defaults.

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.

## 3. Link a Feature Flag

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:

```bash theme={null}
curl -X POST 'https://api.growthbook.io/api/v1/contextual-bandits/cb_xxx/linked-feature/homepage-cta' \
  -H 'Authorization: Bearer YOUR_API_KEY' \
  -H 'Content-Type: application/json' \
  -d '{
    "variations": [
      { "variationId": "0", "value": "Sign up" },
      { "variationId": "1", "value": "Get started free" }
    ]
  }'
```

`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:

```json theme={null}
{
  "featureId": "homepage-cta",
  "ruleId": "fr_abc123",
  "revisionVersion": 4,
  "published": false
}
```

Two companion endpoints round this out:

* `GET /v1/contextual-bandits/:id/linked-features` — the linked flags enriched with live/draft state, per-environment rule state, and variation values (the same payload the CB detail page renders).
* `DELETE /v1/contextual-bandits/:id/linked-feature/:featureId` — removes every `contextual-bandit-ref` rule pointing at this bandit from the flag and drops the linkage. Like the POST, the removal lands in a draft unless you pass `?autoPublish=true`. When the flag has no such rule left, only the linkage is cleared.

## 4. Start the Contextual Bandit

Move the CB out of `draft` (to `status: "running"`) so it is eligible for refresh:

```bash theme={null}
curl -X POST 'https://api.growthbook.io/api/v1/contextual-bandits/cb_xxx/start' \
  -H 'Authorization: Bearer YOUR_API_KEY'
```

The response returns the updated CB: `{ "contextualBandit": { … } }`.

## 5. Refresh — run a snapshot

Triggers the contextual-bandit snapshot pipeline. The orchestrator builds frozen settings, opens a ContextualBanditSnapshot (CBS) doc, runs the warehouse query and the stats engine, and on success persists a ContextualBanditEvent (CBE) with the new per-leaf weights. Each successful snapshot increments the CB's `banditVersion`.

```bash theme={null}
curl -X POST 'https://api.growthbook.io/api/v1/contextual-bandits/cb_xxx/refresh' \
  -H 'Authorization: Bearer YOUR_API_KEY'
```

Response shape:

```json theme={null}
{ "snapshotId": "cbs_abc", "cbeId": "cbe_def" }
```

`cbeId` is omitted when the run failed before producing an event (the CBS doc will be in `status: "error"`).

## 6. Read the latest 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.

```bash theme={null}
curl 'https://api.growthbook.io/api/v1/contextual-bandits/cb_xxx/results' \
  -H 'Authorization: Bearer YOUR_API_KEY'
```

Response (abbreviated; `contextualBanditSnapshot` and `latest` are each `null` until a run exists):

```json theme={null}
{
  "contextualBanditSnapshot": {
    "attributes": ["country"],
    "responses": [ /* per-context stats rows (each tagged with its `leafId`) */ ],
    "leaf_map": [
      /* one entry per decision-tree leaf; `context` is the AND of per-attribute
         clauses, e.g.
         { "leafId": 2, "context": [
             { "attribute": "country", "levels": ["US", "UK"], "operator": "in" },
             { "attribute": "browser", "levels": ["Chrome", "Firefox"], "operator": "not in" }
         ] } */
    ]
  },
  "latest": {
    "id": "cbs_abc",
    "status": "success",
    "error": "",
    "queries": [ /* QueryPointer[] */ ],
    "runStarted": "2026-05-29T12:00:00.000Z",
    "dateCreated": "2026-05-29T12:00:05.000Z",
    "multipleExposures": 0,
    "type": "standard",
    "triggeredBy": "manual"
  }
}
```

`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:

* `GET /v1/contextual-bandits/:id/snapshots` — list recent snapshot runs (optional `?limit=`, max 100).
* `GET /v1/contextual-bandits/:id/snapshots/:snapshotId` — single snapshot status.
* `GET /v1/contextual-bandits/:id/events` — list CBE outputs, one per successful snapshot (optional `?limit=`, max 100).
* `GET /v1/contextual-bandits/:id/events/:eventId` — single CBE.
* `GET /v1/contextual-bandits/:id/current` — current root-level `currentLeafWeights` plus the latest event object (`latestEvent`, or `null`).

The `/current` response looks like:

```json theme={null}
{
  "currentLeafWeights": [
    {
      "leafId": 0,
      "condition": { "country": "US" },
      "weights": [
        { "variationId": "0", "weight": 0.38 },
        { "variationId": "1", "weight": 0.62 }
      ]
    },
    {
      "leafId": 1,
      "condition": { "country": { "$in": ["US", "UK"] } },
      "weights": [
        { "variationId": "0", "weight": 0.41 },
        { "variationId": "1", "weight": 0.59 }
      ]
    },
    {
      "leafId": 2,
      "condition": { "browser": { "$nin": ["Chrome", "Firefox"] } },
      "weights": [
        { "variationId": "0", "weight": 0.55 },
        { "variationId": "1", "weight": 0.45 }
      ]
    }
  ],
  "latestEvent": {
    "id": "cbe_def",
    "contextualBandit": "cb_xxx",
    "snapshotId": "cbs_abc",
    "weightsWereUpdated": true,
    "degreesOfFreedom": 3,
    "dateCreated": "2026-05-29T12:00:05.000Z"
  }
}
```

## 7. Stop the Contextual Bandit

When you're done, stop the CB:

```bash theme={null}
curl -X POST 'https://api.growthbook.io/api/v1/contextual-bandits/cb_xxx/stop' \
  -H 'Authorization: Bearer YOUR_API_KEY'
```

Like `start`, this returns the updated CB under `contextualBandit`.

## Standard CRUD

Both resources also expose the default CRUD endpoints:

* `GET /v1/contextual-bandits` (filter with `?projectId=`, `?datasourceId=`, or `?trackingKey=`), `GET /v1/contextual-bandits/:id`, `POST /v1/contextual-bandits`, `PUT /v1/contextual-bandits/:id`, `DELETE /v1/contextual-bandits/:id`.
* `GET /v1/contextual-bandit-queries` (filter with `?datasourceId=`), `GET /v1/contextual-bandit-queries/:id`, `POST /v1/contextual-bandit-queries`, `PUT /v1/contextual-bandit-queries/:id`, `DELETE /v1/contextual-bandit-queries/:id`.

## 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 `canUpdateFeature`, `canManageFeatureDrafts`, and `canPublishFeature` (scoped to the rule's environments) on the Feature Flag.

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.
