DokumentasjonVis kunden rundtMarkdown
Co-browsing: guide a visitor on their own page
While an operator chats with a visitor, they can see the visitor's screen (the live view) and ask to guide them. Once the visitor says yes, the visitor sees the operator's cursor on their own page — an arrow with the operator's name and picture, like in Teams — and the operator can click links there for them. The visitor can stop it at any time.
It builds on live visitors: the tenant's liveVisitors switch must be on and the
widget (or startTracking()) must run on the page.
How it looks
Operator (stock admin and hosted panel). In the inbox, the eye button in a conversation's tab bar (or «Se kundens skjerm» in the visitor panel) opens the visitor's screen beside the chat. Under the screen:
- «Vis kunden rundt» asks the visitor. The bar says «Venter på svar fra kunden …» with «Avbryt».
- When the visitor says yes, the screen gets an accent frame and the bar says «Du viser kunden rundt». Moving the mouse over the screen moves your cursor on the visitor's page; clicking shows a ripple there, and a click on a link opens it for the visitor. The visitor's own cursor is tagged «Kunden», so both of you can tell the cursors apart.
- «Avslutt», closing the screen, or the visitor's «Stopp» ends it. The live view on the Visitors page has the same bar.
Visitor (stock widget). A card at the top of the page: «Kari vil vise deg rundt — Du ser markøren til Kari på skjermen din, og Kari kan klikke på lenker for deg. Du kan stoppe når som helst.» with «Tillat» and «Nei takk». After a yes, a bar «Kari viser deg rundt · Stopp» stays at the top, and Kari's cursor (accent colour, name tag with picture) moves over the page, a moment behind the operator. It carries on to the next page when a link is followed. Nothing changes on the page before the visitor says yes.
What the operator can do
- Point. The cursor is drawn over the visitor's page. Positions are in the visitor's document (scroll included), so they land on the same spot even if the visitor has scrolled since.
- Click links. A click follows the
<a href>under the point if it ishttp(s), the same way as the visitor clicking it (the site's own router and handlers run). A link that would open a new tab opens in the same tab, so the guide comes along. Anything else (buttons, form fields, text) only gets the ripple: the operator cannot type, submit forms or press buttons for the visitor. - Not: scroll the visitor's page, read what they type, or see anything beyond what the live view already shows (security.md).
Lifecycle
| State | Means | Ends when |
|---|---|---|
requested | The operator asked; the visitor's widget shows the card. | The visitor answers, the operator cancels, 90 s pass, or the watch ends. |
active | The visitor said yes. | Either side stops it, or the watch ends (the operator closed the live view or lost the connection: ≤ 25 s). |
One session per visitor: while a colleague is guiding, a request fails with busy. Requests
need the visitor to be online (offline otherwise). Any operator of the tenant can end a session.
SDK
Operator side (createOperatorClient):
// Ask (also starts watching if nobody does). The answer arrives as a `cobrowse` event.
const session = await client.visitors.cobrowse.request(visitorId, { conversationId });
// While active: your cursor, in the visitor's document coordinates, every ~150 ms while it moves.
await client.visitors.cobrowse.pointer(visitorId, {
url: frame.url, // the page you are looking at
samples: [[0, 410, 1290], [40, 418, 1302]], // [ms into the batch, pageX, pageY]
clicks: [[40, 418, 1302]], // a click: ripple, and a link there is followed
});
await client.visitors.cobrowse.pointer(visitorId, { url, samples: [], clicks: [], away: true }); // left the screen
await client.visitors.cobrowse.end(visitorId);
client.subscribe((event) => {
if (event.type === "cobrowse" && event.visitorId === visitorId) {
// event.session: CobrowseSession | null; event.reason: "declined" | "ended" | "expired"; event.by
}
});pointer fails with not_active once the visitor has stopped it. LiveVisitor.cobrowse has the
current session when you load the visitor list.
Visitor side (createVisitorClient, with startTracking() running; the stock widget does all of
this):
const stop = client.cobrowse.subscribe({
onSession(session, reason) {
// null: over. requested: ask the visitor. active: show «X viser deg rundt · Stopp».
},
onPointer(batch) {
// Ignore batches whose `url` is another page; draw samples at (pageX - scrollX, pageY - scrollY);
// follow http(s) links under clicks.
},
});
await client.cobrowse.answer(true); // or false
await client.cobrowse.end(); // «Stopp»Requests reach the visitor's browser with the next heartbeat (≤ 500 ms while watched, which a request always is) or at once on an open visitor stream. While a session is on, the SDK keeps its own visitor stream open for the cursor batches.
HTTP API
| Method and path | Body / result |
|---|---|
POST /operator/visitors/:id/cobrowse | { conversationId? } → { cobrowse: CobrowseSession }. 409 offline / 409 busy. |
DELETE /operator/visitors/:id/cobrowse | Cancels or stops → 204. |
POST /operator/visitors/:id/cobrowse/pointer | CobrowsePointerInput { url, samples ≤ 60, clicks ≤ 10, away? } → 204. 409 not_active. |
POST /visitor/cobrowse | { accept: boolean } → { cobrowse: CobrowseSession | null }. 409 expired when the request is gone. |
DELETE /visitor/cobrowse | The visitor stops (or turns down) → 204. |
POST /visitor/activity answers cobrowse: CobrowseSession | null too.
CobrowseSession: { state: "requested" | "active", operator: OperatorRef, conversationId, since }.
Realtime (realtime.md): cobrowse (visitorId, session, reason?, by?) on
both streams; cobrowse.pointer (visitorId, operatorId, url, samples, clicks, away?)
on the visitor stream only.