Draft. Not linked or indexed.

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.

How the parts connect
createLiveWatch (browser) -> your server route -> Intray POST /v1/support/sessions

The 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

1

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.

2

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.

.env (server only)
INTRAY_LIVE_KEY=ilw_...
INTRAY_LIVE_BOOTSTRAP_URL=https://<your Intray API base>/v1/support/sessions
SITE_URL=https://app.example.com

SITE_URL must equal the origin you entered in step 1.

3

Install the SDK

Terminal
npm install @intray/live
4

Add 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/intray-live.ts
// 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.

5

Start the SDK in the browser

Start it after login. Destroy it on logout or account switch. Do not reuse one instance across identities.

app/live.ts
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();
6

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.

sessionIdstringRequired

The id the SDK passes to getSession. 1 to 128 letters, digits, underscores, or hyphens.

userIdstringRequired

Your id for the signed-in user. 1 to 128 characters.

accountIdstringRequired

Your id for the user's account or workspace. 1 to 128 characters.

displayNamestringRequired

Shown to your team in Live. Up to 200 characters.

originstringRequired

Must match the origin of the connected application.

The response is { token, expiresAt, serverUrl, sessionId } with Cache-Control: no-store.

StatusMeaning
400The body is missing, is not valid JSON, has an unknown field, or has an invalid origin.
401The key is missing, malformed, or rotated. Or the origin does not match, or Live view is off for the app.
413The body is larger than 8 KB.
415Content-Type is not application/json.
429Too many session requests.
503Live watching is not available on Intray's side.

Options

getSession(sessionId: string) => Promise<LiveBootstrap>Required

Calls your server route and returns its JSON.

isPageAllowed() => booleanOptional

Checked twice a second. While it returns false, nothing is captured and the state is hidden. Defaults to every page.

blockSelectorstringOptional

Added to the default block list. It cannot shorten that list. A blocked element shows as an empty box of the same size.

maskTextSelectorstringOptional

Defaults to *, which masks all text.

publicTextSelectorstringOptional

Opt-in for reviewed static labels, for example [data-support-public]. Blocked and masked regions win over it.

onState(state: string) => voidOptional

Called on every state change. See the states below.

assistancebooleanOptional

Off 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.

controlsContainerHTMLElementOptional

Where 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

connectingOptional
The first connection is opening.
idleOptional

Connected and sharing. Nobody is watching, so nothing is captured.

liveOptional

A teammate is watching and the page is captured.

pausedOptional
The customer called stopSharing().
hiddenOptional

The tab is in the background, or isPageAllowed returned false.

reconnectingOptional

The connection dropped. The SDK retries on its own.

unavailableOptional

Intray refused the session. The SDK makes no further attempts.

stoppedOptional
destroy() 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 publicTextSelector and 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.