# Control Groups API

A [control group](/product/audience-data-and-segmentation/control-groups/) is a hold-out share of an application's users that never receives marketing messages, so the effect of messaging can be measured against it. This API manages control groups and answers per-user membership. Use it to mirror the Control Panel's **Settings > Control groups** actions from your own systems, or to check whether specific user IDs are held out before a send or an import.

## Base URL

```
https://rpc-api.svc-nue.pushwoosh.com
```

All endpoints are served over HTTPS. Requests and responses use `application/json` unless noted otherwise.

## Authentication

Every request must include an `Authorization` header with your [Server API token](/developer/api-reference/api-access-token/#server-api-token):

```
Authorization: Api YOUR_API_TOKEN
```

## Conventions

* **Field naming:** request bodies and query/path parameters accept `lowerCamelCase` (for example, `controlGroupCode`, `userIds`), and the server unmarshals either casing. Responses are always marshaled using the proto field names, in `snake_case` (`application_id`, `in_control_group`, and so on). The response examples and the [Control group object](#control-group-object) reference below use that casing.
* **`code`:** every control group response carries its own code, generated on `Create`. Pass this code as `controlGroupCode` to `Get`, `UpdatePercentage`, `UpdateCountries`, `Disable`, `Reshuffle`, `ForceUpdateCalculation`, `GetCalculationStatus`, `GetAnalytics`, and `CheckControlGroupMembership`.
* **The default group:** the group with an empty `name` is the one a send falls back to when it names no group explicitly. An application can have more than one control group, and later ones are selected by name at send time.

### Error responses

| HTTP status | Meaning |
| :---- | :---- |
| `400 Bad Request` | Invalid argument, such as `percentage` outside 1–20, `userIds` empty or over 1,000 entries, or an unrecognized country code. Also returned (as a `FailedPrecondition` on the wire) by `UpdatePercentage`, `UpdateCountries`, `Disable`, `Reshuffle`, and `Delete` on a control group that belongs to a campaign's own hold-out, per the [caution](#updatepercentage) below. `Reshuffle` alone also refuses this way on a disabled (`percentage` 0) group. |
| `401 Unauthorized` | Missing or invalid `Authorization` header. |
| `403 Forbidden` | The application or control group does not belong to the caller's account. |
| `404 Not Found` | The control group or application was not found. |
| `409 Conflict` | `Create` used a `name` that already exists in the application. |
| `500 Internal Server Error` | Unexpected server-side failure. |

## Endpoints

| Method | Path | Description |
| :---- | :---- | :---- |
| `GET` | `/api/applications/{code}/control_groups` | List an application's control groups |
| `POST` | `/api/applications/{code}/control_groups` | Create a control group |
| `GET` | `/api/applications/{code}/control_groups/{control_group_code}` | Get a single control group |
| `POST` | `/api/applications/{code}/control_groups/{control_group_code}` | Resize a control group |
| `POST` | `/api/applications/{code}/control_groups/{control_group_code}/countries` | Rescope a control group to a set of countries |
| `POST` | `/api/applications/{code}/control_groups/{control_group_code}/disable` | Disable a control group |
| `POST` | `/api/applications/{code}/control_groups/{control_group_code}/reshuffle` | Reshuffle a control group |
| `POST` | `/api/applications/{code}/control_groups/{control_group_code}/recalculate` | Force-recalculate a control group's size |
| `GET` | `/api/applications/{code}/control_groups/{control_group_code}/calculation_status` | Poll a running size calculation |
| `GET` | `/api/applications/{code}/control_groups/{control_group_code}/analytics` | Get Control-vs-Treatment analytics |
| `POST` | `/api/applications/{code}/control_groups/{control_group_code}/membership` | Check membership for a batch of user IDs |
| `DELETE` | `/api/applications/{code}/control_groups/{control_group_code}` | Delete a control group |

## List

Lists every control group configured for an application, including which one is the default a send falls back to.

`GET` `/api/applications/{code}/control_groups`

### Path parameters

| Parameter | Type | Description |
| :---- | :---- | :---- |
| `code` | string | The [application code](/developer/api-reference/api-identifiers/#application-code) to list control groups for. |

### Response

| Field | Type | Description |
| :---- | :---- | :---- |
| `control_groups` | array of [Control group objects](#control-group-object) | Every control group configured for the application. |

## Create

Creates a control group for an application and returns it with its generated code. Send an empty `name` to create the default group: the group a send falls back to when it names none explicitly. A non-empty name creates an additional group that has to be selected explicitly by whoever sends.

`POST` `/api/applications/{code}/control_groups`

### Request body

| Parameter | Type | Required | Description |
| :---- | :---- | :---- | :---- |
| `code` | string | Yes | The application code to create the group in. |
| `name` | string | Yes | Group name, up to 64 characters and without a colon. Must be unique within the application. Send an empty string to create the default group. |
| `percentage` | integer | Yes | Hold-out size as a percentage, 1–20. |
| `segment` | string | No | [Seglang](/developer/api-reference/segmentation-filters-api/segmentation-language/) expression to measure the group over. Omit to measure over the whole base. |
| `countries` | array of strings | No | Lowercase ISO-3166-1 alpha-2 country codes to scope the hold-out to. Omit for every country. Codes are case-insensitive on input. |

##### Request example

```json
{
  "code": "XXXXX-XXXXX",
  "name": "Q3 holdout",
  "percentage": 10
}
```

### Response

Returns `{ "group": { ... } }`, the new [Control group object](#control-group-object).

## Get

Returns one control group by its code, with its hold-out size, generation, and the user counts behind it.

`GET` `/api/applications/{code}/control_groups/{control_group_code}`

### Path parameters

| Parameter | Type | Description |
| :---- | :---- | :---- |
| `code` | string | The application code the group belongs to. |
| `control_group_code` | string | The control group's code. |

### Response

| Field | Type | Description |
| :---- | :---- | :---- |
| `group` | [Control group object](#control-group-object) | The requested control group. |
| `total_users` | integer | All users in the application. |
| `control_group_users` | integer | Users currently held out. |
| `calculation_status` | string | `TASK_STATUS_NOT_STARTED`, `TASK_STATUS_IN_PROGRESS`, or `TASK_STATUS_COMPLETED`. |
| `has_data` | boolean | Whether cached size numbers are available yet. |

## UpdatePercentage

Sets the hold-out percentage (1–20) of one control group. Resizing keeps every existing member: the hold-out grows or shrinks around them rather than being redrawn.

`POST` `/api/applications/{code}/control_groups/{control_group_code}`

<Aside type="caution">
A campaign can create its own hold-out sample, which is a control group too but doesn't appear in [List](#list). This endpoint, [UpdateCountries](#updatecountries), [Disable](#disable), [Reshuffle](#reshuffle), and [Delete](#delete) all refuse to touch one, returning `400 Bad Request` ("control group belongs to a campaign and cannot be changed") instead.
</Aside>

### Request body

| Parameter | Type | Required | Description |
| :---- | :---- | :---- | :---- |
| `percentage` | integer | Yes | New hold-out size as a percentage, 1–20. |

### Response

An empty object on success: `{}`.

## UpdateCountries

Sets the countries one control group is scoped to. Changing the scope restarts the uplift measurement, because the population being compared changes. The hold-out itself is not redrawn.

`POST` `/api/applications/{code}/control_groups/{control_group_code}/countries`

### Request body

| Parameter | Type | Required | Description |
| :---- | :---- | :---- | :---- |
| `countries` | array of strings | Yes | Lowercase ISO-3166-1 alpha-2 country codes. An empty list widens the group back to every country. Codes are case-insensitive on input. |

##### Request example

```json
{
  "countries": ["us", "ca", "gb"]
}
```

### Response

An empty object on success: `{}`.

## Disable

Switches one control group off, keeping it and its generation so that switching it back on restores the same hold-out rather than drawing a new one.

`POST` `/api/applications/{code}/control_groups/{control_group_code}/disable`

### Response

An empty object on success: `{}`.

## Reshuffle

Redraws one control group's hold-out by incrementing its generation. This is the only way to get a different sample: membership is deterministic, so disabling and re-enabling reproduces exactly the same one.

`POST` `/api/applications/{code}/control_groups/{control_group_code}/reshuffle`

<Aside type="caution">
Refuses a disabled group (`percentage` 0) with `400 Bad Request` ("control group is not enabled"). Reshuffling it would only move a hold-out nobody is in, and would silently consume a generation.
</Aside>

### Response

| Field | Type | Description |
| :---- | :---- | :---- |
| `generation` | integer | The group's generation after the reshuffle. |

## ForceUpdateCalculation

Starts a fresh count of one control group's size. The previously cached numbers keep being served until the new count finishes. Poll [GetCalculationStatus](#getcalculationstatus) for progress.

`POST` `/api/applications/{code}/control_groups/{control_group_code}/recalculate`

### Response

An empty object on success: `{}`.

## GetCalculationStatus

Polls just the changing user counts of one control group while a size calculation runs.

`GET` `/api/applications/{code}/control_groups/{control_group_code}/calculation_status`

### Response

| Field | Type | Description |
| :---- | :---- | :---- |
| `total_users` | integer | All users in the application. |
| `control_group_users` | integer | Users held out, counted over the application. |
| `calculation_status` | string | `TASK_STATUS_NOT_STARTED`, `TASK_STATUS_IN_PROGRESS`, or `TASK_STATUS_COMPLETED`. |
| `has_data` | boolean | Whether cached size numbers are available. |

## GetAnalytics

Returns the precomputed Control-vs-Treatment analytics for one control group.

<Aside type="note">
Only the default group (empty `name`) has analytics. The nightly job that computes it still measures one hold-out per application.
</Aside>

`GET` `/api/applications/{code}/control_groups/{control_group_code}/analytics`

### Query parameters

| Parameter | Type | Required | Description |
| :---- | :---- | :---- | :---- |
| `windowDays` | string | No | Lookback window preset: `WINDOW_DAYS_3`, `WINDOW_DAYS_7`, or `WINDOW_DAYS_30`. |

### Response

| Field | Type | Description |
| :---- | :---- | :---- |
| `events` | array of objects | One entry per tracked event, each with `event`, `treatment` and `control` (`users`, `conversions`, `conversion_rate`, `events_per_user`), `uplift_pct`, `incremental_events`, `percent_of_treatment`, `z_score`, `p_value`, `confidence_pct`, and `significance` (`SIGNIFICANCE_NOT_ENOUGH_DATA`, `SIGNIFICANCE_NOT_SIGNIFICANT`, or `SIGNIFICANCE_SIGNIFICANT`). |

## CheckControlGroupMembership

Reports for each user ID whether it is held out by one control group right now. Membership is computed from the ID alone. No user record is read, so an ID the application never saw is answered too, and a disabled group answers `false` for every ID rather than an error. Use this instead of exporting the whole group to check the users of a specific send or import.

`POST` `/api/applications/{code}/control_groups/{control_group_code}/membership`

### Request body

| Parameter | Type | Required | Description |
| :---- | :---- | :---- | :---- |
| `userIds` | array of strings | Yes | User IDs to check, at most 1,000 per call. |

##### Request example

```json
{
  "userIds": ["user-1", "user-2", "user-3"]
}
```

### Response

| Field | Type | Description |
| :---- | :---- | :---- |
| `users` | array of objects | One entry per requested ID, in the order they were given (duplicates included). Each has `user_id` (string) and `in_control_group` (boolean). |

##### Response example

```json
{
  "users": [
    { "user_id": "user-1", "in_control_group": false },
    { "user_id": "user-2", "in_control_group": true },
    { "user_id": "user-3", "in_control_group": false }
  ]
}
```

## Delete

Removes a control group entirely. Deleting the default one leaves the application without a fallback, so sends hold nobody out until a group is created again.

`DELETE` `/api/applications/{code}/control_groups/{control_group_code}`

<Aside type="caution">
This cannot be undone. To stop holding users out while keeping the group and its generation, [disable](#disable) it instead.
</Aside>

### Response

An empty object on success: `{}`.

## Control group object

| Field | Type | Description |
| :---- | :---- | :---- |
| `code` | string | Control group code (format `XXXXX-XXXXX`), stable for the life of the group. |
| `name` | string | Group name; empty is the application's original, unnamed (default) group. |
| `segment` | string | Seglang expression the group is measured over; empty is the whole base. |
| `percentage` | integer | Hold-out size as a percentage, 1–20. Zero means the group is off. |
| `enabled` | boolean | Whether the group is currently holding users out. |
| `generation` | integer | Incremented by every reshuffle; `0` means never reshuffled. |
| `last_modified_at` | string (RFC 3339) | When the group's settings were last changed. |
| `last_modified_by` | string | Email of the user who last changed the group. |
| `application_id` | integer | Numeric ID of the application, the first part of the membership key `<application_id>:<generation>:<name>:<user_id>` that [CheckControlGroupMembership](#checkcontrolgroupmembership) hashes to decide whether a user is held out. |
| `countries` | array of strings | Lowercase ISO-3166-1 alpha-2 country codes the hold-out is scoped to. Empty is every country. |

## Related

<CardGrid>
  <LinkCard title="Control groups (product guide)" href="/product/audience-data-and-segmentation/control-groups/" />
  <LinkCard title="Segmentation (Filters) API" href="/developer/api-reference/segmentation-filters-api/" />
</CardGrid>