Live watching
Live watching streams a signed-in customer's page to the Live view in Intray while someone on your team is watching. When nobody watches, the SDK captures nothing and never downloads the recorder. Intray stores no replay.
All text and every input value are masked until you mark them public.
createLiveWatch (browser) -> your server route -> Intray POST /v1/support/sessionsThe browser never holds the Intray key. Your server reads the signed-in user from its own session, asks Intray for a short-lived token, and returns it.
Set it up
Connect the application in Intray
Open Settings, then Applications, and choose Connect an
application. Enter a name, an environment, and the origin. The origin is
your product's exact HTTPS origin, for example https://app.example.com,
with no path.
Intray shows the server key once, under Save your server key. It starts with ilw_. Store it in your server secrets as INTRAY_LIVE_KEY, then press I've saved it.
Turn on Live view for the application. Workspace owners can watch by default. Add other members under Who can watch and reply.
Set the server variables
The dialog does not show the session URL. It is your Intray API base URL
plus /support/sessions. Find the base under Settings, Developers,
next to "API base URL". It already ends in /v1.
INTRAY_LIVE_KEY=ilw_...
INTRAY_LIVE_BOOTSTRAP_URL=https://<your Intray API base>/v1/support/sessions
SITE_URL=https://app.example.comSITE_URL must equal the origin you entered in step 1.
Install the SDK
npm install @intray/liveAdd one server route
The route must sit behind your login. Take the user and the account from
your own session. Never let the request body choose userId, accountId,
or origin.
// Server only.
export async function bootstrapLiveSession(req: Request, sessionId: string) {
const user = await requireUser(req);
const res = await fetch(process.env.INTRAY_LIVE_BOOTSTRAP_URL!, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.INTRAY_LIVE_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
sessionId,
userId: user.id,
accountId: user.accountId,
displayName: user.name,
origin: process.env.SITE_URL,
}),
});
if (!res.ok) throw new Error(`Intray refused the live session: ${res.status}`);
return res.json(); // { token, expiresAt, serverUrl, sessionId }
}Tokens last 120 seconds. The SDK calls getSession again, with the same sessionId, on every renewal and reconnect. Return the JSON unchanged.
Start the SDK in the browser
Start it after login. Destroy it on logout or account switch. Do not reuse one instance across identities.
import { createLiveWatch } from "@intray/live";
const live = createLiveWatch({
getSession: (sessionId) => api.bootstrapLiveSession({ sessionId }),
isPageAllowed: () => location.pathname.startsWith("/app"),
blockSelector: "[data-support-private]",
});
await live.start();
// On logout or account switch
live.destroy();Check it
Sign in to your product as a test user. In Intray, open Live and select the session. Text shows as masked. Blocked regions show as empty boxes.
Session request
POST /v1/support/sessions with Authorization: Bearer ilw_... and Content-Type: application/json. The body is at most 8 KB. Unknown fields are rejected.
sessionIdstringRequiredThe id the SDK passes to getSession. 1 to 128 letters, digits,
underscores, or hyphens.
userIdstringRequiredYour id for the signed-in user. 1 to 128 characters.
accountIdstringRequiredYour id for the user's account or workspace. 1 to 128 characters.
displayNamestringRequiredShown to your team in Live. Up to 200 characters.
originstringRequiredMust match the origin of the connected application.
The response is { token, expiresAt, serverUrl, sessionId } with Cache-Control: no-store.
| Status | Meaning |
|---|---|
| 400 | The body is missing, is not valid JSON, has an unknown field, or has an invalid origin. |
| 401 | The key is missing, malformed, or rotated. Or the origin does not match, or Live view is off for the app. |
| 413 | The body is larger than 8 KB. |
| 415 | Content-Type is not application/json. |
| 429 | Too many session requests. |
| 503 | Live watching is not available on Intray's side. |
Options
getSession(sessionId: string) => Promise<LiveBootstrap>RequiredCalls your server route and returns its JSON.
isPageAllowed() => booleanOptionalChecked twice a second. While it returns false, nothing is captured and the
state is hidden. Defaults to every page.
blockSelectorstringOptionalAdded to the default block list. It cannot shorten that list. A blocked element shows as an empty box of the same size.
maskTextSelectorstringOptionalDefaults to *, which masks all text.
publicTextSelectorstringOptionalOpt-in for reviewed static labels, for example [data-support-public].
Blocked and masked regions win over it.
onState(state: string) => voidOptionalCalled on every state change. See the states below.
assistancebooleanOptionalOff by default. When on, and Assistance is on for the application, a
teammate can point at things on the customer's screen. With the customer's
approval they can click elements marked data-support-control.
controlsContainerHTMLElementOptionalWhere the approval prompt mounts. Defaults to a floating prompt.
The instance has four methods: start(), stopSharing(), resumeSharing(), and destroy(). Use stopSharing and resumeSharing to give customers their own switch.
States
connectingOptionalidleOptionalConnected and sharing. Nobody is watching, so nothing is captured.
liveOptionalA teammate is watching and the page is captured.
pausedOptionalstopSharing().hiddenOptionalThe tab is in the background, or isPageAllowed returned false.
reconnectingOptionalThe connection dropped. The SDK retries on its own.
unavailableOptionalIntray refused the session. The SDK makes no further attempts.
stoppedOptionaldestroy() was called.What your team can see
- All text and every input value are masked before they leave the browser.
- URL query strings and hashes are stripped.
- These are always blocked:
canvas,video,audio,iframe,object,embed, password and hidden inputs, credit card and one-time-code fields,[data-live-block], and[data-private]. - Block more regions with
blockSelector. Mark any region that shows payment, health, or third-party data. - To make a label readable, pass
publicTextSelectorand mark reviewed static labels..rr-mask,[data-support-mask],[data-live-mask], inputs, textareas, and editable content stay masked even inside a public marker. - Never put a public marker on a container that can hold customer content.
Tell your customers
Viewing shows no indicator in your product. The SDK adds nothing to the page until a teammate asks for control. Disclose live viewing in your privacy policy and terms.
Troubleshooting
The state goes to unavailable. Intray closed the connection with code 4003, 4008, 1008, or 1009 and the SDK stopped. 4003 means the session is not allowed. 4008 means the application reached its limit of live sessions. For 4003, check that Live view is on, that the key was not rotated, and that origin equals the application's origin exactly. Create a new instance after you fix it.
The state stays on reconnecting. getSession is failing or the connection keeps dropping. The SDK retries with a delay that doubles from 1 second up to 30 seconds. Check your server route's status code against the table above. Code 4001 means the token expired, and the SDK reconnects at once. Code 4010 means the customer's uplink was too slow to keep up.
The state is hidden on a page you expected to see. The tab is in the background, or isPageAllowed returned false for that path.
The session shows in Live but the page is blank boxes. That is the default masking. Add publicTextSelector for labels you have reviewed.
getSession throws "Bootstrap is for another session". Your route returned a different sessionId than the one it received. Pass it through unchanged.
Your origin changed. Origins cannot be edited. Connect a new application with the new origin.