Identity

Single sign-on

Your users never see a second login form. Either your platform signs a short JWT and sends the browser to the platform, or your backend asks for a one-time login link.

#Two directions

Both need the brand switch Single sign-on. Every sign-in is recorded (method sso or link) and sent as the client.login event.

#Inbound: your JWT

Set up once per brand in the CRM, Settings › API & Webhooks › Broker login (SSO): issuer, audience, algorithm RS256 (public key or JWKS URL) or HS256 (shared secret), token max age (default 300 s), clock skew (60 s), login link lifetime, Create the client on first sign-in and Reduced data.

Your platformsigns the JWT Browser Octolit platformverifies · records · signs in 302 ?token= GET /login 302 trader
Sign and redirect
import jwt from 'jsonwebtoken';
import { randomUUID } from 'node:crypto';

const token = jwt.sign(
  { sub: 'cust_10042', email: 'anna@example.com', given_name: 'Anna', family_name: 'Lindqvist', jti: randomUUID() },
  process.env.SSO_PRIVATE_KEY,                       // RS256 private key (PEM); or the HS256 shared secret
  { algorithm: 'RS256', issuer: 'https://id.your-platform.com', audience: 'octolit-brand-13', expiresIn: '5m', keyid: 'k2026-1' },
);
res.redirect(`https://api.your-brand.com/api/sso/brands/13/login?token=${encodeURIComponent(token)}&target=trader`);

#Claims

Payload
{
  "iss": "https://id.your-platform.com",
  "aud": "octolit-brand-13",
  "sub": "cust_10042",
  "iat": 1762853400,
  "exp": 1762853700,
  "jti": "3f9c2b1e-8d44-4c0a-9a1b-7e2f5c6d8a90",
  "email": "anna@example.com",
  "given_name": "Anna",
  "family_name": "Lindqvist"
}
Claim
issRequired. Exactly the issuer saved in the CRM.
audRequired. The audience saved in the CRM (string or array containing it).
subRequired. Your client id = external_id (1-191 chars: letters, digits, . _ @ : + -).
iat, expRequired. exp - iat at most the brand's token max age (default 300 s); 60 s clock skew allowed.
jtiRequired. Unique per token - a token works exactly once (token_replayed otherwise).
nbfOptional. Not valid before.
email, name, given_name, family_nameOptional. Used when the client is created on first sign-in (ignored in reduced-data mode).
header kidKey id when the brand uses a JWKS URL; the platform picks the matching key and caches the set.
header algRS256 or HS256, matching the brand setting. Nothing else is accepted.

#Login endpoint

No API key - the token is the credential
GET https://api.your-brand.com/api/sso/brands/{brand_id}/login?token=<jwt>&target=trader|dashboard
POST https://api.your-brand.com/api/sso/brands/{brand_id}/login   { "token": "<jwt>", "target": "trader" }
  • target: trader (default) or dashboard.
  • Success: 302 to https://<brand app>/login?token=…, which opens a live session. The browser must follow redirects and accept cookies for the brand's domain.
  • Failure: 302 to https://<brand app>/login?sso_error=<code>, or JSON { "error": … } when you add &format=json (useful for tests and native apps).
  • Unknown sub: created when Create the client on first sign-in is on, else 404 user_not_found.

Full endpoint details: GET · POST. Build and inspect tokens in the JWT builder.

#Outbound: login links

With a Partner key carrying sso:issue, ask for a link and redirect the browser to it. The link is one-time and expires after the brand's login link lifetime (minutes). token in the answer can be passed to the embed SDK instead of a redirect.

POST /users/cust_10042/sso
curl -X POST "https://api.your-brand.com/api/external/v1/users/cust_10042/sso" \
  -H "X-Api-Key: $OCTOLIT_PARTNER_KEY" \
  -H "Content-Type: application/json" \
  -d '{
  "target": "trader"
}'
201 Created
{
  "login_url": "https://trade.your-brand.com/login?token=otp_...",
  "token": "otp_8f3c...",
  "target": "trader",
  "expires_at": "2026-11-11T09:31:00Z",
  "client_id": 1042
}

#Sessions

GET /users/{external_id}/sessions lists live sessions (app, IP, device, last seen); DELETE …/sessions ends all of them, DELETE …/sessions/{session_id} one. End sessions when the user logs out of your platform or changes their password.

#Error codes

401token_invalidNot a JWT, or a bad header / claims.
401token_signatureThe signature does not verify with the configured key or secret.
401token_algThe algorithm is not the one configured for the brand (RS256 / HS256).
401token_issueriss differs from the issuer saved for the brand.
401token_audienceaud differs from the audience saved for the brand.
401token_expiredexp is in the past.
401token_ageIssued more than max_age_seconds ago, or not yet valid (iat / nbf outside the window).
401token_jtijti missing.
401token_subjectsub missing.
401token_keyNo key found for the kid in the token header (JWKS).
401token_replayedThe jti was already used - every token works once.
403sso_disabledThe brand has single sign-on switched off.
403sso_not_configuredNo issuer / audience / key saved for the brand.
403login_disabledThe user may not sign in.
403user_other_brandThe user belongs to another brand.
403company_inactiveThe platform is paused.
403brand_inactiveThe brand is switched off.
404brand_not_foundNo such brand id.
404user_not_foundUnknown sub and "create unknown clients" is off.
409no_app_domainThe brand has no trader / dashboard domain yet.
422invalid_targettarget must be trader or dashboard.
422invalid_subjectsub is not a valid external_id (1-191 letters, digits, . _ @ : + -).
422create_failedThe client could not be auto-created.
Checklist
Short lifetime (5 min) · a fresh jti per token · RS256 with a JWKS URL so you can rotate keys without a CRM change · clocks in sync (NTP) · test a replay and an expired token before go-live.