Skip to content

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.

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

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

Authentication

Anchor link to

Every request must include an Authorization header with your Server API token:

Authorization: Api YOUR_API_TOKEN

Conventions

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, in snake_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 on Create. Pass this code as controlGroupCode to Get, UpdatePercentage, UpdateCountries, Rename, Disable, Reshuffle, ForceUpdateCalculation, GetCalculationStatus, GetAnalytics, and CheckControlGroupMembership.
  • 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 name is the application’s original group and works the same way.

Error responses

Anchor link to
HTTP statusMeaning
400 Bad RequestInvalid 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 UnauthorizedMissing or invalid Authorization header.
403 ForbiddenThe application or control group does not belong to the caller’s account.
404 Not FoundThe control group or application was not found.
409 ConflictCreate used a name that already exists in the application.
500 Internal Server ErrorUnexpected server-side failure.
MethodPathDescription
GET/api/applications/{code}/control_groupsList an application’s control groups
POST/api/applications/{code}/control_groupsCreate 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}/countriesRescope a control group to a set of countries
POST/api/applications/{code}/control_groups/{control_group_code}/settingsApply size, country and tag scope, and lifetime mode in one call
POST/api/applications/{code}/control_groups/{control_group_code}/display_nameRename a control group
POST/api/applications/{code}/control_groups/{control_group_code}/disableDisable a control group
POST/api/applications/{code}/control_groups/{control_group_code}/reshuffleReshuffle a control group
POST/api/applications/{code}/control_groups/{control_group_code}/recalculateForce-recalculate a control group’s size
GET/api/applications/{code}/control_groups/{control_group_code}/calculation_statusPoll a running size calculation
GET/api/applications/{code}/control_groups/{control_group_code}/analyticsGet Control-vs-Treatment analytics
GET/api/applications/{code}/control_groups/{control_group_code}/cyclesList the group’s closed cycles
POST/api/applications/{code}/control_groups/{control_group_code}/membershipCheck membership for a batch of user IDs
DELETE/api/applications/{code}/control_groups/{control_group_code}Delete a control group

Lists every control group configured for an application, the unnamed original one first.

GET /api/applications/{code}/control_groups

Path parameters

Anchor link to
ParameterTypeDescription
codestringThe application code to list control groups for.
FieldTypeDescription
control_groupsarray of Control group objectsEvery control group configured for the application.

Creates 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
ParameterTypeRequiredDescription
codestringYesThe application code to create the group in.
namestringYesGroup name, up to 64 characters and without a colon. Must be unique within the application. Part of the membership key, so it never changes.
percentageintegerYesHold-out size as a percentage, 1–20.
segmentstringNoSeglang expression to measure the group over. Omit to measure over the whole base.
countriesarray of stringsNoLowercase ISO-3166-1 alpha-2 country codes to scope the hold-out to. Omit for every country. Codes are case-insensitive on input.
scopeTagstringNoA string or boolean tag to scope the hold-out to, AND-ed with countries. Omit for no tag scope. See Tag scope below.
scopeValuesarray of stringsSee noteValues of scopeTag that put a user in scope. Required if scopeTag is set, and must be empty otherwise.
displayNamestringNoName 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
}

Returns { "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
ParameterTypeDescription
codestringThe application code the group belongs to.
control_group_codestringThe control group’s code.
FieldTypeDescription
groupControl group objectThe requested control group.
total_usersintegerAll users in the application.
control_group_usersintegerUsers currently held out.
calculation_statusstringTASK_STATUS_NOT_STARTED, TASK_STATUS_IN_PROGRESS, or TASK_STATUS_COMPLETED.
has_databooleanWhether cached size numbers are available yet.

UpdatePercentage

Anchor link to

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. 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
ParameterTypeRequiredDescription
percentageintegerYesNew hold-out size as a percentage, 1–20.

An empty object on success: {}.

UpdateCountries

Anchor link to

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. 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
ParameterTypeRequiredDescription
countriesarray of stringsYesLowercase 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"]
}

An empty object on success: {}.

UpdateSettings

Anchor link to

Applies 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
ParameterTypeRequiredDescription
percentageintegerYesHold-out size as a percentage, 1–20.
countriesarray of stringsNoLowercase ISO-3166-1 alpha-2 country codes. An empty list widens the group back to every country.
scopeTagstringNoA string or boolean tag to scope the hold-out to, AND-ed with countries. Empty removes the tag scope. See Tag scope below.
scopeValuesarray of stringsSee noteValues of scopeTag that put a user in scope. Required if scopeTag is set, and must be empty otherwise.
modestringNoLifetime of the membership: CONTROL_GROUP_MODE_PERMANENT (default), CONTROL_GROUP_MODE_AUTO_REFRESH, or CONTROL_GROUP_MODE_EXPERIMENT.
refreshPeriodDaysintegerSee noteDays between redraws, 7–365. Required with CONTROL_GROUP_MODE_AUTO_REFRESH, and must be omitted otherwise.
endsAtstring (RFC 3339)See noteWhen 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.

Returns { "group": { ... } }, the updated Control group object.

A 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.

  • scopeTag names the tag; empty means no tag scope. It can’t be Country: scope by country uses countries, not a tag.
  • scopeValues lists which values of scopeTag are in scope. At least one is required when scopeTag is 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 scopeTag is out of scope, the same as a device with no Country tag.
  • 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 changing scopeValues as a set (reordering alone doesn’t count), closes the running cycle with CONTROL_GROUP_CYCLE_CLOSE_REASON_RESCOPED, the same as changing countries. Disabling a group keeps its tag scope, as it already keeps countries.

Sets 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
ParameterTypeRequiredDescription
codestringYesThe application code the group belongs to.
controlGroupCodestringYesThe control group’s code.
displayNamestringNoNew name, up to 64 characters and unique within the application. Send an empty string to show name again.

Returns { "group": { ... } }, the renamed Control group object.

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

An empty object on success: {}.

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

FieldTypeDescription
generationintegerThe group’s generation after the reshuffle.

ForceUpdateCalculation

Anchor link to

Starts 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

An empty object on success: {}.

GetCalculationStatus

Anchor link to

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

FieldTypeDescription
total_usersintegerAll users in the application.
control_group_usersintegerUsers held out, counted over the application.
calculation_statusstringTASK_STATUS_NOT_STARTED, TASK_STATUS_IN_PROGRESS, or TASK_STATUS_COMPLETED.
has_databooleanWhether cached size numbers are available.

GetAnalytics

Anchor link to

Returns 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
ParameterTypeRequiredDescription
windowDaysstringNoLookback window preset: WINDOW_DAYS_3, WINDOW_DAYS_7, or WINDOW_DAYS_30.
FieldTypeDescription
eventsarray of objectsOne 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 to

Lists 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

FieldTypeDescription
cyclesarray of Control group cycle objectsNewest first.

Control group cycle object

Anchor link to
FieldTypeDescription
cycle_numberintegerSequential within the group; the group’s own cycle_number is the one after the last closed here.
modestringLifetime mode the group ran in during this cycle: CONTROL_GROUP_MODE_PERMANENT, CONTROL_GROUP_MODE_AUTO_REFRESH, or CONTROL_GROUP_MODE_EXPERIMENT.
generationintegerThe group’s generation during this cycle.
percentageintegerHold-out size during this cycle.
countriesarray of stringsCountry scope during this cycle; empty is every country.
started_at / ended_atstring (RFC 3339)When this cycle ran.
close_reasonstringWhy the cycle ended: CONTROL_GROUP_CYCLE_CLOSE_REASON_ROLLOVER, _EXPIRED, _RESHUFFLED, _RESIZED, _RESCOPED, _MODE_CHANGED, or _DISABLED.
scope_tagstringTag 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_valuesarray of stringsValues of scope_tag that put a user in scope during this cycle; only set together with scope_tag.

CheckControlGroupMembership

Anchor link to

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. 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
ParameterTypeRequiredDescription
userIdsarray of stringsYesUser IDs to check, at most 1,000 per call.
Request example
Anchor link to
{
"userIds": ["user-1", "user-2", "user-3"]
}
FieldTypeDescription
usersarray of objectsOne 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 }
]
}

Removes 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}

An empty object on success: {}.

Control group object

Anchor link to
FieldTypeDescription
codestringControl group code (format XXXXX-XXXXX), stable for the life of the group.
namestringGroup name, part of the membership key. Empty is the application’s original, unnamed group.
display_namestringName the Control Panel shows. Empty shows name, and the unnamed group as Global.
segmentstringSeglang expression the group is measured over; empty is the whole base.
percentageintegerHold-out size as a percentage, 1–20. Zero means the group is off.
enabledbooleanWhether the group is currently holding users out.
generationintegerIncremented by every reshuffle; 0 means never reshuffled.
last_modified_atstring (RFC 3339)When the group’s settings were last changed.
last_modified_bystringEmail of the user who last changed the group.
application_idintegerNumeric 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.
countriesarray of stringsLowercase ISO-3166-1 alpha-2 country codes the hold-out is scoped to. Empty is every country.
scope_tagstringTag the hold-out is scoped to, AND-ed with countries; empty is no tag scope. See Tag scope.
scope_valuesarray of stringsValues of scope_tag that put a user in scope; a boolean tag uses "true" and "false".