Public API: Flags
The flags API manages feature flags in a feature-flag project over the public REST API. It is the token-authenticated twin of the dashboard: the same flags, the same validation, just authenticated with a service token instead of a browser session.
All flag endpoints live under a project. The org is taken from the token, so the path carries only {projectId}, not {orgId}.
https://app.avsb.cloud/api/v1/projects/{projectId}/flagsEvery endpoint here follows the shared conventions: the { data } envelope, 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. See rate-limit headers for the response headers that show your remaining budget.
Scopes
| Operation | Scope |
|---|---|
| List / get flags, list / get rules | flags:read |
| Create / update / delete a flag, replace variations, archive / unarchive | flags:write |
| Stage an environment config, pause or resume an environment | flags:write |
| Create / update / delete / reorder / duplicate / pause / resume / copy rules | flags:write |
| Replace per-user overrides | flags:write |
| Schedule or cancel a schedule for a flag environment | flags:write |
| Read flag-rule analysis results | results:read |
The two rule results endpoints use results:read, not flags:read: they return statistical analysis, not flag configuration. A token can read results without being able to read or change flag config, and vice versa.
List flags
GET /api/v1/projects/{projectId}/flags: paginated list of flags in the project, newest-updated first.
curl "https://app.avsb.cloud/api/v1/projects/<projectId>/flags?limit=20" \ -H "Authorization: Bearer avsb_svc_..."const projectId = 'cm1a2b3c4d5e6f7g8h9i0j1k2'const url = `https://app.avsb.cloud/api/v1/projects/${projectId}/flags?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}/flags", params={"limit": 20}, headers={"Authorization": f"Bearer {os.environ['AVSB_SERVICE_TOKEN']}"},)data, page = res.json()["data"], res.json()["page"]{ "data": [ { "id": "flag_abc", "shortId": 42, "name": "New checkout", "key": "new_checkout", "type": "BOOLEAN", "status": "DRAFT", "stale": false, "description": null, "createdAt": "2026-06-18T00:00:00.000Z", "updatedAt": "2026-06-18T00:00:00.000Z", "variations": [], "envConfigs": [] } ], "page": { "nextCursor": null, "hasMore": false }}Each flag's envConfigs has one entry per live environment, in the project's environment order. An archived environment's settings are left out.
Pass ?limit= (1–100, default 20) and the opaque ?cursor= from page.nextCursor to page through results. An out-of-range limit is refused with 400 pagination_invalid rather than being clamped: see Conventions for that error shape.
Create a flag
POST /api/v1/projects/{projectId}/flags: create a flag with its variations. The project must be a feature-flag project. Requires flags:write.
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"]Returns 201 with the created flag in { "data": { ... } }.
Field notes
typeis one ofBOOLEAN,STRING,NUMBER,JSON.BOOLEANflags must have exactly two variations with values"true"and"false".NUMBERvariations must hold numeric strings;JSONvariations must hold valid JSON.- Each variation needs its own
keyand its ownvalue. Values are compared the way an SDK would serve them: text ignoring spaces at either end, numbers as numbers (1and1.0are the same), JSON by content whatever the key order. A repeat is refused with400, and the issue points at the second variation'skeyorvalue. keymust be lowercase alphanumeric with underscores and unique within the project (a duplicate returns409).defaultVariationKeymust match one of the variation keys.- A new flag starts in
DRAFTstatus, and a config row is created for every active environment (disabled by default). - A flag's own
statusis one ofDRAFT,ACTIVE,PAUSEDorARCHIVED. It rolls up from its environments:ACTIVEwhile any environment is running,PAUSEDonce none is but one has run.RUNNINGis an environment status, so it appears on environment configs and rules, never as the flag's own status.
{ "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" }}Get a flag
GET /api/v1/projects/{projectId}/flags/{flagId}: {flagId} accepts either the flag id or its numeric shortId. The response includes the flag's variations, per-environment configs, rules, and overrides.
curl "https://app.avsb.cloud/api/v1/projects/<projectId>/flags/<flagId>" \ -H "Authorization: Bearer avsb_svc_..."const projectId = 'cm1a2b3c4d5e6f7g8h9i0j1k2'const flagId = 'flag_abc'const url = `https://app.avsb.cloud/api/v1/projects/${projectId}/flags/${flagId}`const res = await fetch(url, { headers: { Authorization: `Bearer ${process.env.AVSB_SERVICE_TOKEN}` } })const { data } = await res.json()import os, requestsproject_id = "cm1a2b3c4d5e6f7g8h9i0j1k2"flag_id = "flag_abc"res = requests.get( f"https://app.avsb.cloud/api/v1/projects/{project_id}/flags/{flag_id}", headers={"Authorization": f"Bearer {os.environ['AVSB_SERVICE_TOKEN']}"},)data = res.json()["data"]{ "data": { "id": "flag_abc", "shortId": 42, "name": "New checkout", "key": "new_checkout", "type": "BOOLEAN", "status": "ACTIVE", "variations": [], "envConfigs": [], "rules": [], "overrides": [], "...": "..." }}{ "error": { "code": "not_found", "message": "Flag not found", "docUrl": "https://docs.avsb.cloud/docs/developer-reference/public-api/conventions#not-found-errors", "requestId": "req_9f2c41ab7e0b4d1e8c35a6f0d2b91e77" }}Update a flag
PATCH /api/v1/projects/{projectId}/flags/{flagId}: update the flag's name, description, and key.
curl -X PATCH "https://app.avsb.cloud/api/v1/projects/<projectId>/flags/<flagId>" \ -H "Authorization: Bearer avsb_svc_..." \ -H "Content-Type: application/json" \ -d '{ "name": "New checkout (v2)" }'const projectId = 'cm1a2b3c4d5e6f7g8h9i0j1k2'const flagId = 'flag_abc'const res = await fetch(`https://app.avsb.cloud/api/v1/projects/${projectId}/flags/${flagId}`, { method: 'PATCH', headers: { Authorization: `Bearer ${process.env.AVSB_SERVICE_TOKEN}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ name: 'New checkout (v2)' }),})const { data } = await res.json()import os, requestsproject_id = "cm1a2b3c4d5e6f7g8h9i0j1k2"flag_id = "flag_abc"res = requests.patch( f"https://app.avsb.cloud/api/v1/projects/{project_id}/flags/{flag_id}", headers={"Authorization": f"Bearer {os.environ['AVSB_SERVICE_TOKEN']}"}, json={"name": "New checkout (v2)"},)data = res.json()["data"]key can only change before the flag has ever gone live. The key is the name your SDK calls (getBoolFlag('new_checkout')), so renaming one that is already being evaluated would silently return the default value everywhere. Once any environment has been enabled, a key change returns 400. Renaming a brand-new flag to match a code rename is fine. A key already used by another flag in the project returns 409, the same shape shown under Create a flag above.
Everything else about a flag is changed through the endpoints below: the variation set has its own PUT, per-environment state has the toggle and pause/resume endpoints, and the archived state has archive/unarchive.
Stage a flag's environment config
PATCH /api/v1/projects/{projectId}/flags/{flagId}/envs/{envId}: writes the flag's stored configuration for one environment.
curl -X PATCH \ "https://app.avsb.cloud/api/v1/projects/<projectId>/flags/<flagId>/envs/<envId>" \ -H "Authorization: Bearer avsb_svc_..." \ -H "Content-Type: application/json" \ -d '{ "enabled": false, "defaultVariationId": "<variationId>" }'const projectId = 'cm1a2b3c4d5e6f7g8h9i0j1k2'const flagId = 'flag_abc'const envId = 'clx2b3c4d5e6f7g8h9i0j1k2'const res = await fetch( `https://app.avsb.cloud/api/v1/projects/${projectId}/flags/${flagId}/envs/${envId}`, { method: 'PATCH', headers: { Authorization: `Bearer ${process.env.AVSB_SERVICE_TOKEN}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ enabled: false, defaultVariationId: 'var_off' }), },)const { data } = await res.json()import os, requestsproject_id = "cm1a2b3c4d5e6f7g8h9i0j1k2"flag_id = "flag_abc"env_id = "clx2b3c4d5e6f7g8h9i0j1k2"res = requests.patch( f"https://app.avsb.cloud/api/v1/projects/{project_id}/flags/{flag_id}/envs/{env_id}", headers={"Authorization": f"Bearer {os.environ['AVSB_SERVICE_TOKEN']}"}, json={"enabled": False, "defaultVariationId": "var_off"},)data = res.json()["data"]| Field | Notes |
|---|---|
enabled | Whether the environment is serving. Setting it runs or pauses the environment immediately, exactly like POST .../run and POST .../pause below, and republishes the datafile in the same step. It is read back as the environment's current serving state. |
defaultVariationId | The variation served when no rule matches. Must belong to this flag. Staged until the next publish. |
Send at least one of the two; an empty body returns 400. Scheduling fields are not accepted here, because they belong to the schedule endpoints.
defaultVariationId records configuration and marks the environment as having pending changes. It does not publish a datafile, so SDKs keep receiving exactly what they received before the call until the environment is next published.
enabled is different. It is a shorthand for POST .../run and POST .../pause (next section): enabled: false stops the environment for real visitors the moment the call returns, and enabled: true starts it. Both go through the same transition those endpoints use, so the answer and the audit trail are the same whichever you call.
If the environment has a future scheduled enable pending, setting enabled: true returns 409 schedule_conflict: cancel the schedule first. Setting it to false is never blocked.
{ "error": { "code": "schedule_conflict", "message": "Environment is scheduled to enable at a future time. Cancel the schedule first.", "docUrl": "https://docs.avsb.cloud/docs/developer-reference/public-api/conventions#conflict-errors", "requestId": "req_9f2c41ab7e0b4d1e8c35a6f0d2b91e77" }}Get the {envId} values for a project from the environments API.
Pause and resume an environment (the kill switch)
Two endpoints for incident use. They act on every rule in the environment at once and republish the flag datafile in the same transaction, so the change is live on the next SDK fetch and is pushed immediately to any streaming SDK.
POST /api/v1/projects/{projectId}/flags/{flagId}/envs/{envId}/pause
curl -X POST \ "https://app.avsb.cloud/api/v1/projects/<projectId>/flags/<flagId>/envs/<envId>/pause" \ -H "Authorization: Bearer avsb_svc_..."const projectId = 'cm1a2b3c4d5e6f7g8h9i0j1k2'const flagId = 'flag_abc'const envId = 'clx2b3c4d5e6f7g8h9i0j1k2'const res = await fetch( `https://app.avsb.cloud/api/v1/projects/${projectId}/flags/${flagId}/envs/${envId}/pause`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.AVSB_SERVICE_TOKEN}` } },)const { data } = await res.json()import os, requestsproject_id = "cm1a2b3c4d5e6f7g8h9i0j1k2"flag_id = "flag_abc"env_id = "clx2b3c4d5e6f7g8h9i0j1k2"res = requests.post( f"https://app.avsb.cloud/api/v1/projects/{project_id}/flags/{flag_id}/envs/{env_id}/pause", headers={"Authorization": f"Bearer {os.environ['AVSB_SERVICE_TOKEN']}"},)data = res.json()["data"]{ "data": { "status": "PAUSED", "previousStatus": "RUNNING", "changed": true, "affectedRules": [ { "id": "rule_abc", "name": "Gradual rollout", "status": "RUNNING" } ], "promotedRuleIds": [], "draftInvalidated": false, "baseVersion": "2026-10-06T09:41:07.512Z", "publish": { "publishedAt": "2026-10-06T09:41:07.903Z", "sdkKey": "sdk_production_ttqm0eaj4vth1krcb2xn" } }}affectedRulesare the rules that stop serving (on pause) or start serving (on run).promotedRuleIdslists theREADYrules a run turnedRUNNING. It is empty for a pause.draftInvalidatedistruewhen a saved, unpublished draft for this environment was moved to history because the published state changed under it.baseVersionandpublishappear only when something changed.publishnames the datafile that went out (publishedAt, and the environment'ssdkKey).
POST .../envs/{envId}/run resumes it, with the same response shape:
curl -X POST \ "https://app.avsb.cloud/api/v1/projects/<projectId>/flags/<flagId>/envs/<envId>/run" \ -H "Authorization: Bearer avsb_svc_..."const res = await fetch( `https://app.avsb.cloud/api/v1/projects/${projectId}/flags/${flagId}/envs/${envId}/run`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.AVSB_SERVICE_TOKEN}` } },)const { data } = await res.json()res = requests.post( f"https://app.avsb.cloud/api/v1/projects/{project_id}/flags/{flag_id}/envs/{env_id}/run", headers={"Authorization": f"Bearer {os.environ['AVSB_SERVICE_TOKEN']}"},)data = res.json()["data"]Three properties worth relying on when you wire this into alerting:
- Safe to fire blind. Pausing an already-paused environment returns
200withchanged: false, not an error. Your incident script never has to check the current state first. - A schedule never blocks a pause.
rundeclines with the same409 schedule_conflictshape shown above while a future scheduled enable is pending.pausehas no such guard: a kill switch that a schedule can refuse is not a kill switch. - An environment that has never been started has nothing to pause. It is not serving anyone, so
pausereturns409and says so, withdetails.reasonset toENV_NOT_STARTED. If your script only needs every environment off, send{ "enabled": false }to the config endpoint instead: on a never-started environment that returns200and changes nothing.
{ "error": { "code": "idempotency_conflict", "message": "This environment has never been started, so there is nothing to pause. Start it with Run when you are ready.", "details": { "reason": "ENV_NOT_STARTED" }, "docUrl": "https://docs.avsb.cloud/docs/developer-reference/public-api/conventions#idempotency-key", "requestId": "req_9f2c41ab7e0b4d1e8c35a6f0d2b91e77" }}affectedRules lists the rules whose evaluation actually flipped, so you can log what the switch stopped.
The first run of an environment also marks the flag as live for good. From then on the flag's key can no longer be renamed (see Update a flag), because SDKs already ask for it by name.
Rules: rollouts and A/B tests
Rules are what a flag does in an environment. Each rule is one of two types:
TARGETED_DELIVERY: serve one variation to a share of an audience. This is a rollout.AB_TEST: split traffic across variations and measure the result.
Rules are per environment, so the same flag can run a 50/50 test in development and a 5% rollout in production. Rule order is evaluation order: the first matching rule wins.
List rules
GET /api/v1/projects/{projectId}/flags/{flagId}/envs/{envId}/rules: rules in evaluation order, cursor-paginated. Takes the same ?limit= / ?cursor= pair as List flags, including the same pagination_invalid error on a bad limit.
curl "https://app.avsb.cloud/api/v1/projects/<projectId>/flags/<flagId>/envs/<envId>/rules" \ -H "Authorization: Bearer avsb_svc_..."const url = `https://app.avsb.cloud/api/v1/projects/${projectId}/flags/${flagId}/envs/${envId}/rules`const res = await fetch(url, { headers: { Authorization: `Bearer ${process.env.AVSB_SERVICE_TOKEN}` } })const { data, page } = await res.json()res = requests.get( f"https://app.avsb.cloud/api/v1/projects/{project_id}/flags/{flag_id}/envs/{env_id}/rules", headers={"Authorization": f"Bearer {os.environ['AVSB_SERVICE_TOKEN']}"},)data, page = res.json()["data"], res.json()["page"]Create a rule
POST .../envs/{envId}/rules. Requires flags:write.
curl -X POST \ "https://app.avsb.cloud/api/v1/projects/<projectId>/flags/<flagId>/envs/<envId>/rules" \ -H "Authorization: Bearer avsb_svc_..." \ -H "Content-Type: application/json" \ -d '{ "type": "TARGETED_DELIVERY", "name": "Gradual rollout", "trafficAllocation": 0.05, "variations": [{ "variationId": "<variationId>", "percentage": 1 }], "audienceIds": [], "enabled": true }'const res = await fetch( `https://app.avsb.cloud/api/v1/projects/${projectId}/flags/${flagId}/envs/${envId}/rules`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.AVSB_SERVICE_TOKEN}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ type: 'TARGETED_DELIVERY', name: 'Gradual rollout', trafficAllocation: 0.05, variations: [{ variationId: 'var_on', percentage: 1 }], audienceIds: [], enabled: true, }), },)const { data } = await res.json()res = requests.post( f"https://app.avsb.cloud/api/v1/projects/{project_id}/flags/{flag_id}/envs/{env_id}/rules", headers={"Authorization": f"Bearer {os.environ['AVSB_SERVICE_TOKEN']}"}, json={ "type": "TARGETED_DELIVERY", "name": "Gradual rollout", "trafficAllocation": 0.05, "variations": [{"variationId": "var_on", "percentage": 1}], "audienceIds": [], "enabled": True, },)data = res.json()["data"]Returns 201 with the created rule and its variations, audiences, and metrics.
Field notes:
trafficAllocation(0 to 1) is what share of matching visitors enter the rule at all. Required, because there is no safe default.0.05is a 5% rollout.variationsare{ variationId, percentage }pairs. ATARGETED_DELIVERYrule takes exactly one at1. AnAB_TESTrule takes at least two whose percentages sum to1. Both are checked, and a bad split returns422.audienceIdsnarrows the rule to one or more audiences. Empty means everyone.metricIdsare{ eventId, isPrimary }pairs for an A/B test's measurement.enableddefaults totrue, but it does not put a rule live: the rule'sstatusdoes. Every new rule starts inDRAFTand serves no one, and the create response shows"status": "DRAFT". Start it withPOST .../rules/{ruleId}/run. It goes live at once when the environment is running, or becomesREADYand goes live the next time the environment runs. Running the environment does not start aDRAFTrule.statsEngineandvarianceReductiondefault to the project's settings.keyis optional; when left out it is slugified fromname(or falls back toruleif both are blank).distributionMode(MANUALorEVEN_SPLIT) andhashAttribute(defaultuserId) control how visitors are bucketed into the split. Most integrations never need to set either.priorEffectMeanandpriorEffectSdset an informative Bayesian prior on the expected lift. Send both together or neither; the SD must be a positive number. Leave both unset for the default, uninformative prior.
{ "error": { "code": "validation_failed", "message": "A/B Test variation percentages must sum to 1.0", "docUrl": "https://docs.avsb.cloud/docs/developer-reference/public-api/conventions#validation-errors", "requestId": "req_9f2c41ab7e0b4d1e8c35a6f0d2b91e77" }}Update a rule
PATCH .../envs/{envId}/rules/{ruleId}: send only what changes.
curl -X PATCH \ "https://app.avsb.cloud/api/v1/projects/<projectId>/flags/<flagId>/envs/<envId>/rules/<ruleId>" \ -H "Authorization: Bearer avsb_svc_..." \ -H "Content-Type: application/json" \ -d '{ "trafficAllocation": 0.25 }'const ruleId = 'rule_abc'const res = await fetch( `https://app.avsb.cloud/api/v1/projects/${projectId}/flags/${flagId}/envs/${envId}/rules/${ruleId}`, { method: 'PATCH', headers: { Authorization: `Bearer ${process.env.AVSB_SERVICE_TOKEN}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ trafficAllocation: 0.25 }), },)const { data } = await res.json()rule_id = "rule_abc"res = requests.patch( f"https://app.avsb.cloud/api/v1/projects/{project_id}/flags/{flag_id}/envs/{env_id}/rules/{rule_id}", headers={"Authorization": f"Bearer {os.environ['AVSB_SERVICE_TOKEN']}"}, json={"trafficAllocation": 0.25},)data = res.json()["data"]Sending variations, audienceIds, or metricIds replaces that whole set for the rule. Values that have not moved are left untouched, so repeating the same full-form save produces no further writes.
enabled on an update is not a stored setting. true runs the rule and false pauses it, exactly like the rule run and pause endpoints below, and the datafile is republished in the same step. A rule that cannot start yet (unsaved edits, or a concluded rule) returns the same refusal those endpoints return.
Analysis settings are frozen once a rule has launched. statsEngine, varianceReduction, customAlpha, mccMethod, ropeLow, ropeHigh, bayesianThreshold, and sequentialHorizon all decide how a rule is judged. Changing one mid-flight would move the win bar under a running test, so after launch they return 400. The informative-prior pair, priorEffectMean and priorEffectSd, is NOT in this frozen list and can still be changed after launch. Duplicate the rule to try a different setup.
{ "error": { "code": "validation_failed", "message": "Analysis config is locked after the rule has launched", "docUrl": "https://docs.avsb.cloud/docs/developer-reference/public-api/conventions#validation-errors", "requestId": "req_9f2c41ab7e0b4d1e8c35a6f0d2b91e77" }}Delete a rule
DELETE .../envs/{envId}/rules/{ruleId}: returns the deleted rule's id.
curl -X DELETE \ "https://app.avsb.cloud/api/v1/projects/<projectId>/flags/<flagId>/envs/<envId>/rules/<ruleId>" \ -H "Authorization: Bearer avsb_svc_..."const res = await fetch( `https://app.avsb.cloud/api/v1/projects/${projectId}/flags/${flagId}/envs/${envId}/rules/${ruleId}`, { method: 'DELETE', headers: { Authorization: `Bearer ${process.env.AVSB_SERVICE_TOKEN}` } },)const { data } = await res.json()res = requests.delete( f"https://app.avsb.cloud/api/v1/projects/{project_id}/flags/{flag_id}/envs/{env_id}/rules/{rule_id}", headers={"Authorization": f"Bearer {os.environ['AVSB_SERVICE_TOKEN']}"},)data = res.json()["data"]{ "data": { "id": "rule_abc" } }Reorder rules
PUT .../envs/{envId}/rules/reorder: order is evaluation order, so this changes behaviour.
curl -X PUT \ "https://app.avsb.cloud/api/v1/projects/<projectId>/flags/<flagId>/envs/<envId>/rules/reorder" \ -H "Authorization: Bearer avsb_svc_..." \ -H "Content-Type: application/json" \ -d '{ "ruleIds": ["<ruleId2>", "<ruleId1>"] }'const res = await fetch( `https://app.avsb.cloud/api/v1/projects/${projectId}/flags/${flagId}/envs/${envId}/rules/reorder`, { method: 'PUT', headers: { Authorization: `Bearer ${process.env.AVSB_SERVICE_TOKEN}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ ruleIds: ['rule_def', 'rule_abc'] }), },)const { data } = await res.json()res = requests.put( f"https://app.avsb.cloud/api/v1/projects/{project_id}/flags/{flag_id}/envs/{env_id}/rules/reorder", headers={"Authorization": f"Bearer {os.environ['AVSB_SERVICE_TOKEN']}"}, json={"ruleIds": ["rule_def", "rule_abc"]},)data = res.json()["data"]{ "data": { "reordered": 2 } }Two rules to know about:
- List every rule in the environment. A partial list would leave the rules you did not name holding positions that clash with the new ones, so an incomplete
ruleIdsreturns400. Read the list first, reorder it, send it back. - A/B tests evaluate before rollouts. Ordering an
AB_TESTrule after aTARGETED_DELIVERYrule returns422.
{ "error": { "code": "validation_failed", "message": "Rule order must respect type priority: AB_TEST before TARGETED_DELIVERY", "docUrl": "https://docs.avsb.cloud/docs/developer-reference/public-api/conventions#validation-errors", "requestId": "req_9f2c41ab7e0b4d1e8c35a6f0d2b91e77" }}Duplicate a rule
POST .../envs/{envId}/rules/{ruleId}/duplicate: copies the rule, its split, its audiences, and its metrics inside the same environment.
curl -X POST \ "https://app.avsb.cloud/api/v1/projects/<projectId>/flags/<flagId>/envs/<envId>/rules/<ruleId>/duplicate" \ -H "Authorization: Bearer avsb_svc_..."const res = await fetch( `https://app.avsb.cloud/api/v1/projects/${projectId}/flags/${flagId}/envs/${envId}/rules/${ruleId}/duplicate`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.AVSB_SERVICE_TOKEN}` } },)const { data } = await res.json()res = requests.post( f"https://app.avsb.cloud/api/v1/projects/{project_id}/flags/{flag_id}/envs/{env_id}/rules/{rule_id}/duplicate", headers={"Authorization": f"Bearer {os.environ['AVSB_SERVICE_TOKEN']}"},)data = res.json()["data"]Returns 201 with the duplicated rule, the same shape shown under Create a rule above. The copy always arrives disabled, with a free <key>-copy key. Duplicating is how you draft the next iteration, so it never starts serving traffic on its own. This is also the way to change analysis settings that a launched rule has frozen.
Pause and resume one rule
The environment-level pause and resume from above stops every rule at once. These two act on a single rule, so you can pause the misbehaving A/B test and leave a targeted-delivery rollout in the same environment serving.
POST .../envs/{envId}/rules/{ruleId}/pause
curl -X POST \ "https://app.avsb.cloud/api/v1/projects/<projectId>/flags/<flagId>/envs/<envId>/rules/<ruleId>/pause" \ -H "Authorization: Bearer avsb_svc_..."const res = await fetch( `https://app.avsb.cloud/api/v1/projects/${projectId}/flags/${flagId}/envs/${envId}/rules/${ruleId}/pause`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.AVSB_SERVICE_TOKEN}` } },)const { data } = await res.json()res = requests.post( f"https://app.avsb.cloud/api/v1/projects/{project_id}/flags/{flag_id}/envs/{env_id}/rules/{rule_id}/pause", headers={"Authorization": f"Bearer {os.environ['AVSB_SERVICE_TOKEN']}"},)data = res.json()["data"]{ "data": { "status": "PAUSED", "previousStatus": "RUNNING", "changed": true, "publish": { "publishedAt": "2026-10-06T09:41:07.903Z", "sdkKey": "sdk_production_ttqm0eaj4vth1krcb2xn" } }}publish appears only when something changed. POST .../envs/{envId}/rules/{ruleId}/run starts or resumes the rule, with the same response shape. In an environment that is running, the rule comes back RUNNING and serves at once. Otherwise it comes back READY and starts serving the next time the environment runs. Both legs republish the flag datafile in the same transaction, so the change reaches the next SDK fetch (and any streaming SDK immediately). Like the environment-level pause, pausing one rule is never refused by a pending schedule; pausing an already-paused rule is safe to fire blind, the same way environment pause is.
Copy rules between environments
POST .../envs/{envId}/rules/copy: copy every rule this flag has in another environment of the same project, appended after whatever rules the target environment already has. This is "promote development to production" in one call, instead of recreating every rule's variations, audiences, and metrics by hand. Requires flags:write.
curl -X POST \ "https://app.avsb.cloud/api/v1/projects/<projectId>/flags/<flagId>/envs/<envId>/rules/copy" \ -H "Authorization: Bearer avsb_svc_..." \ -H "Content-Type: application/json" \ -d '{ "sourceEnvId": "<sourceEnvId>" }'const sourceEnvId = 'clx9y8x7w6v5u4t3s2r1q0p9'const res = await fetch( `https://app.avsb.cloud/api/v1/projects/${projectId}/flags/${flagId}/envs/${envId}/rules/copy`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.AVSB_SERVICE_TOKEN}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ sourceEnvId }), },)const { data } = await res.json()source_env_id = "clx9y8x7w6v5u4t3s2r1q0p9"res = requests.post( f"https://app.avsb.cloud/api/v1/projects/{project_id}/flags/{flag_id}/envs/{env_id}/rules/copy", headers={"Authorization": f"Bearer {os.environ['AVSB_SERVICE_TOKEN']}"}, json={"sourceEnvId": source_env_id},)data = res.json()["data"]Returns 201 with the list of newly created rules, each in the same shape shown under Create a rule above:
{ "data": [ { "id": "rule_ghi", "flagId": "flag_abc", "environmentId": "clx2b3c4d5e6f7g8h9i0j1k2", "type": "AB_TEST", "key": "checkout-test", "enabled": false, "status": "DRAFT", "launchedAt": null, "...": "..." } ]}Each copy is a new rule in DRAFT status, so nothing starts serving as a side effect: the target environment is only marked as having pending changes. Run the environment (or the individual rules) once you are ready. A copy carries:
- The source rule's
key. If the target environment already uses that key, the copy gets<key>-copy(then<key>-copy-2, and so on). enabled, as the source rule has it, plus the traffic allocation, distribution mode, hash attribute, variations and their split, audiences, and metrics.- Every analysis setting: stats engine, variance reduction,
customAlpha,mccMethod, the ROPE band,bayesianThreshold, the prior, andsequentialHorizon.
A copy never carries the source rule's run history (status, launch date, conclusion), its exclusion group membership, or its per-user overrides: those belong to the source environment's traffic. Because the copy has never run, its analysis settings stay editable until you run it.
Three things that make this call fail:
sourceEnvIdnames the environment already in the path. Copying an environment onto itself is refused with400.sourceEnvIdbelongs to a different project.withContractverifies the path{envId}against your org, butsourceEnvIdarrives in the body and gets its own check. An environment from another project (or another org) returns404.- The source environment has no rules for this flag. There is nothing to copy, so the call returns
400rather than a silent no-op.
Replace per-user overrides
PUT /api/v1/projects/{projectId}/flags/{flagId}/envs/{envId}/overrides: choose which users get which variation in one environment. Requires flags:write.
An override sends one user ID straight to a variation, before targeting and traffic splits are checked. Each rule has its own allowlist, and the environment has one more. If a user is in both, the environment's allowlist wins.
- An entry without
ruleIdbelongs to the environment's allowlist. - An entry with
ruleIdbelongs to that rule's allowlist. The rule must be one of this flag's rules in this environment.
Every allowlist the body names is replaced as a whole. Allowlists the body does not mention stay as they are, and an empty overrides list clears the environment's allowlist.
curl -X PUT \ "https://app.avsb.cloud/api/v1/projects/<projectId>/flags/<flagId>/envs/<envId>/overrides" \ -H "Authorization: Bearer avsb_svc_..." \ -H "Content-Type: application/json" \ -d '{ "overrides": [ { "userId": "qa-tester-1", "variationId": "<variationId>" }, { "userId": "beta-user-7", "variationId": "<variationId>", "ruleId": "<ruleId>" } ] }'const res = await fetch( `https://app.avsb.cloud/api/v1/projects/${projectId}/flags/${flagId}/envs/${envId}/overrides`, { method: 'PUT', headers: { Authorization: `Bearer ${process.env.AVSB_SERVICE_TOKEN}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ overrides: [ { userId: 'qa-tester-1', variationId: onVariationId }, { userId: 'beta-user-7', variationId: onVariationId, ruleId }, ], }), },)const { data } = await res.json()res = requests.put( f"https://app.avsb.cloud/api/v1/projects/{project_id}/flags/{flag_id}/envs/{env_id}/overrides", headers={"Authorization": f"Bearer {os.environ['AVSB_SERVICE_TOKEN']}"}, json={ "overrides": [ {"userId": "qa-tester-1", "variationId": on_variation_id}, {"userId": "beta-user-7", "variationId": on_variation_id, "ruleId": rule_id}, ] },)data = res.json()["data"]Returns 200 with every override in the environment after the change, newest first:
{ "data": [ { "id": "ovr_abc", "flagId": "flag_abc", "environmentId": "clx2b3c4d5e6f7g8h9i0j1k2", "ruleId": null, "userId": "qa-tester-1", "variationId": "var_on", "createdAt": "2026-10-02T09:00:00.000Z", "variation": { "name": "On", "key": "on", "value": "true" } } ]}The new lists are saved straight away and reach your SDKs with the environment's next publish, like a staged defaultVariationId. To send them out now, call POST /api/v1/projects/{projectId}/environments/{envId}/publish (see Environments).
What makes this call fail:
- A user is listed twice in one allowlist. Each user can be listed once per allowlist, so the call returns
400withdetails.issuespointing at the second entry (for exampleoverrides.1.userId), and nothing is written. The same user can sit in the environment's allowlist and in a rule's allowlist. - A
variationIdis not one of this flag's variations, or aruleIdis not one of its rules in this environment. The call returns400with the entry's path indetails.issues. - The flag or the environment is not in the project. The call returns
404.
Replace a flag's variations
PUT /api/v1/projects/{projectId}/flags/{flagId}/variations: the body is the complete set.
curl -X PUT \ "https://app.avsb.cloud/api/v1/projects/<projectId>/flags/<flagId>/variations" \ -H "Authorization: Bearer avsb_svc_..." \ -H "Content-Type: application/json" \ -d '{ "variations": [ { "id": "<variationId>", "name": "Off", "key": "off", "value": "false" }, { "id": "<variationId>", "name": "On", "key": "on", "value": "true" } ], "defaultVariationKey": "off" }'const res = await fetch( `https://app.avsb.cloud/api/v1/projects/${projectId}/flags/${flagId}/variations`, { method: 'PUT', headers: { Authorization: `Bearer ${process.env.AVSB_SERVICE_TOKEN}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ variations: [ { id: 'var_off', name: 'Off', key: 'off', value: 'false' }, { id: 'var_on', name: 'On', key: 'on', value: 'true' }, ], defaultVariationKey: 'off', }), },)const { data } = await res.json()res = requests.put( f"https://app.avsb.cloud/api/v1/projects/{project_id}/flags/{flag_id}/variations", headers={"Authorization": f"Bearer {os.environ['AVSB_SERVICE_TOKEN']}"}, json={ "variations": [ {"id": "var_off", "name": "Off", "key": "off", "value": "false"}, {"id": "var_on", "name": "On", "key": "on", "value": "true"}, ], "defaultVariationKey": "off", },)data = res.json()["data"]- An entry with an
idupdates that variation. An entry without one creates a variation. An existing variation absent from the body is deleted. - An
idthat does not belong to this flag returns400rather than being skipped. - A variation still used by a rule or a per-user override cannot be deleted (
400). Detach it first. defaultVariationKeymust match one of the variations in the body. Every environment config is repointed at it.- Boolean flags cannot change an existing variation's
value, and keep exactly two variations. Those two values,"true"and"false", are the contract everygetBoolFlagcall evaluates against, so a third variation is refused too. Renaming them is fine. - Keys and values stay unique, exactly as on create: a repeated
keyor a second variation serving the same value is refused with400at that field. - A key cannot be deleted and re-used in the same call. If the body drops a variation and adds a new one with the same
key, the request is refused with400naming that key. Dropping and re-adding a key looks like a rename but is a delete plus a create, so anything still pointing at the old variation (a rule split, a per-user override, a saved result) would be silently repointed at a different row. Do it in two calls if that is genuinely what you want.
Between 2 and 4 variations, matching the flag builder. Every refusal above shares the 400 validation_failed shape shown under Create a flag above, with a message naming the specific rule that was broken.
Archive and unarchive a flag
POST /api/v1/projects/{projectId}/flags/{flagId}/archive retires a flag and makes it deletable.
curl -X POST "https://app.avsb.cloud/api/v1/projects/<projectId>/flags/<flagId>/archive" \ -H "Authorization: Bearer avsb_svc_..."const res = await fetch(`https://app.avsb.cloud/api/v1/projects/${projectId}/flags/${flagId}/archive`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.AVSB_SERVICE_TOKEN}` },})const { data } = await res.json()res = requests.post( f"https://app.avsb.cloud/api/v1/projects/{project_id}/flags/{flag_id}/archive", headers={"Authorization": f"Bearer {os.environ['AVSB_SERVICE_TOKEN']}"},)data = res.json()["data"]Archiving is refused with 400 while any environment is still enabled. Archiving a live flag would silently stop serving it, which is a kill switch under the wrong name, so pause the environments first.
{ "error": { "code": "validation_failed", "message": "Pause all environments before archiving", "docUrl": "https://docs.avsb.cloud/docs/developer-reference/public-api/conventions#validation-errors", "requestId": "req_9f2c41ab7e0b4d1e8c35a6f0d2b91e77" }}POST .../flags/{flagId}/unarchive restores the flag to PAUSED, never straight to running. Bringing a flag back and deciding which environments serve it again are two separate decisions. Only archived flags can be unarchived; anything else returns the same 400 validation_failed shape shown above, worded "Only ARCHIVED flags can be unarchived".
Both calls republish the datafile of every environment in the project before they return: an archived flag leaves the files SDKs download, and an unarchived one comes back switched off. Deleting a flag republishes the same way.
curl -X POST "https://app.avsb.cloud/api/v1/projects/<projectId>/flags/<flagId>/unarchive" \ -H "Authorization: Bearer avsb_svc_..."const res = await fetch(`https://app.avsb.cloud/api/v1/projects/${projectId}/flags/${flagId}/unarchive`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.AVSB_SERVICE_TOKEN}` },})const { data } = await res.json()res = requests.post( f"https://app.avsb.cloud/api/v1/projects/{project_id}/flags/{flag_id}/unarchive", headers={"Authorization": f"Bearer {os.environ['AVSB_SERVICE_TOKEN']}"},)data = res.json()["data"]Delete a flag
DELETE /api/v1/projects/{projectId}/flags/{flagId}: only flags in DRAFT or ARCHIVED status can be deleted. Returns the deleted flag's id.
curl -X DELETE "https://app.avsb.cloud/api/v1/projects/<projectId>/flags/<flagId>" \ -H "Authorization: Bearer avsb_svc_..."const res = await fetch(`https://app.avsb.cloud/api/v1/projects/${projectId}/flags/${flagId}`, { method: 'DELETE', headers: { Authorization: `Bearer ${process.env.AVSB_SERVICE_TOKEN}` },})const { data } = await res.json()res = requests.delete( f"https://app.avsb.cloud/api/v1/projects/{project_id}/flags/{flag_id}", headers={"Authorization": f"Bearer {os.environ['AVSB_SERVICE_TOKEN']}"},)data = res.json()["data"]{ "data": { "id": "flag_abc" } }A flag still ACTIVE or PAUSED returns the same 400 validation_failed shape shown under Archive a flag above, worded "Only DRAFT or ARCHIVED flags can be deleted". Archive it first.
Schedule a flag environment
POST /api/v1/projects/{projectId}/flags/{flagId}/envs/{envId}/schedule: schedule when a flag turns on (and optionally off) in one environment.
curl -X POST \ "https://app.avsb.cloud/api/v1/projects/<projectId>/flags/<flagId>/envs/<envId>/schedule" \ -H "Authorization: Bearer avsb_svc_..." \ -H "Content-Type: application/json" \ -d '{ "scheduledEnableAt": "2026-07-01T09:00:00.000Z", "endSpec": { "kind": "none" }, "timezone": "UTC" }'const res = await fetch( `https://app.avsb.cloud/api/v1/projects/${projectId}/flags/${flagId}/envs/${envId}/schedule`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.AVSB_SERVICE_TOKEN}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ scheduledEnableAt: '2026-07-01T09:00:00.000Z', endSpec: { kind: 'none' }, timezone: 'UTC', }), },)const { data } = await res.json()res = requests.post( f"https://app.avsb.cloud/api/v1/projects/{project_id}/flags/{flag_id}/envs/{env_id}/schedule", headers={"Authorization": f"Bearer {os.environ['AVSB_SERVICE_TOKEN']}"}, json={ "scheduledEnableAt": "2026-07-01T09:00:00.000Z", "endSpec": {"kind": "none"}, "timezone": "UTC", },)data = res.json()["data"]scheduledEnableAtis an ISO datetime (ornullto leave the enable time unset).endSpeccontrols when it turns off:{ "kind": "none" },{ "kind": "absolute", "scheduledDisableAt": "<iso>" }, or{ "kind": "relative", "days": <1–365>, "timeOfDay": "HH:mm" }.timezoneanchors the relative schedule.
Returns 200 with the updated environment config:
{ "data": { "flagId": "flag_abc", "environmentId": "clx2b3c4d5e6f7g8h9i0j1k2", "enabled": false, "schedulingEnabled": true, "scheduledEnableAt": "2026-07-01T09:00:00.000Z", "scheduledDisableAt": null, "timezone": "UTC" }}Cancel a schedule
DELETE /api/v1/projects/{projectId}/flags/{flagId}/envs/{envId}/schedule: cancel the schedule. Pass ?scope=enable, ?scope=disable, or ?scope=both (default) to cancel just the on, just the off, or the whole schedule.
curl -X DELETE \ "https://app.avsb.cloud/api/v1/projects/<projectId>/flags/<flagId>/envs/<envId>/schedule?scope=both" \ -H "Authorization: Bearer avsb_svc_..."const url = `https://app.avsb.cloud/api/v1/projects/${projectId}/flags/${flagId}/envs/${envId}/schedule?scope=both`const res = await fetch(url, { method: 'DELETE', headers: { Authorization: `Bearer ${process.env.AVSB_SERVICE_TOKEN}` },})const { data } = await res.json()res = requests.delete( f"https://app.avsb.cloud/api/v1/projects/{project_id}/flags/{flag_id}/envs/{env_id}/schedule", params={"scope": "both"}, headers={"Authorization": f"Bearer {os.environ['AVSB_SERVICE_TOKEN']}"},)data = res.json()["data"]Read flag-rule results
These endpoints use the results:read scope.
GET /api/v1/projects/{projectId}/flags/{flagId}/rules/{ruleId}/results: compute the statistical analysis for one rule of the flag.
curl "https://app.avsb.cloud/api/v1/projects/<projectId>/flags/<flagId>/rules/<ruleId>/results" \ -H "Authorization: Bearer avsb_svc_..."const url = `https://app.avsb.cloud/api/v1/projects/${projectId}/flags/${flagId}/rules/${ruleId}/results`const res = await fetch(url, { headers: { Authorization: `Bearer ${process.env.AVSB_SERVICE_TOKEN}` } })const { data } = await res.json()res = requests.get( f"https://app.avsb.cloud/api/v1/projects/{project_id}/flags/{flag_id}/rules/{rule_id}/results", headers={"Authorization": f"Bearer {os.environ['AVSB_SERVICE_TOKEN']}"},)data = res.json()["data"]Optional query params: from / to (ISO dates, no cap on the span you ask for: your plan's retention window narrows it, and the window actually used comes back in dateRange), engine (BAYESIAN, FREQUENTIST, or SEQUENTIAL to render under a chosen engine), and repeated attribute=key:value filters. Repeat attribute with a different key to filter by more than one attribute at once (combined with AND); repeat the same key to match any of several values (combined with OR).
{ "data": { "ruleId": "rule_abc", "dateRange": { "from": null, "to": null }, "officialEngine": "BAYESIAN", "renderedEngine": "BAYESIAN", "metrics": [], "timeSeries": [], "healthScore": { "score": 1, "status": "healthy", "guardrails": [] }, "segmentLift": null }}Compare engines
GET /api/v1/projects/{projectId}/flags/{flagId}/rules/{ruleId}/results/compare: compute the same rule side-by-side under every stats engine, for confidence that the read doesn't hinge on one engine's assumptions.
curl "https://app.avsb.cloud/api/v1/projects/<projectId>/flags/<flagId>/rules/<ruleId>/results/compare" \ -H "Authorization: Bearer avsb_svc_..."const url = `https://app.avsb.cloud/api/v1/projects/${projectId}/flags/${flagId}/rules/${ruleId}/results/compare`const res = await fetch(url, { headers: { Authorization: `Bearer ${process.env.AVSB_SERVICE_TOKEN}` } })const { data } = await res.json()res = requests.get( f"https://app.avsb.cloud/api/v1/projects/{project_id}/flags/{flag_id}/rules/{rule_id}/results/compare", headers={"Authorization": f"Bearer {os.environ['AVSB_SERVICE_TOKEN']}"},)data = res.json()["data"]{ "data": { "ruleId": "rule_abc", "dateRange": { "from": null, "to": null }, "officialEngine": "BAYESIAN", "segmentLift": null, "engines": { "BAYESIAN": { "ruleId": "rule_abc", "renderedEngine": "BAYESIAN" }, "FREQUENTIST": { "ruleId": "rule_abc", "renderedEngine": "FREQUENTIST" }, "SEQUENTIAL": { "ruleId": "rule_abc", "renderedEngine": "SEQUENTIAL" } } }}This endpoint also accepts from / to and treats them exactly as the single-rule endpoint above does: there is no fixed cap on the span, and a wider request is narrowed to your plan's retention window, with the window actually used in dateRange. It takes no engine or attribute params, since the whole point is computing all three engines over the full audience at once.