> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Co-browse: live screen assistance from the chat widget

> Let a support agent join a visitor's live browser session from the inbox — watch the page in real time and guide it with the visitor's consent, with a capture-time privacy policy set before the first session.

# Co-browse: live screen assistance

Co-browse is the real-time counterpart to [session replay](/guides/session-replay): instead of playing back what a visitor did, an agent joins the visitor's live page as it happens. When a customer is stuck on your checkout, sign-up, or account settings, live assistance resolves it in the conversation instead of over a back-and-forth of screenshots.

This page covers the order of operations from widget install to first session — the consent model, the privacy policy you set before going live, the agent join/end lifecycle, and guided-control packets. For the full request/response schema, see the [Co-browse API reference](/api-reference/cobrowse).

## When to co-browse

Reach for co-browse when the session's value depends on what the visitor sees right now — a checkout field that won't validate, a multi-step form abandoned midway, an agent who can't reproduce what the visitor describes. It runs inside a conversation the visitor already has open in the chat widget, so the agent joins the page without asking for a share link.

Two consent postures, chosen by the visitor when they start the session:

* **Observe** (`control: false`) — the agent watches the live page only. The page DOM is mirrored; nothing the agent types or clicks reaches the visitor.
* **Guided control** (`control: true`) — in addition to observing, the agent can highlight elements, scroll, and fill form fields on the visitor's page. The consent ceiling is set by the visitor; an agent can request control on join, but an observe-only session can never be escalated to control. The visitor can re-confirm or revoke the grant at any time by re-POSTing their start call.

Choose guided control for form coaching ("here's where the account number goes"), observe-only for diagnosing what the visitor sees ("your billing page shows an empty name field; here's where to set it"). Both postures transmit only the DOM of the page the visitor is on — not a pixel stream — so masking applies at capture time, before anything is published.

## Prerequisites

Before a session can start, you need the native chat widget installed and the operator's role set. Co-browse is not available on SMS, WhatsApp, voice, or email threads — only on web-chat conversations.

* **Widget installed.** Install the native chat widget on the pages your visitors use — the `./widget` export of `@devotel-orbit/web`, or the hosted `orbit-chat.js` bootstrap. See the [widget install guide](/guides/inbox-setup) for the embed steps; a page without the widget script can never carry a session.
* **Operator role.** The agent joining from the inbox needs the `conversations:write` scope plus an `owner`, `admin`, or `developer` role on their API key or dashboard session. Agents with only `conversations:read` can see the "Co-browse available" affordance but can't join.

## End-to-end flow

A session moves through four steps, visitor-side then agent-side:

1. **Visitor starts.** The visitor starts co-browsing from the widget — usually when they answer the agent's question "can I see what you're looking at?" The widget calls `POST /widget/conversations/{id}/cobrowse` with the visitor's session token; the session is stamped on the conversation's metadata and fans out to the inbox in real time. The body is optional; `control` defaults to `false`.
2. **Inbox shows the session.** The dashboard Inbox polls the session state on open web-chat conversations. When the visitor has initiated, the conversation panel shows a "Co-browse available" affordance; once an agent joins, it flips to "Co-browse live" with a per-session consent indicator.
3. **Agent joins.** Call `POST /api/v1/cobrowse/{conversationId}/join` with the visitor's conversation id. The endpoint mints a data-channel join token and a scoped `room_name` bound to {org, conversation, session}, flips the session to `active`, and stamps the agent and start time. Pass `control: true` in the body to request guided control; the response's `control` field is the effective grant after the visitor's consent cap.
4. **Agent's client connects.** Use the returned `token` and `ws_url` to attach an agent-side viewer to the room so the agent's browser can render the page mirror. The token expires after `expires_in` (30 minutes); re-mint by re-calling join, which is idempotent on an already-active session.

The sequence as calls:

```bash theme={null}
# 1. Visitor started via the widget (not shown — widget token auth)

# 2. Inbox shows the session; read it from the agent side
curl https://api.orbit.devotel.io/api/v1/cobrowse/conv_abc123 \
  -H "X-API-Key: dv_live_sk_your_key_here"

# 3. Agent joins (observe) — receives token, ws_url, room_name
curl -X POST https://api.orbit.devotel.io/api/v1/cobrowse/conv_abc123/join \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "control": false }'
```

From here the agent joins the room over the media data channel with a viewer client; the visitor's browser publishes the serialized DOM and every capture packet respects the privacy policy below.

## Guided control

Once the visitor granted `control: true`, an agent that joined with `control: true` in their request can send `highlight`, `scroll`, and `form_fill` packets on the session. Each one travels as a broadcast into the data room; the visitor's browser rerenders the hint — a pulsing box around a selector, a scroll, or the value you wrote into a form field.

The packet is checked against the privacy policy before it leaves the platform — a selector or input type inside any blocked region or always-blocked hard floor is rejected with `403 COBROWSE_CONTROL_BLOCKED`, and `form_fill` attempts are audit-logged. A session where the visitor joined observe-only rejects control with `403 COBROWSE_CONTROL_NOT_GRANTED`.

The sequence as calls:

```bash theme={null}
# Agent joined with control: true (visitor granted)
curl -X POST https://api.orbit.devotel.io/api/v1/cobrowse/conv_abc123/join \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "control": true }'

# Guide: highlight the field
curl -X POST https://api.orbit.devotel.io/api/v1/cobrowse/conv_abc123/control \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{ "action": "highlight", "selector": "#card-number" }'
```

A successful `POST .../control` returns `204 No Content` — the packet was broadcast into the room. Keep control packets small: each request travels as a dispatch payload, and a packet that exceeds the 1 MB dispatch ceiling returns `502 COBROWSE_CONTROL_FAILED`.

## Set the capture-time privacy policy first

Co-browse mirrors the visitor's live DOM. The privacy policy decides what the capture client masks in the visitor's page **before** it publishes a DOM packet — masking happens in the visitor's browser, and masked content never leaves the page. It also doubles as the server-side guard on the control endpoint: a selector or input type the policy blocks can never be captured or remotely controlled.

The default posture is fail-closed — with nothing configured, every input is masked and `tel`, `email`, and `cc` input types are blocked. Password fields and a small set of built-in payment/card selectors are always blocked and cannot be relaxed by this endpoint.

Set the policy before your first live session, and cover your checkout, payment, and account-settings pages explicitly:

```bash theme={null}
curl -X PUT https://api.orbit.devotel.io/api/v1/cobrowse/privacy \
  -H "X-API-Key: dv_live_sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "mask_all_inputs": true,
    "block_selectors": [".payment-panel", "#credit-card-form"],
    "blocked_input_types": ["tel", "email", "cc"],
    "unmask_on_visitor_click": false
  }'
```

| Field                     | Purpose                                                                                                                                                                                     |
| ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `mask_all_inputs`         | Mask every `<input>`, `<textarea>`, and `<select>` value. Keep this on unless you have a specific reason to capture form values verbatim.                                                   |
| `allowlist_selectors`     | When `mask_all_inputs` is `false`, only these selectors publish unmasked. Ignored while `mask_all_inputs` is on.                                                                            |
| `block_selectors`         | Tenant-added selectors whose subtree is never published and can never be targeted by a control packet — use for payment iframes, ID uploads, or anything that shouldn't be mirrored at all. |
| `blocked_input_types`     | Input types to block by type, from `tel`, `email`, `cc`. `password` is always blocked and isn't a member of this list.                                                                      |
| `unmask_on_visitor_click` | Lets the visitor click a masked field on their own screen to reveal it locally; never changes what is published to the agent.                                                               |

The same toggle set is backed by the **Settings → Channels → Native chat** dashboard panel, so operators can adjust masking without an API client. The change takes effect on the next co-browse session bootstrap for both the visitor and the joining agent — it doesn't retroactively change a session already in progress.

Because off-screen DOM is never transmitted, co-browse isn't a screen share: the capture client only sends the DOM tree the visitor's page exposes, minus whatever your policy masks, over a scoped data channel. Choosing selectors to block — and documenting that decision with your DPO — is the tenant-owned control that keeps co-browse aligned with your compliance posture. Password fields, `X-Frame-Options`-protected iframes, and your own `block_selectors` are all excluded at capture time.

## Ending a session

Either side can end the session; both stop the data room:

* **Agent ends.** `POST /api/v1/cobrowse/{conversationId}/end` stamps `status: ended`, `ended_at`, and `ended_by: "agent"`. The inbox panel's **End session** button calls this endpoint. Ending an already-ended session is an idempotent no-op.
* **Visitor ends.** The widget calls `POST /widget/conversations/{id}/cobrowse/end` with the visitor token; `ended_by` becomes `"visitor"`. Either a visitor closing the widget or a system cleanup stamps `ended_by: "system"`.

Best practices for session hygiene — keep these so a session can't linger live when nobody is using it:

1. End sessions on resolution, not just on conversation close. An ended session can never be joined again; the visitor re-initiates from the widget so consent is re-confirmed.
2. Re-initiation re-confirms consent — don't reuse an ended room. The re-join endpoint is idempotent on a live session and never revives an ended one.
3. The visitor can revoke at any time. If an agent requests control on a session the visitor opened observe-only, the `403` rejection is the consent model working, not an error.
4. Sessions on closed/archived threads can't be joined — the affordance hides once the thread is terminal.
5. Tokens expire after 30 minutes. Re-mint via join rather than holding a long-lived token; the process re-stamps the agent without advancing `started_at`.

## Failure ladder

When a session won't start, stalls, or refuses control, work the ladder in the [Troubleshooting: co-browse session stuck](/troubleshooting/cobrowse-stuck) page. The common classes:

* `COBROWSE_NOT_ACTIVE` on join — the visitor hasn't started co-browsing, or the previous session ended. Ask the visitor to re-initiate from the widget.
* A frame blocked by the customer site's `X-Frame-Options` or CSP `frame-ancestors` — the embedded viewer can't render the page.
* A session that joins and then freezes — an edge point-of-presence dropped the data-channel socket; reconnect with the client SDK and re-mint the token.
* A control or custom-tool packet refused with `502` — the dispatch payload exceeded the 1 MB ceiling; trim the selector and `value`.

The troubleshooting page walks each class through a remedy ladder and reads session errors from the conversation activity feed when the HTTP verdict alone doesn't pin the stage.

## Minimal HTML that starts a session

The hosted widget bootstrap is an `orbit-chat` custom element on your page; the visitor opens chat, and starts co-browsing from the widget UI — no integration code is required on your end. The element below is the least the page needs:

```html theme={null}
<script
  async
  src="https://api.orbit.devotel.io/widget/v1/orbit-chat.js"
  data-public-key="pk_live_your_widget_key"
></script>

<orbit-chat></orbit-chat>
```

The visitor's start call is made by the widget with its own session token — the agent-side verbs in this guide and the [Co-browse API reference](/api-reference/cobrowse) use your API key. Keep the visitor-side start in the widget, and use the API key surface only for join, end, control, and the privacy policy.

## Endpoint summary

| Method | Path                                        | Purpose                                |
| ------ | ------------------------------------------- | -------------------------------------- |
| `GET`  | `/api/v1/cobrowse/{conversationId}`         | Read the current session state         |
| `POST` | `/api/v1/cobrowse/{conversationId}/join`    | Join and mint an agent token           |
| `POST` | `/api/v1/cobrowse/{conversationId}/end`     | End the session from the agent side    |
| `GET`  | `/api/v1/cobrowse/privacy`                  | Read the capture-time privacy policy   |
| `PUT`  | `/api/v1/cobrowse/privacy`                  | Update the capture-time privacy policy |
| `POST` | `/api/v1/cobrowse/{conversationId}/control` | Send a guided-control packet           |

## See also

* [Co-browse API reference](/api-reference/cobrowse) — the full session, privacy, and control record shapes and error codes
* [Session replay](/guides/session-replay) — the asynchronous counterpart to live co-browse
* [Troubleshooting: co-browse session stuck](/troubleshooting/cobrowse-stuck) — the failure ladder for each stuck-session class
* [Inbox setup](/guides/inbox-setup) — widget install and agent roles
