Skip to navigation

Gating features in a client

Surfaces say what to show; entitlements say how much the plan allows

A client that renders the whole product to everyone and lets the server refuse will show users buttons that do not work. Two endpoints tell a client what to show before the user taps anything.

Surfaces — what to show

GET /api/v1/feature-flags returns surfaces: the state of each product surface for this caller, already resolved from the plan and the rollout together.

{
"surfaces": {
"calculator": "available",
"matrix": "locked",
"matrix_calculator": "hidden",
"consumer_calculator": "locked",
"mcp": "hidden",
"commercial_fallback": "calculator"
},
"surface_reasons": {
"matrix": "Matrix isn't available on this account."
},
"limits": {
"calculator_runs": {
"limit": 10,
"surfaces": ["calculator", "matrix_calculator"],
"message": "This account has 10 calculator runs a day."
}
}
}
StateWhat the client does
availableList it and serve it
lockedOffer it, but show the sentence in surface_reasons instead of running it
hiddenLeave it out; a deep link to it goes to commercial_fallback

Render surfaces as given rather than working it out from the plan. surface_reasons and limits carry the words to show, worded for the client named in the X-Finput-Client header, so a client never writes its own.

Fetch this once on launch so gated surfaces render correctly on first paint instead of flickering through defaults.

The response also carries a flags object. Flag names are not a public contract and change without notice; gate on surfaces, not on flags.

Entitlements — what the plan allows

GET /api/v1/billing/entitlements returns the resolved plan entitlements plus current usage:

{
"entitlements": {
"calc.runs_per_day": null,
"calc.consumer_enabled": true,
"matrix.enabled": true,
"matrix.runs_per_day": null,
"scenarios.max_saved": null,
"mcp.enabled": true
},
"usage": {
"calc_runs": { "limit": null, "used": 3, "remaining": null }
}
}

null on a cap means unlimited. Guard the usage lookup: usage.calc_runs is not guaranteed to be present.

Re-read this after a plan change, and refresh the usage block after each calculator run rather than counting locally — the caps are enforced server-side and a local count will drift between devices.

Neither key set is frozen. Plans gain entitlement keys and products gain surfaces, so read a key that is absent as its safe default — false, 0, or hidden — rather than treating the response as an exhaustive enum.

The server enforces every gate on the request itself, whatever the client shows: a refused request answers 403 with feature_disabled or entitlement_required, and a spent daily allowance answers 429 with cap_hit.