Skip to main content

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.

EndpointAuthStatus
GET .../flagsmanifest.readv0
POST .../flagsmanifest.writev0
GET .../flags/{flag}manifest.readv0
PUT .../flags/{flag}manifest.writev0
PATCH .../flags/{flag}manifest.writev0
DELETE .../flags/{flag}manifest.writev0
GET .../flags/{flag}/environments/{env}manifest.readv0
PUT .../flags/{flag}/environments/{env}manifest.writev0
PATCH .../flags/{flag}/environments/{env}manifest.writev0
DELETE .../flags/{flag}/environments/{env}manifest.writev0
GET .../segmentsmanifest.readv0
POST .../segmentsmanifest.writev0
GET .../segments/{segment}manifest.readv0
PUT .../segments/{segment}manifest.writev0
PATCH .../segments/{segment}manifest.writev0
DELETE .../segments/{segment}manifest.writev0
GET .../namespacemanifest.readv0
PUT .../namespacemanifest.writev0
PATCH .../namespacemanifest.writev0
DELETE .../namespacemanifest.writev0

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 testing flag 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 ….

HeaderPurpose
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: trueRun 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-EmailOverride the git commit author. Defaults come from the token's owner / minting user.
X-Exd-Commit-MessageOverride 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.


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[] and referenced_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:

ParameterDescription
typeFilter by flag type (boolean, string, integer, float, json).
lifecycleFilter by lifecycle (development, active, retired).
ownerExact-match owner string.
tagExact-match tag (one tag).
references_segmentFilter 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 null removes 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 forbidden409 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_referenced with details.referenced_by carrying 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 slug400 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:

CodeHTTPDescription
flag_already_exists409POST /flags against a key that already exists. details.flag carries the key.
segment_already_exists409Same, for POST /segments.
catchall_env_required409DELETE on …/environments/_. PATCH the catch-all's variant to change its behavior.
segment_referenced409DELETE on a segment cited by flag rules or other segments. details.referenced_by lists the references.
merge_patch_invalid400PATCH 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_failed422Post-mutation manifest failed lint. Same shape as PUT /manifest's 422.
version_conflict409If-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:

CategoryLimitWindowScope
Resource endpoints, write (POST/PUT/PATCH/DELETE)300 requests1 minutePer namespace
Resource endpoints, read (GET)1,200 requests1 minutePer namespace

(Rate-limit enforcement is itself deferred — see the implementation status table. When it ships, these are the budgets.)


See also