# 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. |
| `active`              | Whether the flag can enable for any identity.                                                                        |
| `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.                                                                |
| `rules[].cohortId`    | Cohort in the same project.                                                                                          |
| `rules[].distinctIds` | Exact override list, up to 50 IDs, each up to 255 characters.                                                        |
| `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/*`:

```bash
curl https://cohorly-service.velloalabs.com/api/flags?projectId=1 \
  -H "Authorization: Bearer $API_KEY"
```

Send a project token on `/flags/evaluate`:

```bash
curl -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

GET /api/flags List the flags accessible to the authenticated account.

projectId integer 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.

```bash
curl "https://cohorly-service.velloalabs.com/api/flags?projectId=1" \
  -H "Authorization: Bearer $API_KEY"
```

```js
const response = await fetch(
  "https://cohorly-service.velloalabs.com/api/flags?projectId=1",
  { headers: { Authorization: `Bearer ${API_KEY}` } },
);
const flags = await response.json();
```

```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
{ "error": "unauthorized" }
```

### Create a flag

POST /api/flags Create a flag in a project.

projectId integer 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 Lowercase runtime key matching ^\[a-z0-9]\[a-z0-9\_-]{0,199}$.

name string Human-readable name.

description string | null Optional description.

active boolean true Whether evaluations can enable the flag.

variants FlagVariant\[] Optional list of up to 10 variants. Percentages must total 100 when non-empty.

rules FlagRule\[] Optional ordered list of up to 10 rules. An omitted list is empty and matches nobody.

```bash
curl -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.

```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
{ "error": "variants: rolloutPct must sum to exactly 100 (got 90)" }
```

```json
{ "error": "key taken" }
```

### Read, update, and delete a flag

GET /api/flags/{id} Get one flag by numeric id.

id integer Flag id from the list or create response.

PUT /api/flags/{id} Partially update a flag. The key is immutable.

The request can include `name`, `description`, `active`, `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.

```bash
curl -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 /api/flags/{id} Delete a flag.

Deletion removes the flag definition. Remove or retire the corresponding
application code separately.

```json
{ "ok": true }
```

```json
{ "error": "variants: rolloutPct must sum to exactly 100 (got 90)" }
```

```json
{ "error": "not found" }
```

Cross-organization flag ids return `404`, so the API does not disclose the
existence of another organization's flags.

## Remote evaluation

POST /flags/evaluate Evaluate all or selected flags for one distinct\_id.

distinct\_id string Identity for targeting and deterministic rollout bucketing.

flag\_keys string\[] Optional filter. When omitted, returns every flag in the project.

token string Project token alternative to the X-Cohorly-Token header.

```bash
curl -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"]
  }'
```

```json
{
  "flags": {
    "checkout-redesign": {
      "enabled": true,
      "variant": "new",
      "payload": { "layout": "compact" },
      "reason": "rule:0"
    }
  }
}
```

```json
{ "status": 0, "error": "distinct_id required" }
```

```json
{ "status": 0, "error": "invalid token" }
```

```json
{ "status": 0, "error": "rate limited" }
```

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.

## Local-evaluation definitions

GET /flags/local-evaluation Fetch definitions that server SDKs can evaluate in-process.

Authenticate with the project flag secret, not the project token:

```bash
curl 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": [] }
```

```json
{ "status": 0, "error": "invalid flag secret" }
```

```json
{ "status": 0, "error": "rate limited" }
```

## Flag secrets

The project flag secret is generated through the account API. Only the
organization owner or superadmin can mint, rotate, or revoke it.

POST /api/projects/{id}/flag-secret Mint or rotate a project's flag secret.

```bash
curl -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.

DELETE /api/projects/{id}/flag-secret Revoke a project's flag secret without creating a replacement.

```bash
curl -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" }`.
