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

# Embedding ClarityQ

> Put ClarityQ inside your own product with an iframe. Your users ask questions of their data without leaving your app and without a ClarityQ login — your backend vouches for each one with a short-lived token.

## Quick test

Create an API key at **Organization Settings → API → Generate Key** — it is shown once. Mint yourself a token with it:

```bash theme={null}
curl -X POST https://app.clarityq.ai/api/v1/products/PRODUCT_ID/embed/token \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"email": "you@yourcompany.com"}'
```

`email` is the only required field; `name` and `ttl_seconds` are in Step 1.

Take the `token` from the response and open:

```
https://app.clarityq.ai/PRODUCT_ID/embed#token=YOUR_TOKEN
```

Once that works, drop it into any page:

```html theme={null}
<iframe
  src="https://app.clarityq.ai/PRODUCT_ID/embed#token=YOUR_TOKEN"
  title="ClarityQ"
  allow="clipboard-write"
  style="width:100%; height:100%; border:0"></iframe>
```

The frame fills whatever you give it, so the container needs a real height. Aim for 1000px of width or more — answers open a side panel for charts and SQL, and it gets cramped below that.

That token is tied to you and expires in an hour, so the chat will stop working — the three steps below replace it with one token per user, refreshed automatically.

## How it works

Your user logs into your app → your frontend asks your backend for a token → your backend calls ClarityQ with your API key → your page loads the iframe, and hands it a fresh token whenever it asks for one.

|             | Lives                                                                 | Can do                            |
| ----------- | --------------------------------------------------------------------- | --------------------------------- |
| **API key** | Your server, in a secret manager                                      | Mint a token for any address      |
| **Token**   | Your page — in the iframe URL at boot, then swapped in by postMessage | Act as one user, until it expires |

## Identity

**The email you send is the identity.** Same address next visit, same conversation history. It becomes an ordinary ClarityQ user — so if that address already uses ClarityQ, it *is* that person, with the history and remembered preferences they already have.

## Step 1 — Mint tokens on your server

Using the same API key, expose one endpoint behind your own login:

```js theme={null}
app.post('/api/clarityq-token', requireLogin, async (req, res) => {
  const response = await fetch(
    `https://app.clarityq.ai/api/v1/products/${PRODUCT_ID}/embed/token`,
    {
      method: 'POST',
      headers: {
        'X-API-Key': process.env.CLARITYQ_API_KEY,
        'Content-Type': 'application/json'
      },
      body: JSON.stringify({
        email: req.user.email,           // from the session, never the body
        name: req.user.name,
        ttl_seconds: 3600
      })
    }
  );
  res.json(await response.json());       // { token, expires_in }
});
```

Every field, error and a playground to try it: [Mint an embed token](/integrations/embed/mint-embed-token).

## Step 2 — Render the iframe and answer one message

The embed manages its own lifetime. At about 80% of its token's lifetime it posts
`clarityq:token-expiring` to your page. You reply with a fresh token and it swaps it in
place: no reload, no timer, nothing lost. An answer that is streaming keeps streaming;
text your user has typed stays typed.

| Message                                           | Direction         | Meaning                                                                                                                                                                |
| ------------------------------------------------- | ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `clarityq:token-expiring`                         | embed → your page | The token is at \~80% of its lifetime. Reply soon.                                                                                                                     |
| `clarityq:token` `{ token }`                      | your page → embed | Your reply. Swapped in place, invisibly.                                                                                                                               |
| `clarityq:token-expired`                          | embed → your page | Backstop — the token died before a swap arrived (slept laptop, throttled tab). Reply the same way: the expired screen clears and the session resumes without a reload. |
| `clarityq:conversation-created`                   | embed → your page | A brand-new conversation got its id (Step 3).                                                                                                                          |
| `clarityq:title` `{ conversationId, title }`      | embed → your page | The conversation got its title, a few seconds into the first answer. Rename your panel row in place — no re-fetch, no polling (Step 3).                                |
| `clarityq:open-conversation` `{ conversationId }` | your page → embed | Show another conversation, or `null` for a new chat. Internal navigation — instant, no reload, no token needed (Step 3).                                               |

```js theme={null}
const CLARITYQ = 'https://app.clarityq.ai';
const PRODUCT_ID = 'your-product-id';

let token = null;   // the current one — Step 3 reuses it

// No timers, no expiry math — the embed asks when it wants a new one.
async function mintToken() {
  token = (await (await fetch('/api/clarityq-token')).json()).token;
  return token;
}

const frame = document.createElement('iframe');

async function boot() {
  frame.src = `${CLARITYQ}/${PRODUCT_ID}/embed#token=${encodeURIComponent(await mintToken())}`;
  frame.title = 'ClarityQ';
  frame.allow = 'clipboard-write';
  frame.style.cssText = 'width:100%;height:100%;border:0;display:block';
  document.getElementById('clarityq').appendChild(frame);
}

// One handler for both events — the warning and the backstop want the same reply.
window.addEventListener('message', async (event) => {
  if (event.origin !== CLARITYQ) return;              // always check the origin
  const type = event.data?.type;
  if (type === 'clarityq:token-expiring' || type === 'clarityq:token-expired') {
    event.source.postMessage(
      { type: 'clarityq:token', token: await mintToken() },
      CLARITYQ                                        // never '*' — this message carries the token
    );
  }
});

boot();
```

If your mint endpoint fails once, do nothing special — the embed falls through to
`clarityq:token-expired`, and the same listener answers it when your endpoint recovers.
The request that failed in between is lost; the conversation is not.

## Step 3 — Add a history panel (optional)

The iframe is chat only — one conversation at a time, no list, no new-chat button — so it drops into your product without bringing a second navigation with it.

Skip this step and every visit starts a fresh chat. Nothing is lost: ClarityQ stores each conversation either way, and they are all still there under that user's identity. Your users just have no route back to them.

If you want that route, the split is: **ClarityQ stores the conversations, you render the list.** Two things to wire.

**1. List them** — [List conversations](/integrations/embed/list-conversations), authenticated
with the token you already hold (`Authorization: Bearer`, not the API key). It returns only
that user's conversations, scoped server-side. Sort on `last_updated`; `description` is the
title — a new conversation starts as `New Chat` and gets its real title a few seconds into
the first answer, announced by `clarityq:title` so you never re-fetch for it.

**2. Open them, and keep them named** — switching is a postMessage into the running iframe: internal navigation, so it is instant and needs no token. A brand-new chat announces its id the same way — catch it or that conversation is orphaned — and its title arrives moments later:

```js theme={null}
// Clicking a row in your panel
function openConversation(id) {
  frame.contentWindow.postMessage(
    { type: 'clarityq:open-conversation', conversationId: id }, CLARITYQ
  );
}

// Your "New chat" button
function newConversation() {
  frame.contentWindow.postMessage(
    { type: 'clarityq:open-conversation', conversationId: null }, CLARITYQ
  );
}

window.addEventListener('message', (event) => {
  if (event.origin !== CLARITYQ) return;

  // A brand-new conversation gets its id here. Add the row optimistically —
  // it will not come back from the API until the first message is stored.
  if (event.data?.type === 'clarityq:conversation-created') {
    addRowToPanel({ id: event.data.conversationId, description: 'New chat' });
  }

  // Its title lands here a few seconds later. Rename the row in place.
  if (event.data?.type === 'clarityq:title') {
    renameRowInPanel(event.data.conversationId, event.data.title);
  }
});
```

## Open in ClarityQ (optional)

A button in your own header that hands the conversation to the full ClarityQ app — same
conversation, same user, nothing to set up:

```html theme={null}
<a href="https://app.clarityq.ai/PRODUCT_ID/ama/CONVERSATION_ID"
   target="_blank" rel="noopener">Open in ClarityQ</a>
```

The button works for anyone who can sign into ClarityQ. Embed users are ordinary
ClarityQ users under the same email, so there are two ways in: your organization's SSO,
or an invitation sent from ClarityQ user management — either one lands them in the
conversation, with their history. Users with neither hit a login screen they cannot
pass, so only show the button to people who have a way in.

## Troubleshooting

| What you see                               | Where  | Cause and fix                                                                                                                                                     |
| ------------------------------------------ | ------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `401`                                      | Mint   | Despite the wording, this is the API key: `X-API-Key` missing, malformed, revoked or expired.                                                                     |
| Session has expired                        | iframe | The token died and no `clarityq:token` arrived — your listener is missing, or your mint endpoint failed. Reply with a fresh token and it clears without a reload. |
| Expired screen in a loop, swaps don't help | iframe | The `productId` in the URL does not match the token's product. Requests are refused in a way that reads as expiry, so every fresh token fails identically.        |
