Embed the chat on your website or app
The widget puts your persona on any web page, as a chat bubble or inside an element you choose. It loads the chat in an iframe from Persona (or your custom domain), so your page never handles chat data.
Before you start
In the dashboard, open Embed and API:
- Under Allowed sites, add every origin that will show the chat, for example
https://yourbrand.comandhttps://app.yourbrand.com. The chat refuses to load or start sessions anywhere else. - Create a key:
- Server key (
sk_live_…) if your users sign in to your site or app. - Website key (
pk_live_…) if you want anonymous visitors to chat. Also switch on Let anonymous visitors chat.
- Server key (
Your client ID (cli_… or biz_…) is shown on the same page.
Option A: signed-in users (recommended)
Your users already have accounts with you. Your server tells Persona who they are and gets back a short-lived chat session; the browser only ever sees that session token.
Browser (your page) Your server Persona
| getSession() -----> | |
| | POST /api/v1/sessions ----> | verifies sk_live_ key
| | <---- { session.token } ----| creates cs_ token (1h)
| <---- { token } -----| |
| postMessage(token) to the chat frame ------------> | chat calls use the token
1. Add an endpoint on your server that requires your normal login and returns a session token. Node/Express:
app.post("/api/persona-session", requireLogin, async (req, res) => {
const r = await fetch("https://persona.ignited.tools/api/v1/sessions", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.PERSONA_SECRET_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
subclient: { id: req.user.id, name: req.user.name, email: req.user.email },
expires_in: 3600,
}),
});
if (!r.ok) return res.status(502).json({ error: "chat unavailable" });
const { session } = await r.json();
res.json({ token: session.token });
});
Next.js route handler:
// app/api/persona-session/route.ts
import { auth } from "@/auth"; // your auth
export async function POST() {
const user = await auth();
if (!user) return new Response("Unauthorized", { status: 401 });
const r = await fetch("https://persona.ignited.tools/api/v1/sessions", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.PERSONA_SECRET_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ subclient: { id: user.id, name: user.name, email: user.email } }),
cache: "no-store",
});
const { session } = await r.json();
return Response.json({ token: session.token });
}
Python (FastAPI):
@app.post("/api/persona-session")
async def persona_session(user = Depends(current_user)):
async with httpx.AsyncClient() as http:
r = await http.post(
"https://persona.ignited.tools/api/v1/sessions",
headers={"Authorization": f"Bearer {os.environ['PERSONA_SECRET_KEY']}"},
json={"subclient": {"id": str(user.id), "name": user.name, "email": user.email}},
)
r.raise_for_status()
return {"token": r.json()["session"]["token"]}
2. Add the widget to your page:
<script>window.Persona=window.Persona||{q:[]};["init","open","close","destroy"].forEach(function(m){window.Persona[m]=window.Persona[m]||function(){window.Persona.q.push([m].concat([].slice.call(arguments)))}});</script>
<script src="https://persona.ignited.tools/widget.js" async></script>
<script>
Persona.init({
clientId: "cli_YOUR_ID",
getSession: async () => {
const res = await fetch("/api/persona-session", { method: "POST", credentials: "include" });
return (await res.json()).token;
},
});
</script>
The widget calls getSession when the chat opens and again whenever a session expires, so users never see an expired chat.
Option B: anonymous visitors
For a public site where visitors don't sign in. Use your website key; it's safe in page source because it only works from your allowed sites and can only start anonymous chats.
<script>window.Persona=window.Persona||{q:[]};["init","open","close","destroy"].forEach(function(m){window.Persona[m]=window.Persona[m]||function(){window.Persona.q.push([m].concat([].slice.call(arguments)))}});</script>
<script src="https://persona.ignited.tools/widget.js" async></script>
<script>
Persona.init({ clientId: "cli_YOUR_ID", publishableKey: "pk_live_YOUR_KEY" });
</script>
Each browser gets a random visitor id (kept in that browser's storage), so a returning visitor continues as the same subclient. Anonymous sessions last 2 hours and are rate limited (see Errors and limits).
Widget options
| Option | Type | Default | |
|---|---|---|---|
clientId | string | required | Your client ID |
getSession | () => Promise<string | {token}> | Signed-in mode. Return a cs_… token from your server | |
publishableKey | string | Anonymous mode. Your pk_live_… key | |
mode | "bubble" | "inline" | "bubble" | Floating button, or fill an element |
target | selector or element | Inline mode: where to render. Give it a height | |
position | "right" | "left" | "right" | Bubble position |
color | hex | #111111 | Bubble button color |
label | string | "Open chat" | Bubble accessible label |
open | boolean | false | Open the bubble on load |
onError | (error) => void | console.error | Session errors |
Methods: Persona.open(), Persona.close(), Persona.destroy(), and Persona.init(options) again to switch users (for example after your user signs in or out).
Sign-out
When a user signs out of your app, call Persona.destroy() in the page and, from your server, end their chat sessions:
curl -X DELETE https://persona.ignited.tools/api/v1/sessions \
-H "Authorization: Bearer $PERSONA_SECRET_KEY" \
-H "Content-Type: application/json" \
-d '{"subclient_id":"user_123"}'
Mobile apps
Use a hosted link in a web view: your server creates a session and opens session.url (see Hosted links). The token rides in the URL fragment, which is never sent to a server.
Custom domain
If you've connected a custom domain, load https://chat.yourbrand.com/widget.js and call https://chat.yourbrand.com/api/v1/... instead. The widget always talks to the domain it was loaded from.