Embed an agent on your website
Add a Graine voice or chat agent to your own site with two script tags, restrict it to your domains, and hand it context from the page.
Embedding puts an agent on your own website. A visitor talks to it there, in the page, without a phone call and without leaving to a hosted link.
Putting an agent inside a mobile app instead? Use in-app agents. The script tag below cannot run there, and an in-app agent can do more: it sees the screen the customer is on and can act on it.
Conversations open on the agent's web-call variant, never on the agent that takes phone calls. The two share a prompt but are separate runtime agents, so changing the embed cannot affect your outbound calling.
Install
Open your agent in the dashboard, go to Install & domains, and copy the snippet. It looks like this:
Paste it into any page you want the agent on, just before </body>.
Two script tags rather than one: the first loads the loader, the second configures it. Keeping them separate means the loader is cached across your whole site while the configuration stays per-page — so you can pass different context on a pricing page than on a support page.
The publishable key is not a secret
publishableKey ships in your page source, where anyone can read it. That is
expected and safe, because the key alone does nothing — it only works from a
domain you have allowlisted.
This is why the domain allowlist is the actual security control, not the key. An empty allowlist means the embed will not load anywhere.
Rotating the key immediately breaks every page still serving the old snippet, with no grace period. Update your site first, then rotate — not the other way round.
Restrict it to your domains
In Install & domains, add every hostname the widget may load from:
Add each host explicitly. example.com does not cover www.example.com —
they are different origins to a browser, and the allowlist is matched the way
the browser sees it, not the way people write it.
Include your staging hostname too, or the widget will silently fail to load there while working in production.
Give the agent page context
If your agent's prompt uses variables, pass them at load time. The visitor never sees them; the agent receives them as fields rather than having to ask.
Only variables your agent already declares are accepted; unknown keys are ignored rather than passed through, so a typo fails quietly instead of injecting unexpected text into the prompt.
Let the agent act on the page
The agent can scroll, open, highlight, fill and click on the visitor's behalf. Same contract as the mobile SDK: declare on the agent, implement on the page.
Declare it in the dashboard under Agent → Embed & Widgets → "What this agent may do in your app", with the same name. A handler with no declaration is never offered to the model; a declaration with no handler is refused at call time.
defineAction is safe to call before load() — handlers are held until the
widget mounts — and removeAction(name) withdraws one when a section unmounts.
Clicking something the visitor cannot find
The useful pattern is matching on the visitor's own words, not on your markup:
Pointing at something
Navigate with window.location, not window.open. The handler runs from a
callback inside the widget's frame, and a popup opened from there is exactly
what popup blockers exist to stop. The visitor sees nothing happen and the
agent has already said it opened.
What to return
| Return | The agent says |
|---|---|
{ status: "ok", message } | Repeats your message as a completed action |
{ status: "refused", message } | Explains why, using your reason |
{ status: "error", message } | Apologises and offers an alternative |
A refusal with a real reason beats a false success. "No such section. Available: top, pricing, faq" gives the agent something useful to say next; ok on a
scroll that did not happen tells the visitor to look at something they cannot see.
Answer within about five seconds. The runtime is blocked on your handler. For
anything slower, return immediately and report the outcome through setScreen
when it finishes.
Interactive components
A plain web chat can only send text, which makes the visitor do the typing — read three plans and type which one, spell out an email, type a date. Every one of those is somewhere people give up.
Components replace typing with a tap, and hand your agent a structured answer instead of a sentence it has to interpret:
| Component | The visitor | Your agent receives |
|---|---|---|
| Lead form | Fills name / phone / email, taps Submit | full_name, phone, email as fields |
| Plan comparison | Swipes cards, taps one | The exact plan id and price |
| Info card | Reads, optionally taps a call-to-action | Which card, which action |
| Calendar slots | Picks a day, taps a time | The exact day and slot |
| Quick actions | Taps one of a row of replies | Which reply |
A component is a tool your agent calls, not a special prompt syntax. You enable it on the agent; the model decides when to show it.
Troubleshooting
| What you see | Why | Fix |
|---|---|---|
| Nothing renders | The page's origin is not allowlisted | Add the exact hostname, including www. if that is what visitors use |
| Worked, then stopped | The key was rotated | Copy the new snippet from Install & domains |
| Loads on production, not staging | Staging host missing | Add the staging hostname |
| Agent ignores your variables | The variable is not declared on the agent | Add it to the agent's prompt variables first |

