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.

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’s working_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.