Feature flags API
Per-environment typed flags — list, get, upsert, delete, compare, promote, and the runtime read used by Shell apps and Coderunners.
Base path: /v1/workspaces/{workspace_id}/flags
See also: Feature flags (runtime examples) and the gf flags commands in Developers.
Environments
Every flag belongs to one environment. On list, get, upsert, delete and runtime, environment_id is an optional query parameter:
- Omitted → the workspace default environment: the active environment named
dev, or the only active environment if there is exactly one. The response echoes the resolvedenvironment_id. - Passed → must be an environment of this workspace (
404otherwise) andactive(409otherwise). - No active environments at all →
409.
Pass it explicitly for anything that must not silently fall back to dev. List environment ids with GET /v1/workspaces/{id}/environments or gf environments ls.
Keys and values
Keys are trimmed and lowercased, then must match ^[a-z][a-z0-9_.-]{0,62}$: they start with a letter and continue with up to 62 lowercase letters, digits, -, _ or .. Examples: new-checkout, ui.dark_mode. Anything else is 400. compare, promote and runtime are reserved (400 on PUT).
Values are validated against value_type (default boolean). A mismatch is 422:
value_type | Accepts | Stored as |
|---|---|---|
boolean | true / false, or the strings "true" / "false" | boolean |
string | a string up to 10,000 chars; null | string (null → "") |
number | integer or float (booleans rejected) | number |
json | any JSON value, including null | as sent |
description is optional, max 255 chars.
Auth
| Operation | Platform token (gf token, portal) | M2M token | Shell end-user JWT | Workload-identity JWT |
|---|---|---|---|---|
GET /flags, GET /flags/{key}, GET /flags/compare | workspace read | ✅ | ❌ 401 (use /runtime) | ❌ 403 |
GET /flags/runtime | workspace read | ✅ | ✅ with any role from the app's sign-in | ❌ 403 |
PUT /flags/{key}, POST /flags/promote | workspace write | ❌ 403 (read-only) | ❌ | ❌ 403 |
DELETE /flags/{key} | workspace delete | ❌ 403 (read-only) | ❌ | ❌ 403 |
read = owner, admin, writer, member. write / delete = owner, admin, writer. Writes are blocked with 423 while the workspace is frozen, and are audited (flag.upserted, flag.deleted, flag.promoted).
Runtime read
GET /v1/workspaces/{workspace_id}/flags/runtime?environment_id=The app-facing read: a compact {key: value} map, for Shell federated apps and Coderunners.
{
"workspace_id": "7965d229-0c0f-4e08-a096-21e04a0fb364",
"environment_id": "0b1e…",
"flags": { "new-checkout": true, "max-items": 25, "banner": { "text": "Hi" } }
}- Headers: weak
ETag,Cache-Control: private, no-cache,Vary: Authorization. - Revalidate: send
If-None-Match: <etag>→304 Not Modified(no body) when nothing changed. - Polling: no more often than every 30 s. No push channel yet. There is no per-route rate limit, but be a good citizen.
- Environment: Shell apps pass
window.__GF_ENVIRONMENT_ID__; Coderunners passGROUNDFLOOR_ENVIRONMENT_ID(injected on deploy). - Values are not secrets: every signed-in end user of the workspace can read them.
List / get / upsert / delete
| Method | Path | Notes |
|---|---|---|
GET | /v1/workspaces/{workspace_id}/flags | Full records (key, value, value_type, description, timestamps) ordered by key, plus the resolved environment_id |
GET | …/flags/{key} | 404 if not set in that environment |
PUT | …/flags/{key} | Body: value, value_type, optional description. Creates or updates in one environment only. |
DELETE | …/flags/{key} | 204; 404 if not set in that environment |
curl -s -X PUT -H "Authorization: Bearer $(gf token)" \
-H "Content-Type: application/json" \
-d '{"value":true,"value_type":"boolean","description":"New checkout flow"}' \
"https://platform.groundfloor.cloud/v1/workspaces/$WORKSPACE_ID/flags/new-checkout?environment_id=$ENV_ID"Compare and promote
GET /v1/workspaces/{workspace_id}/flags/compare?source_environment_id=&target_environment_id=
POST /v1/workspaces/{workspace_id}/flags/promoteCompare returns one row per key with status only_source | only_target | both_same | both_differ, plus both values and types. Both ids are required.
Promote body:
{
"source_environment_id": "…",
"target_environment_id": "…",
"keys": ["new-checkout"],
"mode": "skip_existing",
"dry_run": true
}keys omitted → every source key. mode: skip_existing (default, leaves keys already in the target) or overwrite. The response lists copied, skipped and per-key failed (the call still returns 200).
For both, the environments must differ (400) and be active (409).
Errors
| Status | When |
|---|---|
400 | Invalid or reserved key; compare/promote with the same source and target |
401 | Missing or invalid bearer token (including a Shell JWT on a management route) |
403 | Missing workspace permission; M2M write; workload-identity token; site user without a site role on /runtime |
404 | Unknown workspace, environment (in this workspace) or flag |
409 | Environment not active; workspace has no active environments |
422 | value doesn't match value_type; invalid body |
423 | Workspace frozen (writes) |
Errors use the standard {"detail": "…"} body. The OpenAPI document at /openapi.json carries the same descriptions per operation.
Related
- Environments API
- M2M API
- Secrets API — separate channel; values never appear in secret diffs