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

# Headless, React and React Native

> Run Callers web calls behind your own UI: hide the launcher, drive calls from code, use React, or embed calls in a React Native WebView.

You don't have to use the floating launcher. The SDK can run behind your own buttons, inside your page layout, or with no visible UI at all while your code renders the call from callbacks.

## Choose a level of control

| Goal | Setup |
| - | - |
| Your own launch button, built-in call window | `showFab: false`, call `call()` from your button |
| Call window inside your layout | `containerId: 'my-element'` |
| Show the widget only on some screens | `hidden: true`, then `show()` / `hide()` |
| Fully custom UI | `hidden: true`, render state from callbacks, control with methods |

## Your own button

With `showFab: false`, the idle launcher is hidden. The call window still opens when a call starts, so visitors get the transcript, forms and controls.

```javascript theme={"dark"}
const webCall = new CallersWebSdk.WebCall({ token, showFab: false });
document.querySelector('#talk').addEventListener('click', () => webCall.call());
```

On the script tag, use `data-show-fab="false"` and call `window.Callers.widget.call()`.

## Fully custom UI

With `hidden: true` the SDK renders nothing; you build the UI from callbacks and drive it with methods.

```javascript theme={"dark"}
const { WebCall } = window.CallersWebSdk;

const webCall = new WebCall({
  token,
  hidden: true,
  onStatusChange: (status) => render({ status }),
  onCallStarted: () => render({ live: true }),
  onCallEnded: () => render({ live: false }),
  onMuteChange: (muted) => render({ muted }),
  onTranscriptChanged: (messages) =>
    render({ messages: messages.filter((m) => !m.skip && m.content) }),
  onError: (error) => render({ error: error.message }),
});

callButton.onclick = () => webCall.call();
hangupButton.onclick = () => webCall.hangup();
muteButton.onclick = () => (webCall.isMuted() ? webCall.unmute() : webCall.mute());
sendForm.onsubmit = (event) => {
  event.preventDefault();
  webCall.sendInput(input.value);
};
```

Things to handle yourself in a custom UI:

* **In-call forms.** Messages with a `widget` field are forms. Render them, then send the answer with `sendInput(JSON.stringify(values), message.id)`. Skip messages where `widget.viewOnly` is `true`; they expect no answer.
* **Unspoken messages.** Hide messages with `skip: true`.
* **Bot protection.** With a hidden widget, the SDK shows the Turnstile check centered on the page. To place it yourself, [bring your own token](https://docs.callers.ai/sdk/bot-protection.md#bring-your-own-token).
* **Text conversations.** Starting a text-only conversation is built into the widget's composer; in a hidden setup, start with `call()` and use `sendInput()` for typed messages.

## React

Load the SDK once, create the `WebCall` in an effect, and destroy it on unmount.

```tsx useCallersWebCall.ts theme={"dark"}
import { useEffect, useRef, useState } from 'react';

const SDK_URL = 'https://cdn.callers.ai/sdk.js';
let sdkPromise: Promise<any> | null = null;

function loadCallersSdk(): Promise<any> {
  if ((window as any).CallersWebSdk) return Promise.resolve((window as any).CallersWebSdk);
  sdkPromise ??= new Promise((resolve, reject) => {
    const script = document.createElement('script');
    script.src = SDK_URL;
    script.async = true;
    script.onload = () => resolve((window as any).CallersWebSdk);
    script.onerror = () => reject(new Error('Failed to load the Callers SDK'));
    document.head.appendChild(script);
  });
  return sdkPromise;
}

export function useCallersWebCall(token: string | null) {
  const callRef = useRef<any>(null);
  const [status, setStatus] = useState('disconnected');
  const [messages, setMessages] = useState<any[]>([]);

  useEffect(() => {
    if (!token) return;
    let cancelled = false;

    loadCallersSdk().then(({ WebCall }) => {
      if (cancelled) return;
      callRef.current = new WebCall({
        token,
        showFab: false,
        onStatusChange: setStatus,
        onTranscriptChanged: (all: any[]) => setMessages(all.filter((m) => !m.skip && m.content)),
      });
    });

    return () => {
      cancelled = true;
      callRef.current?.destroy();
      callRef.current = null;
    };
  }, [token]);

  return {
    status,
    messages,
    start: () => callRef.current?.call(),
    hangup: () => callRef.current?.hangup(),
  };
}
```

Because the script is loaded without `data-agent`, it doesn't create a widget of its own. In React Strict Mode, effects run twice in development; the cleanup above destroys the first instance, so only one widget remains.

## React Native

The SDK runs in browsers, so in a React Native app run it inside a WebView. The WebView page loads the SDK, and your native code talks to it with messages.

```tsx CallScreen.tsx theme={"dark"}
import { WebView } from 'react-native-webview';

const html = (token: string) => `<!doctype html>
<html><head><meta name="viewport" content="width=device-width, initial-scale=1" /></head>
<body>
  <script src="https://cdn.callers.ai/sdk.js"></script>
  <script>
    const post = (data) => window.ReactNativeWebView.postMessage(JSON.stringify(data));
    const webCall = new CallersWebSdk.WebCall({
      token: ${JSON.stringify(token)},
      showFab: false,
      onStatusChange: (status) => post({ type: 'status', status }),
      onCallEnded: () => post({ type: 'ended' }),
    });
    window.addEventListener('message', (event) => {
      const { type } = JSON.parse(event.data);
      if (type === 'start') webCall.call();
      if (type === 'hangup') webCall.hangup();
    });
  </script>
</body></html>`;

export function CallScreen({ token }: { token: string }) {
  return (
    <WebView
      source={{ html: html(token), baseUrl: 'https://app.example.com' }}
      originWhitelist={['*']}
      mediaPlaybackRequiresUserAction={false}
      allowsInlineMediaPlayback
      mediaCapturePermissionGrantType="grant"
      onMessage={(event) => console.log(JSON.parse(event.nativeEvent.data))}
    />
  );
}
```

Checklist:

* **Use token authentication.** A WebView page has no reliable origin for the domain list. Fetch the token from your backend in native code and inject it.
* **Microphone permission.** Declare it in the app (`NSMicrophoneUsageDescription` on iOS, `RECORD_AUDIO` on Android) and let the WebView use it, as above.
* **Bot protection.** Turnstile has no native SDK. Render it in the WebView, or let the SDK's built-in check run there, and pass the token to `call(captchaToken)`. See [Bot protection](https://docs.callers.ai/sdk/bot-protection.md#bring-your-own-token).
* **Starting audio.** Start the call from a user tap (forwarded as a message) so the platform allows audio playback.

## Low-level `SipPhone`

The SDK also exports `SipPhone`, the calling client `WebCall` uses internally. It needs one-time call credentials from the Callers API and doesn't handle tokens, transcripts, forms or UI. Use `WebCall` with `hidden: true` instead unless support advises otherwise.

## Related

> **[Methods](https://docs.callers.ai/sdk/methods.md)**
>
> Everything you can call.

> **[Events and transcript](https://docs.callers.ai/sdk/events-and-transcript.md)**
>
> Everything you can listen to.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.