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 login makes 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 their start, stop and results.
  • 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}/triggers and /projects/{p}/webhooks; approval policies are the environment’s approvals, set with PATCH /projects/{p}/environments/{e}.
  • Audit log: /audit-log, /audit-log/export and /audit-destinations.
  • Export: GET /export streams the whole organisation as a zip, for an admin or owner; featflags export and featflags import use it.