Skip to main content

Server API conventions

Conventions shared by every endpoint in the server-api tree. Per-endpoint pages link back here rather than restating these.


Base URL and versioning

All endpoints are rooted at /api/v1. The version number is part of the URL path. No version negotiation via headers is supported — clients that need a different version must update their base URL.

https://<your-exd-host>/api/v1/...

All request and response bodies are JSON unless otherwise noted (manifest upload and download use multipart/form-data and application/octet-stream respectively). The Content-Type: application/json header must be set on all JSON request bodies.


Authentication

Every API request carries a bearer credential in the Authorization header. The credential may be a human session token issued after tenant login or a service token issued through the token-management API. See tokens for credential format and lifecycle; access-control for the per-endpoint permission model.

Authorization: Bearer exd_write_7gHkLmN3pQrStUvWxYz2AbCdEfGhJkLmNpQr3s

Human tenant roles

Human users authenticate through a tenant. A tenant is mapped either to an SSO provider or to a verified email domain.

RoleScopePermissions
tenant_adminOne tenantFull access to all flag namespaces in the tenant.
namespace_adminOne flag namespaceFull access inside that flag namespace.

For email-domain tenants, every verified user in the tenant is a tenant admin. For SSO tenants, tenant-admin status is configured explicitly or derived from trusted SSO claims.

Service tokens

TypePrefixBinding
namespace-writeexd_write_One flag namespace
namespace-readexd_read_One flag namespace
namespace-clientexd_client_One flag namespace + one environment (public-by-design)
tenant-adminexd_tenant_One tenant
superadminexd_admin_Installation

Attempting to use a token outside its bound scope returns 403 Forbidden (forbidden) or 404 Not Found where the resource-visibility rules require resource hiding.


Error response format

All error responses use a consistent JSON envelope:

{
"error": {
"code": "manifest_lint_failed",
"message": "Manifest validation failed with 2 errors",
"request_id": "01HV5XK2GFQT8N3JRDCP7MW04B",
"details": {
"error_count": 2,
"warning_count": 1,
"errors": [
{
"code": "E006",
"severity": "error",
"file": "segments/checkout-redesign-rollout-10.toml",
"line": 34,
"message": "Bucket range must satisfy 0 <= start <= end <= 9999"
}
]
}
}
}

request_id is a ULID present on every response — including successes, in the X-Request-Id header. Include it when reporting issues.

Error codes

CodeHTTP statusDescription
manifest_lint_failed422The uploaded manifest archive failed lint validation. details carries the full lint report. Upload rejected.
version_conflict409If-Version was supplied and the current server version does not match. Fetch the latest version and retry.
namespace_not_found404No flag namespace exists with the given slug under the path's tenant.
tenant_not_found404No tenant exists with the given slug.
flag_not_found404The requested flag key does not exist in the flag namespace's current manifest.
segment_not_found404The requested segment key does not exist in the flag namespace's current manifest.
unauthorized401The bearer credential is missing, malformed, expired, revoked, or otherwise cannot authenticate a principal.
forbidden403The principal is authenticated but lacks the required permission.
invalid_request400The request body or query parameters failed validation. details contains field-level error messages.
schema_version_mismatch422One or more files in the uploaded manifest declare a schema_version whose major component differs from the server's current supported major.
archive_too_large413The uploaded tar.gz archive exceeds the flag-namespace size limit (default: 50 MB uncompressed).
invalid_subscription400The subscription on GET /api/v1/events is malformed or exceeds documented caps. details carries reason (too_many_namespaces, too_many_flags, unknown_namespace, unknown_flag, malformed) and, where applicable, namespace and flag.
closure_token_invalid401The signed token on GET .../closure is missing, expired, malformed, or does not match the (principal, tenant, namespace, version, subscription) tuple it was issued for. Reconnect to the event stream to obtain a fresh token.
closure_unavailable410The closure for (version, subscription) is no longer reconstructable — the requested version has been pruned past the retention horizon. Reconnect to the event stream and treat the next event as a fresh subscription.
rate_limit_exceeded429The request rate limit has been exceeded. See X-RateLimit-Reset for when the limit resets.
internal_error500Unexpected server-side error. Include the request_id in any bug report.

Pagination

All list endpoints support cursor-based pagination. Cursor-based pagination is preferred over offset-based because it is stable under concurrent writes — inserting a new flag namespace between two pages does not cause items to be skipped or duplicated.

Query parameters

ParameterDefaultDescription
limit50Items per page. Maximum 200.
afterOpaque cursor string from the previous response's next_cursor. Omit for the first page.

Response fields

Every list response includes:

FieldDescription
next_cursorOpaque string to pass as after on the next request. null if there are no more pages.

Example

# First page
curl "https://exd.example.com/api/v1/namespaces?limit=2" \
-H "Authorization: Bearer exd_admin_2mNpQrStUvWxYzAbCdEfGhJkLmNoPqRsTuVwXy"

# Response includes: "next_cursor": "01HV5XKCZAQT8N3JRDCP7MW04L"

# Second page
curl "https://exd.example.com/api/v1/namespaces?limit=2&after=01HV5XKCZAQT8N3JRDCP7MW04L" \
-H "Authorization: Bearer exd_admin_2mNpQrStUvWxYzAbCdEfGhJkLmNoPqRsTuVwXy"

Cursors are opaque and must NOT be parsed or constructed by clients. Cursors are valid for 24 hours; stale cursors return invalid_request with the message cursor_expired.


Rate limits

All responses include rate-limit headers:

HeaderDescription
X-RateLimit-LimitMaximum requests allowed in the current window.
X-RateLimit-RemainingRequests remaining in the current window.
X-RateLimit-ResetUnix timestamp (seconds) at which the current window resets.

Exceeding the limit returns 429 Too Many Requests (rate_limit_exceeded). Retry-After is set to the number of seconds until the limit resets.

Limits by endpoint category

CategoryLimitWindowScope
Evaluate, authenticated callers (namespace-read, namespace-write, admin tokens, human sessions)10,0001 minutePer flag namespace
Evaluate, namespace-client callers6001 minutePer (namespace, environment, Origin)
Evaluate, namespace-client callers without Origin (native callers)601 minutePer (namespace, environment, source IP)
Upload (PUT /manifest)601 minutePer flag namespace
Lint (POST /manifest/lint)1201 minutePer flag namespace
Namespace management3001 minutePer token
Tenant management3001 minutePer token
Manifest download6001 minutePer flag namespace
Snapshot (GET /manifest/snapshot)301 minuteGlobal for superadmin; per tenant for tenant-limited
Event stream (GET /events)16 concurrent connectionsPer token
Closure (GET .../closure)6001 minutePer (token, tenant, namespace)

Rate limits are enforced using a sliding-window algorithm. Limits apply to authenticated requests; unauthenticated requests are rejected before reaching rate-limit accounting. The namespace-client per-Origin scope is intentionally narrower than the per-namespace authenticated scope: a single flag namespace running both server-side and browser evaluation has effectively independent quotas for the two modes, and a single misbehaving Origin cannot exhaust the namespace's headroom for legitimate authenticated callers.

Higher throughput? If your use case requires more than these per-namespace limits, prefer the client-side SDK (manifest distribution mode), which downloads the manifest locally and evaluates in-process with zero network latency and no server-side rate limit. Browser deployments that exceed the namespace-client per-Origin quota should batch via /evaluate/all on page load and cache results client-side rather than re-evaluating per interaction.


See also

  • README — server-api index by route family.
  • access-control — permission model behind every endpoint.
  • tokens — credential format and lifecycle.