Control Groups API
A control group 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
Anchor link tohttps://rpc-api.svc-nue.pushwoosh.comAll endpoints are served over HTTPS. Requests and responses use application/json unless noted otherwise.
Authentication
Anchor link toEvery request must include an Authorization header with your Server API token:
Authorization: Api YOUR_API_TOKENConventions
Anchor link to- 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, insnake_case(application_id,in_control_group, and so on). The response examples and the Control group object reference below use that casing. code: every control group response carries its own code, generated onCreate. Pass this code ascontrolGroupCodetoGet,UpdatePercentage,UpdateCountries,Rename,Disable,Reshuffle,ForceUpdateCalculation,GetCalculationStatus,GetAnalytics, andCheckControlGroupMembership.- Several groups: an application can have several control groups. Every enabled group holds users out of every marketing send within its own countries and tag, independently of the other groups, and groups may overlap. A send doesn’t select a group. The group with an empty
nameis the application’s original group and works the same way.
Error responses
Anchor link to| HTTP status | Meaning |
|---|---|
400 Bad Request | Invalid argument, such as percentage outside 1–20, userIds empty or over 1,000 entries, an unrecognized country code, scopeValues set without scopeTag, scopeTag naming a tag the account doesn’t have, or a scopeValues entry UpdateSettings rejects (see below). Also returned (as a FailedPrecondition on the wire) by UpdatePercentage, UpdateCountries, UpdateSettings, Disable, Reshuffle, and Delete on a control group that belongs to a campaign’s own hold-out, per the caution 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
Anchor link to| 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}/settings | Apply size, country and tag scope, and lifetime mode in one call |
POST | /api/applications/{code}/control_groups/{control_group_code}/display_name | Rename a control group |
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 |
GET | /api/applications/{code}/control_groups/{control_group_code}/cycles | List the group’s closed cycles |
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
Anchor link toLists every control group configured for an application, the unnamed original one first.
GET /api/applications/{code}/control_groups
Path parameters
Anchor link to| Parameter | Type | Description |
|---|---|---|
code | string | The application code to list control groups for. |
Response
Anchor link to| Field | Type | Description |
|---|---|---|
control_groups | array of Control group objects | Every control group configured for the application. |
Create
Anchor link toCreates a named control group for an application and returns it with its generated code. The new group holds users out of every marketing send within its own countries and tag, alongside the application’s other enabled groups.
POST /api/applications/{code}/control_groups
Request body
Anchor link to| 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. Part of the membership key, so it never changes. |
percentage | integer | Yes | Hold-out size as a percentage, 1–20. |
segment | string | No | Seglang 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. |
scopeTag | string | No | A string or boolean tag to scope the hold-out to, AND-ed with countries. Omit for no tag scope. See Tag scope below. |
scopeValues | array of strings | See note | Values of scopeTag that put a user in scope. Required if scopeTag is set, and must be empty otherwise. |
displayName | string | No | Name the Control Panel shows, up to 64 characters. Omit to show name. |
Request example
Anchor link to{ "code": "XXXXX-XXXXX", "name": "Q3 holdout", "percentage": 10}Response
Anchor link toReturns { "group": { ... } }, the new Control group object.
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
Anchor link to| Parameter | Type | Description |
|---|---|---|
code | string | The application code the group belongs to. |
control_group_code | string | The control group’s code. |
Response
Anchor link to| Field | Type | Description |
|---|---|---|
group | 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
Anchor link toSets 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. Superseded by UpdateSettings, which applies size, scope, and lifetime mode in one call, but still supported.
POST /api/applications/{code}/control_groups/{control_group_code}
Request body
Anchor link to| Parameter | Type | Required | Description |
|---|---|---|---|
percentage | integer | Yes | New hold-out size as a percentage, 1–20. |
Response
Anchor link toAn empty object on success: {}.
UpdateCountries
Anchor link toSets 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. Superseded by UpdateSettings, which also sets a tag scope, but still supported.
POST /api/applications/{code}/control_groups/{control_group_code}/countries
Request body
Anchor link to| 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
Anchor link to{ "countries": ["us", "ca", "gb"]}Response
Anchor link toAn empty object on success: {}.
UpdateSettings
Anchor link toApplies a control group’s size, country and tag scope, and lifetime mode in one call. A change that alters who is held out or how long for (size, scope, mode, refresh period, or end date) closes the running cycle and starts a new one. Sending the current values changes nothing. Refuses a control group that belongs to a campaign’s own hold-out, the same way as UpdatePercentage.
POST /api/applications/{code}/control_groups/{control_group_code}/settings
Request body
Anchor link to| Parameter | Type | Required | Description |
|---|---|---|---|
percentage | integer | Yes | Hold-out size as a percentage, 1–20. |
countries | array of strings | No | Lowercase ISO-3166-1 alpha-2 country codes. An empty list widens the group back to every country. |
scopeTag | string | No | A string or boolean tag to scope the hold-out to, AND-ed with countries. Empty removes the tag scope. See Tag scope below. |
scopeValues | array of strings | See note | Values of scopeTag that put a user in scope. Required if scopeTag is set, and must be empty otherwise. |
mode | string | No | Lifetime of the membership: CONTROL_GROUP_MODE_PERMANENT (default), CONTROL_GROUP_MODE_AUTO_REFRESH, or CONTROL_GROUP_MODE_EXPERIMENT. |
refreshPeriodDays | integer | See note | Days between redraws, 7–365. Required with CONTROL_GROUP_MODE_AUTO_REFRESH, and must be omitted otherwise. |
endsAt | string (RFC 3339) | See note | When an experiment switches off, at least 30 days ahead. Required with CONTROL_GROUP_MODE_EXPERIMENT, and must be omitted otherwise. |
Every field is applied as sent, the same as UpdateCountries: an empty countries widens the group back to every country, and an empty scopeTag removes the tag scope.
Response
Anchor link toReturns { "group": { ... } }, the updated Control group object.
Tag scope
Anchor link toA control group can hold out only users whose value of one string or boolean tag is in a set you choose, AND-ed with countries if both are set. Set it with Create or UpdateSettings.
scopeTagnames the tag; empty means no tag scope. It can’t beCountry: scope by country usescountries, not a tag.scopeValueslists which values ofscopeTagare in scope. At least one is required whenscopeTagis set, and none can repeat. A string tag’s values can’t be empty strings. A boolean tag’s values must each be"true"or"false".- A device with no value for
scopeTagis out of scope, the same as a device with noCountrytag. - Membership itself doesn’t change: the formula that assigns users is untouched by scope. Tag and country scope are both a per-device check on top of it, narrowing which of a selected user’s devices are actually excluded, not who the formula selects.
- A user-level tag is copied onto every one of that user’s devices when it’s set, so the device-level check above already covers user-level tags too, not only device-level ones.
- Changing
scopeTag, or changingscopeValuesas a set (reordering alone doesn’t count), closes the running cycle withCONTROL_GROUP_CYCLE_CLOSE_REASON_RESCOPED, the same as changingcountries. Disabling a group keeps its tag scope, as it already keepscountries.
Rename
Anchor link toSets the name the Control Panel shows for one control group. name is part of the membership key and doesn’t change, so the group keeps the same users and its running cycle.
POST /api/applications/{code}/control_groups/{control_group_code}/display_name
Request body
Anchor link to| Parameter | Type | Required | Description |
|---|---|---|---|
code | string | Yes | The application code the group belongs to. |
controlGroupCode | string | Yes | The control group’s code. |
displayName | string | No | New name, up to 64 characters and unique within the application. Send an empty string to show name again. |
Response
Anchor link toReturns { "group": { ... } }, the renamed Control group object.
Disable
Anchor link toSwitches 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
Anchor link toAn empty object on success: {}.
Reshuffle
Anchor link toRedraws 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
Response
Anchor link to| Field | Type | Description |
|---|---|---|
generation | integer | The group’s generation after the reshuffle. |
ForceUpdateCalculation
Anchor link toStarts a fresh count of one control group’s size. The previously cached numbers keep being served until the new count finishes. Poll GetCalculationStatus for progress.
POST /api/applications/{code}/control_groups/{control_group_code}/recalculate
Response
Anchor link toAn empty object on success: {}.
GetCalculationStatus
Anchor link toPolls 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
Anchor link to| 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
Anchor link toReturns the precomputed Control-vs-Treatment analytics for one control group.
GET /api/applications/{code}/control_groups/{control_group_code}/analytics
Query parameters
Anchor link to| Parameter | Type | Required | Description |
|---|---|---|---|
windowDays | string | No | Lookback window preset: WINDOW_DAYS_3, WINDOW_DAYS_7, or WINDOW_DAYS_30. |
Response
Anchor link to| 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). |
ListControlGroupCycles
Anchor link toLists the closed cycles of one control group, newest first. Each cycle is the membership the group ran with between two settings changes, together with the settings it was drawn from. The running (current) cycle isn’t in this list. Its settings are on the Control group object itself.
GET /api/applications/{code}/control_groups/{control_group_code}/cycles
Response
Anchor link to| Field | Type | Description |
|---|---|---|
cycles | array of Control group cycle objects | Newest first. |
Control group cycle object
Anchor link to| Field | Type | Description |
|---|---|---|
cycle_number | integer | Sequential within the group; the group’s own cycle_number is the one after the last closed here. |
mode | string | Lifetime mode the group ran in during this cycle: CONTROL_GROUP_MODE_PERMANENT, CONTROL_GROUP_MODE_AUTO_REFRESH, or CONTROL_GROUP_MODE_EXPERIMENT. |
generation | integer | The group’s generation during this cycle. |
percentage | integer | Hold-out size during this cycle. |
countries | array of strings | Country scope during this cycle; empty is every country. |
started_at / ended_at | string (RFC 3339) | When this cycle ran. |
close_reason | string | Why the cycle ended: CONTROL_GROUP_CYCLE_CLOSE_REASON_ROLLOVER, _EXPIRED, _RESHUFFLED, _RESIZED, _RESCOPED, _MODE_CHANGED, or _DISABLED. |
scope_tag | string | Tag the cycle’s hold-out was scoped to, AND-ed with countries. Empty for no tag scope and for every cycle closed before tag scope existed, even if the group had one later. |
scope_values | array of strings | Values of scope_tag that put a user in scope during this cycle; only set together with scope_tag. |
CheckControlGroupMembership
Anchor link toReports 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. A group with a country or tag scope holds a user out only if one of their devices is in that scope, so an unseen ID (no device at all) comes back false there even though the same ID would come back true on an unscoped group. 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
Anchor link to| Parameter | Type | Required | Description |
|---|---|---|---|
userIds | array of strings | Yes | User IDs to check, at most 1,000 per call. |
Request example
Anchor link to{ "userIds": ["user-1", "user-2", "user-3"]}Response
Anchor link to| 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
Anchor link to{ "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
Anchor link toRemoves a control group entirely. It stops holding users out, and its statistics can no longer be opened. The application’s other groups keep working.
DELETE /api/applications/{code}/control_groups/{control_group_code}
Response
Anchor link toAn empty object on success: {}.
Control group object
Anchor link to| Field | Type | Description |
|---|---|---|
code | string | Control group code (format XXXXX-XXXXX), stable for the life of the group. |
name | string | Group name, part of the membership key. Empty is the application’s original, unnamed group. |
display_name | string | Name the Control Panel shows. Empty shows name, and the unnamed group as Global. |
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 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. |
scope_tag | string | Tag the hold-out is scoped to, AND-ed with countries; empty is no tag scope. See Tag scope. |
scope_values | array of strings | Values of scope_tag that put a user in scope; a boolean tag uses "true" and "false". |