Appearance
Evaluation
Evaluation is the hot path: read a flag or config's value for a given context. Plus three side-doors for the dashboard's rule builder and one for previewing changes before applying them.
For the conceptual model, see Concepts → Evaluation.
Evaluate
http
POST /api/v1/envs/{envId}/evaluate
Content-Type: application/json
{
"context": { "userId": "u_42", "plan": "pro", "country": "US" },
"keys": ["new-onboarding", "checkout.max-items"],
"verboseReason": false
}Bearer scope: read action. Session role: viewer+.
keys is one of:
- An array of keys (
["a","b"]) — evaluate exactly these. Missing keys come back asnullenvelopes. - The literal string
"*"— evaluate every flag and config in the environment. - Omitted or
null— same as"*".
verboseReason: true includes a full predicate-tree trace on rule_match reasons.
asOf: "2026-04-15T18:00:00Z" (optional) evaluates against the historical ruleset at that timestamp. Capped at 90 days. Mutually exclusive with verboseReason-only updates.
Response: 200 → EvaluateResponse
json
{
"environmentId": "9f7a32b5-…",
"version": 142,
"evaluations": {
"new-onboarding": {
"value": true,
"defaultValue": false,
"reason": { "kind": "rule_match", "ruleIndex": 0, "matchedValue": true }
},
"checkout.max-items": {
"value": 50,
"defaultValue": 100,
"reason": { "kind": "rule_match", "ruleIndex": 1, "matchedValue": 50 }
}
}
}Errors: 400 invalid_request, 403 scope_denied, 404 not_found, 503 ruleset_not_cached.
For scoped bearer tokens with keys: "*", the response map is filtered to scope-matching keys server-side (an empty result is legitimate, not a 403). With explicit keys, each (env, key, read) tuple is checked; the first miss is a 403 naming the offending key.
Preview
http
POST /api/v1/envs/{envId}/evaluate/preview
Content-Type: application/json
{
"spotCheck": [
{ "userId": "u_42", "plan": "enterprise" },
{ "userId": "u_99", "plan": "free" }
],
"ruleset": {
"flags": [
{ "key": "ui.theme", "type": "string", "rules": [], "defaultValue": "midnight" }
]
},
"verboseReason": false
}Counterfactual evaluation: hand it a transient ruleset, get back side-by-side {live, preview} envelopes per context. The transient ruleset is not stored server-side; the env's version doesn't move.
The transient ruleset is subject to the same save-time validation a write would be — invalid rulesets 400 with the same shape POST /flags would have produced.
spotCheck is capped at 50 contexts. Use this to spot-check named personas, not to scan a user base.
Response: 200 → EvaluatePreviewResponse
json
{
"environmentId": "9f7a32b5-…",
"liveVersion": 142,
"spotCheck": [
{
"context": { "userId": "u_42", "plan": "enterprise" },
"live": { "ui.theme": { "value": "classic", "defaultValue": "classic", "reason": { "kind": "default" } } },
"preview": { "ui.theme": { "value": "midnight", "defaultValue": "midnight", "reason": { "kind": "default" } } }
}
]
}Errors: 400 invalid_request, 403 scope_denied, 404 not_found, 503 ruleset_not_cached.
History
http
POST /api/v1/envs/{envId}/evaluate/history
Content-Type: application/json
{
"context": { "userId": "u_42" },
"since": "2026-03-15T00:00:00Z",
"until": "2026-04-15T00:00:00Z",
"primitiveKey": "new-onboarding",
"verboseReason": false
}Replays the audit log within (since, until] for one context and emits per-primitive (timestamp, version, value, reason) change-points — the timeline of how this user's resolution drifted as the ruleset changed.
primitiveKey (optional) narrows to a single flag / config and bypasses the per-primitive bound. until defaults to "now". Window capped at 90 days. Bounded result sets set truncated: true rather than returning 500.
Response: 200 → EvaluateHistoryResponseErrors: 400 invalid_request, 403 scope_denied, 404 not_found.
Ruleset version
http
GET /api/v1/envs/{envId}/ruleset/version
If-None-Match: W/"142"A cheap version-only probe. Returns 200 → { "version": 143 } if the env has moved, or 304 Not Modified (with a fresh ETag) if not. Used by SDKs as an SSE-fallback poll: the response body is tiny and the round-trip cost is dominated by the conditional check.
Response: 200 → { "version": 143 } | 304 Not ModifiedHeaders: ETag: W/"<version>"
Validation helpers
These power inline rule-builder feedback in the dashboard.
Validate a regex
http
POST /api/v1/validate/regex
Content-Type: application/json
{ "pattern": "^admin\\+.+@example\\.com$" }Compiles the pattern under the portable RE2 subset and returns { ok: bool, error?: string }. Unauthenticated; called on every keystroke in the rule builder.
Response: 200 → { ok: true } or 200 → { ok: false, error: "lookahead is not portable" }
Validate a condition
http
POST /api/v1/envs/{envId}/validate/condition
Content-Type: application/json
{
"condition": { "field": "plan", "$equals": "pro" },
"primitiveType": "flag"
}Dry-runs validation on a condition tree, returning per-path errors for inline UI rendering. primitiveType (one of flag, config, segment) opts into type-aware checks; omit for a structural check only.
Response: 200 → { ok: bool, errors: ConditionValidationError[] }Errors: 400 invalid_request, 403 scope_denied, 404 not_found.
Validate-and-evaluate a draft rule
http
POST /api/v1/envs/{envId}/validate/evaluate
Content-Type: application/json
{
"context": { "userId": "u_42", "plan": "pro" },
"draftRule": { "if": { "field": "plan", "$equals": "pro" }, "value": true },
"primitiveType": "flag",
"primitiveKey": "new-onboarding"
}Runs a draft rule against a context. Two modes:
- Splice (when
primitiveKeyis set) — inserts the draft rule at the front of the named primitive's existing rules and evaluates against the full list. - Isolation (when
primitiveKeyis omitted) — synthesizes a single-rule primitive ofprimitiveTypeand evaluates the context against it.
Response: 200 → { value, reason }Errors: 400 invalid_request, 403 scope_denied, 404 not_found, 503 ruleset_not_cached.