Resource endpoints
REST-style CRUD on individual flags, segments, flag env blocks, and the namespace descriptor — for small operations (kill switch, clear testing, flip public_evaluate) that don't warrant a full GitOps round-trip. The server still GitOps-commits every mutation under the covers: each successful request lands as a new row in manifest_versions with source = "rest" and a commit on the namespace's bare git repo, sharing the per-namespace push lock with PUT /manifest and git push.
The normative contract is docs/spec/server-rest-resources.md. This page is the operator-facing reference.
| Endpoint | Auth | Status |
|---|---|---|
GET .../flags | manifest.read | v0 |
POST .../flags | manifest.write | v0 |
GET .../flags/{flag} | manifest.read | v0 |
PUT .../flags/{flag} | manifest.write | v0 |
PATCH .../flags/{flag} | manifest.write | v0 |
DELETE .../flags/{flag} | manifest.write | v0 |
GET .../flags/{flag}/environments/{env} | manifest.read | v0 |
PUT .../flags/{flag}/environments/{env} | manifest.write | v0 |
PATCH .../flags/{flag}/environments/{env} | manifest.write | v0 |
DELETE .../flags/{flag}/environments/{env} | manifest.write | v0 |
GET .../segments | manifest.read | v0 |
POST .../segments | manifest.write | v0 |
GET .../segments/{segment} | manifest.read | v0 |
PUT .../segments/{segment} | manifest.write | v0 |
PATCH .../segments/{segment} | manifest.write | v0 |
DELETE .../segments/{segment} | manifest.write | v0 |
GET .../namespace | manifest.read | v0 |
PUT .../namespace | manifest.write | v0 |
PATCH .../namespace | manifest.write | v0 |
DELETE .../namespace | manifest.write | v0 |
See conventions for the auth header, error envelope, pagination, and rate limits.
When to use this API vs PUT /manifest
Use the resource API when:
- You want to flip one env's variant (kill switch).
- You want to clear a
testingflag on an env after a rollout completes. - You want to bump a bucket-rollout segment from 10% → 25%.
- You want to add or remove a single rule.
- You want to delete a retired segment that no flag still references.
Use PUT /manifest (or git push) when:
- You're authoring a brand-new namespace.
- You're refactoring many flags / segments together (one atomic version per related change).
- You want to preserve hand-authored TOML comments — the resource API renders canonical TOML and does not preserve comments.
- You want offline editing, branching, and review (the GitOps workflow).
Both surfaces share the same lint pipeline, the same manifest_versions table, and the same SSE event stream. The per-namespace push lock means a REST mutation, a PUT /manifest, and a git push cannot interleave their version bumps.
Concurrency, dry-run, and commit attribution
Every mutation accepts these optional headers, plus the standard Authorization: Bearer ….
| Header | Purpose |
|---|---|
If-Match: "v<N>-sha256:<hex>" | Per-resource concurrency guard. Reject the mutation unless the resource's current ETag matches. The strong validator: a given numbered version's canonical render is byte-deterministic, so two clients at the same version see byte-identical ETags. * is rejected (use unconditional or If-Match proper). |
If-Version: <N> | Per-namespace concurrency guard. Reject the mutation unless the namespace's current manifest_version equals N. Equivalent to PUT /manifest's If-Version. |
X-Exd-Dry-Run: true | Run the full pipeline (render → lint → check), return the would-be response, don't commit. No manifest_versions row, no commit, no SSE event. |
X-Exd-Author-Name, X-Exd-Author-Email | Override the git commit author. Defaults come from the token's owner / minting user. |
X-Exd-Commit-Message | Override the server-generated commit message (single line, ≤ 200 chars). The default is rest: <verb> <resource>[: <field-summary>]. |
Responses set ETag: "v<N>-sha256:<hex>" and X-Exd-Manifest-Version: <N>. The ETag changes whenever the canonical render of the resource bytes changes — use it as the value for If-Match on a follow-up mutation.
No-op detection. A PUT whose canonical render is byte-identical to the on-disk file is a no-op: no commit, no version bump, no event. The response is 200 OK with the unchanged manifest_version and ETag.
namespace-client tokens cannot use this surface. They're scoped to public flag evaluation, not authoring.
Reference graph (_links)
Flag and segment responses carry a _links block listing related resources:
- A flag lists
referenced_segments[]— segments cited by its rules (direct references only, per-rule citation locations). - A segment lists
referenced_by.flags[]andreferenced_by.segments[]— flag rules and other segments' predicates that cite it. - An env block sub-resource lists
referenced_segments[]for the rules in that specific env.
Walk _links.href recursively to construct the transitive closure if you need it.
Flag endpoints
GET /api/v1/tenants/{tenant}/namespaces/{namespace}/flags
List flags in the namespace.
Auth: manifest.read.
Query parameters:
| Parameter | Description |
|---|---|
type | Filter by flag type (boolean, string, integer, float, json). |
lifecycle | Filter by lifecycle (development, active, retired). |
owner | Exact-match owner string. |
tag | Exact-match tag (one tag). |
references_segment | Filter to flags whose any-env rules cite the given segment key. |
Response: 200 OK
{
"flags": [
{
"key": "checkout-redesign",
"type": "boolean",
"description": "Routes /checkout to the redesigned flow.",
"owner": "payments-team",
"lifecycle": "active",
"tags": ["checkout", "q2-2026"],
"etag": "\"v8-sha256:9c2e1ab4...\"",
"manifest_version": 8,
"_links": {
"self": { "href": "/api/v1/tenants/acme/namespaces/payments/flags/checkout-redesign" }
}
}
],
"manifest_version": 8,
"next_cursor": null
}
POST /api/v1/tenants/{tenant}/namespaces/{namespace}/flags
Create a new flag.
Auth: manifest.write.
Request body: Full flag representation with key at the top level. The wire shape mirrors the [flag] TOML table — fields like type, variants, environments translate verbatim.
{
"key": "express-checkout",
"type": "boolean",
"description": "Skip the cart review step.",
"owner": "checkout-team",
"lifecycle": "development",
"tags": ["checkout"],
"variants": { "on": true, "off": false },
"environments": {
"_": { "variant": "off" }
}
}
Response: 201 Created — full flag representation as in GET .../flags/{flag}, plus Location and ETag response headers.
Errors: 409 flag_already_exists (key collision), 422 manifest_lint_failed (post-mutation manifest fails lint).
GET /api/v1/tenants/{tenant}/namespaces/{namespace}/flags/{flag}
Fetch one flag.
Auth: manifest.read.
Optional headers: If-None-Match: "..." (conditional GET — 304 Not Modified if the cached tag matches).
Response: 200 OK
{
"key": "checkout-redesign",
"type": "boolean",
"description": "Routes /checkout to the redesigned flow.",
"owner": "payments-team",
"lifecycle": "active",
"tags": ["checkout", "q2-2026"],
"private_attributes": [],
"variants": { "on": true, "off": false },
"environments": {
"_": { "variant": "off" },
"production": {
"variant": "off",
"testing": true,
"rules": [
{ "description": "Always on for internal employees",
"segment": "internal-employees", "variant": "on" }
]
}
},
"etag": "\"v8-sha256:9c2e...\"",
"manifest_version": 8,
"_links": {
"self": { "href": ".../flags/checkout-redesign" },
"namespace": { "href": ".../namespaces/payments" },
"manifest_archive": { "href": ".../manifest" },
"manifest_version": { "href": ".../manifest/versions/8" },
"evaluate": { "href": ".../evaluate" },
"environments": {
"_": { "href": ".../flags/checkout-redesign/environments/_" },
"production": { "href": ".../flags/checkout-redesign/environments/production" }
},
"referenced_segments": [
{
"key": "internal-employees",
"href": ".../segments/internal-employees",
"ref_count": 1,
"locations": [{ "environment": "production", "rule_index": 0 }]
}
]
}
}
PUT /api/v1/tenants/{tenant}/namespaces/{namespace}/flags/{flag}
Replace a flag wholesale.
Auth: manifest.write.
Request body: Full flag representation (no top-level key — it comes from the URL).
Response: 200 OK — full flag representation. Idempotent: re-PUTing identical content is a no-op (no version bump, response carries the unchanged ETag).
Errors: 404 flag_not_found (use POST to create), 409 version_conflict (If-Match / If-Version mismatch), 422 manifest_lint_failed.
PATCH /api/v1/tenants/{tenant}/namespaces/{namespace}/flags/{flag}
Apply a JSON Merge Patch to a flag.
Auth: manifest.write.
Content-Type: application/merge-patch+json is required (other types → 400 invalid_request). Per RFC 7396:
- Setting a field to a value replaces it.
- Setting a field to
nullremoves it. - Arrays are replaced wholesale (no per-element merging).
- Server-managed keys (
_links,etag,manifest_version,schema_version,is_implicit,dry_run) are forbidden in the patch body →400 merge_patch_invalid.
Example: mark a flag retired and remove its tags.
curl -X PATCH https://exd.example.com/api/v1/tenants/acme/namespaces/payments/flags/checkout-redesign \
-H "Authorization: Bearer exd_write_..." \
-H "Content-Type: application/merge-patch+json" \
-d '{"lifecycle": "retired", "tags": []}'
DELETE /api/v1/tenants/{tenant}/namespaces/{namespace}/flags/{flag}
Delete a flag.
Auth: manifest.write.
Response: 204 No Content. 404 flag_not_found if the flag does not exist.
Nothing in the schema references a flag by key, so there is no flag_referenced precondition today. (The error code is reserved for a future schema that adds flag-to-flag references.)
Flag env-block sub-resource
The most common one-field operations — kill switch, clear testing, append a rule — live inside one [flag.environments.<env>] block. The sub-resource lets you mutate it without resending the rest of the flag.
GET /api/v1/tenants/{tenant}/namespaces/{namespace}/flags/{flag}/environments/{env}
Fetch one env block.
Auth: manifest.read.
A GET on a named env that the flag does not declare returns the implicit shape (empty rules, testing: false, variant: null) with is_implicit: true. Resolution still falls through to _. The sub-resource's ETag is the parent flag's ETag.
Response: 200 OK
{
"variant": "off",
"testing": true,
"rules": [
{ "segment": "internal-employees", "variant": "on" }
],
"etag": "\"v8-sha256:9c2e...\"",
"manifest_version": 8,
"is_implicit": false,
"_links": {
"self": { "href": ".../flags/checkout-redesign/environments/production" },
"flag": { "href": ".../flags/checkout-redesign" },
"catchall_env": { "href": ".../flags/checkout-redesign/environments/_" },
"referenced_segments": [
{ "key": "internal-employees",
"href": ".../segments/internal-employees",
"ref_count": 1,
"locations": [{ "rule_index": 0 }] }
]
}
}
PUT .../flags/{flag}/environments/{env}
Replace one env block.
Auth: manifest.write.
Body: { "variant": "...", "rules": [...], "testing": false } (any subset; _ MUST carry variant).
PATCH .../flags/{flag}/environments/{env}
Merge-patch one env block (RFC 7396; same Content-Type rules as flag PATCH).
This is the workhorse for small operations. Worked examples below.
DELETE .../flags/{flag}/environments/{env}
Remove one named env block, falling through to _.
DELETE …/environments/_ is forbidden → 409 catchall_env_required. The catch-all is structurally mandatory per lint code E037. To "delete" its current behavior, PATCH its variant instead.
Segment endpoints
The segment surface mirrors flags. The major difference is the referential-integrity precondition on DELETE: a segment cited by any flag rule or another segment's predicate cannot be deleted until the references are removed.
GET .../segments
Query parameters: shape=predicate|bucket|both, referenced_by_flag=<flag-key>.
POST .../segments
Create a new segment. Body carries key at the top level plus the segment fields (description, predicate, bucket).
GET .../segments/{segment}
Returns the segment representation with _links.referenced_by.{flags,segments} describing who cites it.
PUT .../segments/{segment} and PATCH .../segments/{segment}
Same conventions as the flag PUT / PATCH. Same Content-Type requirement for PATCH.
DELETE .../segments/{segment}
Preconditions:
- Segment exists. Otherwise
404 segment_not_found. - No flag rule or other segment predicate cites it. Otherwise
409 segment_referencedwithdetails.referenced_bycarrying the same shape as_links.referenced_by.
The server does not cascade. Remove the references first (PATCH each citing resource), then retry the DELETE.
{
"error": {
"code": "segment_referenced",
"message": "segment 'legacy-tier' is referenced by 2 flag(s) and 1 segment(s); remove the references first",
"details": {
"referenced_by": {
"flags": [
{"key": "checkout-redesign", "ref_count": 1,
"locations": [{"environment":"production","rule_index":2}]},
{"key": "homepage-banner", "ref_count": 1,
"locations": [{"environment":"_","rule_index":0}]}
],
"segments": [
{"key": "us-employees-or-legacy", "ref_count": 1}
]
}
},
"request_id": "01HV5XKE..."
}
}
Namespace descriptor (singleton)
The namespace descriptor is the JSON representation of namespace.toml. It's a singleton resource (no key) addressed at …/namespace. namespace.toml is structurally optional — when it's absent, the linter falls back to untyped-env mode and infers the slug from the directory name; the REST surface mirrors that with is_implicit: true on GET.
GET .../namespace
Auth: manifest.read.
Always succeeds. Returns the on-disk content when present, or the inferred-default body with is_implicit: true when absent.
{
"slug": "payments",
"display_name": "Payments Team",
"description": "Feature flags for the payments service and checkout domain",
"telemetry_enabled": true,
"raw_entity_ids": false,
"private_attributes": ["user.email", "user.phone"],
"environments": {
"development": { "display_name": "Development" },
"staging": { "display_name": "Staging" },
"production": { "display_name": "Production", "public_evaluate": false }
},
"etag": "\"v8-sha256:1a2b...\"",
"manifest_version": 8,
"is_implicit": false,
"_links": {
"self": { "href": ".../namespace" },
"namespace": { "href": ".../namespaces/payments" },
"manifest_archive": { "href": ".../manifest" },
"manifest_version": { "href": ".../manifest/versions/8" },
"flags": { "href": ".../flags" },
"segments": { "href": ".../segments" }
}
}
PUT .../namespace
Auth: manifest.write. Replace the descriptor; creates namespace.toml if absent.
The slug field is read-only on this resource — its value comes from the URL. A body that supplies a different slug → 400 invalid_request. (Renames are a tenant-level operation outside this API.)
PATCH .../namespace
Auth: manifest.write. Merge-patch the descriptor (RFC 7396; same Content-Type rules as flag PATCH).
DELETE .../namespace
Auth: manifest.write. Remove namespace.toml, dropping the manifest to untyped-env mode. Future GETs return the implicit shape.
Rejected with 409 namespace_client_token_bound if any namespace-client token is bound to a typed-env-only feature (e.g., public_evaluate = true on an env). Rotate or revoke those tokens first.
Worked examples
Kill switch on a single env
The on-call engineer needs to disable checkout-redesign in production immediately.
# Read the current ETag.
curl -s https://exd.example.com/api/v1/tenants/acme/namespaces/payments/flags/checkout-redesign/environments/production \
-H "Authorization: Bearer $TOKEN" \
-D - -o /dev/null | grep -i '^etag:'
# etag: "v8-sha256:9c2e..."
# Flip to "off", drop testing, clear rules. If-Match guards against
# a concurrent edit between the read and the write.
curl -X PATCH https://exd.example.com/api/v1/tenants/acme/namespaces/payments/flags/checkout-redesign/environments/production \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/merge-patch+json" \
-H 'If-Match: "v8-sha256:9c2e..."' \
-H "X-Exd-Author-Name: Ada Lovelace" \
-H "X-Exd-Author-Email: ada@acme.example" \
-H "X-Exd-Commit-Message: kill switch: disable checkout-redesign in production" \
-d '{"variant":"off","testing":null,"rules":[]}'
The response carries the post-mutation ETag and the bumped manifest_version. Downstream SDKs see a version SSE event with the new closure delta the moment the commit lands.
Clear testing after rollout completes
The minimal one-field operation:
curl -X PATCH https://exd.example.com/api/v1/tenants/acme/namespaces/payments/flags/checkout-redesign/environments/production \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/merge-patch+json" \
-d '{"testing": null}'
Bump a bucket-rollout segment from 10% → 25%
curl -X PATCH https://exd.example.com/api/v1/tenants/acme/namespaces/payments/segments/checkout-redesign-rollout-10 \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/merge-patch+json" \
-d '{"bucket": {"end": 2499}}'
The rest of the bucket fields (entity_id_attribute, salt, start) merge through from the prior on-disk state. The segment's name no longer reflects the percentage — accept that mismatch, or do a POST + reference rewrite + DELETE dance (a rename verb is on the deferred list).
Toggle public_evaluate on production
curl -X PATCH https://exd.example.com/api/v1/tenants/acme/namespaces/payments/namespace \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/merge-patch+json" \
-d '{"environments": {"production": {"public_evaluate": true}}}'
The merge-patch goes deep — the other env entries (and the rest of the descriptor) are untouched.
Dry-run any mutation
Add X-Exd-Dry-Run: true to any PUT / PATCH / DELETE. The server runs the full pipeline (render → lint → reference check) and returns the would-be response — including the lint warnings list and the new ETag — without committing, bumping manifest_versions, or emitting an SSE event.
curl -X PATCH https://exd.example.com/api/v1/tenants/acme/namespaces/payments/segments/legacy-tier \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/merge-patch+json" \
-H "X-Exd-Dry-Run: true" \
-d '{"description": "Marked for deletion in Q3 2026"}'
Error codes
The resource API uses the standard error envelope from conventions. Codes specific to this surface:
| Code | HTTP | Description |
|---|---|---|
flag_already_exists | 409 | POST /flags against a key that already exists. details.flag carries the key. |
segment_already_exists | 409 | Same, for POST /segments. |
catchall_env_required | 409 | DELETE on …/environments/_. PATCH the catch-all's variant to change its behavior. |
segment_referenced | 409 | DELETE on a segment cited by flag rules or other segments. details.referenced_by lists the references. |
merge_patch_invalid | 400 | PATCH body is not a JSON object, or it carries a forbidden key (_links, etag, manifest_version, schema_version, is_implicit, dry_run, request_id). |
manifest_lint_failed | 422 | Post-mutation manifest failed lint. Same shape as PUT /manifest's 422. |
version_conflict | 409 | If-Match or If-Version did not match the current value. details.expected + details.actual describe the mismatch. |
Rate limits
Per conventions § Rate Limits. Spec § 12 reserves:
| Category | Limit | Window | Scope |
|---|---|---|---|
| Resource endpoints, write (POST/PUT/PATCH/DELETE) | 300 requests | 1 minute | Per namespace |
| Resource endpoints, read (GET) | 1,200 requests | 1 minute | Per namespace |
(Rate-limit enforcement is itself deferred — see the implementation status table. When it ships, these are the budgets.)
See also
- Spec (normative):
docs/spec/server-rest-resources.md. - manifest endpoints — the archive surface this API sits beside.
- conventions — shared auth, error envelope, pagination, rate-limit headers.
- reference/manifest/ — the on-disk manifest schema each resource maps to. Flag →
05-flag.md; segment →06-segment.md; predicate →07-predicates.md; resolution algorithm →09-resolution.md. - reference/manifest/11-diagnostics.md — the diagnostic codes that surface in
422 manifest_lint_failedresponses.