Embedding with the Viewer SDK
Instead of opening a hosted watch link (or embedding it in an iframe), you can render the co-browsing screen natively inside your own React application with the @upscopeio/viewer-sdk package. This gives you full control over the UI around the Visitor's screen: you build the toolbar, the buttons, and the layout, and the SDK provides the screen itself plus hooks to control the Session.
Use this when you want co-browsing to feel like a native part of your product. If you just need to open a session quickly, the secure watch link or embedded session control are simpler options.
Installation
npm install @upscopeio/viewer-sdk
React 18 (react and react-dom) are peer dependencies.
Getting a Viewer Token
The SDK authenticates with a viewer token generated by the viewer token endpoint. The token must be requested by your server (the REST API key must never reach the browser) and handed to the browser right before connecting: tokens are short lived, and requesting one creates the Session.
// On your server
const response = await fetch(`https://api.upscope.io/v1.4/visitors/${visitorId}/viewer_token`, {
body: JSON.stringify({
viewer: { id: "123", name: "Joe Smith" },
}),
headers: {
"Authorization": `Bearer ${UPSCOPE_API_KEY}`,
"Content-Type": "application/json",
},
method: "POST",
});
// Pass token, endpoint, region and short_id to your frontend
const { endpoint, region, short_id, token } = await response.json();
The response fields map directly onto the ViewerProvider props below. For on-premise accounts, endpoint points at your own on-premise server.
Rendering the Viewer
Wrap your viewer UI in a ViewerProvider, and render the Visitor's screen with the Content component:
import { Content, ViewerProvider, useUpscopeViewer } from "@upscopeio/viewer-sdk";
function CobrowsingViewer({ endpoint, region, shortId, token }) {
return (
<ViewerProvider endpoint={endpoint} idleDisconnectSeconds={600} region={region} shortId={shortId} token={token}>
<Screen />
</ViewerProvider>
);
}
function Screen() {
const { connected, endSession, sessionStatus } = useUpscopeViewer();
return (
<div>
{sessionStatus !== "active" && <p>Waiting for the visitor to accept...</p>}
<Content color="red" controlTool="cursor" maxHeight={1200} maxWidth={1200} />
{connected && endSession && <button onClick={() => endSession()}>End session</button>}
</div>
);
}
`ViewerProvider` Props
| Prop | Type | Description |
|---|---|---|
token | string | The token from the viewer token endpoint. |
shortId | string | The Visitor's short id (short_id in the response). |
region | string | The region the Visitor is connected to (region in the response). |
endpoint | string, optional | The server to connect to (endpoint in the response). Required for on-premise accounts; on cloud accounts it can be omitted and the connection is derived from region. |
idleDisconnectSeconds | number | null | Disconnect the viewer after this many seconds without interaction. Pass null to never disconnect for idleness. |
`Content` Props
Content renders the Visitor's screen on a canvas and forwards the selected control tool's interactions (clicks, scrolling, typing, drawing) to the Visitor.
| Prop | Type | Description |
|---|---|---|
color | string | The color used for this viewer's cursor, drawings, and pointer. |
controlTool | "cursor" | "drawing" | "pointer" | null | The active tool. cursor remote-controls the page, drawing draws on it, pointer highlights without interacting. null disables interaction. |
maxWidth | number | Maximum width of the rendered screen, in pixels. |
maxHeight | number | Maximum height of the rendered screen, in pixels. |
activeConnectionId | string | null, optional | Which of the Visitor's connections (browser tabs or devices) to display. Defaults to the most recently active one. |
zoomAreaLabel | string, optional | Accessibility label for the zoom area. |
Controlling the Session
`useUpscopeViewer()`
Returns Session-level state and actions. Action functions are undefined while unavailable (for example before the Session is active), so you can use their presence to enable or disable UI.
| Field | Type | Description |
|---|---|---|
connecting | boolean | Whether the SDK is connecting to the server. |
connected | boolean | Whether the SDK is connected to the server. |
sessionStatus | "waiting" | "pendingRequest" | "active" | undefined | The Session status: waiting for the Visitor, waiting for the Visitor to accept, or active. |
loadingStagesLeft | ("accept" | "connect" | "load" | "firstFrame")[] | The steps left before the Visitor's screen renders. Empty when the screen is visible; useful for progress UI. |
endSession | (() => void) | undefined | Ends the Session. |
visitor | object | undefined | The Visitor being observed, with fields like shortId, identities, metadata, deviceType, browserName, locationCountry, and isOnline. |
connections | object[] | The Visitor's open connections (browser tabs or devices). Each has a uniqueConnectionId, the currentUrl, a status ("active" or "disconnected"), and that connection's SDK configuration. |
latestActiveConnectionId | string | undefined | The id of the Visitor's most recently active connection. |
viewers | object[] | The viewers connected to the Session, each with a connectionId, name, externalId, screen size, and audio status. |
viewersCount | number | The number of viewers connected to the Session. |
mode | string | undefined | The current sharing mode: "visitor_browser" (co-browsing), "visitor_screen" (full screen sharing), "visitor_app" (mobile app), "own_viewer_screen" (you are sharing your screen), or "other_viewer_screen" (another viewer is sharing theirs). |
availableModes | string[] | The sharing modes the Session can switch to (same values as mode). |
changeMode | (mode) => void | Requests a switch to another sharing mode. Requires the allow_request_mode permission. |
agentIdleness | "active" | "willDisconnect" | "disconnected" | The idle-disconnect state driven by idleDisconnectSeconds. Use willDisconnect to warn the viewer before they are disconnected. |
forceRefresh | (() => void) | undefined | Requests a full refresh of the Visitor's screen data. |
waitingForceRefresh | boolean | Whether a forced refresh is in progress. |
expectedDisconnectInfo | object | undefined | Present when the active connection is expected to disconnect briefly (for example, the Visitor is navigating to another page). Contains an optional title and message to show, and returnTimeSeconds when the Visitor is expected back. |
sendCustomMessage | (message: object) => void | Sends a JSON message to the Visitor's SDK. |
listen | ((event, handler) => void) | undefined | Subscribes to low-level Session events from the server connection. |
setErrorHandler | (handler: (error: ViewerError) => void) => void | Registers the error handler (see Handling Errors). |
dataBounceSpeed | number | The latest measured round-trip time to the Visitor, in milliseconds. |
`useUpscopeConnection(connectionId?)`
Returns state and actions for one of the Visitor's connections (defaulting to the most recently active one). Like above, each action is undefined whenever it is unavailable — because of the Session mode, the SDK configuration, or the permissions the token was generated with.
| Field | Type | Description |
|---|---|---|
currentUrl | string | null | The URL the Visitor is currently on. |
size | { width, height } | undefined | The size of the shared screen. |
setCurrentUrl | ((url: string) => void) | undefined | Redirects the Visitor to another URL. Requires the allow_redirect permission. |
reloadPage | (() => void) | undefined | Reloads the Visitor's page. Requires the allow_redirect permission. |
historyBack | (() => void) | undefined | Navigates the Visitor's browser history back. Requires the allow_redirect permission. |
historyForward | (() => void) | undefined | Navigates the Visitor's browser history forward. Requires the allow_redirect permission. |
requestControl | (() => void) | undefined | Asks the Visitor for remote control. Only defined when the Visitor's SDK is configured to require a control request and control is off. |
requestFullTab | (() => void) | undefined | Asks the Visitor to share their full browser tab, when parts of the page (like cross-origin iframes) cannot be co-browsed. Requires the allow_request_full_tab permission. |
stopFullTab | (() => void) | undefined | Stops full tab sharing and returns to co-browsing. |
runConsoleCommand | ((command: string) => void) | undefined | Runs a command in the Visitor's console. Requires the allow_console permission. |
consoleLogs | object[] | The Visitor's console output, when remote console is enabled. Each entry has an id, a timestamp, a level ("error", "warn", "info", "log", "debug", "input", or "output"), the message, and an optional stack. |
confetti | (() => void) | undefined | Throws confetti on the Visitor's screen. |
emojiConfetti | ((emoji: string) => void) | undefined | Throws emoji confetti on the Visitor's screen. |
Remote control actions require the Visitor to have granted control; when control is inactive, calling them triggers the remote_control:inactive error instead.
Handling Errors
Register an error handler to react to connection and remote control problems:
const { setErrorHandler } = useUpscopeViewer();
useEffect(() => {
setErrorHandler((error) => {
if (error === "connection:expired") {
// Fetch a new viewer token and remount the ViewerProvider
}
});
}, [setErrorHandler]);
| Error | Description |
|---|---|
connection:authentication | The token is invalid. |
connection:expired | The token has expired. Generate a new one. |
connection:user_not_found | The Visitor does not exist. |
connection:user_not_online | The Visitor is not online. |
connection:version_not_supported | The SDK version is not supported by the server. |
connection:data | The connection received invalid data. |
remote_control:inactive | A remote control action was attempted while the viewer does not have control. |
remote_control:click_not_allowed | Remote click is not allowed. |
remote_control:scroll_not_allowed | Remote scroll is not allowed. |
remote_control:type_not_allowed | Remote type is not allowed. |
remote_control:element_not_allowed | The element is blocked from remote control by the Visitor's SDK configuration. |
Audio and Video Calls
Audio and video calls are managed by the AudioProvider and VideoProvider components. Nest them inside the ViewerProvider and use the useAudio and useVideo hooks from components inside them.
Calls are subject to the permissions the token was generated with (allow_audio and allow_video), and video is also configured through the Visitor's SDK, which controls whether it is two way or one way. Audio calls are not available on premise, and viewer tokens for on-premise accounts currently have video calls disabled as well.
import { AudioProvider, CallRingingHandler, VideoFrame, VideoProvider, ViewerProvider } from "@upscopeio/viewer-sdk";
function CobrowsingViewer({ endpoint, region, shortId, token }) {
return (
<ViewerProvider endpoint={endpoint} idleDisconnectSeconds={600} region={region} shortId={shortId} token={token}>
<AudioProvider>
<VideoProvider i18n={{ requestVideo: "Request video" }}>
<CallRingingHandler onAudioRinging={promptForCall} onVideoRinging={promptForCall} />
<VideoFrame />
<Screen />
</VideoProvider>
</AudioProvider>
</ViewerProvider>
);
}
`useAudio()`
Returns the audio call state and actions. Must be used inside an AudioProvider.
| Field | Type | Description |
|---|---|---|
audioState | "off" | "active" | Whether an audio call is active on the Session. |
audioRinging | boolean | Whether an incoming audio call is ringing, waiting for this viewer to accept. |
acceptAudioCall | () => void | Accepts the incoming audio call and starts requesting microphone access. |
toggleAudio | ((on: boolean) => void) | undefined | Starts (true) or ends (false) the audio call. undefined when this browser or the Visitor's device does not support audio calls. |
muted | boolean | Whether this viewer's microphone is muted. |
setMuted | (mute: boolean) => void | Mutes or unmutes this viewer's microphone. |
inputDevices | object[] | The available microphones, as browser MediaDeviceInfo objects. Populated once the viewer has granted microphone access. |
outputDevices | object[] | The available speakers, as browser MediaDeviceInfo objects. |
activeInputDeviceId | string | undefined | The id of the microphone in use. |
activeOutputDeviceId | string | undefined | The id of the speaker in use. |
setInputDevice | (deviceId: string) => void | Switches to another microphone. |
setOutputDevice | ((deviceId: string) => void) | undefined | Switches to another speaker. undefined in browsers that do not support switching audio output. |
viewerAudioAvailable | boolean | Whether this browser supports audio calls. |
visitorAudioAvailable | boolean | Whether the Visitor's device supports audio calls. |
viewerAudioError | "not_authorized" | "no_input" | "no_output" | undefined | A problem with this viewer's audio: microphone access was denied, or no microphone or speaker was found. |
visitorAudioError | "not_connected" | "not_authorized" | "no_input" | "no_output" | undefined | A problem with the Visitor's audio. |
visitorAudioStatus | "off" | "ringing" | "accepted" | "authorizing" | "active" | Where the Visitor is in the call flow: not in the call, being rung, accepted, granting microphone access, or connected. |
`VideoProvider` Props
| Prop | Type | Description |
|---|---|---|
i18n | { requestVideo: string } | The labels used by the video call widget. requestVideo is the label of the button that asks the Visitor to turn their camera on. |
`useVideo()`
Returns the video call state and actions. Must be used inside a VideoProvider.
| Field | Type | Description |
|---|---|---|
videoViewerState | "off" | "ringing" | "authorizing:{connection_id}" | "active:{connection_id}" | The viewers' side of the video call: off, ringing, or which viewer connection is granting camera access or streaming. |
videoVisitorState | "off" | "ringing" | "authorizing:{connection_id}" | "active:{connection_id}" | The Visitor's side of the video call. |
videoRinging | boolean | Whether an incoming video call is ringing, waiting for this viewer to accept. |
ownVideoOn | boolean | Whether this viewer's own camera is on (granting access or streaming). |
beginVideo | () => void | Starts the video call in the direction allowed by the Visitor's SDK configuration (two way, visitor to viewer, or viewer to visitor). |
endVideo | () => void | Ends the video call on both sides. |
toggleOwnVideo | () => void | Turns this viewer's own camera on or off. |
videoRequestToVisitor | () => void | Asks the Visitor to turn their camera on. |
viewerVideoError | "not_authorized" | "no_input" | undefined | A problem with this viewer's video: camera access was denied or no camera was found. |
visitorVideoError | "not_authorized" | "no_input" | undefined | A problem with the Visitor's video. |
The remaining fields returned by the hook (sendVideoCallFrame, setVideoMeter, reportViewerVideoUnauthorized, i18n) are plumbing used by VideoFrame; you will not normally call them yourself.
`VideoFrame`
Renders the floating video call widget: this viewer's camera preview, the Visitor's video, and the buttons to toggle cameras. It attaches its UI directly to the page (it renders nothing in your React tree), so simply mount it anywhere inside the VideoProvider.
| Prop | Type | Description |
|---|---|---|
className | string, optional | An extra class for the widget container. |
closeButtons | boolean, optional | Whether the video tiles show close buttons. Defaults to true. |
draggable | boolean, optional | Whether the widget can be dragged around the page. Defaults to true. |
pipWindow | Window, optional | A document Picture-in-Picture window to render the widget into, instead of the page. |
`CallRingingHandler`
Coordinates incoming call prompts and plays the ringtone. When a call starts ringing, the matching callback is called with an accept function: show your own prompt, and call accept(true) or accept(false) with the viewer's answer. Return a cleanup function — it is called when the ringing stops (for example, the Visitor cancelled the call), so you can dismiss the prompt.
import { CallRingingHandler, OnRinging } from "@upscopeio/viewer-sdk";
const promptForCall: OnRinging = (accept) => {
const dismissPrompt = showIncomingCallPrompt({
onAccept: () => accept(true),
onRefuse: () => accept(false),
});
return dismissPrompt;
};
| Prop | Type | Description |
|---|---|---|
onAudioRinging | (accept: (accepted: boolean) => void) => () => void, optional | Called when an incoming audio call starts ringing. |
onVideoRinging | (accept: (accepted: boolean) => void) => () => void, optional | Called when an incoming video call starts ringing. |
If audio and video ring at the same time, only onAudioRinging fires, and accepting or refusing it also accepts or refuses the video call.