API reference
Authenticate with an API token, call the main endpoints for projects, flags, targeting, lists, history and keys, and handle errors and rate limits.
Everything the app does goes through the same HTTP API, so you can script any of it. The CLI and the Terraform provider cover the common jobs; this page is for when you call the API yourself.
Authentication
Create an API token (fft_…) under Settings → API tokens and send it as a bearer token:
export API=https://app.featflags.io/api
export TOKEN=fft_…
curl -H "Authorization: Bearer $TOKEN" "$API/projects"
The token is shown once, when it’s created. A token belongs to one organisation and only ever acts there, so paths don’t name the organisation: /api/projects/acme/… is the project acme of the token’s organisation.
- A personal token belongs to a member, and never has more access than they do: if their role shrinks, so does the token, and it stops working when they leave the organisation.
featflags loginmakes one for you. - A service token belongs to no one, for CI and integrations, with only the roles you give it: in the organisation, in one project, or (on Business) in one environment.
Owners manage the organisation’s tokens and see them all; everyone else sees and makes their own, with at most their own roles. A token can expire on a date you set, and revoking it is immediate.
Requests and responses
Bodies are JSON. Two headers work on any request that changes something:
Featflags-Comment: <text>records why with the change. Environments can require one, and refuse changes without it.Featflags-Apply-At: <RFC 3339 time>schedules the change for later instead (Team and Business).
A change in an environment that requires approval, or one with Featflags-Apply-At, answers 202 with the held change instead of applying it. Changes that would leave things as they are record nothing.
Errors
Errors are JSON with an error message, and sometimes more:
| Status | Means | Body |
|---|---|---|
| 400 | The request is malformed. | { "error": "…" } |
| 401 | No token, or it’s unknown, expired or revoked. | { "error": "unauthorized" } |
| 402 | Your plan doesn’t include this, or you’ve reached a limit. | { "error": "plan_limit", "limit": "projects", "plan": "free", "message": "the Free plan allows 1 project: upgrade for more" } |
| 402 | The organisation is read-only after a failed payment: reads work, changes don’t. | { "error": "billing", "message": "…" } |
| 403 | The token’s roles don’t allow it. | { "error": "missing permission: write flag_state in project web, environment production", "missing": "…" } |
| 404 | No such thing in your organisation. Another organisation’s projects are always 404, never 403, so nothing can be probed. | { "error": "project web not found" } |
| 409 | It conflicts with the current state, such as deleting a flag that isn’t archived. | { "error": "…" } |
| 422 | The result would be invalid: every problem is listed. | { "error": "invalid configuration", "details": ["environment production requires a comment"] } |
| 429 | Too many requests: wait Retry-After seconds. |
{ "error": "rate_limited", "message": "more than 10 writes a second from this credential" } |
| 503 | featflags is in maintenance: flags are served, changes wait. Try again after Retry-After. |
{ "error": "maintenance", "message": "…" } |
Rate limits
Each organisation can make 30 changes and 300 reads a second, and each token (or person signed in to the app) 10 changes and 100 reads a second, so one runaway script leaves its colleagues most of the organisation’s share. Every request counts, refused ones included. Past a limit you get a 429 with Retry-After; the CLI and the Terraform provider wait and retry on their own.
SDKs don’t use the API and aren’t limited by it: they read flags from a CDN.
Projects and environments
curl -H "Authorization: Bearer $TOKEN" "$API/projects" # projects you can read, with their environments
curl -H "Authorization: Bearer $TOKEN" "$API/projects/acme" # one project: flags, segments, environments and their keys
curl -X POST -H "Authorization: Bearer $TOKEN" -H 'content-type: application/json' "$API/projects/acme/environments" \
-d '{"key":"staging"}'
Flags and targeting
Create a flag (boolean, with variants on and off, unless you give variants):
curl -X POST -H "Authorization: Bearer $TOKEN" -H 'content-type: application/json' "$API/projects/acme/flags" \
-d '{"key":"new-checkout","description":"Checkout rewrite","client_visible":true}'
Set its targeting in an environment. Send only the parts you change; rules replaces the whole ordered list. Percentages are in thousandths of a percent, so weights add up to 100000:
curl -X PATCH -H "Authorization: Bearer $TOKEN" -H 'content-type: application/json' -H 'Featflags-Comment: start the rollout' \
"$API/projects/acme/flags/new-checkout/environments/production" -d '{
"enabled": true,
"targets": [{ "variant": "on", "keys": ["u-alice", "u-bob"] }],
"rules": [{ "id": "staff", "conditions": [{ "op": "in_segment", "values": ["staff"] }], "serve": { "variant": "on" } }],
"default": { "rollout": { "split": [{ "variant": "on", "weight": 20000 }, { "variant": "off", "weight": 80000 }] } }
}'
A condition is { "kind", "attr", "op", "values", "negate" }, with kind user unless you say otherwise; the SDK specification lists every operator. Other flag endpoints:
| Endpoint | What |
|---|---|
GET /projects/{p}/flags |
Flags with their status per environment; filter with status, env, ready_to_archive. |
GET /projects/{p}/flags/{f} |
One flag, with its release plans. |
PATCH /projects/{p}/flags/{f} |
Description, client visibility, temporary, archived: true or false. |
DELETE /projects/{p}/flags/{f} |
Deletes an archived flag. |
PUT /projects/{p}/flags/{f}/environments/{e}/rules/{id} |
One rule, placed by priority. |
GET /projects/{p}/flags/{f}/compare?from=staging&to=production |
The two environments side by side. |
POST /projects/{p}/flags/{f}/environments/{e}/copy |
Copies parts from another environment, as one change. |
GET /projects/{p}/flags/{f}/stats?env=production&hours=24 |
Evaluations by variant and hour. |
Segments
curl -X PUT -H "Authorization: Bearer $TOKEN" -H 'content-type: application/json' "$API/projects/acme/segments/staff" \
-d '{"match":[[{"attr":"email","op":"ends_with","values":["@acme.com"]}]]}'
match is a list of groups: a context matches when every condition of any one group does.
Upload a list
Lists of keys, one per line, for a segment’s included or excluded keys of one context kind, or a flag’s individual targets for one variant. The upload replaces the list and answers 202 with a job, which goes live in the background; follow it until its status is done:
curl -X PUT -H "Authorization: Bearer $TOKEN" --data-binary @customers.txt "$API/projects/acme/segments/customers/lists/included/user"
curl -X PUT -H "Authorization: Bearer $TOKEN" --data-binary @qa.txt "$API/projects/acme/flags/new-checkout/environments/production/targets/on/user"
curl -H "Authorization: Bearer $TOKEN" "$API/projects/acme/lists/jobs/12"
# → { "id": 12, "status": "uploading", "progress": { "uploaded": 1260, "shards": 32768 }, … }
An empty body deletes the list. A later upload to the same list replaces one still in progress.
History
curl -H "Authorization: Bearer $TOKEN" "$API/projects/acme/changes?target_key=new-checkout&env=production&limit=20"
curl -X POST -H "Authorization: Bearer $TOKEN" "$API/projects/acme/changes/412/restore"
Each change has its author, comment, status, and the state before and after. Restoring writes a change’s before back, as a new change. Held changes are listed with status=pending_approval,scheduled, and POST …/changes/{id}/approve, reject, cancel and apply act on them.
SDK keys
curl -X POST -H "Authorization: Bearer $TOKEN" -H 'content-type: application/json' "$API/projects/acme/environments/production/keys" \
-d '{"kind":"server","name":"server-2026-10"}' # the key is in the answer, this once
curl -X PATCH -H "Authorization: Bearer $TOKEN" -H 'content-type: application/json' "$API/projects/acme/environments/production/keys/2" \
-d '{"expires_at":"2026-10-22T09:00:00Z"}' # the old key stops then
curl -X DELETE -H "Authorization: Bearer $TOKEN" "$API/projects/acme/environments/production/keys/2" # or now
Everything else
- Experiments and metrics:
/projects/{p}/metrics,/projects/{p}/experiments, and theirstart,stopandresults. - Release templates and plans:
/projects/{p}/release-templates, and/projects/{p}/flags/{f}/environments/{e}/release-plan. - Triggers and webhooks:
/projects/{p}/flags/{f}/environments/{e}/triggersand/projects/{p}/webhooks; approval policies are the environment’sapprovals, set withPATCH /projects/{p}/environments/{e}. - Audit log:
/audit-log,/audit-log/exportand/audit-destinations. - Export:
GET /exportstreams the whole organisation as a zip, for an admin or owner;featflags exportandfeatflags importuse it.