Skip to main content
Client code is public. Assume every string in your bundle, every network call, and every local storage entry can be read and replayed. These rules keep a Flexprice integration safe under that assumption.

Never ship an API key

An API key authenticates your whole environment. If it is in a bundle, an env var prefixed VITE_ or NEXT_PUBLIC_, a mobile binary, or a request header the browser sends, treat it as leaked: delete it in the dashboard under Developers and create a new one. What goes to the client instead:

Treat the portal token as a credential

  • Fetch it over your own authenticated endpoint. Never place it in a shared URL, an email, or a log line.
  • Keep it in memory for the page. If you must persist, use sessionStorage, not localStorage.
  • Re-mint on expiry by reading expires_at; do not extend or reuse.
  • When the token opens a hosted checkout, never pass it back as the provider’s return URL. See Portal payments.

Enforce on the server, display on the client

An entitlement rendered in the browser is a hint for the UI. The check that blocks a request has to run where the user cannot edit it:
The client can read the same data to grey out a button; it should not be the only place the rule lives.

Cache with a short TTL and a fallback

Entitlement and balance reads sit on hot paths. Cache them on your server, not in the browser:
  • Freshness window of 30 to 60 seconds for entitlements. Usage limits move with every event, but a minute of staleness is rarely a billing problem. Keep the entry itself far longer than that window. A 60 second TTL on the key deletes the value you wanted to serve during an outage.
  • Invalidate on webhooks. entitlement.updated, subscription.updated, subscription.plan_changed, and wallet.transaction.created tell you when a cached value is stale. See the event catalog.
  • Decide the failure mode up front. If Flexprice is unreachable, serve the last cached value; if there is none, choose fail-open or fail-closed per feature. Free-tier limits usually fail open; paid-only features usually fail closed.
Freshness is a field on the entry, not the entry’s TTL. That is what makes the last line reachable: an expired key is gone, so a cache that drops the value after 60 seconds can only ever fall back to DEFAULT_ENTITLEMENTS.

Do not send usage events from the browser

An event from the browser can be forged, replayed, or dropped by an ad blocker. Emit usage from the server that performs the billable work, with an event_id so retries dedupe. If the only place the action happens is the client, send a request to your backend and let it emit the event.

Validate URLs the API returns

Payment and setup actions return an action.url for the browser to open. Refuse any scheme other than http or https before redirecting:

Restrict what a server key can do

For the backend that serves the client, use a key with the narrowest role that works: an event-ingestor role for the usage path, a read-only role for the proxy that serves entitlements. See Manage API keys and RBAC.

Client-side security checklist

  • No x-api-key header is sent from any client
  • Portal tokens are minted per visit and never logged or shared
  • Gating decisions run on the server
  • Entitlement cache has a TTL and a webhook invalidation
  • Failure mode per feature is written down
  • Usage events originate on the server with an event_id
  • Returned URLs are scheme-checked before redirect
  • Server keys use the narrowest role

Fetch entitlements and usage on the client