Skip to content

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 flag express-checkout.
  • curl is installed. jq is optional.

The examples set the SDK key once in a shell variable. Replace the placeholder with your own key:

Set the SDK 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.

EndpointPurposeCredentials
GET /health, GET /health/readyTell you whether the edge can evaluate flags.None.
GET /health/liveTells you the edge process runs.None.
GET /health/detailShows the stream state, config source, and config age.None.
GET /metricsPrometheus metrics.None.
POST /evaluateEvaluates 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/flagsEvaluates 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:

SDK key format
<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.

HeaderValue
AuthorizationBearer <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

SituationStatuserrorCode
No Authorization: Bearer and no X-API-Key header401UNAUTHORIZED
Malformed key: no dot, empty client ID or API key, or client ID not a UUID401UNAUTHORIZED
Unknown client or wrong API key401UNAUTHORIZED
The context has an environment_id that differs from the client's environment401UNAUTHORIZED
Client disabled, API key correct403FORBIDDEN
Web client, and Origin missing or not in the client's Web Origins (see CORS)403FORBIDDEN
Backend unavailable or failing, after retries502none, 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:

No SDK key
curl -si -X POST http://localhost:8081/ofrep/v1/evaluate/flags/express-checkout \
  -H 'Content-Type: application/json' \
  -d '{"context":{"targetingKey":"user-1"}}'
Response
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.

Preflight
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'
Response
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:5173

Actual request. The client that the key identifies decides:

Client typeOriginResult
WebIn the client's Web OriginsServed. The response has Access-Control-Allow-Origin: <Origin>, Access-Control-Expose-Headers: ETag, FluxGate-Config-Age, FluxGate-Config-Source and Vary: Origin.
WebMissing or not listed403 with Vary: Origin.
BackendAnyServed, without CORS headers.

For /evaluate, the edge's own client decides.

A Web client from its allowed origin
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"}}'
Response
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:

Response
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-Source is stream (the backend stream), local (the edge's own snapshot file), bootstrap (a signed snapshot exported from the backend), or none.
  • FluxGate-Config-Age is the age of the flags in whole seconds. It is 0 while the stream that delivered the flags is open. For local and bootstrap it 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.

Health check
curl -si http://localhost:8081/health
Response body
{
  "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.

SeriesTypeMeaning
fluxgate_edge_config_age_secondsgaugeAge of the served flags in seconds. Absent while nothing is loaded.
fluxgate_edge_upstream_connectedgauge1 while the backend stream is open, else 0.
fluxgate_edge_config_source{source}gauge1 for the current source, 0 for the others.
fluxgate_edge_featuresgaugeCached flags of the edge's team.
fluxgate_edge_evaluation_events_dropped_totalcounterEvaluation events dropped because their queue was full.
fluxgate_edge_assignments_dropped_totalcounterSticky assignments dropped because their queue was full.
fluxgate_edge_relay_childrengaugeChild 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

FieldRequiredDescription
flagKeyyesThe key of the flag.
context.bucketingKeyyesPlaces the user in a rollout.
context.<attribute>noAny other field is an attribute for targeting rules. An environment_id attribute must equal the edge client's environment.
Evaluate as the edge client
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
Response
{
  "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:

Unknown flag
curl -s -X POST http://localhost:8081/evaluate \
  -H 'Content-Type: application/json' \
  -d '{"flagKey":"no-such-flag","context":{"bucketingKey":"user-1"}}'
Response
{"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

StatusCauseBody
400Body 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.
401environment_id differs from the edge client's environment.Environment mismatch for client
403The edge client is a Web client and the request has no allowed Origin.{"error":"FORBIDDEN","message":"..."}
502The 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

Request
{
  "context": {
    "targetingKey": "user-1",
    "country": "CA",
    "user_tier": "plus"
  }
}

targetingKey is required and must not be empty. The other fields are attributes.

One flag
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"}}'
Response
{"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:

Response for a client in Production, where the flag is not deployed
{"key":"express-checkout","value":false,"reason":"DISABLED"}

Errors

StatusCauseBody
400targetingKey is empty.{"key":"express-checkout","errorCode":"TARGETING_KEY_MISSING","errorDetails":"targetingKey is required and cannot be empty"}
400The 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, 403See Authentication errors.{"errorCode":"UNAUTHORIZED","errorDetails":"..."}
404The flag does not exist.{"key":"no-such-flag","errorCode":"FLAG_NOT_FOUND","errorDetails":"The requested feature flag does not exist"}
502The 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.

Bulk evaluation
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"}}'; echo
Response
HTTP/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.

Repeat the call with the ETag
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"}}'
Response
HTTP/1.1 304 Not Modified
etag: b5f5d0eda7573c253768016224af5c2fd7644e574de0546c51018a0140bb11c0
  • The ETag covers the flag set, the client's environment, and the evaluation context (targetingKey and all attributes). A call with the same context, the same key, and unchanged flags gets 304 with no body.
  • A different context, such as another targetingKey or country, gets a fresh 200 with 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 returns 200.
  • Browsers can read the ETag header of a Web client's response, because the edge lists it in Access-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.

ReasonMeaning
TARGETING_MATCHA criterion of the stage matched. A weighted percentage split also reports this reason.
STATICA simple on/off flag with a deployed stage. The value is true, and no rule is evaluated.
DISABLEDThe 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.

URLContent
http://localhost:8081/docs/Swagger UI. You can send requests from the page.
http://localhost:8081/api-doc/openapi.jsonThe 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.