Skip to main content

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.

Opt-in today, may become required later

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

  1. Your backend calls POST /v1/sessions on relay's server-side session API, authenticated with a secret key (X-Secret-Key header).
  2. The response is a single-use session_token (sgt_live_* / sgt_test_*) that expires 15 minutes after minting.
  3. Your frontend fetches that token from your own endpoint as the page loads, and passes it as sessionToken to Intellirent.init().
  4. The SDK presents the token exactly once, at token exchange. The server marks it consumed; it cannot be replayed for a second mount.
  5. If the token expired before the mount could complete (a restored tab, a slow page load), the SDK calls your onTokenExpired callback 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

HeaderRequiredDescription
X-Secret-KeyYesYour sk_live_* / sk_test_* secret key
Content-TypeYesapplication/json

Request body — every field is optional; an empty body ({}) is valid:

FieldTypeDescription
applicant_refstringYour own reference for the applicant (max 128 chars). Opaque to relay — not parsed or validated beyond length.
product_scopeobject | arrayOpaque reference data, passed through unparsed (max 2048 bytes serialized). Not a place for PII.
allowed_originsstring[]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.
metadataobjectOpaque, integrator-defined metadata (max 4096 bytes serialized). Not a place for PII.
account_idstringMint 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" } }:

StatusCodeMeaning
400invalid_jsonRequest body is not valid JSON
400validation_errorA field failed validation (message names the field and reason)
400invalid_originAn allowed_origins entry isn't a bare scheme://host origin (no path/query)
400origin_not_registeredAn allowed_origins entry isn't covered by your account's registered origins for this environment
403forbidden_target_accountaccount_id isn't a child of the account that owns the secret key
404account_not_foundaccount_id doesn't resolve to an existing account
409no_registered_originsNo active key with registered origins exists for this account/environment yet — register one before minting
429rate_limitedPer-secret-key mint rate limit exceeded (60/minute by default); retry after the Retry-After header
500internal_errorUnexpected 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;
},
});
  • onTokenExpired fires at most once per mount() 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 omitting sessionToken altogether.
  • 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.