Web widget

A chat bubble with your agent on your own site, with one line of code and only on the domains you allow.

Updated 2026-10-06

One line on your site and a chat bubble shows up in the bottom-right corner: your visitors talk to your agent, no account needed.

Turn it on

From Agents → your agent → Channels → Web widget: flip the switch, list your domains and copy the code. Or from the terminal:

bash
ghosty agents embed enable <agent-id> --domains "mystore.com,*.mystore.io" --welcome "Hi! How can I help?"

It gives you the code to paste before </body>:

html
<script src="https://www.ghosty.studio/widget.js" data-agent="emb_…" data-color="#7c3aed" async></script>

It works the same on hand-written HTML, WordPress, Shopify, Wix or any site that lets you paste a <script>.

Where it loads: your domains

The widget only opens on the domains you list. If someone copies your code to another site, the browser won't load it.

  • mystore.com also covers www.mystore.com.
  • *.mystore.com covers any subdomain (blog.mystore.com, shop.mystore.com).
  • With no domains it loads nowhere.
  • https only.

What a visitor can do

  • Only chat with your agent. Each visitor has their own conversation and can't see anyone else's; the agent runs isolated per visitor.
  • They talk as a customer: the agent reads your databases and documents per what you granted the channel, but doesn't administer (no boards, email or databases without write permission).
  • The conversation is kept in their browser and picks up where they left off.
  • Every message lands in your CRM as a web-channel conversation.

Customize it

Everything changes in Channels → Web widget, by API (PATCH /api/v2/agents/{id}/embed) or from the terminal. The code you already pasted doesn't change: the bubble fetches its look when it loads (a change can take up to a minute to show).

OptionFlagDefault
First message (the greeting the visitor sees)--welcome T"¡Hola! ¿En qué te ayudo?"
Header name--title Tthe agent's name
Line under the name--subtitle Tnone
Avatar (https JPEG/PNG/WebP image)--avatar URL (none removes it)the agent's ghost
Button icon--icon chat|avatar|ghostchat
Message box text--placeholder T"Escríbele a …"
Corner and distance (px)--position right|left · --offset 20,40right, 20 px
Panel theme--theme light|dark|autolight (auto follows the visitor)
Main color--color #hex#7c3aed; the text on top is white or black by contrast
Label next to the button--bubble-text "Questions?"none
Open by itself--auto-open 10 (off)off; once per visit and never on phones
Your own suggestions (up to 4)--starters "Prices?|Shipping?"none (Ghosty's are never shown)

The avatar is copied to img.ghosty.studio, so the widget keeps working if your image moves.

bash
ghosty agents embed <agent-id> --title "Palmera Legal" --subtitle "Replies in minutes" \  --avatar https://palmeralegal.mx/logo.png --icon avatar --position left --theme auto \  --bubble-text "Questions?" --starters "How much is a consultation?|Where are you?"

Fine style

To make it look exactly like your site, the style block changes the typeface, the corners and the color of each piece. It takes no free CSS: only these keys, each one validated. A key set to null goes back to its default; --style none resets them all.

Key
What it changes
Looks like
fontFamily
A Google Font from an allow-list: Inter, Barlow, Barlow Condensed, Poppins, Montserrat, Roboto, Open Sans, Lato, Nunito, DM Sans, Work Sans, Manrope, Rubik, IBM Plex Sans, Source Sans 3, Raleway, Merriweather, Lora, Playfair Display, Libre Baskerville, Cormorant Garamond, Space Grotesk, Source Serif 4
AaAaAa
radius
Corners of the panel, bubbles, box and send button (0–24 px; 0 = square)
headerBg · headerText
Header background and text
Asistente
titleUppercase
Title in UPPERCASE with letter spacing
Asistente
statusDot
Status dot next to the title (a color)
Asistente
headerAvatar
false hides the avatar in the header
panelBg
Panel background
userBubbleBg · userBubbleText
Visitor bubble
Do you ship?
agentBubbleBg · agentBubbleText
Agent bubble (without it, the agent writes with no bubble)
Sure!
inputBg
Message box
Type…
sendBg · sendIcon · sendArrow
Send button, its arrow and its direction (up or right)
↑→
launcherBg · launcherIcon
Floating button (always round)
typing · typingColor
The "typing" dots while the agent replies: bounce (the default), flow (wave), flashing (blink) or shimmer (glowing "Thinking…" text, no dots), and their color (without it, your brand color)

Colors are #rrggbb. A text color you leave out is computed by contrast against its background.

Example: Palmera Legal style — dark green header with the title in uppercase, gold dot, square corners everywhere, green and beige bubbles, Barlow, and gold typing dots in a wave:

bash
ghosty agents embed <agent-id> --style '{  "fontFamily": "Barlow", "radius": 0,  "headerBg": "#2d4a2e", "headerText": "#ffffff", "titleUppercase": true, "statusDot": "#c8a96a", "headerAvatar": false,  "panelBg": "#fbf9f4",  "userBubbleBg": "#2d4a2e", "userBubbleText": "#ffffff",  "agentBubbleBg": "#f3efe6", "agentBubbleText": "#1f2a1f",  "inputBg": "#ffffff", "sendBg": "#f3efe6", "sendIcon": "#2d4a2e", "sendArrow": "right",  "launcherBg": "#2d4a2e", "launcherIcon": "#ffffff",  "typing": "flow", "typingColor": "#c8a96a"}'

--font and --radius are shortcuts for style.fontFamily and style.radius.

Typing dots

While the agent replies, the chat shows three dots. By default they take your brand color (the header's). --typing-color gives them another one and --typing changes the animation:

typing
What it does
Looks like
bounce
They hop one after another (the default)
flow
They grow in a wave
flashing
They blink, like WhatsApp
shimmer
No dots: the "Thinking…" text with a glow running across it
Pensando
bash
ghosty agents embed <agent-id> --typing-color "#e11d48"ghosty agents embed <agent-id> --typing flow

--typing-color none goes back to the brand color. In the agent's page it lives under Channels → Web widget → Advanced.

Where the theme comes from

In the card you pick New (your values), From Teams or From my site, with a preview:

  • From Teams: takes the active brand of your Ghosty Teams workspace (Settings → Brand): primary goes to the header, the button and the visitor bubble; accent to the send button and the status dot; surface to the background; and its typeface if it's on the list.
  • From my site: reads the colors and typeface from your page's CSS (not from images). If your site paints everything with JavaScript, it tells you it found no colors.

Both are imported as editable values: then tweak whatever you want in Advanced → Style. Apply them again to refresh.

bash
ghosty agents embed <agent-id> --theme-source teams [--workspace my-space]ghosty agents embed <agent-id> --theme-source site --site-url https://palmeralegal.mxghosty agents embed <agent-id> theme > theme.json        # export the current themeghosty agents embed <agent-id> --style-file theme.json   # apply it back (or apply your own)

Texts while it works

While the agent thinks, the widget shows a few phrases in Ghosty's voice («Me estoy poniendo los lentes 👓», «Estoy abriendo mi libreta», «Estoy pensando»…). If they don't fit your business, change them in the card (Advanced → Texts, with a preview), through the API (copy) or with the CLI. Whatever you don't change keeps Ghosty's phrase.

KeyWhen it showsDefault
wakingWhile it wakes up«Me estoy poniendo los lentes 👓»
connectingWhile it connects«Estoy abriendo mi libreta»
thinkingWhile it thinks (list of 1–8; they rotate every 4 s)«Estoy pensando», «Estoy leyendo tu mensaje»…
stillWorkingAfter a minute and a half«Sigo trabajando»
cantStartIf it didn't start within 45 s (next to the button)«No pude arrancar.»
interruptedIf the answer was cut by a failure on our side (your own text shows without the technical reason)«Me quedé a medias: …»
retryLabelThe retry / resume button«Reintentar»
showDurationThe «· 18 s» while waiting and the «Trabajó N s» footertrue
showCopyButtonThe Copy button under the answerstrue

Plain one-line text, up to 80 characters (no < or >). It's a patch per key: null brings that phrase back to Ghosty's and --copy none brings all of them back. An unknown key or a long text answers 400 with the reason.

Example, a law firm («Palmera Legal»):

bash
ghosty agents embed <agent-id> --copy '{  "waking": "Un momento, por favor",  "connecting": "Abriendo su expediente",  "thinking": ["Revisando su consulta…", "Consultando el expediente…"],  "stillWorking": "Seguimos revisando su caso",  "showDuration": false}'ghosty agents embed <agent-id> --copy-file texts.json   # the same object from a file

agents embed <id> theme exports copy too, and --style-file applies it back.

React

If your site is React (Next.js, Remix, Vite…), you have three paths. All three talk to the same agent and honor your domains and caps.

You want…Use
The usual bubble, mounted by React<GhostyWidget token="emb_…" />
Your own design, with the logic solveduseGhostyChat({ token }) (headless)
A component of your own, editable and dependency-freeghosty agents embed <agent-id> --out src/components/GhostyChat.tsx
A site without React (WordPress, Shopify, HTML)the <script> above
bash
npm i @ghostystudio/widget

ghosty agents embed <agent-id> prints this snippet with your token already in it (and in --json, under react).

The bubble: <GhostyWidget>

tsx
import { GhostyWidget } from "@ghostystudio/widget";
export default function Layout({ children }) {  return (    <>      {children}      <GhostyWidget token="emb_…" position="left" style={{ launcherBg: "#2d4a2e", radius: 0, fontFamily: "Barlow" }} />    </>  );}

It mounts and cleans up with the component. It takes the saved look and whatever you pass as props wins: position, offsetX, offsetY, bubbleText, defaultOpen, open + onOpenChange (controlled), style with the same keys as (including the inside ones: bubbles, input and send, validated the same way as when saved) and copy with the ones in (copy={{ thinking: ["Reviewing your question…"] }}).

Your own chat: useGhostyChat

tsx
import { useState } from "react";import { useGhostyChat } from "@ghostystudio/widget";
export function MyChat() {  const { messages, send, stop, status, error } = useGhostyChat({ token: "emb_…" });  const [text, setText] = useState("");  const busy = status === "thinking" || status === "streaming";  return (    <div className="my-chat">      {messages.map((m) => (        <p key={m.id} className={m.role === "user" ? "me" : "agent"}>{m.text}</p>      ))}      {status === "thinking" && <p className="agent">…</p>}      {error && <p className="error">{error}</p>}      <form onSubmit={(e) => { e.preventDefault(); send(text); setText(""); }}>        <input value={text} onChange={(e) => setText(e.target.value)} placeholder="Type…" />        {busy ? <button type="button" onClick={stop}>Stop</button> : <button>Send</button>}      </form>    </div>  );}
  • messages: { id, role: "user" | "assistant", text, pending? }; the agent's text streams in live.
  • status: connecting · idle · thinking (sent, no text yet) · streaming.
  • error: caps and notices arrive as text ready to show.
  • copy: your (complete, with Ghosty's where you changed nothing) to draw your own waiting state: copy?.thinking[0]. null until they arrive.
  • On return, messages brings back the visitor's previous conversation (one id per browser in localStorage, key ghosty:widget:vid).

Your own component: --out

If you'd rather not depend on a package (shadcn style), the CLI writes a complete, editable component into your project, with your token and current theme (colors, font, corners, title, placeholder, welcome, suggestions) as constants at the top of the file, and comments saying where to change each thing:

bash
ghosty agents embed <agent-id> --out src/components/GhostyChat.tsx   # or .jsx without types

It won't overwrite an existing file without --force. It ships useGhostyChat() (the logic) and GhostyChat (the UI) separately: change the UI without touching the connection.

Domains and CORS

From your page, headless and component requests go straight to Ghosty: they only pass if the Origin is one of your widget domains (same rules: yourstore.com includes www., *.yourstore.com its subdomains, https only). Any other site gets 403 and the browser blocks the response. It doesn't work on localhost: test on an authorized domain (or use <GhostyWidget>, which lives in the iframe).

Caps and cost

Turns are billed to your account, like any other channel. So nobody burns them:

CapDefaultChange
Messages per visitor per day30--per-visitor N
Messages per IP per day60--per-ip N

If your plan or key runs out, the visitor sees "not available right now" and you see the reason with ghosty agents embed <agent-id>.

Change it or turn it off

bash
ghosty agents embed <agent-id>                          # state, domains and the codeghosty agents embed <agent-id> --domains "other.com"    # replaces the listghosty agents embed <agent-id> --color "#0e1317"        # main color (no need to copy the code again)ghosty agents embed <agent-id> --rotate                 # new token: the old code stops workingghosty agents embed disable <agent-id>                  # turns it off everywhere, instantly

Requirements

  • Claude or MiniGhosty (ghosty-lite) engine: they isolate each visitor. It can't be turned on with another engine.

EasyBits agents

Agents created under Agents → EasyBits keep their usual widget: an <iframe> with their embed token.

html
<iframe  src="https://www.ghosty.studio/embed/YOUR_EMBED_TOKEN"  style="width:100%;max-width:420px;height:600px;border:0;border-radius:16px"  allow="clipboard-write"></iframe>
RouteWhat it does
GET /api/embed/lookup?embedToken=…Resolves the token's agentId.
POST /api/embed/message/:agentIdSends a message; replies over SSE. Header Authorization: Bearer <embedToken>.