Skip to content

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 as null envelopes.
  • 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 primitiveKey is set) — inserts the draft rule at the front of the named primitive's existing rules and evaluates against the full list.
  • Isolation (when primitiveKey is omitted) — synthesizes a single-rule primitive of primitiveType and evaluates the context against it.

Response: 200 → { value, reason }Errors: 400 invalid_request, 403 scope_denied, 404 not_found, 503 ruleset_not_cached.