Feature Flags API
Manage project feature flags, evaluate them for an identity, and fetch definitions for server-side local evaluation.
The Feature Flags API has two credential scopes:
/api/*management endpoints use an account credential: a Firebase ID token or the superadmin API key./flags/*runtime endpoints use a project token for remote evaluation or a project flag secret for local-evaluation definitions.
The project token is suitable for client applications. A flag secret exposes targeting definitions and must remain server-side.
Flag model
| Field | Description |
|---|---|
key | Immutable runtime key. Lowercase, starts with a letter or digit, and may contain - or _. Maximum 200 characters. |
type | One of release, experiment, operational, kill_switch, permission. Descriptive only - it never changes evaluation. Defaults to release. |
active | Whether the flag can enable for any identity. |
archived_at | Epoch ms, or null. A non-null value means the flag is archived: it is absent from POST /flags/evaluate (like a deleted flag) and excluded from GET /api/flags and GET /flags/local-evaluation by default. |
variants | Up to 10 variants. Non-empty variant rollout percentages must sum to 100. |
rules | Up to 10 ordered rules. The first matching rule wins. In the dashboard, each rule is shown as a "Strategy" (Standard, Specific users, Gradual rollout, or Cohort) classified from its shape - the wire shape is unchanged. |
rules[].cohortId | Cohort in the same project. |
rules[].distinctIds | Exact override list ("Specific users" in the dashboard), up to 50 IDs, each up to 255 characters. A non-empty list requires rolloutPct: 100. |
rules[].rolloutPct | Percentage of matching identities that enter the rule. |
rules[].variant | Optional variant key to serve when the rule matches. |
With no variants, a matching rule returns a simple enabled result. With
variants, a matching rule returns the selected variant and its payload. With no
rules, evaluation is disabled with reason no_match.
Authentication
Send an account credential on /api/*:
bashcurl https://cohorly-service.velloalabs.com/api/flags?projectId=1 \-H "Authorization: Bearer $API_KEY"
Send a project token on /flags/evaluate:
bashcurl -X POST https://cohorly-service.velloalabs.com/flags/evaluate \-H "Content-Type: application/json" \-H "X-Cohorly-Token: YOUR_PROJECT_TOKEN" \-d '{ "distinct_id": "user-123" }'
The token can also be supplied as the request body's token field. The
X-Cohorly-Token header applies to the whole request.
Manage flags
List flags
List the flags accessible to the authenticated account.
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
projectId | integer | Optional | - | Project to list. For a Firebase user token, omission resolves to that organization's oldest project. For a superadmin key, omission uses the legacy default project when one exists and otherwise returns 400; pass it explicitly for predictable behavior. |
archived | string | Optional | - | Pass `1` to include archived flags alongside active ones. Omitted (or any other value) excludes archived flags - the default, and what /flags/local-evaluation always uses. |
Create a flag
Create a flag in a project.
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
projectId | integer | Optional | - | Project that owns the flag. For a Firebase user token, omission resolves to that organization's oldest project. For a superadmin key, omission uses the legacy default project when one exists and otherwise returns 400; pass it explicitly for predictable behavior. |
key | string | Required | - | Lowercase runtime key matching ^[a-z0-9][a-z0-9_-]{0,199}$. |
name | string | Required | - | Human-readable name. |
description | string | null | Optional | - | Optional description. |
active | boolean | Optional | true | Whether evaluations can enable the flag. |
type | string | Optional | release | One of release, experiment, operational, kill_switch, permission. Descriptive only. |
variants | FlagVariant[] | Optional | - | Optional list of up to 10 variants. Percentages must total 100 when non-empty. |
rules | FlagRule[] | Optional | - | Optional ordered list of up to 10 rules. An omitted list is empty and matches nobody. |
bashcurl -X POST https://cohorly-service.velloalabs.com/api/flags \-H "Authorization: Bearer $API_KEY" \-H "Content-Type: application/json" \-d '{"projectId": 1,"key": "checkout-redesign","name": "Checkout redesign","description": "Gradual rollout of the new checkout","active": true,"variants": [{ "key": "control", "rolloutPct": 50 },{ "key": "new", "rolloutPct": 50, "payload": { "layout": "compact" } }],"rules": [{ "distinctIds": ["user-123"], "rolloutPct": 100, "variant": "new" },{ "cohortId": 42, "rolloutPct": 25 }]}'
The response is the complete created flag. The key is unique within the project and cannot be changed later.
Read, update, and delete a flag
Get one flag by numeric id.
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
id | integer | Required | - | Flag id from the list or create response. |
Partially update a flag. The key is immutable.
The request can include name, description, active, type, variants,
and rules. If variants change, existing rules are checked against the new
variant keys. Send rules: [] to make an active flag explicitly disabled by
configuration. Every PUT writes a flag_events row (see Activity below);
when active flips, the recorded action is activated/deactivated instead
of updated.
bashcurl -X PUT https://cohorly-service.velloalabs.com/api/flags/4 \-H "Authorization: Bearer $API_KEY" \-H "Content-Type: application/json" \-d '{"active": true,"rules": [{ "rolloutPct": 50, "variant": "new" }]}'
Delete a flag.
Deletion removes the flag definition. Remove or retire the corresponding application code separately.
Cross-organization flag ids return 404, so the API does not disclose the
existence of another organization's flags.
bashcurl "https://cohorly-service.velloalabs.com/api/flags?projectId=1" \-H "Authorization: Bearer $API_KEY"
json[{"id": 3,"project_id": 1,"created_by": 7,"key": "new-checkout","name": "New checkout","description": null,"active": true,"variants": [],"rules": [{ "rolloutPct": 100 }],"created_at": 1753900800000,"updated_at": 1753900800000}]
json{"id": 4,"project_id": 1,"created_by": 7,"key": "checkout-redesign","name": "Checkout redesign","description": "Gradual rollout of the new checkout","active": true,"variants": [{ "key": "control", "rolloutPct": 50 },{ "key": "new", "rolloutPct": 50, "payload": { "layout": "compact" } }],"rules": [{ "distinctIds": ["user-123"], "rolloutPct": 100, "variant": "new" },{ "cohortId": 42, "rolloutPct": 25 }],"created_at": 1753900800000,"updated_at": 1753900800000}
json{ "ok": true }
Archive, revive, and delete
Archive is the reversible way to turn a flag off for good, for now; delete is the one-way action and is only offered once a flag is archived, so the API never exposes two destructive paths on a live flag.
Archive a flag.
Revive an archived flag.
Both are idempotent: archiving an already-archived flag (or reviving an
already-active one) returns the flag unchanged and writes no extra event.
DELETE /api/flags/{id} returns 409 { "error": "not archived" } unless the
flag is already archived.
bashcurl -X POST https://cohorly-service.velloalabs.com/api/flags/4/archive \-H "Authorization: Bearer $API_KEY"
json{ "id": 4, "active": true, "archived_at": 1753900800000, "...": "..." }
Preview (Playground)
Evaluate one flag for a distinct_id, using an account credential.
The dashboard's Playground tab calls this so a person testing a flag does not
need a project token. It runs the exact same evaluator as
POST /flags/evaluate, scoped to this one flag.
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
distinctId | string | Required | - | Identity to evaluate. |
bashcurl -X POST https://cohorly-service.velloalabs.com/api/flags/4/preview \-H "Authorization: Bearer $API_KEY" \-H "Content-Type: application/json" \-d '{ "distinctId": "qa-user-1" }'
json{ "enabled": true, "variant": null, "payload": null, "reason": "rule:0" }
Activity
List a flag's audit trail, newest first.
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
limit | integer | Optional | 50 | Maximum rows to return, up to 200. |
bashcurl "https://cohorly-service.velloalabs.com/api/flags/4/events?limit=20" \-H "Authorization: Bearer $API_KEY"
action is one of created, updated, activated, deactivated,
archived, revived. activated/deactivated fire instead of
updated when a PUT flips active. actor_user_id is null when the
superadmin key made the change. Deleting a flag deletes its event history with
it (ON DELETE CASCADE).
json[{"id": 91,"project_id": 1,"flag_id": 4,"actor_user_id": 7,"action": "activated","before": { "active": false, "...": "..." },"after": { "active": true, "...": "..." },"created_at": 1753900800000}]
Remote evaluation
Evaluate all or selected flags for one distinct_id.
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
distinct_id | string | Required | - | Identity for targeting and deterministic rollout bucketing. |
flag_keys | string[] | Optional | - | Optional filter. When omitted, returns every flag in the project. |
token | string | Optional | - | Project token alternative to the X-Cohorly-Token header. |
bashcurl -X POST https://cohorly-service.velloalabs.com/flags/evaluate \-H "Content-Type: application/json" \-H "X-Cohorly-Token: YOUR_PROJECT_TOKEN" \-d '{"distinct_id": "user-123","flag_keys": ["checkout-redesign"]}'
Remote evaluation uses the identity cluster's canonical id for cohort checks and rollout bucketing. An anonymous id linked to a user id therefore evaluates as the same identity. Exact overrides also match the raw submitted id.
json{"flags": {"checkout-redesign": {"enabled": true,"variant": "new","payload": { "layout": "compact" },"reason": "rule:0"}}}
Local-evaluation definitions
Fetch definitions that server SDKs can evaluate in-process.
Authenticate with the project flag secret, not the project token:
bashcurl https://cohorly-service.velloalabs.com/flags/local-evaluation \-H "Authorization: Bearer $COHORLY_FLAG_SECRET"
The response contains only the fields required for local evaluation:
json{"flags": [{"key": "checkout-redesign","name": "Checkout redesign","active": true,"variants": [{ "key": "control", "rolloutPct": 50 },{ "key": "new", "rolloutPct": 50, "payload": { "layout": "compact" } }],"rules": [{ "rolloutPct": 100, "variant": "new" }],"localEvaluable": true}]}
localEvaluable is false when a rule references a cohort. Cohort membership
depends on server-side event and profile data, so SDKs must use remote
evaluation for that flag. Local evaluation hashes the raw distinct_id, while
remote evaluation hashes the canonical identity id. Pass the identified user id
to local server evaluation when identity linking matters.
json{ "flags": [] }
Flag secrets
The project flag secret is generated through the account API. Only the organization owner or superadmin can mint, rotate, or revoke it.
Mint or rotate a project's flag secret.
bashcurl -X POST https://cohorly-service.velloalabs.com/api/projects/1/flag-secret \-H "Authorization: Bearer $API_KEY"
The response contains { "secret": "..." } once. A rotation invalidates the
old value immediately. GET /api/projects reports only has_flag_secret, not
the secret itself.
Revoke a project's flag secret without creating a replacement.
bashcurl -X DELETE https://cohorly-service.velloalabs.com/api/projects/1/flag-secret \-H "Authorization: Bearer $API_KEY"
The response is { "ok": true }. Revocation is idempotent. New local
definition requests return 401; running server SDKs retain their last
successful definitions by design. Restart them or remove the flag secret from
their configuration to force remote evaluation immediately.
Limits and errors
| Limit | Value |
|---|---|
| Flags per project | 100 |
| Variants per flag | 10 |
| Rules per flag | 10 |
| Override IDs per rule | 50 |
| Override ID length | 255 characters |
| Request body | 1 MB |
| Shared per-IP rate limit | 100 requests per 10 seconds |
| Flags endpoint rate limit | 1,000 requests per 10 seconds per project, plus the shared per-IP limit |
Rate-limited responses are 429 and include a Retry-After header. SDKs
should honor that header and retry with backoff. Invalid JSON, missing fields,
invalid rollout totals, unknown variants, and foreign cohorts return 400.
Creating a flag with a duplicate key returns 409 with key taken; creating
one after the 100-flag project limit returns 409 with flag limit reached.
Requests larger than 1 MB return 413 with
{ "status": 0, "error": "payload too large" }.