Public API: Roles
A role is a named bundle of permissions you assign to members. Every org ships with five built-in roles (Owner, Admin, Developer, Collaborator, Viewer) and may define up to ten custom roles. Each role carries ten boolean permission flags (e.g. createExperiments, publishProduction, viewReports).
Roles are organization-scoped, so this API is org-level: the path carries no {orgId}, since the org is taken from your service token.
https://app.avsb.cloud/api/v1/rolesEvery endpoint here follows the shared conventions: the { data } envelope, an Idempotency-Key on every write below, and the standard error shape. Every /api/v1 token is rate-limited the same way everywhere: a scoped token gets 600 reads and 120 writes per minute, and an admin:* token gets 600 requests per minute, reads and writes together. Every endpoint below also needs two plan features at once: Integrations and API access, and Enterprise pack. Missing either one fails the call with 403 feature_disabled, even with a fully-permissioned token. See rate-limit headers and Refusals at 403.
The AvsB dashboard limits role management to organization admins. This API does not. A token carrying roles:write can create a custom role with every permission flag turned on, or change what an existing custom role is allowed to do. The Owner role itself can't be edited or deleted through this API (see below). But who holds the Owner role is a different action: it happens over the Members API instead, which has its own, separate protection. Keep roles:write off any token that should not change what other members are allowed to do.
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 roles, get one role | roles:read |
| Create, update, delete a role | roles:write |
The permission flags
Every role sets these ten booleans:
manageProjects, editProjectSettings, createExperiments, editCodeVariations, publishProduction, viewReports, exportData, manageSupport, manageIntegrations, viewFlagDrafts.
List roles
GET /api/v1/roles: every role in the org, built-in roles first then custom roles oldest-first. Cursor-paginated (?limit= up to 100, defaulting to 20; ?cursor=).
curl "https://app.avsb.cloud/api/v1/roles?limit=20" \ -H "Authorization: Bearer avsb_svc_..."const res = await fetch('https://app.avsb.cloud/api/v1/roles?limit=20', { headers: { Authorization: `Bearer ${process.env.AVSB_SERVICE_TOKEN}` },})const { data } = await res.json()import os, requestsres = requests.get( "https://app.avsb.cloud/api/v1/roles", params={"limit": 20}, headers={"Authorization": f"Bearer {os.environ['AVSB_SERVICE_TOKEN']}"},)data = res.json()["data"]{ "data": [ { "id": "<roleId>", "orgId": "<orgId>", "name": "Analyst", "description": "", "isBuiltIn": false, "icon": null, "createdAt": "2026-06-16T00:00:00.000Z", "manageProjects": false, "editProjectSettings": false, "createExperiments": true, "editCodeVariations": false, "publishProduction": false, "viewReports": true, "exportData": false, "manageSupport": false, "manageIntegrations": false, "viewFlagDrafts": true } ], "page": { "nextCursor": null, "hasMore": false }}{ "error": { "code": "pagination_invalid", "message": "cursor is malformed", "docUrl": "https://docs.avsb.cloud/docs/developer-reference/public-api/conventions#cursor-pagination", "requestId": "req_9f2c41ab7e0b4d1e8c35a6f0d2b91e77" }}Create a role
POST /api/v1/roles: create a custom role. name and all ten permission flags are required; description and icon are optional. name must be 2 to 30 characters. An org may hold at most ten custom roles (else 400).
The smallest working request sends just the name and the ten flags:
curl -X POST https://app.avsb.cloud/api/v1/roles \ -H "Authorization: Bearer avsb_svc_..." \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "name": "Analyst", "manageProjects": false, "editProjectSettings": false, "createExperiments": true, "editCodeVariations": false, "publishProduction": false, "viewReports": true, "exportData": false, "manageSupport": false, "manageIntegrations": false, "viewFlagDrafts": true }'const res = await fetch('https://app.avsb.cloud/api/v1/roles', { method: 'POST', headers: { Authorization: `Bearer ${process.env.AVSB_SERVICE_TOKEN}`, 'Content-Type': 'application/json', 'Idempotency-Key': crypto.randomUUID(), }, body: JSON.stringify({ name: 'Analyst', manageProjects: false, editProjectSettings: false, createExperiments: true, editCodeVariations: false, publishProduction: false, viewReports: true, exportData: false, manageSupport: false, manageIntegrations: false, viewFlagDrafts: true, }),})const { data } = await res.json()import os, uuid, requestsres = requests.post( "https://app.avsb.cloud/api/v1/roles", headers={ "Authorization": f"Bearer {os.environ['AVSB_SERVICE_TOKEN']}", "Idempotency-Key": str(uuid.uuid4()), }, json={ "name": "Analyst", "manageProjects": False, "editProjectSettings": False, "createExperiments": True, "editCodeVariations": False, "publishProduction": False, "viewReports": True, "exportData": False, "manageSupport": False, "manageIntegrations": False, "viewFlagDrafts": True, },)data = res.json()["data"]Add description and icon to that same body so the role is easy to recognise in the dashboard. The response always returns every field:
{ "data": { "id": "<roleId>", "orgId": "<orgId>", "name": "Analyst", "description": "Reads reports, cannot change anything live", "isBuiltIn": false, "icon": "chart-bar", "createdAt": "2026-06-16T00:00:00.000Z", "manageProjects": false, "editProjectSettings": false, "createExperiments": true, "editCodeVariations": false, "publishProduction": false, "viewReports": true, "exportData": false, "manageSupport": false, "manageIntegrations": false, "viewFlagDrafts": true }}{ "error": { "code": "validation_failed", "message": "Maximum of 10 custom roles per organization.", "docUrl": "https://docs.avsb.cloud/docs/developer-reference/public-api/conventions#validation-errors", "requestId": "req_9f2c41ab7e0b4d1e8c35a6f0d2b91e77" }}Get a role
GET /api/v1/roles/{roleId}: one role by id. A role outside your org returns 404.
curl https://app.avsb.cloud/api/v1/roles/<roleId> \ -H "Authorization: Bearer avsb_svc_..."const roleId = '<roleId>'const res = await fetch(`https://app.avsb.cloud/api/v1/roles/${roleId}`, { headers: { Authorization: `Bearer ${process.env.AVSB_SERVICE_TOKEN}` },})const { data } = await res.json()import os, requestsrole_id = "<roleId>"res = requests.get( f"https://app.avsb.cloud/api/v1/roles/{role_id}", headers={"Authorization": f"Bearer {os.environ['AVSB_SERVICE_TOKEN']}"},)data = res.json()["data"]Returns the same shape as one item in the list above.
{ "error": { "code": "not_found", "message": "Role not found", "docUrl": "https://docs.avsb.cloud/docs/developer-reference/public-api/conventions#not-found-errors", "requestId": "req_9f2c41ab7e0b4d1e8c35a6f0d2b91e77" }}Update a role
PATCH /api/v1/roles/{roleId}: partial update of permission flags (and, for custom roles, name / description / icon). Built-in roles accept permission changes only; renaming a built-in role returns 400. The Owner role cannot be modified (403).
curl -X PATCH https://app.avsb.cloud/api/v1/roles/<roleId> \ -H "Authorization: Bearer avsb_svc_..." \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $(uuidgen)" \ -d '{ "publishProduction": true }'const roleId = '<roleId>'const res = await fetch(`https://app.avsb.cloud/api/v1/roles/${roleId}`, { method: 'PATCH', headers: { Authorization: `Bearer ${process.env.AVSB_SERVICE_TOKEN}`, 'Content-Type': 'application/json', 'Idempotency-Key': crypto.randomUUID(), }, body: JSON.stringify({ publishProduction: true }),})const { data } = await res.json()import os, uuid, requestsrole_id = "<roleId>"res = requests.patch( f"https://app.avsb.cloud/api/v1/roles/{role_id}", headers={ "Authorization": f"Bearer {os.environ['AVSB_SERVICE_TOKEN']}", "Idempotency-Key": str(uuid.uuid4()), }, json={"publishProduction": True},)data = res.json()["data"]Returns the full role, the same shape as Create's response, with the changed field updated (publishProduction: true here).
Renaming a built-in role returns 400. A role outside your org returns 404, the same shape as Get a role above. Roles do not support conditional writes: a role read sends no ETag, and role updates are last-write-wins. See ETag / If-Match.
{ "error": { "code": "forbidden", "message": "Cannot modify Owner role", "docUrl": "https://docs.avsb.cloud/docs/developer-reference/public-api/conventions#refusals-at-403", "requestId": "req_9f2c41ab7e0b4d1e8c35a6f0d2b91e77" }}Delete a role
DELETE /api/v1/roles/{roleId}: delete a custom role. Members and pending invitations holding it are reassigned to the Viewer role. Built-in roles cannot be deleted (403).
curl -X DELETE https://app.avsb.cloud/api/v1/roles/<roleId> \ -H "Authorization: Bearer avsb_svc_..." \ -H "Idempotency-Key: $(uuidgen)"const roleId = '<roleId>'const res = await fetch(`https://app.avsb.cloud/api/v1/roles/${roleId}`, { method: 'DELETE', headers: { Authorization: `Bearer ${process.env.AVSB_SERVICE_TOKEN}`, 'Idempotency-Key': crypto.randomUUID(), },})const { data } = await res.json()import os, uuid, requestsrole_id = "<roleId>"res = requests.delete( f"https://app.avsb.cloud/api/v1/roles/{role_id}", headers={ "Authorization": f"Bearer {os.environ['AVSB_SERVICE_TOKEN']}", "Idempotency-Key": str(uuid.uuid4()), },)data = res.json()["data"]{ "data": { "id": "<roleId>", "deleted": true }}{ "error": { "code": "forbidden", "message": "Cannot delete built-in roles", "docUrl": "https://docs.avsb.cloud/docs/developer-reference/public-api/conventions#refusals-at-403", "requestId": "req_9f2c41ab7e0b4d1e8c35a6f0d2b91e77" }}