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

# React SDK

> Drop the Custral chat widget into a React app, or build your own on the same engine.

**`@custral/widget`** is the Custral chat widget as a React component, plus the
headless engine underneath it. It uses a **publishable key** (`pk_…`), which is
safe to ship to the browser.

<Info>
  No bundler, or not a React app? Load the standalone
  [`widget.js`](/comms/chat/widget-install) script instead. It is this same
  package, built as a self-contained bundle with React inlined.
</Info>

<Warning>
  This package was private (`@repo/widget`) until the browser packages were
  reorganised, and Settings once generated an install snippet naming it. That
  name was never on npm. Use `@custral/widget`.
</Warning>

## Install

<CodeGroup>
  ```bash npm theme={null}
  npm install @custral/widget
  ```

  ```bash pnpm theme={null}
  pnpm add @custral/widget
  ```
</CodeGroup>

React and React DOM are **peer** dependencies (18 or 19), so the widget renders
on your copy of React rather than bundling a second one.

## The widget

One component, mounted once, anywhere in your tree. It renders its own bubble
and panel and positions itself.

```tsx theme={null}
import {CustralChatWidget} from "@custral/widget";

export function App() {
  return (
    <>
      <YourApp />
      <CustralChatWidget publishableKey="pk_..." />
    </>
  );
}
```

<ParamField path="publishableKey" type="string" required>
  Your publishable key (`pk_…`), from **Settings → Applications**.
</ParamField>

<ParamField path="channelId" type="string">
  Target a specific widget channel. Defaults to the workspace's first.
</ParamField>

<ParamField path="baseUrl" type="string">
  Defaults to `https://api.custral.com`.
</ParamField>

<ParamField path="darkMode" type="boolean">
  Force dark or light. Omitted, the widget reads the host page.
</ParamField>

<ParamField path="visitor" type="{name?, email?, phone?}">
  Pre-fill who the visitor is. Skips the intake form.
</ParamField>

<ParamField path="identityToken" type="string">
  A token minted by **your server** that vouches for who this visitor is. See
  below.
</ParamField>

## Identifying a visitor

`visitor` is convenient and self-reported: it fills the form in, and the person
on the other end typed it. That is fine for lead capture and not fine for
anything that has to be right.

`identityToken` is the version the product will trust. Your server mints it with
its secret key, so the claim comes from you rather than from the browser:

```ts theme={null}
// Your server, holding a secret key and the `widget:identify` scope.
import {Custral} from "@custral/sdk";

const custral = new Custral({apiKey: process.env.CUSTRAL_SECRET_KEY!});
const {token} = await custral.widget.createVisitorToken({externalId: user.id, email: user.email});
```

```tsx theme={null}
<CustralChatWidget publishableKey="pk_..." identityToken={token} />
```

<Warning>
  Never mint an identity token in the browser. It authenticates with your secret
  key, and anything that can call it can vouch for any of your users.
</Warning>

Without a token, a visitor's identity is only ever what they typed into a chat
box on a public page. With one, the conversation attaches to the right person in
your CRM. See [widget identity](/dev/api-reference/widget-identity).

## Building your own UI

The component is assembled from parts the package also exports, so you can
replace the shell and keep the machinery.

```tsx theme={null}
import {useCustralChat, ChatPanel} from "@custral/widget";

function MyChat() {
  const chat = useCustralChat({publishableKey: "pk_..."});
  return <ChatPanel {...chat} />;
}
```

`useCustralChat` owns the conversation: init, socket, messages, typing,
intake, conversation list, booking. `ChatBubble`, `ChatPanel`, `ChatHeader`,
`ChatMessageList`, `ChatMessage`, `ChatInput`, `IntakeForm`, `ConversationList`
and `SchedulePicker` are the pieces it drives.

For no React at all, `CustralChat` is the engine on its own:

```ts theme={null}
import {CustralChat} from "@custral/widget";

const chat = new CustralChat({publishableKey: "pk_..."});

await chat.init(); // widget config + any prior visitor session
chat.onMessage((msg) => console.log("agent says:", msg.text));
await chat.sendMessage("Hi, I need help with billing.");
```

<ParamField path="new CustralChat({publishableKey, channelId?, baseUrl?, identityToken?})">
  Construct a headless client.
</ParamField>

<ParamField path="await chat.init()">
  Loads widget config, restores any prior visitor from `localStorage`, and
  returns the resolved config (or `null` on failure).
</ParamField>

<ParamField path="chat.onMessage(handler) → unsubscribe">
  Registers a callback for agent messages pushed in real time.
</ParamField>

<ParamField path="await chat.sendMessage(text)">
  Sends a visitor message, creating a conversation on the first send.
</ParamField>

<ParamField path="await chat.identify({name?, email?, phone?})">
  Attaches self-reported contact info for lead capture.
</ParamField>

<ParamField path="chat.shutdown()">
  Tears down the socket and in-memory session. Does not clear `localStorage`.
</ParamField>

## Troubleshooting

<AccordionGroup>
  <Accordion title="`CustralChat: publishableKey is required` / init returns null">
    The widget needs a publishable key (`pk_…`), not a secret key. Create one in
    **Settings → Applications**. If `init()` returns `null`, the `/widget/init`
    call failed: check the browser console and confirm the key and `channelId`.
  </Accordion>

  <Accordion title="No real-time messages arrive">
    Delivery uses a WebSocket to `/widget`. If your environment blocks
    WebSockets, history still loads on demand, but pushed agent replies will not
    arrive. Look for `Socket connected` in the console.
  </Accordion>

  <Accordion title="Hooks throw, or the widget renders blank">
    Two copies of React in one page. React and React DOM are peers here for that
    reason; check your bundler is deduping them.
  </Accordion>

  <Accordion title="I was importing from @custral/ui or @repo/widget">
    `CustralChat` and the components are all in `@custral/widget` now. Change the
    import and nothing else.
  </Accordion>
</AccordionGroup>


## Related topics

- [SDKs](/dev/sdks/overview.md)
- [TypeScript SDK](/dev/sdks/typescript.md)
- [Developer Overview](/dev/overview.md)
- [Browser SDK](/dev/sdks/browser.md)
- [Real-time updates](/dev/realtime/overview.md)
