Public API: Exclusion groups
An exclusion group makes a set of experiments mutually exclusive: a visitor entering one experiment in the group is held out of the others. Each member experiment gets a contiguous slice of the group's 0–100% traffic range, so the slices never overlap.
All exclusion-group endpoints live under a project and authenticate with a service token. The org is taken from the token, so the path carries only {projectId} (and {groupId} / {experimentId} for nested resources), not {orgId}.
https://app.avsb.cloud/api/v1/projects/{projectId}/exclusion-groupsA group or experiment that isn't in the token's org and project returns 404; existence is hidden.
Scopes
A scope is a named permission on your token. It decides exactly what that token is allowed to read or change.
| Operation | Scope |
|---|---|
| List / get groups | experiments:read |
| Create / update / delete groups | experiments:write |
| Attach / detach experiments | experiments:write |
List exclusion groups
GET /api/v1/projects/{projectId}/exclusion-groups: newest-updated first, cursor-paginated. Each group carries allocatedPercent (the sum of its members' traffic) and a member count.
curl "https://app.avsb.cloud/api/v1/projects/<projectId>/exclusion-groups?limit=20" \ -H "Authorization: Bearer avsb_svc_..."const projectId = 'cm1a2b3c4d5e6f7g8h9i0j1k2'const url = `https://app.avsb.cloud/api/v1/projects/${projectId}/exclusion-groups?limit=20`const res = await fetch(url, { headers: { Authorization: `Bearer ${process.env.AVSB_SERVICE_TOKEN}` } })const { data, page } = await res.json()import os, requestsproject_id = "cm1a2b3c4d5e6f7g8h9i0j1k2"res = requests.get( f"https://app.avsb.cloud/api/v1/projects/{project_id}/exclusion-groups", params={"limit": 20}, headers={"Authorization": f"Bearer {os.environ['AVSB_SERVICE_TOKEN']}"},)data, page = res.json()["data"], res.json()["page"]{ "data": [ { "id": "<groupId>", "shortId": 1, "projectId": "<projectId>", "name": "Checkout funnel", "description": null, "createdAt": "2026-06-18T00:00:00.000Z", "updatedAt": "2026-06-18T00:00:00.000Z", "_count": { "experiments": 2 }, "allocatedPercent": 0.5 } ], "page": { "nextCursor": null, "hasMore": false }}Pass ?cursor=<page.nextCursor> to fetch the next page. limit defaults to 20, max 100. An out-of-range limit is refused rather than clamped: see Conventions for that error shape.
Get an exclusion group
GET /api/v1/projects/{projectId}/exclusion-groups/{groupId}: returns the group plus its membership rows (each with the assigned traffic slot), ordered by slot start.
curl https://app.avsb.cloud/api/v1/projects/<projectId>/exclusion-groups/<groupId> \ -H "Authorization: Bearer avsb_svc_..."const projectId = 'cm1a2b3c4d5e6f7g8h9i0j1k2'const groupId = 'clx3c4d5e6f7g8h9i0j1k2l3'const url = `https://app.avsb.cloud/api/v1/projects/${projectId}/exclusion-groups/${groupId}`const res = await fetch(url, { headers: { Authorization: `Bearer ${process.env.AVSB_SERVICE_TOKEN}` } })const { data } = await res.json()import os, requestsproject_id = "cm1a2b3c4d5e6f7g8h9i0j1k2"group_id = "clx3c4d5e6f7g8h9i0j1k2l3"res = requests.get( f"https://app.avsb.cloud/api/v1/projects/{project_id}/exclusion-groups/{group_id}", headers={"Authorization": f"Bearer {os.environ['AVSB_SERVICE_TOKEN']}"},)data = res.json()["data"]{ "data": { "id": "<groupId>", "shortId": 1, "projectId": "<projectId>", "name": "Checkout funnel", "description": null, "createdAt": "2026-06-18T00:00:00.000Z", "updatedAt": "2026-06-18T00:00:00.000Z", "experiments": [ { "id": "<membershipId>", "exclusionGroupId": "<groupId>", "experimentId": "<experimentId>", "trafficPercent": 0.5, "rangeStart": 0, "rangeEnd": 4999, "createdAt": "2026-06-18T00:00:00.000Z", "experiment": { "id": "<experimentId>", "shortId": 5, "name": "Hero", "status": "DRAFT" } } ] }}A group id from another org or project, or one that never existed, returns 404 not_found.
Create an exclusion group
POST /api/v1/projects/{projectId}/exclusion-groups: name is the only required field. Requires experiments:write.
curl -X POST https://app.avsb.cloud/api/v1/projects/<projectId>/exclusion-groups \ -H "Authorization: Bearer avsb_svc_..." \ -H "Content-Type: application/json" \ -d '{ "name": "Checkout funnel" }'const projectId = 'cm1a2b3c4d5e6f7g8h9i0j1k2'const res = await fetch(`https://app.avsb.cloud/api/v1/projects/${projectId}/exclusion-groups`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.AVSB_SERVICE_TOKEN}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ name: 'Checkout funnel' }),})const { data } = await res.json()import os, requestsproject_id = "cm1a2b3c4d5e6f7g8h9i0j1k2"res = requests.post( f"https://app.avsb.cloud/api/v1/projects/{project_id}/exclusion-groups", headers={"Authorization": f"Bearer {os.environ['AVSB_SERVICE_TOKEN']}"}, json={"name": "Checkout funnel"},)data = res.json()["data"]{ "data": { "id": "<groupId>", "shortId": 1, "projectId": "<projectId>", "name": "Checkout funnel", "description": null, "createdAt": "2026-06-18T00:00:00.000Z", "updatedAt": "2026-06-18T00:00:00.000Z", "_count": { "experiments": 0 }, "allocatedPercent": 0 }}Add description (up to 2000 characters) for your own team's context. Leave it out and it comes back as null, not an empty string; the shape is otherwise identical to the request above:
curl -X POST https://app.avsb.cloud/api/v1/projects/<projectId>/exclusion-groups \ -H "Authorization: Bearer avsb_svc_..." \ -H "Content-Type: application/json" \ -d '{ "name": "Checkout funnel", "description": "Mutually exclusive checkout tests" }'Responds 201 either way. A new group has no members, so its allocatedPercent is always 0. A project id from another org returns 404 not_found.
Update an exclusion group
PATCH /api/v1/projects/{projectId}/exclusion-groups/{groupId}: rename or re-describe a group.
Only name and description are mutable. Mutations support X-Avsb-If-Match (or If-Match) for optimistic concurrency.
curl -X PATCH https://app.avsb.cloud/api/v1/projects/<projectId>/exclusion-groups/<groupId> \ -H "Authorization: Bearer avsb_svc_..." \ -H "Content-Type: application/json" \ -d '{ "name": "Checkout funnel v2" }'const projectId = 'cm1a2b3c4d5e6f7g8h9i0j1k2'const groupId = 'clx3c4d5e6f7g8h9i0j1k2l3'const url = `https://app.avsb.cloud/api/v1/projects/${projectId}/exclusion-groups/${groupId}`const res = await fetch(url, { method: 'PATCH', headers: { Authorization: `Bearer ${process.env.AVSB_SERVICE_TOKEN}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ name: 'Checkout funnel v2' }),})const { data } = await res.json()import os, requestsproject_id = "cm1a2b3c4d5e6f7g8h9i0j1k2"group_id = "clx3c4d5e6f7g8h9i0j1k2l3"res = requests.patch( f"https://app.avsb.cloud/api/v1/projects/{project_id}/exclusion-groups/{group_id}", headers={"Authorization": f"Bearer {os.environ['AVSB_SERVICE_TOKEN']}"}, json={"name": "Checkout funnel v2"},)data = res.json()["data"]{ "data": { "id": "<groupId>", "shortId": 1, "projectId": "<projectId>", "name": "Checkout funnel v2", "description": null, "createdAt": "2026-06-18T00:00:00.000Z", "updatedAt": "2026-06-19T08:30:00.000Z" }}A group id from another org or project, or one that never existed, returns the same 404 not_found shown under Get an exclusion group above.
Delete an exclusion group
DELETE /api/v1/projects/{projectId}/exclusion-groups/{groupId}: returns the removed id.
curl -X DELETE https://app.avsb.cloud/api/v1/projects/<projectId>/exclusion-groups/<groupId> \ -H "Authorization: Bearer avsb_svc_..."const projectId = 'cm1a2b3c4d5e6f7g8h9i0j1k2'const groupId = 'clx3c4d5e6f7g8h9i0j1k2l3'const url = `https://app.avsb.cloud/api/v1/projects/${projectId}/exclusion-groups/${groupId}`const res = await fetch(url, { method: 'DELETE', headers: { Authorization: `Bearer ${process.env.AVSB_SERVICE_TOKEN}` },})const { data } = await res.json()import os, requestsproject_id = "cm1a2b3c4d5e6f7g8h9i0j1k2"group_id = "clx3c4d5e6f7g8h9i0j1k2l3"res = requests.delete( f"https://app.avsb.cloud/api/v1/projects/{project_id}/exclusion-groups/{group_id}", headers={"Authorization": f"Bearer {os.environ['AVSB_SERVICE_TOKEN']}"},)data = res.json()["data"]{ "data": { "id": "<groupId>", "removed": true } }A group must be empty before it can be deleted. Detach every experiment first, otherwise the call returns 400.
Attach an experiment
POST /api/v1/projects/{projectId}/exclusion-groups/{groupId}/experiments: assign an experiment a contiguous traffic slot inside the group. trafficPercent is a fraction in (0, 1].
curl -X POST https://app.avsb.cloud/api/v1/projects/<projectId>/exclusion-groups/<groupId>/experiments \ -H "Authorization: Bearer avsb_svc_..." \ -H "Content-Type: application/json" \ -d '{ "experimentId": "<experimentId>", "trafficPercent": 0.5 }'const projectId = 'cm1a2b3c4d5e6f7g8h9i0j1k2'const groupId = 'clx3c4d5e6f7g8h9i0j1k2l3'const url = `https://app.avsb.cloud/api/v1/projects/${projectId}/exclusion-groups/${groupId}/experiments`const res = await fetch(url, { method: 'POST', headers: { Authorization: `Bearer ${process.env.AVSB_SERVICE_TOKEN}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ experimentId: 'clx4d5e6f7g8h9i0j1k2l3m4', trafficPercent: 0.5 }),})const { data } = await res.json()import os, requestsproject_id = "cm1a2b3c4d5e6f7g8h9i0j1k2"group_id = "clx3c4d5e6f7g8h9i0j1k2l3"res = requests.post( f"https://app.avsb.cloud/api/v1/projects/{project_id}/exclusion-groups/{group_id}/experiments", headers={"Authorization": f"Bearer {os.environ['AVSB_SERVICE_TOKEN']}"}, json={"experimentId": "clx4d5e6f7g8h9i0j1k2l3m4", "trafficPercent": 0.5},)data = res.json()["data"]Responds 201 with the new membership row, including the assigned rangeStart/rangeEnd.
{ "data": { "id": "<membershipId>", "exclusionGroupId": "<groupId>", "experimentId": "<experimentId>", "trafficPercent": 0.5, "rangeStart": 0, "rangeEnd": 4999, "createdAt": "2026-06-18T00:00:00.000Z", "experiment": { "id": "<experimentId>", "shortId": 5, "name": "Hero", "status": "DRAFT" } }}The call returns 400 when:
- the experiment is
RUNNING(pause it first) orCOMPLETED(a finished experiment can never be added to a group), - it is already in another group,
- the total allocated traffic would exceed 100%, or
- there is no contiguous slot large enough (remove an experiment to defragment).
Detach an experiment
DELETE /api/v1/projects/{projectId}/exclusion-groups/{groupId}/experiments/{experimentId}: remove an experiment from the group and free its slot.
curl -X DELETE https://app.avsb.cloud/api/v1/projects/<projectId>/exclusion-groups/<groupId>/experiments/<experimentId> \ -H "Authorization: Bearer avsb_svc_..."const projectId = 'cm1a2b3c4d5e6f7g8h9i0j1k2'const groupId = 'clx3c4d5e6f7g8h9i0j1k2l3'const experimentId = 'clx4d5e6f7g8h9i0j1k2l3m4'const url = `https://app.avsb.cloud/api/v1/projects/${projectId}/exclusion-groups/${groupId}/experiments/${experimentId}`const res = await fetch(url, { method: 'DELETE', headers: { Authorization: `Bearer ${process.env.AVSB_SERVICE_TOKEN}` },})const { data } = await res.json()import os, requestsproject_id = "cm1a2b3c4d5e6f7g8h9i0j1k2"group_id = "clx3c4d5e6f7g8h9i0j1k2l3"experiment_id = "clx4d5e6f7g8h9i0j1k2l3m4"res = requests.delete( f"https://app.avsb.cloud/api/v1/projects/{project_id}/exclusion-groups/{group_id}/experiments/{experiment_id}", headers={"Authorization": f"Bearer {os.environ['AVSB_SERVICE_TOKEN']}"},)data = res.json()["data"]{ "data": { "experimentId": "<experimentId>", "removed": true } }A RUNNING experiment cannot be detached; pause it first. A membership row that does not exist, whether the group, the experiment, or the pairing between them is wrong, returns 404 not_found.