Session Tokens
A publishable key (pk_live_* / pk_test_*) is public by construction — it ships in
your page's source and network requests, so on its own it cannot prove that a
screening session was actually started by you rather than replayed by someone who
copied it. Session tokens close that gap: your backend mints a single-use token
and your frontend presents it once, at mount, as server-side proof of authorization.
Session tokens are optional today. Omitting one leaves SDK behavior exactly as
before — this is an additive feature, not a breaking change. Support is included in
Intellirent.init() starting in SDK v0.12.0.
Some accounts may in the future be configured to require a session token on every mount. Until then, a mount without one succeeds — but treat that as a transitional grace period, not a guarantee, if your account has been told otherwise by your relay account team.
How it fits together
- Your backend calls
POST /v1/sessionson relay's server-side session API, authenticated with a secret key (X-Secret-Keyheader). - The response is a single-use
session_token(sgt_live_*/sgt_test_*) that expires 15 minutes after minting. - Your frontend fetches that token from your own endpoint as the page loads,
and passes it as
sessionTokentoIntellirent.init(). - The SDK presents the token exactly once, at token exchange. The server marks it consumed; it cannot be replayed for a second mount.
- If the token expired before the mount could complete (a restored tab, a slow
page load), the SDK calls your
onTokenExpiredcallback once, asking for a replacement, and retries the exchange with it.
Your backend relay session API Your frontend relay SDK
| | | |
|-- POST /v1/sessions -->| | |
| (X-Secret-Key) | | |
|<- { session_token }----| | |
| | | |
|<------------------------------ fetch token --------| |
|------------------------------ { session_token } -->| |
| | |--- Intellirent.init({ |
| | | sessionToken }) -->|
| |<---------------------------------- exchange ----|
| | (consumes token once) | |
Session tokens authenticate the mount, not the person. You still supply
userContext on every init() call the same as always — session tokens are
additive proof that your server authorized this particular session, not a
replacement for user identity.
Getting a secret key
Secret keys (sk_live_* / sk_test_*) authenticate your backend — never embed
one in a browser bundle or mobile app. Create one from your relay account
dashboard's Secret Keys page. Each key is scoped to an environment (live or
test) and can be revoked independently at any time. Secret-key revocation is
immediate. Publishable-key deactivation is not quite the same: it can take up to
~30 seconds to propagate to new mounts (publishable-key lookups are cached
in-process for that long), and it does not revoke a JWT already issued before
deactivation — that JWT keeps authorizing requests until it naturally expires
(up to 1 hour later).
If a secret key is ever presented from something that looks like a browser request, relay's backend logs it as a suspected leak. Treat that as a signal to rotate the key.
Minting a token: POST /v1/sessions
Contact integration support or your account team for the current base URL for this endpoint — it is a distinct API from the widget token-exchange API your SDK already calls, and does not yet have a public custom domain.
Headers
| Header | Required | Description |
|---|---|---|
X-Secret-Key | Yes | Your sk_live_* / sk_test_* secret key |
Content-Type | Yes | application/json |
Request body — every field is optional; an empty body ({}) is valid:
| Field | Type | Description |
|---|---|---|
applicant_ref | string | Your own reference for the applicant (max 128 chars). Opaque to relay — not parsed or validated beyond length. |
product_scope | object | array | Opaque reference data, passed through unparsed (max 2048 bytes serialized). Not a place for PII. |
allowed_origins | string[] | Narrows which origins may consume this specific grant. Must be a subset of your account's already-registered origins for the key's environment — this can only narrow the registered set, never widen it. Max 20 entries. |
metadata | object | Opaque, integrator-defined metadata (max 4096 bytes serialized). Not a place for PII. |
account_id | string | Mint on behalf of a child account (e.g. an agent under your TSP). Must be a child of the account owning the secret key. |
Response — 200 OK
{
"session_token": "sgt_live_xxxxxxxxxxxxxxxxxxxxxxxx",
"expires_at": "2026-08-03T14:15:00.000Z"
}
The token is returned exactly once. There is no endpoint to retrieve it again — only its hash is stored server-side. Store it only long enough to hand it to your frontend.
Errors — all error responses share the shape { "error": { "code", "message", "requestId" } }:
| Status | Code | Meaning |
|---|---|---|
| 400 | invalid_json | Request body is not valid JSON |
| 400 | validation_error | A field failed validation (message names the field and reason) |
| 400 | invalid_origin | An allowed_origins entry isn't a bare scheme://host origin (no path/query) |
| 400 | origin_not_registered | An allowed_origins entry isn't covered by your account's registered origins for this environment |
| 403 | forbidden_target_account | account_id isn't a child of the account that owns the secret key |
| 404 | account_not_found | account_id doesn't resolve to an existing account |
| 409 | no_registered_origins | No active key with registered origins exists for this account/environment yet — register one before minting |
| 429 | rate_limited | Per-secret-key mint rate limit exceeded (60/minute by default); retry after the Retry-After header |
| 500 | internal_error | Unexpected server-side error |
A GET /v1/whoami route (same X-Secret-Key auth, no side effects) is also
available to verify a key works and see which account/environment it resolves to
— useful for smoke-testing a new key without minting a real grant.
Using the token
Pass the minted token as sessionToken when initializing the SDK:
// Your frontend, after fetching a fresh token from YOUR OWN backend endpoint.
// This example assumes your backend proxies relay's POST /v1/sessions
// response as-is — adjust the field name if your backend wraps it differently.
const { session_token } = await fetch("/api/mint-session-token").then((r) => r.json());
const sdk = Intellirent.init({
publishableKey: "pk_live_xxxxxxxxxxxxxxxx",
userContext: { userId: "user_123", userType: "CONSUMER" },
sessionToken: session_token,
});
await sdk.mount("#container", { view: "screening", screening: { unitAddress } });
Do not hard-code a session token or embed it in a build — it is single-use and
expires 15 minutes after minting. Fetch a fresh one from your own backend as the
page loads, immediately before calling init().
Handling expiry: onTokenExpired
A 15-minute grant covers mint-to-mount — a page load, not a user's whole working
session — but a restored tab or a slow load can outlive it. Supply
onTokenExpired to recover instead of failing the mount:
const sdk = Intellirent.init({
publishableKey: "pk_live_xxxxxxxxxxxxxxxx",
userContext: { userId: "user_123", userType: "CONSUMER" },
sessionToken: session_token,
onTokenExpired: async () => {
// Mint a fresh one from your backend and return it. The SDK retries the
// exchange exactly once with the replacement.
const fresh = await fetch("/api/mint-session-token").then((r) => r.json());
return fresh.session_token;
},
});
onTokenExpiredfires at most once permount()call, and only when the server reports the presented grant as expired.- Return a fresh token (string) to retry the exchange once.
- Return
undefined, or omit the callback entirely, and initialization proceeds without a grant — which succeeds today, on the same terms as omittingsessionTokenaltogether. - The SDK does not retry for any other rejection reason (already-consumed, invalid, revoked, origin mismatch) — those are treated as unrecoverable and the mount proceeds untokenized rather than looping.
Known limitation
Whether a supplied sessionToken was actually accepted is not currently surfaced
to the host page through any public callback (onError, onWarning, or a
mount() return value) — it is used only internally to drive the one-shot
onTokenExpired retry described above. If you need to confirm server-side that a
particular mount was grant-authorized, do so from your own backend's audit trail
rather than from the SDK.