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}.

Plain text
https://app.avsb.cloud/api/v1/projects/{projectId}/flags
Plain text1 line

Every 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

OperationScope
List / get flags, list / get rulesflags:read
Create / update / delete a flag, replace variations, archive / unarchiveflags:write
Stage an environment config, pause or resume an environmentflags:write
Create / update / delete / reorder / duplicate / pause / resume / copy rulesflags:write
Replace per-user overridesflags:write
Schedule or cancel a schedule for a flag environmentflags:write
Read flag-rule analysis resultsresults:read
Info

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_..."
Shell2 lines
Response
{  "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 }}
JSON19 lines

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"  }'
Shell13 lines

Returns 201 with the created flag in { "data": { ... } }.

Field notes

  • type is one of BOOLEAN, STRING, NUMBER, JSON. BOOLEAN flags must have exactly two variations with values "true" and "false". NUMBER variations must hold numeric strings; JSON variations must hold valid JSON.
  • Each variation needs its own key and its own value. Values are compared the way an SDK would serve them: text ignoring spaces at either end, numbers as numbers (1 and 1.0 are the same), JSON by content whatever the key order. A repeat is refused with 400, and the issue points at the second variation's key or value.
  • key must be lowercase alphanumeric with underscores and unique within the project (a duplicate returns 409).
  • defaultVariationKey must match one of the variation keys.
  • A new flag starts in DRAFT status, and a config row is created for every active environment (disabled by default).
  • A flag's own status is one of DRAFT, ACTIVE, PAUSED or ARCHIVED. It rolls up from its environments: ACTIVE while any environment is running, PAUSED once none is but one has run. RUNNING is an environment status, so it appears on environment configs and rules, never as the flag's own status.
409: key already exists
{  "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"  }}
JSON8 lines

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_..."
Shell2 lines
Response (truncated)
{  "data": {    "id": "flag_abc",    "shortId": 42,    "name": "New checkout",    "key": "new_checkout",    "type": "BOOLEAN",    "status": "ACTIVE",    "variations": [],    "envConfigs": [],    "rules": [],    "overrides": [],    "...": "..."  }}
JSON15 lines
404: flag not found
{  "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"  }}
JSON8 lines

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)" }'
Shell4 lines
Warning

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>" }'
Shell5 lines
FieldNotes
enabledWhether 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.
defaultVariationIdThe 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 stages a change; enabled serves immediately

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.

409: a schedule is already pending
{  "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"  }}
JSON8 lines

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_..."
Shell3 lines
Response
{  "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" }  }}
JSON14 lines
  • affectedRules are the rules that stop serving (on pause) or start serving (on run).
  • promotedRuleIds lists the READY rules a run turned RUNNING. It is empty for a pause.
  • draftInvalidated is true when a saved, unpublished draft for this environment was moved to history because the published state changed under it.
  • baseVersion and publish appear only when something changed. publish names the datafile that went out (publishedAt, and the environment's sdkKey).

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_..."
Shell3 lines

Three properties worth relying on when you wire this into alerting:

  • Safe to fire blind. Pausing an already-paused environment returns 200 with changed: false, not an error. Your incident script never has to check the current state first.
  • A schedule never blocks a pause. run declines with the same 409 schedule_conflict shape shown above while a future scheduled enable is pending. pause has 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 pause returns 409 and says so, with details.reason set to ENV_NOT_STARTED. If your script only needs every environment off, send { "enabled": false } to the config endpoint instead: on a never-started environment that returns 200 and changes nothing.
409: the environment has never been started
{  "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"  }}
JSON9 lines

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_..."
Shell2 lines

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  }'
Shell12 lines

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.05 is a 5% rollout.
  • variations are { variationId, percentage } pairs. A TARGETED_DELIVERY rule takes exactly one at 1. An AB_TEST rule takes at least two whose percentages sum to 1. Both are checked, and a bad split returns 422.
  • audienceIds narrows the rule to one or more audiences. Empty means everyone.
  • metricIds are { eventId, isPrimary } pairs for an A/B test's measurement.
  • enabled defaults to true, but it does not put a rule live: the rule's status does. Every new rule starts in DRAFT and serves no one, and the create response shows "status": "DRAFT". Start it with POST .../rules/{ruleId}/run. It goes live at once when the environment is running, or becomes READY and goes live the next time the environment runs. Running the environment does not start a DRAFT rule.
  • statsEngine and varianceReduction default to the project's settings.
  • key is optional; when left out it is slugified from name (or falls back to rule if both are blank).
  • distributionMode (MANUAL or EVEN_SPLIT) and hashAttribute (default userId) control how visitors are bucketed into the split. Most integrations never need to set either.
  • priorEffectMean and priorEffectSd set 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.
422: split does not sum to 1
{  "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"  }}
JSON8 lines

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 }'
Shell5 lines

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.

Warning

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.

400: analysis config is locked
{  "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"  }}
JSON8 lines

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_..."
Shell3 lines
Response
{ "data": { "id": "rule_abc" } }
JSON1 line

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>"] }'
Shell5 lines
Response
{ "data": { "reordered": 2 } }
JSON1 line

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 ruleIds returns 400. Read the list first, reorder it, send it back.
  • A/B tests evaluate before rollouts. Ordering an AB_TEST rule after a TARGETED_DELIVERY rule returns 422.
422: wrong type order
{  "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"  }}
JSON8 lines

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_..."
Shell3 lines

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_..."
Shell3 lines
Response
{  "data": {    "status": "PAUSED",    "previousStatus": "RUNNING",    "changed": true,    "publish": { "publishedAt": "2026-10-06T09:41:07.903Z", "sdkKey": "sdk_production_ttqm0eaj4vth1krcb2xn" }  }}
JSON8 lines

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>" }'
Shell5 lines

Returns 201 with the list of newly created rules, each in the same shape shown under Create a rule above:

Response (truncated)
{  "data": [    { "id": "rule_ghi", "flagId": "flag_abc", "environmentId": "clx2b3c4d5e6f7g8h9i0j1k2", "type": "AB_TEST", "key": "checkout-test", "enabled": false, "status": "DRAFT", "launchedAt": null, "...": "..." }  ]}
JSON5 lines

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, and sequentialHorizon.

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:

  • sourceEnvId names the environment already in the path. Copying an environment onto itself is refused with 400.
  • sourceEnvId belongs to a different project. withContract verifies the path {envId} against your org, but sourceEnvId arrives in the body and gets its own check. An environment from another project (or another org) returns 404.
  • The source environment has no rules for this flag. There is nothing to copy, so the call returns 400 rather 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 ruleId belongs to the environment's allowlist.
  • An entry with ruleId belongs 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>" }    ]  }'
Shell10 lines

Returns 200 with every override in the environment after the change, newest first:

Response (truncated)
{  "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" }    }  ]}
JSON14 lines

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 400 with details.issues pointing at the second entry (for example overrides.1.userId), and nothing is written. The same user can sit in the environment's allowlist and in a rule's allowlist.
  • A variationId is not one of this flag's variations, or a ruleId is not one of its rules in this environment. The call returns 400 with the entry's path in details.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"  }'
Shell11 lines
  • An entry with an id updates that variation. An entry without one creates a variation. An existing variation absent from the body is deleted.
  • An id that does not belong to this flag returns 400 rather than being skipped.
  • A variation still used by a rule or a per-user override cannot be deleted (400). Detach it first.
  • defaultVariationKey must 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 every getBoolFlag call evaluates against, so a third variation is refused too. Renaming them is fine.
  • Keys and values stay unique, exactly as on create: a repeated key or a second variation serving the same value is refused with 400 at 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 with 400 naming 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_..."
Shell2 lines

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.

400: an environment is still enabled
{  "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"  }}
JSON8 lines

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_..."
Shell2 lines

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_..."
Shell2 lines
Response
{ "data": { "id": "flag_abc" } }
JSON1 line

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"  }'
Shell9 lines
  • scheduledEnableAt is an ISO datetime (or null to leave the enable time unset).
  • endSpec controls when it turns off: { "kind": "none" }, { "kind": "absolute", "scheduledDisableAt": "<iso>" }, or { "kind": "relative", "days": <1–365>, "timeOfDay": "HH:mm" }.
  • timezone anchors the relative schedule.

Returns 200 with the updated environment config:

Response
{  "data": {    "flagId": "flag_abc",    "environmentId": "clx2b3c4d5e6f7g8h9i0j1k2",    "enabled": false,    "schedulingEnabled": true,    "scheduledEnableAt": "2026-07-01T09:00:00.000Z",    "scheduledDisableAt": null,    "timezone": "UTC"  }}
JSON11 lines

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_..."
Shell3 lines

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_..."
Shell2 lines

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).

JSON
{  "data": {    "ruleId": "rule_abc",    "dateRange": { "from": null, "to": null },    "officialEngine": "BAYESIAN",    "renderedEngine": "BAYESIAN",    "metrics": [],    "timeSeries": [],    "healthScore": { "score": 1, "status": "healthy", "guardrails": [] },    "segmentLift": null  }}
JSON12 lines

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_..."
Shell2 lines
JSON
{  "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" }    }  }}
JSON13 lines

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.

Next steps

Was this helpful?