Co-browse session model
Co-browse lets a support agent join a visitor’s live browser page while a conversation is in flight. The consent model and the privacy masking contract that governs it are the two halves of one idea: the visitor decides how much an agent may do, and a tenant-owned policy decides what the page may reveal. This page explains how those two halves interact, and why they are exactly the same policy contract that session replay uses. The endpoint-by-endpoint walkthrough lives in Co-browse: live screen assistance; the request/response shapes are in the Co-browse API reference.Co-browse vs session replay
Both surfaces capture the visitor’s DOM — but they answer different questions, at different times:
What never differs is the masking posture. Both surfaces apply the same capture-side redaction contract — masking decided in the visitor’s browser before any packet is published, so the API never receives the raw, unredacted DOM. A tenant that relaxes masking in one place does it once, and both surfaces respect it.
Consent postures
A session opens with one of two postures, chosen by the visitor when they start co-browsing from the widget:- Observe (
control: false) — the agent watches the live page. The visitor’s browser publishes DOM; nothing the agent types or clicks reaches the page.controldefaults tofalsewhen the start call omits it. - Guided control (
control: true) — in addition to observing, the agent may publishhighlight,scroll, andform_fillpackets that the visitor’s browser renders back into the page.
- The ceiling is set at session start, by the visitor. An agent’s join request asks for control (
POST /api/v1/cobrowse/{conversationId}/joinwithcontrol: true), but the effective grant is the AND of the agent’s request and the visitor’s stored consent. An observe-only session can never be escalated to guided control — the grant is enforced when the agent’s token is minted, not just in the dashboard UI, and the data-channel grant for an agent only permits publishing control packets when the visitor consented. - The visitor can re-confirm or revoke at any time. The widget re-POSTs the start call with a new
controlvalue, and the stored ceiling moves with it. - Sessions never revive. Once a session ends (
endedby the visitor, the agent, or system cleanup), it cannot be joined again — the visitor re-initiates from the widget so consent is re-confirmed before any agent attaches.
Capture-side redaction
The masking decision happens in the visitor’s browser, before a DOM packet is published — the server never touches the raw page. The policy that decides it is the one you set onPUT /api/v1/cobrowse/privacy, persisted on your widget configuration’s feature flags as cobrowse_privacy (a JSONB blob, so updating it is a settings write, not a schema change).
Two properties drive the whole posture:
- Default-deny. With nothing configured — or a stored value that drifted — every input value is masked and the entire block-by-type list (
tel,email,cc) is blocked. A tenant that never opens the settings panel still streams a fully redacted page; an operator must explicitly relax masking. - A hard floor that survives any tenant setting.
input[type="password"]and a small set of built-in payment/card selectors (for exampleinput[autocomplete="cc-number"]andinput[autocomplete="cc-csc"]) are always blocked. Tenant-added entries inblock_selectorsandblocked_input_typesonly ever add restrictions — they can never remove the hard floor.
Because masking applies to the page’s DOM tree — not a pixel stream of the whole screen — content the page itself never exposes is never transmitted. Choosing what to block, and documenting that decision with your DPO, is the tenant-owned control that keeps co-browse aligned with your compliance posture.
The server-side control guard
The same policy doubles as a server-side gate on the control endpoint. Before ahighlight, scroll, or form_fill packet is broadcast into the room, its target selector and input_type are checked against the effective policy:
- A packet targeting a selector inside any blocked region — a tenant entry or the always-blocked hard floor — is rejected with
403 COBROWSE_CONTROL_BLOCKEDand never reaches the visitor’s browser. - A
form_fillpacket on an observe-only session is rejected with403 COBROWSE_CONTROL_NOT_GRANTED. - A packet that passes the guard is broadcast; accepted
form_fillpackets are audit-logged, and blocked attempts are audit-logged too.
block_selectors more than a capture filter: it is the same list that keeps an agent’s remote pointer out of the regions the tenant chose to protect.
Session lifecycle
A session is stamped on the conversation it belongs to and moves through:- Visitor starts — the widget calls its start endpoint; the session is
requestedwith the consent ceiling stored on it. - Agent joins —
POST /api/v1/cobrowse/{conversationId}/joinmints a data-channel token bound to the organization, conversation, and session, flips the session toactive, and stamps the agent and start time. - Control packets — while the visitor granted control and the agent requested it,
POST .../controlpackets flow through the guard above. - End — either side ends;
statusbecomesendedwithended_bystamped asvisitor,agent, orsystem. An ended session is a terminal state — re-initiation re-confirms consent.
Parity with session replay
Session replay ships the same default-deny, capture-time masking contract for recorded sessions. Co-browse ports that exact contract to the live DOM stream, so the two capture surfaces carry an identical redaction model rather than two diverging ones. Co-browse adds two things replay does not need: a control-channel block list (because co-browse has a live agent→visitor control surface) and theunmask_on_visitor_click affordance (a live-session UX toggle that never widens what is published). If you maintain one masking policy deliberately, both surfaces stay consistent with it.
Worked example: relax masking on one selector, verify on both sides
Your checkout page shows a shipping-preference widget the agent needs to read aloud, masked today by the defaultmask_all_inputs. Relax masking for that one selector only:
- Start a fresh session from the widget (a session already running keeps its bootstrap-time snapshot — end it and re-start).
- As the agent, read the policy back with
GET /api/v1/cobrowse/privacyand join withPOST /api/v1/cobrowse/{conversationId}/join; the response echoes the effective policy underdata.privacy. - On the visitor’s page, the
#shipping-preferencessubtree now publishes unmasked to the agent view; every other input stays masked, becausemask_all_inputs: falsestill masks anything the allowlist does not cover.
#shipping-preferences input[autocomplete="cc-number"] remains blocked even though its ancestor is allowlisted — relaxing masking never relaxes the floor.
See also
- Co-browse: live screen assistance — the step-by-step walkthrough from widget install to first session
- Session replay — the asynchronous counterpart that shares the masking contract
- Co-browse API reference — endpoint shapes, the session object, and the error ladder
- Troubleshooting: co-browse session stuck — the failure ladder for each stuck-session class