Install
npm i @gtwy/chat # web + React
npm i @gtwy/chat-react-nativeSetup
Install the SDK, mint a token, render the component.
npm i @gtwy/chat # web + React
npm i @gtwy/chat-react-nativeServer-side, one per user session. Never ship your API key to the browser.
const token = jwt.sign(
{ user_id: "cus_4127" },
process.env.GTWY_JWT_SECRET,
{ expiresIn: "1h" }
);import { GtwyChat } from "@gtwy/chat";
<GtwyChat
botId="bot_4127"
token={token}
theme={{ primary: "#0E9F6E", radius: 12 }}
position="bottom-right"
onMessage={(m) => track("chat_message", m)}
/>| Prop | Type | Default | Notes |
|---|---|---|---|
| botId | string | — | Required |
| token | string | — | Signed server-side, scoped to one user |
| theme | object | light | primary, radius, font, mode |
| position | string | bottom-right | Or inline, to embed in a page |
| threadId | string | auto | Resume a specific conversation |
| open | boolean | false | Controlled open state |
const chat = GtwyChat.instance();
chat.open();
chat.send("show me last quarter");
chat.close();
chat.on("message:sent", (m) => {});
chat.on("message:received", (m) => {});
chat.on("tool:called", (t) => {});
chat.on("conversation:end", (c) => {});
// resume where the user left off
chat.setThread(user.lastThreadId);Customisation
Knowledge and tools
Connect a knowledge base and it answers from your documents, citing what it used. Connect tools and it can act — look up an order, book a slot, open a ticket.
GTWY Knowledge →Troubleshooting
Check the script actually rendered in the page source, not just in your template. On most platforms the cause is a cached page or code pasted into a preview rather than the published site. Then confirm the bot ID matches a published bot — a draft bot loads nothing.
Allow embed.gtwy.ai in script-src and connect-src. If you use a nonce, pass it on the script tag; the widget injects no inline styles that need unsafe-inline.
401 means the signature or expiry failed: check the secret matches and the clock on your server is right. 403 means the token is valid but scoped to a different user than the one being requested.
The widget renders in a shadow root, so global CSS should not reach it. If it does, something is matching on * at the document level — usually a reset applying to every element including custom ones.
Usually a viewport meta tag missing on the host page, or a fixed-position parent clipping the launcher. Set the widget to full-screen mode on small viewports.
Free forever tier, no card. Five minutes from here to live.