Drop-in Widget — pouchy.ai/embed

The zero-code way to put a Pouchy companion in any page: one <iframe> renders a complete chat UI (bubbles, composer, streaming replies), authenticated by the same session token your backend already mints (POST /v1/sessions) or a user's PAT. When you outgrow the canned UI, everything here is available headless via @pouchy_ai/companion-sdk — the widget is built on it.

Embed

<iframe
  src="https://pouchy.ai/embed?token=SESSION_TOKEN&theme=dark&accent=%23ff6b81"
  style="width: 380px; height: 560px; border: 0; border-radius: 16px"
  allow="clipboard-write"
></iframe>

Query params

Param Values Effect
token pchy_… Session token or PAT. Optional — can be delivered via pouchy:init instead (below).
theme dark Dark palette. Default light.
accent %23rrggbb (URL-encoded hex) Recolors the send button + user bubbles. Text ink is auto-picked by luminance so any accent stays readable.

Channel mode — no backend (?channel=)

The embed above needs a session token, which needs POST /v1/sessions, which needs the project Secret Key — i.e. a server. A merchant on a hosted storefront (Shopify, Squarespace, Wix …) has no server; they have a theme file. For them the widget speaks the web channel instead:

<iframe
  src="https://pouchy.ai/embed?channel=CHANNEL_TOKEN"
  title="Chat"
  style="position:fixed;right:16px;bottom:16px;width:380px;height:560px;border:0;border-radius:16px;z-index:2147483000"
  allow="clipboard-write"
></iframe>

Create a web channel under 渠道; the dashboard shows this snippet with the token already filled in. ?channel= takes the TOKEN, never a URL — the widget builds location.origin + '/api/channels/' + token itself, so a query string can only ever address Pouchy's own inbound route.

Identity is UNVERIFIED in this mode, and that is the trade. The inbound route is CORS * and the visitorId is client-asserted. The widget stores a random UUID per browser, so an id is unguessable — which is not unforgeable: anyone holding the token can address a visitorId they already know. Sound for product Q&A and storefront help; not sound for orders, addresses or account state. For those, enable verifyVisitor on the connector and sign the visitorId with the inboundSecret from your own server (docs/channel-setup.md → "Web widget visitor identity") — which means having a server, and puts you back on the token path above.

Replies arrive inline on the POST response, so there is no streaming in this mode; pouchy:message still fires when one lands.

postMessage protocol

For parents that prefer not to put the token in a URL, and to drive the widget live. Every message the widget posts carries source: 'pouchy-widget'.

Widget → parent:

Type Payload When
pouchy:loaded The page booted (safe moment to send pouchy:init).
pouchy:ready { session } Connected; the companion is live.
pouchy:message { text } An assistant reply landed.
pouchy:sent { text } A user message left the composer (typed or via pouchy:send).
pouchy:tool_call { id, name, args } The companion called an app tool (declare tools via the SDK for full round-trips).
pouchy:error { code?, message } Connect/turn error.

Parent → widget:

Type Payload Effect
pouchy:init { token, surface? } Connect with this token (alternative to ?token=).
pouchy:send { text } Send a user message programmatically.
pouchy:context { event } Push a world-state event (same shape as SDK sendWorldState).
const frame = document.querySelector('iframe');
window.addEventListener('message', (ev) => {
  if (ev.data?.source !== 'pouchy-widget') return;
  if (ev.data.type === 'pouchy:loaded')
    frame.contentWindow.postMessage({ type: 'pouchy:init', token: SESSION_TOKEN }, 'https://pouchy.ai');
  if (ev.data.type === 'pouchy:message') console.log('companion said:', ev.data.text);
});

Security model

The embedding page is the trust anchor. The widget pins the embedder's origin (learned from the referrer at load, or adopted from the first parent message when the referrer is stripped) and afterwards only accepts control messages from that origin and only posts session/tool payloads back to it — never to '*', never to sibling frames or popups (ev.source !== window.parent is rejected outright). Treat session tokens like the credentials they are: mint them short-lived per user from your backend.

Known limits (v1)

  • Widget chrome is English (the conversation itself follows the agent/user language). A lang param is a planned follow-up.
  • Text modality only — for voice, use the SDK's connectCall in your own UI.