Manifest endpoints
Endpoints for uploading, downloading, validating, and history-walking flag-namespace manifests. The flag-namespace manifest itself is documented in reference/manifest/; this page is the wire contract.
| Endpoint | Auth | Status |
|---|---|---|
PUT /api/v1/tenants/{tenant}/namespaces/{namespace}/manifest | manifest.write | v0 |
GET /api/v1/tenants/{tenant}/namespaces/{namespace}/manifest | manifest.read | v0 |
GET .../manifest/versions | manifest.read | v0 |
GET .../manifest/versions/{version} | manifest.read | v0 |
/api/v1/tenants/{tenant}/namespaces/{namespace}/manifest.git/... | manifest.read / manifest.write | v0 |
POST .../manifest/lint | manifest.read | v0 |
See conventions for the auth header, error envelope, pagination, and rate limits.
PUT /api/v1/tenants/{tenant}/namespaces/{namespace}/manifest
Upload a new manifest version for (tenant, namespace). The body is multipart/form-data with a single field archive containing a .tar.gz of the flag-namespace directory contents (everything inside <namespace-slug>/, NOT the directory itself).
The server runs the full linter on the uploaded archive before accepting it. Any lint errors → manifest_lint_failed (422), current manifest unchanged.
Auth required: namespace admin, tenant_admin for {tenant}, namespace-write bound to (tenant, namespace), tenant-admin token, or superadmin. Permission: manifest.write.
Request
Content-Type: multipart/form-data
| Form field | Type | Required | Description |
|---|---|---|---|
archive | file (tar.gz) | yes | The manifest archive. Maximum 50 MB uncompressed, 5 MB compressed. |
Optional headers
| Header | Description |
|---|---|
If-Version: <N> | Optimistic concurrency control. If the current server-side manifest version is not <N>, upload is rejected with 409 version_conflict. Use If-Version: 0 for the first upload to a flag namespace that has no prior manifest. Omit to unconditionally replace (force-push). |
Response: 200 OK
{
"version": 8,
"uploaded_at": "2026-04-25T12:00:00Z",
"flag_count": 13,
"segment_count": 4,
"lint": {
"errors": [],
"warnings": [
{
"code": "W003",
"severity": "warning",
"file": "flags/payment-retry-v2.toml",
"line": 1,
"message": "Flag has no rules in any environment and always returns its `value`"
}
],
"infos": []
},
"request_id": "01HV5XK6UFQT8N3JRDCP7MW04F"
}
Example
# Create the archive from the flag-namespace directory
tar -czf payments.tar.gz -C payments/ .
# Upload with optimistic concurrency (current version must be 7)
curl -X PUT https://exd.example.com/api/v1/tenants/acme/namespaces/payments/manifest \
-H "Authorization: Bearer exd_write_7gHkLmN3pQrStUvWxYz2AbCdEfGhJkLmNpQr3s" \
-H "If-Version: 7" \
-F "archive=@payments.tar.gz"
GET /api/v1/tenants/{tenant}/namespaces/{namespace}/manifest
Download the current manifest for (tenant, namespace) as a .tar.gz.
Auth required: namespace admin, tenant_admin, namespace-read, namespace-write, tenant-admin token, or superadmin. Permission: manifest.read.
Optional headers
| Header | Description |
|---|---|
If-None-Match: "v<N>" | Conditional GET. The server compares against the current manifest's ETag ("v<current>"). Match → 304 Not Modified (empty body). Accepts comma-separated lists, the * wildcard, the W/ weak prefix, and bare numeric values (1) for caller convenience. |
Response: 200 OK
Body is application/octet-stream (tar.gz). Response headers:
Content-Type: application/octet-stream
Content-Disposition: attachment; filename="payments-v8.tar.gz"
ETag: "v8"
X-Exd-Manifest-Version: 8
X-Exd-Uploaded-At: 2026-04-25T12:00:00Z
X-Request-Id: 01HV5XK7VFQT8N3JRDCP7MW04G
ETag is a strong validator — a given numbered manifest's archive bytes are immutable, so byte-for-byte equality is guaranteed when the version matches. The version-history endpoint also emits and honors the same ETag format.
Response: 304 Not Modified
Returned when If-None-Match matches the current version. Body is empty; ETag and X-Exd-Manifest-Version are still set so callers can correlate without parsing a body.
Example
# Initial pull
curl https://exd.example.com/api/v1/tenants/acme/namespaces/payments/manifest \
-H "Authorization: Bearer exd_read_4tRvBn9wKjMpXzQsUyAeCdFgHiJkLnOqRtWvYb" \
-o payments-current.tar.gz
# Second poll, conditional. The server replies 304 if nothing has changed.
curl -i https://exd.example.com/api/v1/tenants/acme/namespaces/payments/manifest \
-H "Authorization: Bearer exd_read_4tRvBn9wKjMpXzQsUyAeCdFgHiJkLnOqRtWvYb" \
-H 'If-None-Match: "v8"'
GET /api/v1/tenants/{tenant}/namespaces/{namespace}/manifest/versions
List the version history for (tenant, namespace)'s manifest. Returns a summary of each version without the full archive content.
Auth required: manifest.read.
Query parameters
Supports limit + after (pagination).
Response: 200 OK
{
"versions": [
{
"version": 8,
"uploaded_at": "2026-04-25T12:00:00Z",
"uploaded_by_token_prefix": "exd_write_7gHkLm",
"size_bytes": 4821,
"flag_count": 13,
"segment_count": 4
},
{
"version": 7,
"uploaded_at": "2026-04-24T16:45:11Z",
"uploaded_by_token_prefix": "exd_write_7gHkLm",
"size_bytes": 4603,
"flag_count": 12,
"segment_count": 4
}
],
"next_cursor": "01HV5ABC...",
"request_id": "01HV5XK8WFQT8N3JRDCP7MW04H"
}
| Field | Description |
|---|---|
version | Integer version counter, monotonically increasing per flag namespace. |
uploaded_at | ISO 8601 timestamp of when this version was accepted by the server. |
uploaded_by_token_prefix | First 14 characters of the token used to upload this version. Full tokens are never stored. |
size_bytes | Uncompressed size of the manifest archive. |
flag_count | Number of flag files in this version. |
segment_count | Number of segment files in this version. |
Example
curl "https://exd.example.com/api/v1/tenants/acme/namespaces/payments/manifest/versions?limit=10" \
-H "Authorization: Bearer exd_read_4tRvBn9wKjMpXzQsUyAeCdFgHiJkLnOqRtWvYb"
GET /api/v1/tenants/{tenant}/namespaces/{namespace}/manifest/versions/{version}
Download a specific historical version of the manifest as a .tar.gz.
Auth required: manifest.read.
Optional headers
| Header | Description |
|---|---|
If-None-Match: "v<N>" | Conditional GET against the requested version's ETag. Match → 304 Not Modified. Useful when a debugger has already pinned a historical version locally and wants to confirm bytes have not been re-uploaded. |
Response: 200 OK
Body is application/octet-stream. Response headers follow the same pattern as the current-version download endpoint, with X-Exd-Manifest-Version and ETag set to the requested version.
Example
curl https://exd.example.com/api/v1/tenants/acme/namespaces/payments/manifest/versions/6 \
-H "Authorization: Bearer exd_read_4tRvBn9wKjMpXzQsUyAeCdFgHiJkLnOqRtWvYb" \
-o payments-v6.tar.gz
Git smart-HTTP push
In addition to multipart PUT /manifest, the server exposes each flag namespace as a git repository over standard smart-HTTP. Developers can git clone, edit on disk, commit, and git push to publish a new manifest version. The bare repo at ${EXD_GIT_ROOT}/<tenant>/<slug>.git is the canonical record of manifest history; the manifest_versions table records the (version, commit) mapping.
Endpoints
All rooted at /api/v1/tenants/<tenant>/namespaces/<slug>/manifest.git/:
| Path | Method | Purpose |
|---|---|---|
info/refs?service=git-upload-pack | GET | Smart-HTTP advertisement for clone/fetch |
git-upload-pack | POST | Pack body for clone/fetch |
info/refs?service=git-receive-pack | GET | Smart-HTTP advertisement for push |
git-receive-pack | POST | Pack body for push |
Auth
Both Authorization: Bearer <token> and Authorization: Basic <base64(user:token)> are accepted (the username is ignored by convention). The server returns 401 Unauthorized with WWW-Authenticate: Basic realm="exd" for unauthenticated requests so the git CLI prompts for credentials and credential helpers can supply them.
| Operation | Required permission |
|---|---|
Clone / fetch (git-upload-pack) | namespace-read or higher |
Fast-forward push (git-receive-pack) | namespace-write or higher |
| Non-fast-forward push (force-push) | tenant-admin or superadmin |
Branch policy
Only refs/heads/main is accepted. Pushes to any other ref are rejected by the pre-receive hook with a message identifying the policy. Branch deletion is rejected.
Validation
Lint runs server-side inside the pre-receive hook (the same pipeline as PUT /manifest). Lint errors are streamed to stderr — git's CLI surfaces them prefixed with remote: — and the hook exits non-zero, so the ref does not move. Warnings do not block. On success, the hook produces a tar.gz cache via git archive, writes a manifest_versions row with source = "push", and bumps current_manifest_version atomically.
Concurrency and idempotency
Pushes serialize per flag namespace via an in-process mutex shared with PUT /manifest, so the two upload paths cannot interleave their version bumps. If the bare repo and database get out of sync (e.g., after a crash), the hook detects an existing manifest_versions row for the pushed commit and skips the duplicate write. Lazy backfill replays any pre-existing PUT /manifest versions into the bare repo on the first git request.
Body limits
Smart-HTTP pack uploads are capped at 200 MB (4× the multipart cap, since git history is included).
Token visibility
Authentication credentials are placed in the Authorization header. Using HTTP Basic with embedded URL credentials (http://token:<secret>@…) or the user's git credential helper exposes the secret in the local credential store. Operators should mint short-lived tokens for individual users.
Example workflow
# Clone — Basic creds via the git CLI's standard credential helper.
git clone https://exd.example.com/api/v1/tenants/acme/namespaces/payments/manifest.git
# (git prompts; username = `token`, password = the bearer secret)
cd payments
$EDITOR flags/payment-retry-v2.toml
git add -A
git commit -m "Bump retry budget to 8s"
git push origin main
# remote: exd: lint passed; recorded version 9 at 4f2c1ab9 (warnings: 0)
Example error (lint failure rejects the push)
$ git push origin main
remote: exd lint error [E004] flags/payment-retry-v2.toml:5: variant 'maybe' not declared in [flag.variants]
remote: exd: manifest validation failed with 1 error(s); push rejected
To https://exd.example.com/api/v1/tenants/acme/namespaces/payments/manifest.git
! [remote rejected] main -> main (pre-receive hook declined)
error: failed to push some refs
The previous version remains unchanged.
Deferred: branch protection / required reviewers, repo-level pack-bomb mitigation knobs, hosting bare repos on shared/replicated storage for multi-instance deployments, optional GPG-signed-commit enforcement.
POST /api/v1/tenants/{tenant}/namespaces/{namespace}/manifest/lint
Lint a manifest archive without uploading it or changing flag-namespace state. Useful for CI validation in pull-request checks before merging flag changes.
Auth required: manifest.read (any token type with flag-namespace read access).
Request
Content-Type: multipart/form-data
| Form field | Type | Required | Description |
|---|---|---|---|
archive | file (tar.gz) | yes | The manifest archive to lint. |
Response: 200 OK
Returned even when lint errors are present — the HTTP status reflects whether the request itself succeeded, not whether the manifest is clean.
{
"namespace": "payments",
"passed": false,
"error_count": 1,
"warning_count": 2,
"info_count": 1,
"diagnostics": [
{
"code": "E006",
"severity": "error",
"file": "segments/checkout-redesign-rollout-10.toml",
"line": 34,
"message": "Bucket range must satisfy 0 <= start <= end <= 9999"
},
{
"code": "W003",
"severity": "warning",
"file": "flags/payment-retry-v2.toml",
"line": 1,
"message": "Flag has no rules in any environment and always returns its `value`"
}
],
"request_id": "01HV5XK9XFQT8N3JRDCP7MW04I"
}
Example
tar -czf payments.tar.gz -C payments/ .
curl -X POST https://exd.example.com/api/v1/tenants/acme/namespaces/payments/manifest/lint \
-H "Authorization: Bearer exd_read_4tRvBn9wKjMpXzQsUyAeCdFgHiJkLnOqRtWvYb" \
-F "archive=@payments.tar.gz"
See also
- reference/manifest/ — the manifest format itself.
- reference/manifest/packaging — what goes in the tar.gz;
If-Versionsemantics. - reference/manifest/diagnostics — every
E/W/Icode the lint response can include. exd lint,exd manifest push|pull|versions— CLIs that wrap these endpoints.- evaluation — what callers do once a manifest is published.