Authentication & tokens
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.
- 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, treat it as “the cache was stale for some other reason”
and fetch once more before giving up; don’t loop.
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.