> For clean Markdown of any page, append .md to the page URL. > For a complete documentation index, see https://docs.finput.com.au/overview/building-a-client/gating-features-in-a-client/llms.txt. > For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.finput.com.au/_mcp/server. # Gating features in a client 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. ```json { "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." } } } ``` | State | What the client does | | ----------- | -------------------------------------------------------------------------- | | `available` | List it and serve it | | `locked` | Offer it, but show the sentence in `surface_reasons` instead of running it | | `hidden` | Leave 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. > **Note** > > 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: ```json { "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. > **Note** > > 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`. > Surfaces say what to show; entitlements say how much the plan allows