Reference
Edge server API
You can evaluate flags over HTTP with an SDK key, using the edge server endpoints.
Before you start
- The edge server is running, by default on port 8081. Clients and edge server shows how to start it.
- You have a client and its SDK key. The examples use
checkout-service, a Backend client in the Development environment, with the flagexpress-checkout. curlis installed.jqis optional.
The examples set the SDK key once in a shell variable. Replace the placeholder with your own key:
export FLUXGATE_SDK_KEY=<checkout-service SDK key>The responses below come from a real edge server. Your values depend on the rules you deployed. The ETag values differ on your system.
What the edge server offers
The edge server is the only part of FluxGate your app calls. It keeps a cached copy of your flags and evaluates them locally.
| Endpoint | Purpose | Credentials |
|---|---|---|
GET /health, GET /health/ready | Tell you whether the edge can evaluate flags. | None. |
GET /health/live | Tells you the edge process runs. | None. |
GET /health/detail | Shows the stream state, config source, and config age. | None. |
GET /metrics | Prometheus metrics. | None. |
POST /evaluate | Evaluates one flag as the client configured on the edge. | None from the caller. |
POST /ofrep/v1/evaluate/flags/{key} | Evaluates one flag, OFREP format. | SDK key. |
POST /ofrep/v1/evaluate/flags | Evaluates every flag of the client's team, OFREP format. | SDK key. |
OFREP is the OpenFeature remote evaluation protocol. Any OpenFeature SDK with an OFREP provider can call the two OFREP endpoints. See SDKs and integrations.
Every evaluation needs a bucketing key, which FluxGate uses to place a user in a percentage rollout. /evaluate calls it bucketingKey. OFREP calls it targetingKey. Other fields of the context, such as country and user_tier, are attributes that targeting rules read. See Contexts.
Authentication
OFREP endpoints use an SDK key
An SDK key is the client ID, a dot, and the client's API key:
<clientId>.<apiKey>The client ID is a UUID. The admin UI shows the SDK key on the client page. The edge splits the key at the first dot. Send it in one of these headers. If both are present, Authorization wins.
| Header | Value |
|---|---|
Authorization | Bearer <clientId>.<apiKey> |
X-API-Key | <clientId>.<apiKey> |
The edge never falls back to a secret of its own for these endpoints, and a bare client ID is rejected.
/evaluate uses the edge client
POST /evaluate takes no credentials from the caller. It always evaluates as the client that the edge itself is configured with (EDGE_CLIENT_ID and EDGE_CLIENT_SECRET), in that client's environment.
Authentication errors
| Situation | Status | errorCode |
|---|---|---|
No Authorization: Bearer and no X-API-Key header | 401 | UNAUTHORIZED |
| Malformed key: no dot, empty client ID or API key, or client ID not a UUID | 401 | UNAUTHORIZED |
| Unknown client or wrong API key | 401 | UNAUTHORIZED |
The context has an environment_id that differs from the client's environment | 401 | UNAUTHORIZED |
| Client disabled, API key correct | 403 | FORBIDDEN |
Web client, and Origin missing or not in the client's Web Origins (see CORS) | 403 | FORBIDDEN |
| Backend unavailable or failing, after retries | 502 | none, plain-text body |
The backend checks the API key before the client status. A wrong key for a disabled client gets 401, not 403.
A 401 or 403 from an OFREP endpoint has a JSON body. A request with no key looks like this:
curl -si -X POST http://localhost:8081/ofrep/v1/evaluate/flags/express-checkout \
-H 'Content-Type: application/json' \
-d '{"context":{"targetingKey":"user-1"}}'HTTP/1.1 401 Unauthorized
content-length: 122
content-type: application/json
{"errorCode":"UNAUTHORIZED","errorDetails":"Missing SDK key: send Authorization: Bearer <clientId>.<apiKey> or X-API-Key"}The other 401 cases use the same shape with a different errorDetails: Malformed SDK key: expected <clientId>.<apiKey> and SDK key is invalid. A mismatching environment_id gives environment_id does not match the client's environment. /evaluate returns plain text for the same mismatch, Environment mismatch for client.
Authentication caching
The edge caches authentication results. The cache key is the client ID plus a SHA-256 hash of the API key.
- A success is cached for
EDGE_CACHE__CLIENT_TTL_SECS, 300 seconds by default. A client that you disable, or whose API key you rotate, keeps working at the edge for up to that time. - A rejection is cached for a fixed 30 seconds, so a repeated bad key reaches the backend once. A client that you create or re-enable right after a rejection stays rejected for up to that time.
- A failure of the backend is never cached. The edge retries transient failures and then answers 502. Authentication failures are never retried.
- A cached success never accepts a different API key, and a cached rejection never blocks the correct key.
See Configuration for the edge settings.
CORS
CORS applies to /evaluate and to both OFREP endpoints.
Preflight. An OPTIONS request always returns 204 No Content. A preflight carries no credentials, so the edge does not check the client.
curl -si -X OPTIONS http://localhost:8081/ofrep/v1/evaluate/flags \
-H 'Origin: http://localhost:5173' \
-H 'Access-Control-Request-Method: POST' \
-H 'Access-Control-Request-Headers: authorization,content-type'HTTP/1.1 204 No Content
access-control-allow-methods: POST, OPTIONS
access-control-max-age: 600
vary: Origin
access-control-allow-headers: Authorization, X-API-Key, Content-Type, If-None-Match
access-control-allow-origin: http://localhost:5173Actual request. The client that the key identifies decides:
| Client type | Origin | Result |
|---|---|---|
| Web | In the client's Web Origins | Served. The response has Access-Control-Allow-Origin: <Origin>, Access-Control-Expose-Headers: ETag, FluxGate-Config-Age, FluxGate-Config-Source and Vary: Origin. |
| Web | Missing or not listed | 403 with Vary: Origin. |
| Backend | Any | Served, without CORS headers. |
For /evaluate, the edge's own client decides.
curl -si -X POST http://localhost:8081/ofrep/v1/evaluate/flags/express-checkout \
-H "Authorization: Bearer <juniper-web SDK key>" \
-H 'Origin: http://localhost:5173' \
-H 'Content-Type: application/json' \
-d '{"context":{"targetingKey":"user-1","country":"CA","user_tier":"plus"}}'HTTP/1.1 200 OK
content-length: 91
access-control-expose-headers: ETag, FluxGate-Config-Age, FluxGate-Config-Source
content-type: application/json
access-control-allow-origin: http://localhost:5173
vary: Origin
{"key":"express-checkout","value":"express","reason":"TARGETING_MATCH","variant":"express"}The same call with Origin: http://evil.example, or with no Origin header, returns this:
HTTP/1.1 403 Forbidden
content-length: 80
vary: Origin
content-type: application/json
{"errorCode":"FORBIDDEN","errorDetails":"Origin is not allowed for this client"}On /evaluate, a Web client with a bad origin gets {"error":"FORBIDDEN","message":"..."} instead.
A 401 carries no CORS headers, because the edge does not know the client before it authenticates. A browser therefore shows an opaque network error, not a readable 401, when the SDK key is wrong.
Config age headers
Every evaluation response tells you how fresh the served flags are:
FluxGate-Config-Sourceisstream(the backend stream),local(the edge's own snapshot file),bootstrap(a signed snapshot exported from the backend), ornone.FluxGate-Config-Ageis the age of the flags in whole seconds. It is0while the stream that delivered the flags is open. Forlocalandbootstrapit counts from the time the snapshot was generated. The header is absent while nothing is loaded.
A Web client can read both headers because the edge lists them in Access-Control-Expose-Headers. See Edge resilience.
Endpoints
GET /health and GET /health/ready
Return 200 when the edge can evaluate flags. That is when it holds a complete set of flags, from the backend stream, a local snapshot, or a bootstrap file, or when its backend stream is open. The edge stays ready during a backend outage while it holds flags. Both paths return 503 otherwise, for example right after a start while the backend is unreachable and no snapshot is loaded. Use /health/ready as a readiness probe. /health is the same endpoint, kept for existing probes.
If you set ready_max_age_secs, both paths return 503 once the flags are older than that limit.
curl -si http://localhost:8081/health{
"status": "ready",
"streamConnected": false,
"snapshotLoaded": true,
"snapshotAgeSecs": 42,
"lastSnapshotAt": "2026-10-18T09:15:02Z",
"configSource": "local",
"configAgeSecs": 42
}configSource is stream, local, bootstrap, or none. configAgeSecs is null while nothing is loaded. A 503 that persists on an edge with no snapshot means it cannot reach or authenticate to the backend. Check EDGE_CLIENT_ID and EDGE_CLIENT_SECRET.
GET /health/live
Returns 200 while the edge process serves HTTP, whatever the state of the backend. Use it as a liveness probe. Do not use a readiness path for liveness: an edge that starts during an outage is not ready until it has flags, and a restart loop would follow.
GET /health/detail
Always returns 200. It needs no credentials, and its body holds no secrets and no flag data. It shows the readiness status, streamConnected, configSource, configAgeSecs, lastSyncedAt, featureCount, the team ID, the depth and drop count of the event and assignment queues, a relay block on a relay edge, and the edge version.
GET /metrics
Returns Prometheus text format. It needs no credentials and holds no secrets or flag data.
| Series | Type | Meaning |
|---|---|---|
fluxgate_edge_config_age_seconds | gauge | Age of the served flags in seconds. Absent while nothing is loaded. |
fluxgate_edge_upstream_connected | gauge | 1 while the backend stream is open, else 0. |
fluxgate_edge_config_source{source} | gauge | 1 for the current source, 0 for the others. |
fluxgate_edge_features | gauge | Cached flags of the edge's team. |
fluxgate_edge_evaluation_events_dropped_total | counter | Evaluation events dropped because their queue was full. |
fluxgate_edge_assignments_dropped_total | counter | Sticky assignments dropped because their queue was full. |
fluxgate_edge_relay_children | gauge | Child edges with an open stream. Present on a relay only. |
Alert on fluxgate_edge_upstream_connected and fluxgate_edge_config_age_seconds, not on the health status code.
POST /evaluate
Evaluates one flag as the edge's own client. No credentials from the caller.
Request body
| Field | Required | Description |
|---|---|---|
flagKey | yes | The key of the flag. |
context.bucketingKey | yes | Places the user in a rollout. |
context.<attribute> | no | Any other field is an attribute for targeting rules. An environment_id attribute must equal the edge client's environment. |
curl -s -X POST http://localhost:8081/evaluate \
-H 'Content-Type: application/json' \
-d '{"flagKey":"express-checkout","context":{"bucketingKey":"user-1","country":"CA","user_tier":"plus"}}' | jq{
"flagKey": "express-checkout",
"value": "express",
"variant": "express",
"reason": "TARGETING_MATCH"
}value is the value of the variant. It is a boolean, a string, a number, or a JSON object, depending on the flag. variant is the variant key, or null when the answer is not a variant. errorCode and metadata appear only when set.
An unknown flag is not an HTTP error. The edge answers 200 with reason DEFAULT and an errorCode:
curl -s -X POST http://localhost:8081/evaluate \
-H 'Content-Type: application/json' \
-d '{"flagKey":"no-such-flag","context":{"bucketingKey":"user-1"}}'{"flagKey":"no-such-flag","value":false,"variant":null,"reason":"DEFAULT","errorCode":"FLAG_NOT_FOUND"}ENVIRONMENT_NOT_FOUND is the other errorCode. It appears when the edge cannot evaluate the flag in the client's environment, for example when the flag has no stage there.
Errors
| Status | Cause | Body |
|---|---|---|
| 400 | Body is not valid JSON, or flagKey or bucketingKey is missing. | Plain text, for example Json deserialize error: missing field `bucketingKey` at line 1 column 56. |
| 401 | environment_id differs from the edge client's environment. | Environment mismatch for client |
| 403 | The edge client is a Web client and the request has no allowed Origin. | {"error":"FORBIDDEN","message":"..."} |
| 502 | The backend is unavailable. | Plain text. |
POST /ofrep/v1/evaluate/flags/{key}
OFREP evaluation of one flag. Requires an SDK key. The edge evaluates in the environment of the key's client.
Request body
{
"context": {
"targetingKey": "user-1",
"country": "CA",
"user_tier": "plus"
}
}targetingKey is required and must not be empty. The other fields are attributes.
curl -s -X POST http://localhost:8081/ofrep/v1/evaluate/flags/express-checkout \
-H "Authorization: Bearer $FLUXGATE_SDK_KEY" \
-H 'Content-Type: application/json' \
-d '{"context":{"targetingKey":"user-1","country":"CA","user_tier":"plus"}}'{"key":"express-checkout","value":"express","reason":"TARGETING_MATCH","variant":"express"}variant is missing when the answer has no variant. A flag whose stage is not deployed in the client's environment answers false with the reason DISABLED:
{"key":"express-checkout","value":false,"reason":"DISABLED"}Errors
| Status | Cause | Body |
|---|---|---|
| 400 | targetingKey is empty. | {"key":"express-checkout","errorCode":"TARGETING_KEY_MISSING","errorDetails":"targetingKey is required and cannot be empty"} |
| 400 | The body is not valid JSON, or targetingKey is absent. | Plain text, for example Json deserialize error: missing field `targetingKey` at line 1 column 13. |
| 401, 403 | See Authentication errors. | {"errorCode":"UNAUTHORIZED","errorDetails":"..."} |
| 404 | The flag does not exist. | {"key":"no-such-flag","errorCode":"FLAG_NOT_FOUND","errorDetails":"The requested feature flag does not exist"} |
| 502 | The backend is unavailable. | Plain text. |
POST /ofrep/v1/evaluate/flags
OFREP bulk evaluation. The edge evaluates every flag of the client's team in one request. Requires an SDK key. The request body is the same as for a single flag.
curl -si -X POST http://localhost:8081/ofrep/v1/evaluate/flags \
-H "Authorization: Bearer $FLUXGATE_SDK_KEY" \
-H 'Content-Type: application/json' \
-d '{"context":{"targetingKey":"user-1","country":"CA","user_tier":"plus"}}'; echoHTTP/1.1 200 OK
content-length: 249
content-type: application/json
etag: b5f5d0eda7573c253768016224af5c2fd7644e574de0546c51018a0140bb11c0
{"flags":[{"key":"express-checkout","value":"express","reason":"TARGETING_MATCH","variant":"express"},{"key":"holiday-banner","value":true,"reason":"STATIC"}],"metadata":{"version":"b5f5d0eda7573c253768016224af5c2fd7644e574de0546c51018a0140bb11c0"}}flags has one entry per flag, in the same shape as the single-flag response. holiday-banner is true with reason STATIC because Safety nets deployed it; before that, the same call returns false with reason DISABLED. metadata.version equals the ETag. The echo adds a newline, because the body has none.
The bulk endpoint also accepts two optional query parameters, flagConfigEtag and flagConfigLastModified. OFREP clients send them when a change event triggers a re-fetch. The edge accepts them and still returns the full result.
Errors: 400 for an empty targetingKey ({"errorCode":"TARGETING_KEY_MISSING",...}), 401 and 403 as in Authentication errors, and 502 when the backend is unavailable.
ETags and caching
The bulk response carries an ETag header. Send it back in If-None-Match to skip the body when nothing changed.
curl -si -X POST http://localhost:8081/ofrep/v1/evaluate/flags \
-H "Authorization: Bearer $FLUXGATE_SDK_KEY" \
-H 'If-None-Match: <etag value from the first response>' \
-H 'Content-Type: application/json' \
-d '{"context":{"targetingKey":"user-1","country":"CA","user_tier":"plus"}}'HTTP/1.1 304 Not Modified
etag: b5f5d0eda7573c253768016224af5c2fd7644e574de0546c51018a0140bb11c0- The ETag covers the flag set, the client's environment, and the evaluation context (
targetingKeyand all attributes). A call with the same context, the same key, and unchanged flags gets304with no body. - A different context, such as another
targetingKeyorcountry, gets a fresh200with a new ETag. A change to a flag also gives a new ETag. - A value in quotes (
"<etag>") works as well as the bare value. If-None-Match: *is ignored. The edge returns200.- Browsers can read the
ETagheader of a Web client's response, because the edge lists it inAccess-Control-Expose-Headers.
The edge keeps flags in memory and updates them from the backend stream, so a flag change reaches the edge within moments and the next evaluation uses it. Two further caches matter:
- The authentication cache described in Authentication caching.
- Your SDK. The OFREP web provider keeps the bulk result in the page and re-fetches it when the context changes.
Evaluation reasons
Every successful evaluation has a reason. Treat it as an open string and ignore values you do not know.
| Reason | Meaning |
|---|---|
TARGETING_MATCH | A criterion of the stage matched. A weighted percentage split also reports this reason. |
STATIC | A simple on/off flag with a deployed stage. The value is true, and no rule is evaluated. |
DISABLED | The stage of the client's environment is not deployed. The value is false. |
DEFAULT | /evaluate only. The flag does not exist, or has no stage in the environment. errorCode says which. |
errorCode values you can see in a body are FLAG_NOT_FOUND, ENVIRONMENT_NOT_FOUND, TARGETING_KEY_MISSING, UNAUTHORIZED and FORBIDDEN.
Swagger UI
The edge serves its own API reference.
| URL | Content |
|---|---|
http://localhost:8081/docs/ | Swagger UI. You can send requests from the page. |
http://localhost:8081/api-doc/openapi.json | The OpenAPI document. |
Keep the trailing slash on /docs/. The path /docs without it returns 404. Replace localhost:8081 with the host and port of your edge server.
Next
Use an SDK instead of raw HTTP in SDKs and integrations.