Public API: Audiences

An audience is a named, reusable group of visitors: who an experiment should run for. It is a tree of conditions (a condition group) that the snippet checks in the browser. An experiment attaches one or more audiences to decide who is eligible.

Audiences belong to your whole organization, not to one project, so this API is org-level: the path below carries no {orgId}, since your token already names its organization. An audience can optionally belong to a single project (set projectId when you create it); leave it off for an org-wide audience. Every endpoint here authenticates with a Bearer token: a service token or a personal access token.

Plain text
https://app.avsb.cloud/api/v1/audiences
Plain text1 line

Scopes

A scope is a named permission on your token. It decides exactly what that token is allowed to read or change.

OperationScope
List audiences, get one audienceaudiences:read
Create, update, delete an audienceaudiences:write

Every /api/v1 token is also rate-limited: a scoped token gets 600 reads and 120 writes per minute, and an admin:* token gets 600 requests per minute, reads and writes together. See Conventions for the response headers that show your remaining budget.

List audiences

GET /api/v1/audiences: every audience in the org, newest first. Cursor-paginated (?limit= up to 100, default 20; ?cursor=, the opaque value from page.nextCursor). Pass ?projectId= to narrow the list to that project's audiences plus the org-wide ones. Requires audiences:read.

curl "https://app.avsb.cloud/api/v1/audiences?limit=20" \  -H "Authorization: Bearer avsb_svc_..."
Shell2 lines
Response
{  "data": [    {      "id": "<audienceId>",      "shortId": 4,      "orgId": "<orgId>",      "projectId": "<projectId>",      "name": "Mobile visitors",      "description": null,      "rules": { "logic": "AND", "conditions": [] },      "experimentCount": 2,      "createdAt": "2026-06-16T00:00:00.000Z",      "updatedAt": "2026-06-16T00:00:00.000Z"    }  ],  "page": { "nextCursor": null, "hasMore": false }}
JSON17 lines

An out-of-range limit, or a cursor that is not a value this API produced, is refused rather than silently clamped:

400: limit out of range
{  "error": {    "code": "pagination_invalid",    "message": "limit must be an integer between 1 and 100",    "docUrl": "https://docs.avsb.cloud/docs/developer-reference/public-api/conventions#cursor-pagination",    "requestId": "req_9f2c41ab7e0b4d1e8c35a6f0d2b91e77"  }}
JSON8 lines

Create an audience

POST /api/v1/audiences: name and rules are both required. Returns the new audience, 201 Created. Requires audiences:write. Leave out description and it is stored as an empty string; leave out projectId and the audience is org-wide.

The example below sends every field, including the ones you can leave out. projectId places the audience in one project instead of the whole org; it must name a project your token can see. description is free text for your own team. rules is the required condition group described below.

curl -X POST https://app.avsb.cloud/api/v1/audiences \  -H "Authorization: Bearer avsb_svc_..." \  -H "Content-Type: application/json" \  -d '{    "name": "Mobile visitors",    "description": "iOS and Android traffic on the checkout flow",    "projectId": "<projectId>",    "rules": { "logic": "AND", "conditions": [{ "type": "DEVICE", "operator": "is", "value": ["mobile"] }] }  }'
Shell9 lines
Response
{  "data": {    "id": "<audienceId>",    "shortId": 6,    "orgId": "<orgId>",    "projectId": "<projectId>",    "name": "Mobile visitors",    "description": "iOS and Android traffic on the checkout flow",    "rules": {      "logic": "AND",      "conditions": [        { "type": "DEVICE", "operator": "is", "value": ["mobile"] }      ]    },    "experimentCount": 0,    "createdAt": "2026-06-16T00:00:00.000Z",    "updatedAt": "2026-06-16T00:00:00.000Z"  }}
JSON19 lines

An audience whose rules has no conditions matches every visitor, which is rarely what you meant to save, so the API refuses it unless you say so on purpose:

400: audience has no conditions
{  "error": {    "code": "validation_failed",    "message": "This audience has no conditions, so it matches every visitor. Add a condition, or send matchesEveryVisitorAcknowledged: true to save it as a match-everyone audience.",    "docUrl": "https://docs.avsb.cloud/docs/developer-reference/public-api/conventions#validation-errors",    "requestId": "req_9f2c41ab7e0b4d1e8c35a6f0d2b91e77"  }}
JSON8 lines

Send "matchesEveryVisitorAcknowledged": true alongside rules to save it anyway. The same check runs on PATCH, so clearing every condition off an existing audience needs the same flag.

A type outside the closed set below returns 400 validation_failed, with a details.issues[0].code of invalid_union and a message naming every value that is allowed. See the callout under Targeting rule types for the full behaviour.

The condition group shape

rules is a recursive tree. Every group has a logic (AND or OR) and a conditions array; each entry is either a leaf condition (type, operator, value) or a nested group. An empty group ({ "logic": "AND", "conditions": [] }) matches everyone, and saving one requires the acknowledgement flag described under Create an audience above.

Nest a group to express "on mobile, and in either the US or Canada":

A nested condition group
{  "logic": "AND",  "conditions": [    { "type": "DEVICE", "operator": "is", "value": ["mobile"] },    {      "logic": "OR",      "conditions": [        { "type": "LOCATION", "operator": "is", "value": ["US"] },        { "type": "LOCATION", "operator": "is", "value": ["CA"] }      ]    }  ]}
JSON13 lines
Warning

A group must carry a conditions array; the snippet walks it. A malformed rules value is rejected, because a bad audience can disable every experiment in the project.

Targeting rule types

type is a closed set, written in upper case. These are the only values accepted, and each one takes its own operators and value shape.

typeMatchesOperatorsvalue
DEVICEThe kind of device in useis, is_notArray of desktop, mobile, tablet
BROWSERThe browseris, is_notArray of chrome, firefox, safari, edge, opera, other
PLATFORMThe operating systemis, is_notArray of windows, macos, linux, ios, android, other
LOCATIONThe country, from the visitor's IP addressis, is_notArray of two-letter country codes, such as ["US", "CA"]
LANGUAGEThe browser's language settingis, is_notArray of two-letter language codes, such as ["es", "pt"]
NEW_RETURNINGFirst-time visitors against people who have been beforeis"new" or "returning"
COOKIEA cookie the browser is carryingequals, contains, exists, does_not_existOne string, "name:value", such as "plan:pro"
QUERY_PARAMA value in the page address after the question markequals, contains, exists, does_not_existOne string, "name:value", such as "utm_source:newsletter"
CUSTOM_JSWhatever a small piece of JavaScript returnsreturns_trueThe JavaScript source, as one string
CART_VALUEWhat is in the basket right nowgreater_than, gte, less_than, lte, betweenWhole number of minor units as a string, such as "10000". between takes two: ["5000", "10000"]
PRODUCTS_VIEWEDProducts the visitor has looked atviewed_any, not_viewed_anyArray of SKUs or category names. Add "match": "sku" or "category", and "window": "session" or "30d"
PURCHASE_HISTORYWhat the visitor has bought beforehas_purchased, has_not_purchased, last_purchase_older_than, lifetime_spend_gt, order_count_gtOne string. The first two take no value, so send ""
CUSTOM_ATTRIBUTEData you pass to a server-side or client-side SDK, such as plan or teamDepends on the attribute's own type: see Custom attribute conditionsOne string, or an array for in_list and not_in_list. Also send attribute, the attribute's key

The operators in the table are the only ones each type accepts. Anything else is refused with 400 validation_failed, and the issue lists the operators the type does take in options, at the operator field of the condition:

400: an operator the type does not take
{  "error": {    "code": "validation_failed",    "message": "Request body failed validation",    "details": {      "issues": [        {          "param": "rules.conditions.0.operator",          "path": ["rules", "conditions", 0, "operator"],          "code": "invalid_enum_value",          "message": "\"weird\" is not an operator a Device condition can use. Use one of: is, is_not.",          "received": "weird",          "options": ["is", "is_not"]        }      ]    },    "docUrl": "https://docs.avsb.cloud/docs/developer-reference/public-api/conventions#validation-errors",    "requestId": "req_9f2c41ab7e0b4d1e8c35a6f0d2b91e77"  }}
JSON20 lines

A few condition shapes are checked beyond the table above: value cannot be an empty array, a COOKIE or QUERY_PARAM condition cannot use a blank name before the colon, a CART_VALUE amount must parse as a whole number, with between's two amounts running low to high, a CUSTOM_ATTRIBUTE condition must name its attribute, and a regex pattern must be a valid regular expression. Each of these is refused with 400 validation_failed, and the message names exactly what to fix.

Device, browser, platform, language, cookie, query parameter, custom JavaScript, location and new/returning all read the browser, so they only work where the snippet runs. When a server-side SDK evaluates one of these, it does not error: the condition simply never matches, so the audience quietly excludes everyone. CUSTOM_ATTRIBUTE is the other way round: it matches data your own code passes to an SDK, so a website audience checked by the snippet has nothing to compare it against and never matches.

A type we do not recognise is refused

Anything outside the list above is rejected with 400 validation_failed, and the message names every value that is allowed. Lower case is not accepted either, so "device" fails where "DEVICE" succeeds.

This is deliberate. A rule the platform cannot read never matches anyone, so an audience holding one is not reaching the people it was set up for. An experiment using it then gets less traffic than expected, or none at all. Refusing the write is the only point at which that is still cheap to fix. If you have an audience created before this check existed, the dashboard now flags it: see Unrecognised targeting rules.

Custom attribute conditions

A CUSTOM_ATTRIBUTE condition compares a value your own code passes to an SDK, so it carries one more field than the others: attribute, the key of the attribute to compare, such as plan or country. Without it the condition can never match, so it is refused.

A custom attribute condition
{ "type": "CUSTOM_ATTRIBUTE", "attribute": "plan", "operator": "equals", "value": "pro" }
JSON1 line

The operators follow the attribute's type, as registered under Audiences > Attributes:

Attribute typeOperators
Stringequals, not_equals, contains, not_contains, starts_with, ends_with, regex, in_list, not_in_list, exists, not_exists
Numberequals, not_equals, greater_than, less_than, gte, lte, exists, not_exists
Booleanequals, not_equals, exists, not_exists
JSONcontains, not_contains, exists, not_exists

The API does not look up the attribute's type, so it accepts any operator from this table, plus four spellings every SDK reads the same way: is (as equals), is_not (as not_equals), does_not_contain and does_not_exist. in_list and not_in_list take an array in value. Text comparisons ignore upper and lower case. A regex value is compiled the way the SDKs compile it, and a pattern that does not compile is refused at value, because it could never match anyone.

Get an audience

GET /api/v1/audiences/{audienceId}: one audience by id. Requires audiences:read. An audience outside your org, or an id that does not exist, returns 404.

curl https://app.avsb.cloud/api/v1/audiences/<audienceId> \  -H "Authorization: Bearer avsb_svc_..."
Shell2 lines
Response
{  "data": {    "id": "<audienceId>",    "shortId": 4,    "orgId": "<orgId>",    "projectId": "<projectId>",    "name": "Mobile visitors",    "description": null,    "rules": { "logic": "AND", "conditions": [] },    "experimentCount": 2,    "createdAt": "2026-06-16T00:00:00.000Z",    "updatedAt": "2026-06-16T00:00:00.000Z"  }}
JSON14 lines
404: audience not found
{  "error": {    "code": "not_found",    "message": "Audience not found",    "docUrl": "https://docs.avsb.cloud/docs/developer-reference/public-api/conventions#not-found-errors",    "requestId": "req_9f2c41ab7e0b4d1e8c35a6f0d2b91e77"  }}
JSON8 lines

Update an audience

PATCH /api/v1/audiences/{audienceId}: partial update of name, description, rules, or projectId. Send only the fields you are changing; anything left out keeps its current value. Requires audiences:write. Changing the rules of an audience used by a running experiment re-publishes that project's snippet datafile (the small file the snippet downloads listing every live experiment, flag, and audience rule) automatically.

curl -X PATCH https://app.avsb.cloud/api/v1/audiences/<audienceId> \  -H "Authorization: Bearer avsb_svc_..." \  -H "Content-Type: application/json" \  -d '{ "name": "Mobile & tablet visitors" }'
Shell4 lines
Response
{  "data": {    "id": "<audienceId>",    "shortId": 4,    "orgId": "<orgId>",    "projectId": "<projectId>",    "name": "Mobile & tablet visitors",    "description": null,    "rules": { "logic": "AND", "conditions": [] },    "experimentCount": 2,    "createdAt": "2026-06-16T00:00:00.000Z",    "updatedAt": "2026-06-17T09:00:00.000Z"  }}
JSON14 lines

Send "projectId": null to detach an audience from its project and make it org-wide again.

Info

If you send a projectId that does not name a project your token can see, the response is the same 404 not_found shown above, worded "Audience not found" even though the audience itself was fine. The projectId was the problem.

Delete an audience

DELETE /api/v1/audiences/{audienceId}: hard-deletes the audience, detaching it from every experiment and feature flag rule that referenced it and re-publishing the datafile of any affected project. Requires audiences:write. If a running or scheduled experiment, or a flag rule that is running, ready, paused or concluded, still targets it, the delete is refused instead: the response comes back 409, listing everything in the way.

curl -X DELETE https://app.avsb.cloud/api/v1/audiences/<audienceId> \  -H "Authorization: Bearer avsb_svc_..."
Shell2 lines
Response
{  "data": { "id": "<audienceId>", "deleted": true }}
JSON3 lines

Deleting an audience that does not exist, or belongs to another org, returns the same 404 not_found shown under Get an audience above.

409: audience is still in use
{  "error": {    "code": "validation_failed",    "message": "Cannot delete \"Pro plan\": it is targeting 1 live experiment(s): Checkout redesign, and 1 flag rule(s): \"Pro users\" on New checkout (Production). Remove it from each one first. An experiment or flag rule left with no audience matches every visitor.",    "details": {      "reason": "audience_in_use",      "blockingExperiments": [        {          "id": "<experimentId>",          "shortId": 41,          "name": "Checkout redesign",          "status": "RUNNING",          "projectId": "<projectId>",          "projectShortId": 200007,          "projectName": "Storefront"        }      ],      "blockingFlagRules": [        {          "id": "<ruleId>",          "name": "Pro users",          "status": "RUNNING",          "flagId": "<flagId>",          "flagShortId": 12,          "flagKey": "new_checkout",          "flagName": "New checkout",          "environmentId": "<envId>",          "environmentName": "Production",          "projectId": "<projectId>",          "projectShortId": 200010,          "projectName": "Checkout flags"        }      ]    },    "docUrl": "https://docs.avsb.cloud/docs/developer-reference/public-api/conventions#validation-errors",    "requestId": "req_9f2c41ab7e0b4d1e8c35a6f0d2b91e77"  }}
JSON38 lines

details.reason is always audience_in_use for this refusal, so a script can tell it apart from other 409s without reading the message.

Warning

An audience still attached to a paused, draft, or completed experiment, or to a draft or archived flag rule, deletes cleanly and is detached from it. Only the experiments and flag rules listed above block the delete. A paused flag rule blocks it, unlike a paused experiment, because resuming the rule is one click and it would come back targeting every visitor. The dashboard shows a usage-aware confirmation either way, so confirm usage with GET first if you are scripting this.

Next steps

Was this helpful?