# Embed the Widget

The widget loads from a CDN and is initialized with your project's API key.


### Script tag

Add this to your HTML, just before the closing `</body>` tag:

```html
<script
  src="https://cdn.bubblegram.co/widget.js"
  data-key="pk_live_YOUR_KEY_HERE"
  async
></script>
```

Your API key is shown in the project's **Embed** section in the dashboard, visible after your Telegram group is linked.


### Attributes

| Attribute | Required | Description |
| --- | --- | --- |
| `data-key` | **Required** | Your project's public API key. Starts with `pk_live_`. |
| `data-api` | **Optional** | API base URL. Defaults to `https://api.bubblegram.co`. |
| `data-email` | **Optional** | Pre-fill the visitor's email. Useful on server-rendered pages where the user is already signed in. Skips the email input even when "Require email" is enabled. |


### JavaScript API

After the script loads, a `Bubblegram` object is available on `window`. You can control the widget programmatically:

```js
Bubblegram.open()                        // open the chat panel
Bubblegram.close()                       // close the chat panel
Bubblegram.connect()                     // reconnect WebSocket (if disconnected)
Bubblegram.setEmail('user@example.com')  // pre-fill email for signed-in users
```

The widget has no way to detect your app's auth state on its own. Without `data-email` or `setEmail`, every visitor shows up as anonymous in your dashboard, signed in or not. Call `setEmail` yourself once the email is known after client-side auth. For example, in React or Next.js after the session loads:

```js
useEffect(() => {
  if (session?.user?.email) {
    window.Bubblegram?.setEmail(session.user.email)
  }
}, [session?.user?.email])
```

If this runs before `widget.js` has finished loading (common in SPAs, since the script tag is `async`), `window.Bubblegram` won't exist yet and the call is silently lost. Use the queue stub shown in the React/Next.js examples below to make it safe to call anytime.

Calling `setEmail` with a *different* email than the one already set starts a fresh conversation: the current session is cleared and reinitialized, local message history is wiped, and the WebSocket reconnects, before the new email is saved. The first `setEmail` call for a visitor (no email set yet) doesn't trigger this. Keep this in mind if you call `setEmail` again later, e.g. when a visitor switches accounts, since any messages from the old session won't carry over.

On server-rendered pages (PHP, Rails, Django, etc.) where the email is available at render time, use the `data-email` attribute instead:

```html
<script
  src="https://cdn.bubblegram.co/widget.js"
  data-key="pk_live_YOUR_KEY_HERE"
  data-email="<?= $currentUser->email ?>"
  async
></script>
```

For React, Next.js, or other SPAs, add the script to your root HTML file. In Vite or Create React App, that's `index.html`:

```html
<script>
  window.Bubblegram = window.Bubblegram || (function () {
    var q = []
    var api = { q: q }
    ;['init', 'sendMessage', 'connect', 'open', 'close', 'setEmail', 'destroy'].forEach(function (m) {
      api[m] = function () { q.push([m, Array.prototype.slice.call(arguments)]) }
    })
    return api
  })()
</script>
<script
  src="https://cdn.bubblegram.co/widget.js"
  data-key="pk_live_YOUR_KEY_HERE"
  async
></script>
```

In Next.js, use the built-in `Script` component in your `_app.tsx` or root layout. Load the stub with `beforeInteractive` so it exists before any of your components try to call `Bubblegram`:

```jsx

<Script id="bubblegram-stub" strategy="beforeInteractive">
  {`window.Bubblegram = window.Bubblegram || (function () {
    var q = []
    var api = { q: q }
    ;['init', 'sendMessage', 'connect', 'open', 'close', 'setEmail', 'destroy'].forEach(function (m) {
      api[m] = function () { q.push([m, Array.prototype.slice.call(arguments)]) }
    })
    return api
  })()`}
</Script>
<Script
  src="https://cdn.bubblegram.co/widget.js"
  data-key="pk_live_YOUR_KEY_HERE"
  strategy="afterInteractive"
/>
```


### Attachments

Visitors can attach a file to any widget message: images, PDFs, or a short video. It shows up in Telegram in the same topic as the rest of the conversation, and replying with a file works the same way in the other direction.

**Limits**

| Type | Accepted formats | Max size |
| --- | --- | --- |
| Image | JPEG, PNG, WebP, GIF, HEIC/HEIF | 5 MB |
| PDF | application/pdf | 15 MB |
| Video | MP4, WebM, QuickTime | 20 MB |

HEIC/HEIF photos (the default on iPhone) are converted automatically so they open in any browser. The video limit matches Telegram's own file-fetch limit for bots, so it isn't something we can raise on our end.

> **Info:** **Plan requirement.** Attachments are available on the starter, pro, and custom plans. On the free plan, visitors can't attach files.

