Never ship an API key
An API key authenticates your whole environment. If it is in a bundle, an env var prefixedVITE_ 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, notlocalStorage. - 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: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, andwallet.transaction.createdtell 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.
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 anevent_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 anaction.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-keyheader 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

