> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.finput.com.au/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`.