Embedding the Chat
You can put a loop’s chat on your own web page with one <iframe>. The loop must be published,
and its record must declare the embed delivery and the sites the chat may load on. Your
visitors never sign in to plutonium.io, and they never see a price, a model or a word about the
platform — only the conversation.
1. Declare where the chat may load
In the loop’s record, name the delivery and your site’s origins, then publish the version:
delivery:
kinds: [embed]
origins: [https://www.example.com]
api_key_scopes: []
Each origin starts with https://. The frame tells plutonium.io which page it is on. On a site
the loop does not declare, the chat says it is not set up for that page, and no run starts.
2. Copy the snippet from Keys
On Loops, open the loop and its Keys tab. Under Paste this into your site, press Copy. The snippet has this shape:
<iframe
src="https://plutonium.io/embed?loop=pl_…"
style="width:100%;height:520px;border:0"
sandbox="allow-scripts allow-forms allow-same-origin"
title="Your loop's name"></iframe>
The same snippet works on every site the loop declares. Keep all three sandbox flags:
allow-scripts runs the chat, allow-forms lets the visitor send, and allow-same-origin lets
the frame load its own code. The frame stays on plutonium.io, a different origin from your
site, so it reaches nothing on your page: not its content, its cookies or its storage.
Keys shows the snippet only for a published loop that declares embed and at least one origin.
A search box
If the loop uses a search index, the loop’s Keys tab offers Add a search box over what this loop
searches. Tick it before you copy, and the snippet’s address ends in &search=1. The frame
then shows a search box above the chat. A visitor’s search never reaches a collection index; see
Search Indexes.
Your own accent colour
Under Accent colour, pick the colour of the frame’s buttons and links, for this snippet only.
Leave it empty for the platform’s. The colour must read in both the light and the dark theme:
at least 3:1 against the page in each, with white or dark text that reads on it at 4.5:1. A
colour that fails is refused in words, and the code keeps the platform’s colour. A colour that
passes is added to the address as &accent=%23…, and the frame works out the shade it uses for
links. The frame checks the colour again when it loads, so an address edited by hand to a
colour that fails shows the platform’s colour; your browser’s console says why.
3. Decide who the visitor is
The loop’s identity decides whether the chat knows who your visitor is.
Anonymous visitors
With identity.kind: anonymous, each browser gets its own identity and nobody is asked to sign
in. The snippet alone is enough.
Users your site signs in
With identity.kind: signed_assertion, your server vouches for the user. The frame asks your
page for an assertion, and your page answers. Keys shows the listener to paste beside the
snippet:
<script>
window.addEventListener('message', function (e) {
if (e.origin !== 'https://plutonium.io') return;
if (e.data && e.data.type === 'pio:need-assertion') {
// Mint a fresh assertion for the signed-in visitor — each is single-use.
mintAssertion().then(function (assertion) {
e.source.postMessage({ type: 'pio:assertion', assertion: assertion },
'https://plutonium.io');
});
}
});
</script>
mintAssertion is yours: it asks your own server to sign a JWT for the user with the
loop’s secret. Its sub becomes the user’s id in every run.
An owner finds the secret on the loop’s Keys tab, under Signing secret, once the loop is published: press Show the secret, then Copy the secret. Keep it on your server: anyone who holds it can sign in as any of your visitors. Identity and Keys says how to rotate it.
Mint a new assertion each time the frame asks, because each one can be used only once and the frame asks again when it needs another. Identity in the API reference describes the assertion.
4. Let the loop act on your page
A loop can declare client tools: actions that your own page performs for the user, such as opening a page of your app or turning on a setting. When the loop declares any, Keys shows a second listener with one empty handler per tool. Fill in each handler:
- A handler runs with the signed-in user’s own authority on your site. Check what it does exactly as if the user had clicked.
- What a handler returns, the loop reads as untrusted.
- A tool your page has no handler for, or a handler that throws, tells the loop that this page does not handle it.
Before the loop acts on your page, the chat shows the user a card that describes the action
and its details, and the user approves or declines. Each detail is labelled with its property’s
title in the tool’s input schema, so give every property one the user can read; a property
with no title is labelled with its name. A tool that only moves the page to
another of your pages shows no card, unless the loop’s record asks for one. If the user
declines, the loop reads that as the answer and the conversation goes on.
Keep the chat on topic
Every message a visitor sends starts a run that your budget pays for, and a chat with no
limits answers anything it is asked. Declare on_topic in the loop’s record to say which
messages it is for:
on_topic:
about: questions about booking a class at our studio - classes, times, prices, what to bring and getting here
reply: I can only help with booking a class at our studio.
refuses: true
about is one sentence that names everything the loop answers, and reply is what a visitor
reads when their message is not. A question about something about leaves out reads as off
topic, so name the whole scope: “booking a class” alone would leave out parking and prices.
LoopAssistant writes both for every loop that a stranger can reach, and publish refuses either
one when it is blank. The cards you approve a version on show both,
under What it talks about.
While the loop answers a message, a small decision model reads the message and the last few
turns of the conversation, and judges whether the message is within about. The check runs
beside the answer, never before it, so it adds at most a moment before the first words. Short
follow-ups such as “thanks” or “and then?” count as on topic. Each check’s small cost is part
of what the run spent.
With refuses: true, a visitor whose message is off topic reads reply and never sees the
loop’s answer: the loop holds its first words until the check is done, shows reply the moment
it is (the time under it counts to that moment), and runs none of the answer’s actions. The run spends the check and that one answer’s
model call, and the visitor can type again. Without refuses, the check only records its judgement, and every message is
still answered. Start there to see how the check judges your visitors’ real messages, and turn
refuses on when it judges them well. If the check itself fails, the message is answered.
What your visitor sees
- The conversation, with formatted answers, and a short line such as “Thinking” or “Searching the docs” with three moving dots while the loop works. From the first second the line also shows how long the turn has taken: “Searching the docs · 4 s”.
- If the loop declares
chat: {show_activity: true}, each step the loop takes, in words (“Searched the docs”), as it takes it; when the answer arrives, the steps fold into one line under it that the visitor can open. A loop that does not declare it shows none of this, because a step says what the loop does inside and that is your decision. A step is only ever a tool’s own words (a custom tool’sworking_label), never the model’s text: what the model writes is its answer. - Links in an answer, which open in a new tab. A link to another site shows that site’s address beside it, so the visitor sees where it goes.
- Any card the loop asks them to answer: a form, a choice, an email check or an approval of their own. It shows in the conversation, under the message that asked for it. The action the card asks for is the filled button; the other choice is outlined. The visitor can always refuse: Don’t share on a card that asks for details they type, Skip on a card of choices. An approval your team decides, such as a refund, never reaches the visitor; it waits for your team in the console.
- One box to write in. Enter sends; Shift+Enter starts a new line. While the loop works, a Stop button takes the place of Send, so a visitor can end a run that goes wrong. Enter still sends while the loop works: when the loop then waits for the visitor, what they sent is their answer, in the order they sent it, with nothing to send again. When the loop waits for the visitor’s own answer, Send is back. Send is also back when a member of your team takes the conversation over and writes to the visitor, so the visitor can answer them.
- When the job is done, the loop’s own closing line and Start a new conversation.
- If the visitor reloads the page, the chat comes back as it was, in the same tab, with each answer’s folded steps when the loop shows them (without the time each took). A new tab, or Start a new conversation, starts fresh.
- When the frame cannot hold the chat, for example because the snippet names no loop or the page is opened on its own, the frame says why in its middle instead of showing an empty chat.
Nothing about cost, budgets, models or tools reaches the visitor.
The React SDK
A loop can also declare the sdk delivery, for a React application that you build and render
yourself. The package, embed-sdk-pio, is not published yet, so there is no install line
to give you. Keys says the same, and will carry the line when there is one.
Never put a key in a page
An API key (rk_…) is for your server. A key in a page can be read by anyone who opens it, and
it spends your account’s money. The embed and the SDK never carry a key: they get the visitor’s
own credential through the loop’s public id.