Every entitlement-gated endpoint on api.meteorack.com returns a
QuotaDecision — either as the JSON body of the response (for
/api/v1/entitlements/:productKey style probes) or via response
headers stamped by apps/api's rate-limit middleware. This guide
covers both surfaces.
Decision shape
{
"workspace_id": "11111111-1111-1111-1111-111111111111",
"meter_key": "api.requests_per_second",
"action": "allow",
"reason_code": 1000,
"reason_name": "ALLOWED",
"effective_limit": 100,
"current_usage": 42,
"included_quantity": 100,
"unit": "req/s",
"entitlement_version": "ev_1234567890abcdef",
"evaluated_at": "2026-04-22T20:00:00.000Z",
"cache_ttl_seconds": 1
}Fields you must read on every decision:
| field | why |
|---|---|
action | primary signal — see the table below |
reason_code | stable numeric ID; route on this, not reason_name |
entitlement_version | opaque cache key; invalidate if it changes |
cache_ttl_seconds | upper bound on how long to trust this decision client-side |
Fields you may display to end users:
| field | why |
|---|---|
effective_limit | "5 of 10 seats used" UI |
current_usage | same |
included_quantity | plan-bundled baseline vs. overrides |
unit | human label (req/s, bytes, seats, …) |
Action matrix
The five possible action values map to distinct client behaviors.
This is the only dispatch table you should read — never branch on
reason_code alone, because ranges are extensible (see below).
action | your behavior |
|---|---|
allow | Proceed. No retry, no delay. |
warn | Proceed, log the decision. The response carries DEGRADED_MODE_APPLIED (9002) or ALLOWED_WITH_WARNING (1001) to signal operator-visible degradation. Consider showing a "you're close to the limit" UI. |
throttle | Proceed later. Honor retry_after_seconds. Typical reason: THROTTLED_PLAN_RATE (2000) or THROTTLED_BURST (2001). |
degrade | Proceed with reduced feature set listed in degraded_features (optional). Typical reason: DEGRADED_PRESENCE_OVER_TIER (3000) or DEGRADED_FEATURE_DISABLED (3001). |
reject | Stop. Surface the error to the user. Typical reasons: REJECTED_PLAN_LIMIT (4001), REJECTED_SEAT_LIMIT (4002), REJECTED_SPENDING_CAP (4003), METER_UNKNOWN (9000). |
Reason-code ranges (stable)
Number ranges have semantic meaning, so an unknown code from a newer engine version degrades gracefully:
| range | meaning |
|---|---|
1xxx | allow (success, optionally with warning) |
2xxx | throttle (slow down) |
3xxx | degrade (reduced features) |
4xxx | reject (refused) |
9xxx | engine-internal (cache staleness, unknown meter, degraded-mode overlay) |
If you see a numeric code your client doesn't recognize, fall through to the range-based category rather than failing.
Response headers (rate-limited endpoints)
Endpoints that enforce a rate limit stamp three headers on every response, in parallel to returning the rate-limited payload as JSON:
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 42
X-RateLimit-Reset: 1743465600When action === "throttle", the engine additionally sets:
Retry-After: 3Retry-After is in seconds, matching retry_after_seconds on the
decision body. Use whichever is convenient; they are always equal.
Retry contract
| status | retry? |
|---|---|
allow, warn | no retry (request succeeded) |
throttle | retry once after retry_after_seconds (+ a small jitter, 50–200 ms) |
degrade | no retry for the gated feature; the ungated path succeeds |
reject | no retry — user action required (upgrade plan, request limit increase) |
Never retry a reject on your own. The engine has determined the
workspace cannot perform the action under its current entitlements.
Retrying multiplies the refused cost with no upside.
Caching
The engine stamps cache_ttl_seconds (typically 1 for the hot path) on
every decision. This is the maximum duration your client may
re-use the decision without calling the engine again. It is safe to
cache less aggressively than the TTL suggests.
For adapters that enforce tight budgets (e.g. per-request rate limits), the recommended pattern is:
- First request of a burst: call the engine, cache by
(workspace_id, meter_key, entitlement_version). - Subsequent requests within
cache_ttl_seconds: apply the cached decision, decrementing a local counter. - When
cache_ttl_secondselapses orentitlement_versionchanges (via a push-invalidation hook), re-evaluate.
If your adapter is event-driven and receives entitlement.changed
broadcasts, invalidate immediately on the workspace you received the
event for — do not wait for the TTL.
Degraded-mode behavior
The engine occasionally returns decisions with reason_code = 9002 DEGRADED_MODE_APPLIED. This indicates the live counter read failed
(e.g. Dragonfly blip) and the engine fell back to the meter's
configured degraded_mode:
meter degraded_mode | observed action | what the customer sees |
|---|---|---|
fail_open | unchanged (e.g. allow) | normal behavior |
fail_closed | reject | refused with DEGRADED_MODE_APPLIED |
warn | warn | allowed, with a "service degraded" log |
Treat 9002 the same as the range would suggest — if action === "reject", don't retry; if action === "warn", proceed normally.
Failure modes
| failure | engine response | your client |
|---|---|---|
| Engine timeout / 5xx | 504/503 from apps/api | retry once with backoff; if persistent, assume allow and log |
| Auth failure | 401 | re-authenticate |
| Meter not in catalog | reject + METER_UNKNOWN (9000) | check the meter key spelling; do not retry |
| Workspace not entitled | reject + REJECTED_NOT_ENTITLED (4000) | user must upgrade or activate the product |
Self-serve limit increases
When your user hits REJECTED_PLAN_LIMIT / REJECTED_SEAT_LIMIT
and wants headroom, surface a "Request increase" CTA that POSTs to
/api/v1/entitlements/support-grants (authenticated as the
workspace owner). This creates a pending grant_entitlement_support
approval that a Meteorack operator 4-eyes-approves; on approval, the
engine picks up the grant on its next evaluate cycle and the user
sees the new effective_limit.
See the entitlement-engine grant-limit-bump runbook for the operator-side workflow.