Skip to content

Authentication & tokens

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
}
}

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.

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.

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.

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.