Widget delivery and security
How XICBOT delivers the loader and the chat window: versioned file names, checksums, security policy, response headers, key checks and per-visitor limits.
In plain language
This page explains how the chat window gets to your website. The widget has two parts. A small loader sits on your page. The chat window runs in a frame of its own. This keeps it apart from your page. The chat window checks its files when it loads them. Limits apply to each visitor. The page also explains what happens when there is a fault.
All documentation topics
The widget is made of a small loader on your page and a chat window that runs in a frame of its own. This page explains how both are delivered, which headers they carry, which checksums the chat window uses to load its parts, how the interface checks a widget key and which limits apply to each visitor. For adding the widget to your site, see Embed the widget.
The widget files
Delivery at https://cdn.xicbot.de serves exactly four files. If you serve the widget from your own domain, the same files sit under your path; see Own path on your domain.
| File | Purpose | Content type |
|---|---|---|
einbetten.js | Loader on your page. It reads the attributes of the script tag and builds trigger and frame. It does not send any network request itself. | text/javascript; charset=utf-8 |
rahmen.html | Skeleton of the chat window, page title Chat. It carries <meta name="robots" content="noindex, nofollow">. | text/html; charset=utf-8 |
rahmen.css | Styling of the chat window | text/css; charset=utf-8 |
rahmen.js | Logic of the chat window: loading settings, running the conversation, showing messages | text/javascript; charset=utf-8 |
version.json | Overview of the current versions, see Version overview | application/json; charset=utf-8 |
- The root
/returnsrahmen.html. - A
HEADrequest receives the same headers without a body. - Any other name, an outdated version included, gets status 404 with the text
nicht gefunden.
Versioned names and caching
Every file can be reached under two names: its entry name, such as rahmen.js, and a versioned name like rahmen.1a2b3c4d5e.js. The ten hexadecimal characters are the start of the SHA-256 hash of the delivered content, so a changed file gets a new versioned name. For rahmen.html the hash is taken only after the references to the other two parts have been inserted. A new version of rahmen.css or rahmen.js therefore also produces a new version of the skeleton.
| Request | Cache-Control | Meaning |
|---|---|---|
Entry name, such as einbetten.js | public, max-age=300 | Browsers and caches keep the file for up to five minutes. |
Versioned name, such as rahmen.1a2b3c4d5e.js | public, max-age=31536000, immutable | Valid for a year without asking the server again. |
rahmen.html under either name | no-store | Fetched afresh on every opening because it carries the security policy. |
version.json | no-store | Reflects the state at the moment of the request. |
- Only the current version is served. If the identifier in a versioned name does not match the current content, delivery answers 404
nicht gefundenrather than quietly serving new content under the old name. - Every file states its version in the
X-Xicbot-Versionheader. - The embed code points to the entry name
einbetten.js. A new widget version therefore reaches your page without any change to the embed code once the five-minute cache period has run out.
Checksums in the chat window
In its source, rahmen.html refers to the entry names rahmen.css and rahmen.js. On delivery, both references are replaced by the versioned names, and the SHA-384 hash of each content is added as integrity. The browser can then take the parts from its cache as long as the version is current, and it rejects any file whose content does not match the hash.
rahmen.html html
<link rel="stylesheet" href="rahmen.<version>.css" integrity="sha384-…" crossorigin="anonymous">
<script src="rahmen.<version>.js" integrity="sha384-…" crossorigin="anonymous"></script>crossorigin="anonymous" is required here. The chat window has an opaque origin, so every request it makes counts as cross-origin. Without the attribute the browser would keep the response opaque and the check would fail. So that the parts still load, delivery sends Access-Control-Allow-Origin: * and Cross-Origin-Resource-Policy: cross-origin with every file.
Version overview
/version.json lists the current versions of all four files. The response is not cached (no-store) and any page may read it (Access-Control-Allow-Origin: *).
| Field | Content |
|---|---|
generated | Time of the most recent change among the four files, in ISO 8601 format |
origin | Address the chat window is loaded from; with your own path, your address |
api | Address of the interface the chat window talks to |
files[].name | Entry name of the file |
files[].version | Ten hexadecimal characters of the SHA-256 hash |
files[].versioned_name | Versioned name of the file |
files[].sri | SHA-384 hash in the form sha384-… |
files[].bytes | Size in bytes |
Shell command bash
# Fetch the current versions
curl -s https://cdn.xicbot.de/version.json
# Look at the headers of the loader
curl -sI https://cdn.xicbot.de/einbetten.jsSecurity policy of the chat window
rahmen.html arrives with a policy of its own in the Content-Security-Policy header. It applies inside the chat window only, not to your page; what your own policy has to allow is covered in Content security policy of your page. The addresses in it are the base the chat window is loaded from and the address of the interface, which with your own path means your domain.
| Directive | Value | Effect |
|---|---|---|
default-src | 'none' | Anything not explicitly allowed stays blocked. |
script-src | Base with trailing slash, such as https://cdn.xicbot.de/ | Scripts only from the base of the chat window |
style-src | Base with trailing slash | Styles only from the same base |
connect-src | Interface address with trailing slash | Requests only to the chat interface |
img-src | Base with trailing slash, data: and https: | Images from the base, as data: addresses and over https from other addresses as well. The window only places external images in product cards, and only with an https address. |
base-uri | 'none' | No <base> element can redirect the addresses. |
form-action | 'none' | The window does not submit forms. |
frame-ancestors | * | The chat window may be embedded in any page. Allowed domains decides where a conversation may start. |
- The trailing slash limits the permission to the path of the base. With your own path
https://example.com/chatit covershttps://example.com/chat/, not the rest of the domain. - Separation from your page comes from the sandbox of the frame, not from
frame-ancestors; see Isolation from your page.
Response headers
Each of the four files is sent with these headers:
| Header | Value | Purpose |
|---|---|---|
Content-Type | per file, see The widget files | Content type with UTF-8 character set |
X-Content-Type-Options | nosniff | The browser does not guess a different content type. |
Referrer-Policy | no-referrer | Follow-up requests do not reveal the referring page. |
Access-Control-Allow-Origin | * | The chat window, with its opaque origin, may load and verify the files. |
Cross-Origin-Resource-Policy | cross-origin | The files may be used within other sites. |
Cache-Control | see Versioned names and caching | How long caches keep the file |
X-Xicbot-Version | ten hexadecimal characters | Version of the delivered file |
Content-Security-Policy | only on rahmen.html | Policy of the chat window, see above |
Vary | only on rahmen.html: X-Xicbot-Base, X-Xicbot-Api | Keeps the versions for different base addresses apart in caches. |
Varyis needed because the policy depends on theX-Xicbot-BaseandX-Xicbot-Apiheaders. Without it a cache could hand one customer’s skeleton and policy to the next.X-Frame-Optionsis left out on purpose, since such a header would stop the window from being embedded in your page.- On
cdn.xicbot.de,Strict-Transport-Security: max-age=31536000; includeSubDomainsandX-Robots-Tag: noindex, nofolloware added; see Two embedding routes. Thenoindexin therahmen.htmlskeleton applies on both routes, including your own path. version.jsoncarries onlyContent-Type,X-Content-Type-Options,Access-Control-Allow-OriginandCache-Control.
Addresses in the headers
With your own path, your server sets the X-Xicbot-Base and X-Xicbot-Api headers; see What your server forwards. On cdn.xicbot.de, XICBOT sets them itself and overrides any header of the same name that a visitor sends. These addresses feed the policy of the chat window and the origin and api fields in version.json.
- An address is accepted if it consists of
http://orhttps://, a host name of lower-case letters, digits, dots and hyphens, an optional port and a path of letters, digits and the characters.,_,~and-. - At most 200 characters. Surrounding whitespace is trimmed, and so is a trailing slash.
- The check is strict because the address ends up in the policy, where a semicolon or a space would add a directive.
- A header that is set but unusable yields status 400 with the text
kopf ungueltiginstead of silently falling back to the default address. - Without
X-Xicbot-Api,X-Xicbot-Baseis used for the interface as well.
Isolation from your page
The section Chat window on your website in the app states: The chat window runs in its own frame. It can neither read nor change the page it sits on. This is how that works:
- The frame carries
sandbox="allow-scripts allow-popups allow-popups-to-escape-sandbox"withoutallow-same-origin, plusreferrerpolicy="no-referrer",loading="eager"and the titleChat. - Without
allow-same-originthe chat window has an opaque origin. Your page cannot read its content, and the chat window cannot read yours. That alone does not protect the history: anyone who knows the conversation ID from your page's storage can retrieve it through the API. - The loader passes its settings in the fragment of the frame address after
#: key, interface address, origin of your page, position, language, display mode, the identifier of a running conversation, and the font, corner radius and brand colour of your page. Browsers do not send the fragment to the server, so it does not appear in any access log. - The loader itself sends no network requests. Everything that fetches data runs inside the chat window.
- The chat window calls the interface without cookies or sign-in (
credentials: omit) and without revealing the referring page. - The interface sends cross-origin permission headers only under
/api/v1/public/: for the originnullthat such a frame sends, for the own origins of the app and of delivery, and for origins that XICBOT explicitly allows. Allowed areGET,POST,DELETEandOPTIONSwith the headersContent-TypeandAccept. TheRetry-Afterheader is readable, a preflight is cached for 600 seconds, and requests with credentials are not permitted. - Messages between loader and chat window are checked for sender and origin on both sides; see Checks on both sides.
- The frame is built only after your page has finished loading. With the consent gate switched on, it waits for consent; see Consent and visitor rights.
- Without
data-xicbotthe loader builds nothing. A second loader on the same page has no effect.
Key check at the interface
Every request under /api/v1/public/widget/{key}/ first checks the public key of the assistant. Two separate switches on the assistant decide: it has to be set to active, and Enable chat window has to be switched on. An unknown, a disabled, a paused and a draft assistant give the same answer, so guessing a key does not reveal whether it exists.
| Situation | Response | Effect in the chat window |
|---|---|---|
| Unknown key | 404 unknown_widget | This chat is not available., the trigger disappears |
| Enable chat window is switched off | 404 unknown_widget | as above |
| The assistant is set to Draft or paused rather than active | 404 unknown_widget | as above |
| Your account is not active | 404 unknown_widget | as above |
| The demo access has expired | 404 demo_expired | as above |
| The assistant has no active version yet | Settings with ready: false, 422 no_version when a conversation starts | This assistant is not ready yet. |
| Conversation start from a page outside Allowed domains | 403 host_not_allowed | This chat is not enabled for this website. |
Only unknown_widget and demo_expired make the chat window disappear: it tells the loader that the assistant is gone, and the loader removes trigger, bubble and frame from your page. A 404 without one of these codes usually comes from a wrong address, such as data-api pointing at the wrong host, and is treated as a disruption.
When a conversation starts, the chat window reports the origin of your page as passed on by the loader. The host name is compared in lower case, cut to 120 characters. An entry in Allowed domains includes its subdomains; with an empty list, a conversation can start on any website. Details are in Allowed domains.
Per-visitor limits
A visitor is a source address combined with the widget key. IPv4 addresses count individually, IPv6 addresses per /64 network because a connection there receives a whole network. An IPv4 address written in IPv6 notation counts as an IPv4 address. Counting by key alone would let one visitor use up the limit for everybody. Visitors behind a shared exit, such as a company network, a mobile network or the same /64 network, count as one visitor, however, and share this widget's limits.
| Limit | Value | Applies to |
|---|---|---|
Calls (widget) | 60 per minute | Loading settings, starting a conversation, loading history, rating an answer, revocation, information request, deleting a conversation, rejecting a tool proposal, feedback on a form, viewing attachments. They all share one counter. |
Sending (widget-send) | 12 per minute | Sending a message, confirming a tool proposal, uploading an attachment |
| Calls under your account | Per-minute value of your plan, plus a starting value of 2000 per day | Every call except sending a message and attachments, including confirming a tool proposal and merely loading the settings |
| Questions under your account | Starting values of 10 per minute and 200 per day | Sending a message |
- The first two limits are the same for every account. The other two take their values from your account but are also counted per visitor and key, so one visitor who writes a lot does not slow down anyone at a different address or, for IPv6, in a different /64 network.
- Each account-based limit has a per-minute value against bursts and a per-day value against sustained load.
- When a call belongs to a conversation, the conversation is counted alongside the address, with the same values. Either counter can reject; both are increased only if neither rejected. The conversation counter does not lift the shared limit of an address.
- Your plan sets the per-minute value for calls; see Plans and quotas. XICBOT sets the values for questions for your account.
- A value below 1 switches an account-based limit off.
- Attachments count only against the first two limits.
Response when a limit is exceeded
An account-based limit answers with status 429, a Retry-After header and the body below. wait gives the waiting time in seconds, the same number as Retry-After; the two limit values are those of your account.
antwort.json json
{
"error": "too_many_requests",
"limit_per_min": 10,
"limit_per_day": 200,
"wait": 42
}The widget and widget-send limits also answer with 429 and Retry-After, but without wait in the body. General limits of the interface are described in API limits, the endpoints themselves in API: widget.
How the chat window handles disruptions
After a 429, the chat window waits as long as wait in the body says. Without wait it reads Retry-After, and failing that it uses a fallback. The wait lies between one second and one hour; from 90 seconds on the window shows it in minutes. The wording of every message is listed under Messages.
| Step | Situation | Behaviour |
|---|---|---|
| Loading settings | 429 | Waits for wait or Retry-After, otherwise 30 seconds, then tries again. Counts towards the maximum of six fetches. |
| Loading settings | Network failure or status 500 and above | Further attempts after 15, 30 and 60 seconds, then after 2 and 4 minutes. After the sixth fetch, The chat cannot be reached right now. remains. |
| Loading settings | Any other error | Immediately The chat cannot be reached right now., with no further attempt |
| Starting a conversation | 429 | Waits for wait or Retry-After, otherwise 20 seconds. No cap on attempts. |
| Starting a conversation | Network failure or status 500 and above | Up to three further attempts after 15, 30 and 60 seconds, then The chat cannot be reached right now. |
| Starting a conversation | 403 | This chat is not enabled for this website. |
| Starting a conversation | Other status, such as 422 | The chat could not be started. |
| Loading history | 404 or 410 without a key-check code | An unknown or expired conversation is not an error: the window starts over and shows the greeting if one is set. |
| Loading history | 429 or another disruption | Up to five further attempts, after wait (fallback 20 seconds) or after 30 seconds. Then The conversation could not be loaded. |
| Sending a message | 429 | The visitor’s own bubble disappears and the text returns to the input field if that is empty. Sending stays blocked until the wait is over, fallback 30 seconds. The message Too many messages in a short time. Your message did not arrive and is back in the input field — please send it again in …. shows the time left. |
| Settings, start or history | 404 unknown_widget or demo_expired | Chat window and trigger disappear; see Key check at the interface. |
Delivery check in the app
Each time the embed code is fetched, XICBOT checks afresh whether the delivered file einbetten.js can be read, rather than relying on a stored value. If it cannot, the message The delivered widget file could not be read. The embed code is shown, but the delivery itself should be checked. appears under Embed code via our address. The embed code can still be copied. In that case, please let us know at mail@xicbot.de.
Through the interface, GET /api/v1/assistants/{id}/embed returns the field reason for this: file_missing if the file is absent or cannot be accessed for reading, file_unreadable if reading it fails nonetheless, and null otherwise.
Further reading
- Embed the widget: embed code, own path, allowed domains and messages.
- JavaScript and storage: the loader’s interface on your page.
- Consent and visitor rights: consent gate and revocation.
- API: widget: the public endpoints of the chat window.
- API limits: limits of the rest of the interface.
- Plans and quotas: what your plan sets.
- Data security: how XICBOT protects data.
Frequent questions
Do I need to replace the embed code after a new widget version?
No. The embed code points to the entry name einbetten.js, which browsers cache for up to five minutes. The skeleton rahmen.html is fetched afresh on every opening and refers to the current versioned names.
Can the chat window read the content of my page?
No. The frame runs in a sandbox without allow-same-origin and therefore has an opaque origin. It can neither read nor change your page, and your page cannot read its content. The two only exchange messages that are checked for sender and origin. One of them carries the conversation ID, which the loader keeps in your page's storage; anyone who knows it can retrieve the history through the API.
Why has the chat trigger disappeared from my page?
The interface rejected the key with unknown_widget or demo_expired. Check in the app that Enable chat window is switched on, the assistant is set to active and your account is active. An expired demo access has the same effect.
What does the message about too many requests mean?
A per-visitor limit has been reached. The chat window shows the waiting time and then tries again by itself. Counting happens per source address, for IPv6 per /64 network, and widget key, so visitors with a different address are not affected, while visitors behind the same exit, such as a company network, are.
Why does delivery answer with status 400 when I use my own path?
Your server set X-Xicbot-Base or X-Xicbot-Api to an address that does not fit the accepted pattern, for example with a query string, a space or more than 200 characters. Delivery then answers kopf ungueltig instead of quietly falling back to the default address.
Ask it yourself, Assistant of this website
Automated assistant