Subject Token Frontend Integration
Use a Subject Token to let a browser component read usage data for one subject. Keep the tenant API key on your backend; the browser should receive only the short-lived Subject Token.
Recommended Architecture
Section titled “Recommended Architecture”- 01Browser componentYour backendRequest a token for the current userThe browser calls your backend and never receives the tenant API key.
GET /api/meter/subject-token - 02Your backendMeterIssue a Subject TokenYour backend validates the session, maps the subject, and uses the tenant API key.
POST /v1/subject/grants - 03MeterYour backendReturn the short-lived tokenMeter returns the access token, expiration, and granted capabilities.
access_token + expires_at - 04Your backendBrowser componentReturn it to the current frontend sessionYour backend returns only the short-lived Subject Token to the matching user.
Bearer <subject-token> - 05Browser componentMeterCall a Subject APIThe frontend uses the Subject Token to read usage data for the current subject.
POST /v1/subject/usage/query
Never put the tenant API key in browser code, mobile code, or public
VITE_* configuration. Your backend should determine subject_type and
subject_id from the authenticated user or business session instead of
trusting subject identity submitted by the browser.
1. Configure API Key Permissions
Section titled “1. Configure API Key Permissions”Create a tenant API key in Meter and grant subject_jwt:grant only to the
backend service that needs to issue tokens. The API key authorizes the grant
endpoint. analysis is the capability rule written into the Subject Token;
it allows the token to call usage query, usage details, and analysis query
endpoints.
Use a project-bound tenant API key. The token inherits that project; no project ID is accepted in the grant body. The analysis rule also authorizes realtime balance reads. Optional metrics restrict usage visibility but do not hide shared payer balances. See the endpoint pages below for full contracts.
2. Issue a Token from Your Backend
Section titled “2. Issue a Token from Your Backend”Your backend calls POST /v1/subject/grants:
POST https://billing.example.com/v1/subject/grantsAuthorization: Bearer <tenant-api-key>Content-Type: application/json
{"subject_type":"user","subject_id":"user_123","rules":["analysis"],"metrics":["input_tokens","output_tokens"],"expires_in":900}Return the access_token only to the frontend user associated with the
current business session:
{ "token_type": "Bearer", "access_token": "<subject-token>", "expires_in": 900, "expires_at": 1779687300, "subject": {"type": "user", "id": "user_123"}, "rules": ["analysis"], "metrics": ["input_tokens", "output_tokens"]}Your backend should expose its own endpoint, such as
GET /api/meter/subject-token, to validate the session, map the subject, and
call the Meter grant endpoint. It can return the full Meter response or only
access_token and expires_at.
// Example backend endpoint (pseudocode)app.get('/api/meter/subject-token', async (req, res) => { const user = await requireUser(req) const grant = await meter.post('/v1/subject/grants', { headers: { Authorization: `Bearer ${process.env.METER_TENANT_API_KEY}` }, body: { subject_type: 'user', subject_id: user.id, rules: ['analysis'], metrics: ['input_tokens', 'output_tokens'], expires_in: 900, }, }) res.json(grant)})metrics is optional. When omitted, the token preserves legacy access to all
metrics. When present, it is an exact-name allowlist, and Subject query
requests are evaluated using the intersection of the request metrics and the
token metrics. This same filtering applies to usage details and event counts;
hidden-only events do not appear in result pages or aggregates.
3. Fetch and Use the Token in a Frontend Component
Section titled “3. Fetch and Use the Token in a Frontend Component”Optional: automatic component queries
Section titled “Optional: automatic component queries”To avoid writing fetch, row mapping, cursor paging or chart aggregation, see Getting Started, MeterUsageTable and MeterUsageChart. Your backend still owns token issuance; configure tokenEndpoint="/api/meter/subject-token" to share refresh and query lifecycle. The raw-fetch example below remains available without the component library.
The component can receive a getJwt function. It should call your own
backend endpoint rather than calling the Meter grant endpoint directly:
import { useEffect, useState } from 'react'
export function UsageWidget({ getJwt }: { getJwt: () => Promise<string> }) { const [data, setData] = useState(null) const [error, setError] = useState<Error | null>(null)
useEffect(() => { let cancelled = false getJwt() .then((jwt) => fetch('/v1/subject/usage/query', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${jwt}`, }, body: JSON.stringify({ start_time: Math.floor(Date.now() / 1000) - 86400, end_time: Math.floor(Date.now() / 1000), metrics: ['tokens'], measures: ['quantity'], }), })) .then((response) => { if (!response.ok) throw new Error(`usage query failed: ${response.status}`) return response.json() }) .then((result) => { if (!cancelled) setData(result) }) .catch((err) => { if (!cancelled) setError(err) })
return () => { cancelled = true } }, [getJwt])
if (error) return <div>Unable to load usage.</div> if (!data) return <div>Loading…</div> return <pre>{JSON.stringify(data, null, 2)}</pre>}In production, getJwt should cache the token and expires_at so that every
component render does not issue a new token. When multiple components load at
the same time, they should share the same in-flight refresh request.
4. Handle Expiration and Refresh
Section titled “4. Handle Expiration and Refresh”Subject Tokens are short-lived and cannot be renewed. The frontend should call its own token endpoint again when any of the following occurs:
- The time remaining until
expires_atis less than a safety margin, such as 60 seconds. - A Subject API returns
401because the token is expired or invalid. - The user switches accounts or subjects.
Retry the original request only once after refreshing. This prevents an invalid token from causing an infinite retry loop:
let cached: { token: string; expiresAt: number } | null = nulllet refresh: Promise<string> | null = null
export async function getJwt() { const now = Math.floor(Date.now() / 1000) if (cached && cached.expiresAt - now > 60) return cached.token if (!refresh) { refresh = fetch('/api/meter/subject-token') .then((r) => { if (!r.ok) throw new Error('token request failed'); return r.json() }) .then((grant) => { cached = { token: grant.access_token, expiresAt: grant.expires_at } return grant.access_token }) .finally(() => { refresh = null }) } return refresh}If the business session expires, the subject does not exist, or the backend cannot issue a token, clear the component state and direct the user to sign in again. Never fall back to the tenant API key in the browser.