Configure your session parameters, then hit Reload Preview to load the replay player inline.
Press Enter as a shortcut
Configure your session parameters, then hit Reload Preview to load the replay player inline.
Press Enter as a shortcutA User-Level JWT is a bearer credential scoped to specific scenes/versions and to a project/participant, and it expires.
Best practice: load the iframe with no token in the URL and hand the JWT over from your parent page with Window.postMessage().
Pass the JWT in dynamically, per user. Your parent
site should get the token from its own API request — a call to your
backend service, which mints a short-lived JWT with the narrowest
scene/version claims that user needs — and set
JWT from that response at runtime. Don't hard-code a
token into page source or a build artifact; that exposes it to every
visitor. Keep it in memory only: no localStorage,
cookies, analytics payloads, or logs. The literal
JWT value below is filled in for a quick console test.
{ tokenRequired: true } as soon as
it is listening, then { iframeReady: true } once the
player is up.
{ config: { apiKey: "<jwt>" } } — an
apiKey-only push merges into the live config, so camera and
playback state survive a token swap.
{ tokenRefreshRequired: true, status: 401 } and holds
its requests for 30s. Answer by requesting a new token from your
backend and pushing it — never hold a second static token, and
note that a repeat of the rejected token is ignored.
'*', and pick the iframe by that origin rather than
by a name match — an injected iframe with a similar
src would otherwise receive your token. Pin it with
frame-src in your CSP. On inbound messages, check
both event.source and event.origin.
This keeps the credential out of URLs, logs, and shared links. It does not defend against XSS: any script running on your page can request a token, so keep the CSP tight and untrusted third-party scripts off the pages that mint them.
Paste this into your parent page and modify it based on your scenario:
Drop this into your page to embed the replay with the settings you
configured. The allow list is what lets the player
start audio, go fullscreen, and read XR pose data.
The URL below carries your credential in
config.apiKey, which is fine for an
Organization Key in a development page but not for
a User-level JWT — provide a JWT above and this snippet drops the
token from the URL.
The URL below carries no credential: with a
User-Level JWT or Project Key provided, it stays out of the
src and your parent page pushes it to the iframe over postMessage.
This snippet only mounts the frame — pair it with
Example Code for initializing Token, which handles
the tokenRequired handshake that actually
authenticates it.
The viewer applies each { config } message as a
complete replacement, not a merge — there's no way to send just
{ renderConfig: { ui: false } } and have everything
else stay put. Always resend the whole config
object with only the field(s) you want changed, which means your
parent page has to keep its own live copy of it.
This snippet seeds that copy from the session you've configured
here (including the current credential, for a quick console
test — see the JWT snippet's own caveat about hard-coding
tokens), keeps it in sync from the viewer's
clientConfig messages, and exposes two console
helpers matching the toggles below:
SR_setUIControls(false) — hide the built-in
header/menu chrome (renderConfig.ui).
SR_setFirstPersonCamera(true) — switch Camera
Controls to First Person; false restores whatever
control type was active when this snippet was generated.
Paste this into your parent page's console (with the Session Replay iframe already on the page) and modify it based on your scenario:
The viewer answers { camera: { action, … } } messages
with { cameraState } or { cameraError },
echoing your requestId. Unlike
{ config }, a camera message changes only the camera,
and it needs no credential. Send them after the viewer posts
iframeReady.
Positions are in the viewer's world space: a pose read with
get can be handed back to setPose
unchanged, which is all saving and restoring a view needs. To aim
at something, prefer frameObject(sdkId).
await SR_camera.get() — mode, pose (eye, target,
field of view), whether it is moving.
SR_camera.saveView() /
SR_camera.restoreView() — bookmark and return.
SR_camera.setMode('orbit') — also
'freeLook', 'follow',
'firstPerson', 'topDown',
'fly'.
SR_camera.frameObject(sdkId),
SR_camera.frameScene(),
SR_camera.reset().
SR_camera.subscribe(200) — a throttled stream of
camera changes; replace SR_camera.onChange to
handle them.
Paste this into your parent page's console (with the Session Replay iframe already on the page):
The parent page never calls Aggregation APIs itself. It
hands the viewer the query — config.queryParams plus a
fresh renderConfig.runQueryTimestamp — and the viewer
fires the request with the credential it already holds, renders the
returned cubes, and posts the response back. That is exactly what
Get Aggregation does on the Aggregate tab.
Aggregation spans every session in a project, so it needs a
Project Key (projectkey-…): a
user-level JWT is scoped to one participant and can't read it. The
snippet pushes the key over postMessage first, the same handshake
Example Code for initializing Token uses, then
sends the query.
SR_runAggregation() — run the query for the date
range configured here
().
— run it for another
range; the example is the Custom picker's current range. Dates
are YYYY-MM-DD (or anything
new Date() accepts) and are sent as local-day
boundaries, matching the dashboard's own date filter.
SR_clearAggregation() — remove the rendered cubes.
The viewer answers with { aggregationResult } (the raw
slicer response, the same JSON the Aggregation Data box shows) or
{ aggregationError } when the request fails — the
snippet logs both. Note it seeds a full config snapshot, credential
included, because every { config } message is a
wholesale replace; that literal is for a quick console test, not
for production page source.
Paste this into your parent page's console (with the Session Replay iframe already on the page) and modify it based on your scenario: