For developers
Install on a site
Where the two lines go and how to check that the chat is live.
On this page
The chat is a web component. You add two lines to the layout all your pages share. Everything else comes from the Tolkly server.
The two lines
<script type="module" src="https://api.tolkly.io/widget.js"></script>
<tolkly-chat project="your-project"></tolkly-chat>Replace your-project with the project's id. In the app, open the project's «Install» section,
block «Install on your site». The lines are there with the id filled in, next to a «Copy the code»
button.
widget.jsis about a kilobyte: it loads the chat fromhttps://api.tolkly.io, and if the chat has not come up within 2.5 s, from our next address (visitors in Russia).- The tag needs no
endpoint: the chat talks to the address its script came from. - Load the script from
https://api.tolkly.io. Do not copy it into your repository or bundle. This way new releases reach your site on their own. - Add no other attributes. The chat's name, greeting, colours and who answers (the AI or only
people) come from the server. The element reads them from
GET https://api.tolkly.io/v1/projects/<project>/widgetwhen it connects. Attributes are only for the rare site that wants its own values (attributes).
Where they go
Add both lines once, just before </body>, in the template every page uses: the root layout, the
base template or the theme's footer. Do not add them page by page. For your stack, see
Frameworks.
Single-page apps
In React, Vue, Angular, Next.js or Nuxt, the chat must stay on the page when the route changes:
- Render
<tolkly-chat>once, outside the router outlet: inindex.htmlor the root layout. Inside a page component, the element is removed and created again on every navigation. - Load the script once, not on every page.
- In TypeScript JSX, declare the element instead of casting it to
any(TypeScript and JSX).
What loads on every page
Most visitors never open the chat. So most page views load only the launcher:
| Request | When | Caching |
|---|---|---|
https://api.tolkly.io/widget.js | every page | no-cache with an ETag: a repeat view gets a 304 with no body |
GET …/v1/projects/<project>/widget | every page | the browser keeps it for WIDGET_SETTINGS_MAX_AGE_SECONDS (60 s by default) |
POST …/v1/projects/<project>/identify | every page with data-identity, and on identify() | a POST, never cached (identity) |
https://api.tolkly.io/widget/chat-<hash>.js | when the visitor first reaches for the chat | named by its content, kept for a year |
The chat window loads in three cases:
- the visitor reaches for it: hovers, focuses or taps the launcher;
- the page opens it;
- the visitor already has a conversation going (then it loads at once).
Scripts are sent compressed, with brotli or gzip, whichever the browser accepts. The window's code
comes from the same place as widget.js, under /widget/.
Visitors in Russia
Since June 2025 Russian providers cut connections to Cloudflare after the first ~16 KB, so for a
visitor in Russia without a VPN the chat from https://api.tolkly.io may never finish loading.
widget.js is small enough to arrive whole; it waits for the chat itself, not for a script's
load event (a stalled download fires neither load nor error), and then loads the chat from
our address in Russia. The
address that worked is remembered in the site's localStorage for a day, so the next page starts
with it; a browser that refuses storage only loses that memory. The chat then sends every request
to the address it was loaded from: https://api.tolkly.io https://ru.tolkly.io all serve the same chat and the same
conversations.
Check that it works
- Open two different pages of the site. The chat button shows in the bottom right corner of both.
- In the browser's network panel,
GET https://api.tolkly.io/v1/projects/<project>/widgetanswers200. Its JSON has norefusedfield. Check this in the browser on your site, not withcurl. The server decides by the page'sOriginheader, and it never refuses a request without one. - Within a few minutes of the deploy, the project's «Install» section in the app says «The chat is live on example.com · 2 min ago». Until then it says «Not installed yet».
How the site gets recorded:
- The server takes the site from the
Originheader of the settings request. - It updates the last-seen time at most once per
WIDGET_ORIGIN_TOUCH_SECONDS(300 s by default). - A page on the same host as
https://api.tolkly.iosends noOrigin, so it is not recorded. - A site the owner blocked is never shown there.
If the chat does not show
| What you see | Why, and what to do |
|---|---|
No request to https://api.tolkly.io in the network panel | The browser got a page without the lines. Check the page source, the layout the site really uses and any page cache. |
/widget answers {"refused": "origin_blocked"} | The owner blocked this domain. They can unblock it in the project's «Install» section, block «Domains» (Domains). |
/widget answers {"refused": "origin_not_allowed"} | The project is in strict mode and this domain is not on its list. The owner can allow it (Domains). |
/widget answers 404 | The project attribute does not match any project on this server. |
| The console reports a Content-Security-Policy violation | The site's policy blocks the widget. See Content-Security-Policy. |
The console says tolkly: the identity token was refused (…) | The chat works, but anonymously. See Signed visitor identity. |
A refused site gets 200, not 403. So the page's console stays clean, and the widget simply
draws nothing.
To hand the install to a coding agent, see AI agents.