Skip to main content

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. 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. control defaults to false when the start call omits it.
  • Guided control (control: true) — in addition to observing, the agent may publish highlight, scroll, and form_fill packets that the visitor’s browser renders back into the page.
Three rules hold the consent ceiling:
  1. The ceiling is set at session start, by the visitor. An agent’s join request asks for control (POST /api/v1/cobrowse/{conversationId}/join with control: 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.
  2. The visitor can re-confirm or revoke at any time. The widget re-POSTs the start call with a new control value, and the stored ceiling moves with it.
  3. Sessions never revive. Once a session ends (ended by 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 on PUT /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 example input[autocomplete="cc-number"] and input[autocomplete="cc-csc"]) are always blocked. Tenant-added entries in block_selectors and blocked_input_types only ever add restrictions — they can never remove the hard floor.
The policy fields you can set: 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 a highlight, 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_BLOCKED and never reaches the visitor’s browser.
  • A form_fill packet on an observe-only session is rejected with 403 COBROWSE_CONTROL_NOT_GRANTED.
  • A packet that passes the guard is broadcast; accepted form_fill packets are audit-logged, and blocked attempts are audit-logged too.
An empty or missing selector fails closed — treated as blocked rather than assumed safe. The guard is what makes the tenant’s 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:
  1. Visitor starts — the widget calls its start endpoint; the session is requested with the consent ceiling stored on it.
  2. Agent joinsPOST /api/v1/cobrowse/{conversationId}/join mints a data-channel token bound to the organization, conversation, and session, flips the session to active, and stamps the agent and start time.
  3. Control packets — while the visitor granted control and the agent requested it, POST .../control packets flow through the guard above.
  4. End — either side ends; status becomes ended with ended_by stamped as visitor, agent, or system. An ended session is a terminal state — re-initiation re-confirms consent.
Both sides receive the same policy snapshot: the visitor’s session bootstrap and the agent’s join response each carry the effective privacy policy, so the capture client and the operator see an identical redaction contract. A policy update takes effect on the next session bootstrap — it does not retroactively change a session already in progress.

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 the unmask_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 default mask_all_inputs. Relax masking for that one selector only:
Then verify both sides see the same policy:
  1. Start a fresh session from the widget (a session already running keeps its bootstrap-time snapshot — end it and re-start).
  2. As the agent, read the policy back with GET /api/v1/cobrowse/privacy and join with POST /api/v1/cobrowse/{conversationId}/join; the response echoes the effective policy under data.privacy.
  3. On the visitor’s page, the #shipping-preferences subtree now publishes unmasked to the agent view; every other input stays masked, because mask_all_inputs: false still masks anything the allowlist does not cover.
Because the hard floor is independent of your configuration, a selector like #shipping-preferences input[autocomplete="cc-number"] remains blocked even though its ancestor is allowlisted — relaxing masking never relaxes the floor.

See also