Documentation

    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

    PropTypeDescription
    tokenstringThe token from the viewer token endpoint.
    shortIdstringThe Visitor's short id (short_id in the response).
    regionstringThe region the Visitor is connected to (region in the response).
    endpointstring, optionalThe 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.
    idleDisconnectSecondsnumber | nullDisconnect 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.

    PropTypeDescription
    colorstringThe color used for this viewer's cursor, drawings, and pointer.
    controlTool"cursor" | "drawing" | "pointer" | nullThe active tool. cursor remote-controls the page, drawing draws on it, pointer highlights without interacting. null disables interaction.
    maxWidthnumberMaximum width of the rendered screen, in pixels.
    maxHeightnumberMaximum height of the rendered screen, in pixels.
    activeConnectionIdstring | null, optionalWhich of the Visitor's connections (browser tabs or devices) to display. Defaults to the most recently active one.
    zoomAreaLabelstring, optionalAccessibility 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.

    FieldTypeDescription
    connectingbooleanWhether the SDK is connecting to the server.
    connectedbooleanWhether the SDK is connected to the server.
    sessionStatus"waiting" | "pendingRequest" | "active" | undefinedThe 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) | undefinedEnds the Session.
    visitorobject | undefinedThe Visitor being observed, with fields like shortId, identities, metadata, deviceType, browserName, locationCountry, and isOnline.
    connectionsobject[]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.
    latestActiveConnectionIdstring | undefinedThe id of the Visitor's most recently active connection.
    viewersobject[]The viewers connected to the Session, each with a connectionId, name, externalId, screen size, and audio status.
    viewersCountnumberThe number of viewers connected to the Session.
    modestring | undefinedThe 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).
    availableModesstring[]The sharing modes the Session can switch to (same values as mode).
    changeMode(mode) => voidRequests 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) | undefinedRequests a full refresh of the Visitor's screen data.
    waitingForceRefreshbooleanWhether a forced refresh is in progress.
    expectedDisconnectInfoobject | undefinedPresent 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) => voidSends a JSON message to the Visitor's SDK.
    listen((event, handler) => void) | undefinedSubscribes to low-level Session events from the server connection.
    setErrorHandler(handler: (error: ViewerError) => void) => voidRegisters the error handler (see Handling Errors).
    dataBounceSpeednumberThe 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.

    FieldTypeDescription
    currentUrlstring | nullThe URL the Visitor is currently on.
    size{ width, height } | undefinedThe size of the shared screen.
    setCurrentUrl((url: string) => void) | undefinedRedirects the Visitor to another URL. Requires the allow_redirect permission.
    reloadPage(() => void) | undefinedReloads the Visitor's page. Requires the allow_redirect permission.
    historyBack(() => void) | undefinedNavigates the Visitor's browser history back. Requires the allow_redirect permission.
    historyForward(() => void) | undefinedNavigates the Visitor's browser history forward. Requires the allow_redirect permission.
    requestControl(() => void) | undefinedAsks 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) | undefinedAsks 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) | undefinedStops full tab sharing and returns to co-browsing.
    runConsoleCommand((command: string) => void) | undefinedRuns a command in the Visitor's console. Requires the allow_console permission.
    consoleLogsobject[]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) | undefinedThrows confetti on the Visitor's screen.
    emojiConfetti((emoji: string) => void) | undefinedThrows 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]);
    
    ErrorDescription
    connection:authenticationThe token is invalid.
    connection:expiredThe token has expired. Generate a new one.
    connection:user_not_foundThe Visitor does not exist.
    connection:user_not_onlineThe Visitor is not online.
    connection:version_not_supportedThe SDK version is not supported by the server.
    connection:dataThe connection received invalid data.
    remote_control:inactiveA remote control action was attempted while the viewer does not have control.
    remote_control:click_not_allowedRemote click is not allowed.
    remote_control:scroll_not_allowedRemote scroll is not allowed.
    remote_control:type_not_allowedRemote type is not allowed.
    remote_control:element_not_allowedThe 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.

    FieldTypeDescription
    audioState"off" | "active"Whether an audio call is active on the Session.
    audioRingingbooleanWhether an incoming audio call is ringing, waiting for this viewer to accept.
    acceptAudioCall() => voidAccepts the incoming audio call and starts requesting microphone access.
    toggleAudio((on: boolean) => void) | undefinedStarts (true) or ends (false) the audio call. undefined when this browser or the Visitor's device does not support audio calls.
    mutedbooleanWhether this viewer's microphone is muted.
    setMuted(mute: boolean) => voidMutes or unmutes this viewer's microphone.
    inputDevicesobject[]The available microphones, as browser MediaDeviceInfo objects. Populated once the viewer has granted microphone access.
    outputDevicesobject[]The available speakers, as browser MediaDeviceInfo objects.
    activeInputDeviceIdstring | undefinedThe id of the microphone in use.
    activeOutputDeviceIdstring | undefinedThe id of the speaker in use.
    setInputDevice(deviceId: string) => voidSwitches to another microphone.
    setOutputDevice((deviceId: string) => void) | undefinedSwitches to another speaker. undefined in browsers that do not support switching audio output.
    viewerAudioAvailablebooleanWhether this browser supports audio calls.
    visitorAudioAvailablebooleanWhether the Visitor's device supports audio calls.
    viewerAudioError"not_authorized" | "no_input" | "no_output" | undefinedA 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" | undefinedA 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

    PropTypeDescription
    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.

    FieldTypeDescription
    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.
    videoRingingbooleanWhether an incoming video call is ringing, waiting for this viewer to accept.
    ownVideoOnbooleanWhether this viewer's own camera is on (granting access or streaming).
    beginVideo() => voidStarts the video call in the direction allowed by the Visitor's SDK configuration (two way, visitor to viewer, or viewer to visitor).
    endVideo() => voidEnds the video call on both sides.
    toggleOwnVideo() => voidTurns this viewer's own camera on or off.
    videoRequestToVisitor() => voidAsks the Visitor to turn their camera on.
    viewerVideoError"not_authorized" | "no_input" | undefinedA problem with this viewer's video: camera access was denied or no camera was found.
    visitorVideoError"not_authorized" | "no_input" | undefinedA 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.

    PropTypeDescription
    classNamestring, optionalAn extra class for the widget container.
    closeButtonsboolean, optionalWhether the video tiles show close buttons. Defaults to true.
    draggableboolean, optionalWhether the widget can be dragged around the page. Defaults to true.
    pipWindowWindow, optionalA 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;
    };
    
    PropTypeDescription
    onAudioRinging(accept: (accepted: boolean) => void) => () => void, optionalCalled when an incoming audio call starts ringing.
    onVideoRinging(accept: (accepted: boolean) => void) => () => void, optionalCalled 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.