# Get Experiment Review

> For AI agents: a documentation index is available at [/llms.txt](/llms.txt). Append `.md` to any page URL for markdown, or send `Accept: text/markdown`.

## Get Experiment Review

**GET** `/console/v1/experiments/{id}/reviews/{reviewID}`

Full URL: `https://statsigapi.net/console/v1/experiments/{id}/reviews/{reviewID}`

Get Experiment Review

## Authorizations

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| STATSIG-API-KEY | string | Yes | — | apiKey (header) |

## Path parameters

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| id (path) | string | Yes | — | — |
| reviewID (path) | string | Yes | — | — |

## Response (application/json)

**200** — The requested experiment review.

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| message | string | Yes | — | A simple string explaining the result of the operation. |
| data | object | Yes | — | — |
| data.review_id | string | Yes | — | Unique ID of the review. |
| data.status | string | Yes | — | Lifecycle status of the review. Allowed values: pending, accepted, rejected, committed, changes_requested, pending_webhook_validation, failed_webhook_validation, approved_webhook_validation |
| data.type | string | Yes | — | Kind of change the review proposes. Allowed values: start, stop, pause, restart, abandon, archive, delete, make_decision, update_settings, update_overrides, update_target_applications, update_allowed_reviewers, update_default_impact_multiplier, scheduled_start, scheduled_start_edit, disable_reviews_locally, rollout, schedule_rollout, reallocate, change_enabled_groups, unarchive, update_owners, update_team |
| data.author | object | Yes | — | The user who created the review. |
| data.author.id | string | Yes | — | — |
| data.author.email | string | Yes | — | Constraints: nullable |
| data.reviewers | object[] | Yes | — | Requested reviewers (users and groups). |
| data.reviewers.id | string | Yes | — | — |
| data.reviewers.kind | string | Yes | — | Allowed values: user, group |
| data.description | string | Yes | — | Human-readable description of the proposed change. |
| data.created_at | string | Yes | — | ISO-8601 timestamp of when the review was created. |
| data.content | object | No | — | The proposed change this review would apply, as `{ current, proposed }` over exactly the fields committing the review would write. `current` is the live value read at request time; where commit treats an unset argument as "leave unchanged", `proposed` repeats the live value rather than a default. Populated on single-review responses only — list responses omit it so a page of reviews does not pay an extra read per row. When the review does propose something but it could not be read back, `content` is PRESENT with `unavailable_reason` set and both value objects empty, never `null` — so "this type proposes nothing" stays distinguishable from "we cannot tell you what this would do". This surface's reasons are `review_data_unavailable` (the stored change data is missing or does not match the type the review declares; expected on legacy rows) and `content_unreadable` (the data is present but the before/after could not be assembled; a logged server-side fault). `null` when the review type is a bare action with no recoverable proposed value: `stop`, `pause`, `restart`, `reallocate`, `delete`, `disable_reviews_locally` (always), `start` (unless the review carries a backdated start date that commit will actually write — analysis-only experiments on warehouse-native projects), `scheduled_start_edit` when `new_time` is `ignore` (commit is a no-op), and `update_settings` on an autotune (the stored settings have no experiment-contract shape). Every other type carries content, including `archive`, `unarchive`, `update_owners`, `update_team`, `change_enabled_groups` and `update_allowed_reviewers`, for which the Console currently shows no before/after. Keys by type: `archive`/`unarchive` -> `archived`, `allocation` (archiving a layer-bound experiment zeroes its allocation); `abandon` -> `decision_note_title`, `decision_reason`; `start` -> `start_time`; `scheduled_start`/`scheduled_start_edit` -> `start_time`; `rollout` -> `group_id`, `rollout_percentage`, `rollout_phases`, `has_upcoming_automated_rollout`; `schedule_rollout` -> `group_id`, `rollout_phases`; `make_decision` -> `winning_group_id`, `decision_note_title`, `decision_reason`, and on experiments `overrides`, `id_overrides`, `inline_targeting_rules_json`, `targeting_gate_id`; `update_overrides` -> `overrides`, `id_overrides`, `allow_overrides_in_pulse`; `update_target_applications` -> `target_app_ids`; `update_default_impact_multiplier` -> `default_impact_multiplier`; `update_owners` -> `owners`; `update_team` -> `team_id`, `allowed_reviewer_user_ids`, `allowed_reviewer_group_ids` (moving to a team whose approval mode is `admin_only`/`team_only` widens who may approve future reviews); `update_allowed_reviewers` -> `allowed_reviewer_user_ids`, `allowed_reviewer_group_ids`; `change_enabled_groups` -> `groups` (the full list, with the delta applied); `update_settings` -> a single `settings` object holding the full resolved settings set, including `overrides`, `idOverrides`, `allowOverridesInPulse` and `customScorecardSections`. Constraints: nullable |
| data.content.type | string | Yes | — | Same value as the envelope `type`, repeated for self-containment. Allowed values: start, stop, pause, restart, abandon, archive, delete, make_decision, update_settings, update_overrides, update_target_applications, update_allowed_reviewers, update_default_impact_multiplier, scheduled_start, scheduled_start_edit, disable_reviews_locally, rollout, schedule_rollout, reallocate, change_enabled_groups, unarchive, update_owners, update_team |
| data.content.current | object | Yes | — | The entity's live value at read time, for each field this review type updates. Matches the Console's "Original Version (Before)" column for pending reviews. This is NOT status-dependent: for a committed or rejected review it is still the live entity as it stands now, which for a committed review normally equals `proposed`. |
| data.content.proposed | object | Yes | — | The value this review proposes — what commit will apply. Carries exactly the same keys as `current`. Array-valued fields carry the full array on both sides, never only the changed elements. |
| data.content.unavailable_reason | string | No | — | Present ONLY when the change could not be read back, in which case `current` and `proposed` are both empty objects and carry no meaning. This exists so that "this review type has nothing to propose" (`content: null`) is never confused with "we could not tell you what this review would do". `review_data_unavailable`: the review's stored change data is missing, or does not match the type the review declares — expected on legacy rows. `content_unreadable`: the data is present but the before/after could not be assembled; that is a server-side fault and is logged. Allowed values: review_data_unavailable, content_unreadable |

```json
{
  "message": "string",
  "data": {
    "review_id": "string",
    "status": "pending",
    "type": "start",
    "author": {
      "id": "string",
      "email": "string"
    },
    "reviewers": [
      {
        "id": "string",
        "kind": "user"
      }
    ],
    "description": "string",
    "created_at": "string",
    "content": {
      "type": "start",
      "current": {},
      "proposed": {},
      "unavailable_reason": "review_data_unavailable"
    }
  }
}
```

## Code samples

### cURL

```bash
curl -X GET "https://statsigapi.net/console/v1/experiments/{id}/reviews/{reviewID}" \
  -H "Content-Type: application/json" \
  -H "STATSIG-API-KEY: YOUR_API_KEY"
```

### Python

```python
import requests

response = requests.get(
    "https://statsigapi.net/console/v1/experiments/{id}/reviews/{reviewID}",
    headers={
        "Content-Type": "application/json",
        "STATSIG-API-KEY": "YOUR_API_KEY"
    }
)
data = response.json()
```

### JavaScript

```javascript
const response = await fetch("https://statsigapi.net/console/v1/experiments/{id}/reviews/{reviewID}", {
  method: "GET",
  headers: {
    "Content-Type": "application/json",
    "STATSIG-API-KEY": "YOUR_API_KEY"
  }
});
const data = await response.json();
```

### PHP

```php
<?php
$ch = curl_init("https://statsigapi.net/console/v1/experiments/{id}/reviews/{reviewID}");
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "GET");
curl_setopt($ch, CURLOPT_HTTPHEADER, [
    "Content-Type: application/json",
    "STATSIG-API-KEY: YOUR_API_KEY"
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);
```

### Go

```go
package main

import (
  "bytes"
  "net/http"
)

func main() {
req, _ := http.NewRequest("GET", "https://statsigapi.net/console/v1/experiments/{id}/reviews/{reviewID}", nil)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("STATSIG-API-KEY", "YOUR_API_KEY")
  client := &http.Client{}
  resp, _ := client.Do(req)
  defer resp.Body.Close()
}
```

### Java

```java
HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
      .uri(URI.create("https://statsigapi.net/console/v1/experiments/{id}/reviews/{reviewID}"))
      .method("GET", HttpRequest.BodyPublishers.noBody())
.header("Content-Type", "application/json")
      .header("STATSIG-API-KEY", "YOUR_API_KEY")
      .build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
```

### Ruby

```ruby
require "net/http"
require "json"

uri = URI("https://statsigapi.net/console/v1/experiments/{id}/reviews/{reviewID}")
request = Net::HTTP::Get.new(uri)
request["Content-Type"] = "application/json"
request["STATSIG-API-KEY"] = "YOUR_API_KEY"
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == "https") do |http|
  http.request(request)
end
```
