Authentication & tokens
The token model, its 5-minute lifetime, and the caching pattern you should build around it.
The token
Section titled “The token”POST /api/v1/auth/token exchanges a clientId + clientSecret for a short-lived JWT bearer
token (audience partner-api). Send it as Authorization: Bearer <token> on every other
/api/v1 request.
// POST /api/v1/auth/token{ "clientId": "live_...", "clientSecret": "..." }// 200 OK{ "state": "SUCCESS", "payload": { "accessToken": "eyJhbGciOi...", "tokenType": "Bearer", "expiresIn": 300 }}Why 5 minutes, and no refresh token
Section titled “Why 5 minutes, and no refresh token”The token’s lifetime is a deliberate 5 minutes, and there is no refresh-token grant. This is a considered trade-off, not an oversight:
- Your integration already holds a permanent
clientId+clientSecret— re-authenticating with that credential is the standard machine-to-machine pattern. A refresh token exists to let a client that does NOT hold long-lived credentials (a mobile app, a browser session) stay signed in without re-prompting a human; that problem does not apply here. - A short-lived, non-refreshable token bounds the damage of a leaked token to at most 5 minutes,
with no server-side revocation infrastructure required. The lifetime is accurate: the Gateway
allows at most 30 seconds of clock skew, so an expired token stops working within 30 seconds
of its
exp, not minutes later. - At roughly 12 token fetches per hour if you refetch conservatively (well under the token endpoint’s own rate limit — see Rate limits), the “burden” of re-authenticating is negligible once you cache the token client-side, which you should do regardless of TTL.
The caching pattern
Section titled “The caching pattern”Cache the token in memory (or your own short-TTL store) and refresh proactively — a little before expiry, not reactively after a 401. A simple, dependency-free shape:
let cached = null; // { accessToken, expiresAt }
async function getAccessToken() { const now = Date.now(); // Refresh 30s before the token's real expiry to absorb clock skew and in-flight requests. if (cached && cached.expiresAt - 30_000 > now) { return cached.accessToken; }
const res = await fetch('https://api-omni-stg.linra.net/api/v1/auth/token', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ clientId: CLIENT_ID, clientSecret: CLIENT_SECRET }), }); const { payload } = await res.json();
cached = { accessToken: payload.accessToken, expiresAt: now + payload.expiresIn * 1000, }; return cached.accessToken;}Every call site awaits getAccessToken() instead of holding a token directly — that’s the whole
pattern. If a call still comes back 401, branch on state:
UNAUTHORIZED_TOKEN_EXPIRED— the cache was stale (e.g. your clock runs behind). Fetch a new token once and retry; don’t loop.UNAUTHORIZED_MISSING_TOKEN/UNAUTHORIZED_INVALID_TOKEN— theAuthorizationheader is absent or the token is not one this API accepts (truncated, tampered, not a partner token). Fix the request; a new token only helps if you were sending the wrong one.UPSTREAM_AUTH_FAILED— not your token: a service behind the Gateway refused the Gateway itself. It’s a server-side fault — retry later with backoff and do not re-mint your token.
Every 401 keeps the standard WWW-Authenticate header. See Errors.
Delegation (onBehalfOf)
Section titled “Delegation (onBehalfOf)”If your credential is a parent partner acting for one of its own descendant partners, the token
request accepts an optional onBehalfOf (the descendant’s partner ID). The issued token’s
identity claims (sub, partner scoping) become the descendant’s — everything you do with that
token acts as the descendant, with an internal audit trail recording your credential as the acting
parent. onBehalfOf is rejected (403 FORBIDDEN_NOT_YOUR_DESCENDANT /
FORBIDDEN_DELEGATION_TARGET_INVALID) for any partner that isn’t genuinely your descendant. Most
integrations never need this field.
What’s in the token
Section titled “What’s in the token”You don’t need to decode the token yourself — treat it as opaque — but for context, it carries
your partner identity, hierarchy information, your credential’s environment, and a unique jti.
None of these are inputs you provide; they’re all derived server-side from the validated
credential.

