Feature Flags API Reference
You can manage flags from the dashboard, or from your own code with the public REST API and a token. This page is a quick map: what each endpoint does, and which scope it needs. A scope is a named permission on your token that controls exactly what it can read or change. Two other pages carry the full detail, with a worked example in cURL, TypeScript, and Python for every operation:
Base URL and authentication
Every endpoint sits under a project and needs a bearer token: a service token or a personal access token. The token names your organization, so the path only carries a project id, never an org id.
https://app.avsb.cloud/api/v1/projects/{projectId}/flagsThe response shape
A successful call returns { "data": ... }. A list also returns a page object for pagination. A failed call returns an error object, not just a plain message string, so your code can branch on error.code:
{ "error": { "code": "validation_failed", "message": "A flag with this key already exists in this project", "docUrl": "https://docs.avsb.cloud/docs/developer-reference/public-api/conventions#validation-errors", "requestId": "req_9f2c41ab7e0b4d1e8c35a6f0d2b91e77" }}Every error names a code you can match on, a human message, a docUrl pointing at the exact section that explains it, and a requestId for support. The full list of codes, status codes, and rate limits is on the conventions page.
Flags
A flag's variations are the values it can return, such as on/off, or three named colors. Every rule points at one of them.
| Action | Endpoint | Scope |
|---|---|---|
| List flags | GET /api/v1/projects/{projectId}/flags | flags:read |
| Create a flag | POST /api/v1/projects/{projectId}/flags | flags:write |
| Get one flag | GET /api/v1/projects/{projectId}/flags/{flagId} | flags:read |
| Update a flag's name, description, or key | PATCH /api/v1/projects/{projectId}/flags/{flagId} | flags:write |
| Delete a flag | DELETE /api/v1/projects/{projectId}/flags/{flagId} | flags:write |
| Archive a flag | POST /api/v1/projects/{projectId}/flags/{flagId}/archive | flags:write |
| Restore an archived flag | POST /api/v1/projects/{projectId}/flags/{flagId}/unarchive | flags:write |
| Replace a flag's variations | PUT /api/v1/projects/{projectId}/flags/{flagId}/variations | flags:write |
{flagId} accepts either the flag's id or its short numeric id, the same one shown in the dashboard.
A few limits worth knowing before you build against this:
- Every flag needs 2 to 4 variations. A
BOOLEANflag needs exactly 2, with values"true"and"false". - You can only delete a flag while it is
DRAFTorARCHIVED. - Archiving is refused while any environment is still running. Pause every environment first, then archive.
- Unarchiving returns the flag to
PAUSED, never straight back toRUNNING. Resuming traffic is a separate, per-environment decision. - Once a flag has been enabled in any environment, its
keycan no longer change. Your code reads flags by key, so a silent rename would make every call fall back to the default value.
Create a flag: a worked example
The smallest request that works is a name, a key, a type, its variations, and which one is the default:
curl -X POST "https://app.avsb.cloud/api/v1/projects/<projectId>/flags" \ -H "Authorization: Bearer avsb_svc_..." \ -H "Content-Type: application/json" \ -d '{ "name": "New checkout", "key": "new_checkout", "type": "BOOLEAN", "variations": [ { "name": "Off", "key": "off", "value": "false" }, { "name": "On", "key": "on", "value": "true" } ], "defaultVariationKey": "off" }'const projectId = 'cm1a2b3c4d5e6f7g8h9i0j1k2'const res = await fetch(`https://app.avsb.cloud/api/v1/projects/${projectId}/flags`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.AVSB_SERVICE_TOKEN}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ name: 'New checkout', key: 'new_checkout', type: 'BOOLEAN', variations: [ { name: 'Off', key: 'off', value: 'false' }, { name: 'On', key: 'on', value: 'true' }, ], defaultVariationKey: 'off', }),})const { data } = await res.json()import os, requestsproject_id = "cm1a2b3c4d5e6f7g8h9i0j1k2"res = requests.post( f"https://app.avsb.cloud/api/v1/projects/{project_id}/flags", headers={"Authorization": f"Bearer {os.environ['AVSB_SERVICE_TOKEN']}"}, json={ "name": "New checkout", "key": "new_checkout", "type": "BOOLEAN", "variations": [ {"name": "Off", "key": "off", "value": "false"}, {"name": "On", "key": "on", "value": "true"}, ], "defaultVariationKey": "off", },)data = res.json()["data"]A fuller request uses the optional fields. description is free text shown in the dashboard, and a flag can hold up to 4 variations, not just 2:
{ "name": "Checkout button color", "key": "checkout_button_color", "description": "Which color the primary checkout button renders", "type": "STRING", "variations": [ { "name": "Blue", "key": "blue", "value": "blue" }, { "name": "Green", "key": "green", "value": "green" }, { "name": "Orange", "key": "orange", "value": "orange" } ], "defaultVariationKey": "blue"}Either call returns 201 with the new flag in DRAFT status, disabled in every environment:
{ "data": { "id": "flag_abc", "shortId": 42, "name": "New checkout", "key": "new_checkout", "type": "BOOLEAN", "status": "DRAFT", "stale": false }}A key that already exists in the project returns 409:
{ "error": { "code": "validation_failed", "message": "A flag with this key already exists in this project", "docUrl": "https://docs.avsb.cloud/docs/developer-reference/public-api/conventions#validation-errors", "requestId": "req_9f2c41ab7e0b4d1e8c35a6f0d2b91e77" }}The Public API: Flags page shows every other flag, rule, and variation call the same way.
Turning flags on and off (the kill switches)
Two levels of switch stop traffic without deploying code. Both republish the flag's data file, the small JSON file your SDK reads, so the change is live on the next SDK fetch.
| Action | Endpoint | Scope |
|---|---|---|
| Stop every rule in one environment | POST /api/v1/projects/{projectId}/flags/{flagId}/envs/{envId}/pause | flags:write |
| Resume every rule in one environment | POST /api/v1/projects/{projectId}/flags/{flagId}/envs/{envId}/run | flags:write |
| Pause one rule, leaving the rest of the environment serving | POST /api/v1/projects/{projectId}/flags/{flagId}/envs/{envId}/rules/{ruleId}/pause | flags:write |
| Resume one rule | POST /api/v1/projects/{projectId}/flags/{flagId}/envs/{envId}/rules/{ruleId}/run | flags:write |
Both pause endpoints are safe to call blind, from an alert or a CI job. Pausing something already paused answers 200 with changed: false, not an error, and pausing is never blocked by a pending schedule. Use the two rule-level endpoints for a single misbehaving A/B test. Use the environment-level pair to stop everything the flag serves in one environment at once.
A smaller endpoint changes two things about one environment. defaultVariationId, the variation served when no rule matches, is staged until the environment's next publish. enabled is a shorthand for the run and pause endpoints above: setting it to false stops the environment for real visitors the moment the call returns, and true starts it, with the same schedule guard as run. It is read back as the environment's current serving state.
| Action | Endpoint | Scope |
|---|---|---|
Stage the fallback variation, or run and pause through enabled | PATCH /api/v1/projects/{projectId}/flags/{flagId}/envs/{envId} | flags:write |
| Replace the per-user overrides (the environment and rule allowlists), staged until the next publish | PUT /api/v1/projects/{projectId}/flags/{flagId}/envs/{envId}/overrides | flags:write |
Rules
A rule decides who gets which variation. A targeted delivery rule serves one variation to an audience, a named group of visitors your targeting rules define. An A/B test rule splits traffic and measures the result instead. Rules belong to one environment, so the same flag can run a 50/50 test in development and a 5% rollout in production.
| Action | Endpoint | Scope |
|---|---|---|
| List rules in one environment | GET /api/v1/projects/{projectId}/flags/{flagId}/envs/{envId}/rules | flags:read |
| Create a rule | POST /api/v1/projects/{projectId}/flags/{flagId}/envs/{envId}/rules | flags:write |
| Get one rule | GET /api/v1/projects/{projectId}/flags/{flagId}/envs/{envId}/rules/{ruleId} | flags:read |
| Update a rule | PATCH /api/v1/projects/{projectId}/flags/{flagId}/envs/{envId}/rules/{ruleId} | flags:write |
| Delete a rule | DELETE /api/v1/projects/{projectId}/flags/{flagId}/envs/{envId}/rules/{ruleId} | flags:write |
| Duplicate a rule, disabled, in the same environment | POST /api/v1/projects/{projectId}/flags/{flagId}/envs/{envId}/rules/{ruleId}/duplicate | flags:write |
| Copy every rule from another environment | POST /api/v1/projects/{projectId}/flags/{flagId}/envs/{envId}/rules/copy | flags:write |
| Reorder every rule (evaluation order) | PUT /api/v1/projects/{projectId}/flags/{flagId}/envs/{envId}/rules/reorder | flags:write |
| Read one rule's results | GET /api/v1/projects/{projectId}/flags/{flagId}/rules/{ruleId}/results | results:read |
Rule order is evaluation order: the first matching rule wins. Reorder takes the full list of rule ids in the new order, { "ruleIds": ["rule_1", "rule_2", ...] }. A partial list would leave the rest ambiguous, so it is refused. Copy takes { "sourceEnvId": "..." } and appends every rule from that environment onto the end of this one. Nothing starts running as a side effect: run the copied rules yourself once you are ready.
Environments
An environment is a separate resource from flags. It is where one version of your flags runs, such as production or staging, and it has its own SDK key. Environments is its own item in the project sidebar, next to Settings, not inside it, and it only appears for feature-flag projects.
| Action | Endpoint | Scope |
|---|---|---|
| List environments | GET /api/v1/projects/{projectId}/environments | projects:read |
| Create an environment | POST /api/v1/projects/{projectId}/environments | projects:write |
| Get one environment | GET /api/v1/projects/{projectId}/environments/{envId} | projects:read |
| Update an environment's name or color | PATCH /api/v1/projects/{projectId}/environments/{envId} | projects:write |
| Delete an environment | DELETE /api/v1/projects/{projectId}/environments/{envId} | projects:write |
| Publish the data file for an environment | POST /api/v1/projects/{projectId}/environments/{envId}/publish | projects:write |
Environments use projects:read and projects:write, not the flags:* scopes above. A token scoped only to flags:write cannot create or delete an environment. Deleting one is refused if it would leave the project with fewer than two active environments, and the built-in Production and Development environments can never be deleted. See Public API: Environments for full examples.
The event definitions a rule measures against are managed from the dashboard today, so a token cannot list, create, or change them. Per-user overrides are different: a token with flags:write can replace them with PUT /api/v1/projects/{projectId}/flags/{flagId}/envs/{envId}/overrides, described under Replace per-user overrides.
SDK heartbeat
The SDK sends a heartbeat after every successful data file fetch, automatically. You never call this yourself; it is what marks an environment "connected" in the dashboard.
POST /api/sdk/heartbeat
This one endpoint is public: no bearer token, because the SDK identifies itself with its environment's SDK key instead.
{ "sdkKey": "sdk_production_xxxxxxxxxxxxxxxx", "sdkType": "@avsbhq/browser", "sdkVersion": "1.5.0"}A successful heartbeat returns { "success": true }. Note this endpoint predates the rest of the API and does not use the { "data": ... } / { "error": {...} } envelope described above:
{ "success": true }{ "error": "Unknown SDK key" }