Authentication & capabilities
The vendor token model, its 5-minute lifetime, and how capability-scoping works.
The token
Section titled “The token”POST /api/v1/auth/token exchanges a clientId + clientSecret for a short-lived JWT bearer
token (audience vendor-api). Send it as Authorization: Bearer <token> on every other request to
the vendor Gateway (api-omni-vendors.linra.net).
sequenceDiagram participant Vendor as Your integration participant Gateway as Vendor Gateway participant Identity as Identity (issuer)
Vendor->>Gateway: POST /api/v1/auth/token<br/>{ clientId, clientSecret } Gateway->>Identity: Validate credential alt valid + active vendor Identity-->>Gateway: capabilities, vendorId Gateway-->>Vendor: 200 accessToken (aud=vendor-api, 5 min TTL) Vendor->>Gateway: GET/POST/PATCH ... Authorization: Bearer accessToken Gateway->>Gateway: check capability claim for this route Gateway-->>Vendor: 200/201 payload, or 403 FORBIDDEN_CAPABILITY_NOT_GRANTED else invalid credential or inactive vendor Identity-->>Gateway: rejected Gateway-->>Vendor: 401 UNAUTHORIZED_INVALID_CREDENTIALS / UNAUTHORIZED_VENDOR_INACTIVE end Note over Vendor,Gateway: Token expires after 5 minutes — cache it and re-fetch proactively, never on every request.// POST /api/v1/auth/token{ "clientId": "vnd_...", "clientSecret": "..." }// 200 OK{ "state": "SUCCESS", "payload": { "accessToken": "eyJhbGciOi...", "tokenType": "Bearer", "expiresIn": 300 }}A wrong clientId/clientSecret, or an inactive vendor, fails with 401:
// 401 Unauthorized{ "state": "UNAUTHORIZED_INVALID_CREDENTIALS", "payload": null, "details": {} }// or, for an inactive vendor:{ "state": "UNAUTHORIZED_VENDOR_INACTIVE", "payload": null, "details": {} }Both are deliberately the SAME generic shape whether the clientId doesn’t exist or the secret is
simply wrong — this endpoint never tells you which half failed, so a caller can’t enumerate valid
client ids one guess at a time. Calling this endpoint too often (more than 10 times in 60 seconds)
fails with 429 and a Retry-After header — see
Rate limits & errors.
Why 5 minutes, and no refresh token
Section titled “Why 5 minutes, and no refresh token”Same trade-off the partner plane makes, applied here: your integration already holds a permanent
clientId + clientSecret, so re-authenticating with that credential is the standard
machine-to-machine pattern — there’s no separate short-lived-session actor that a refresh token
would exist to serve. 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 caching pattern
Section titled “The caching pattern”Cache the token in memory and refresh proactively — a little before expiry, not reactively after a 401:
let cached = null; // { accessToken, expiresAt }
async function getAccessToken() { const now = Date.now(); if (cached && cached.expiresAt - 30_000 > now) { return cached.accessToken; }
const res = await fetch('https://api-omni-vendors-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;}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.
No delegation
Section titled “No delegation”Unlike the partner plane’s onBehalfOf, the vendor token request accepts no delegation field.
Vendor has no hierarchy the way a partner does — your credential’s identity is always your own.
Capabilities live in the token
Section titled “Capabilities live in the token”Your credential is issued with a fixed set of capabilities (Offers, Stock, Cost, Orders,
Account — any combination, see the
Vendor Integration Standard).
The issued token’s claims carry exactly that set — every vendor endpoint checks it from the token
alone, with no additional lookup back to your credential record. Account gates only the
credentials-write door (generate/activate-deactivate/delete a credential) — the other four gate
the commercial (Offers/Cost/Stock/Orders/Webhooks) endpoints.
A missing capability is a 403, not a partial/filtered response. If your credential doesn’t
hold Orders, calling GET /api/v1/scent/orders doesn’t return an empty or truncated list — it
rejects outright:
// 403 Forbidden{ "state": "FORBIDDEN_CAPABILITY_NOT_GRANTED", "payload": null, "details": { "requiredCapability": "Orders" }}If you believe your integration needs a capability your current credential doesn’t have, contact
your onboarding contact — capabilities are set when a credential is issued, and self-service
generation (POST /api/v1/credentials, Account capability) can only ever match the UNION of
what your OTHER active credentials already hold, never add a genuinely new one to an existing
credential or grant a superset on a new one (see the Standard’s
Onboarding section).
Because capabilities are baked into the token at issuance time, a capability change to your credential takes effect on your NEXT token fetch, not retroactively on an already-issued token — one more reason the short 5-minute lifetime is convenient rather than costly.

