Documentation

flowhelp widget embed: customer guide

English version of the Polish guide. Code, identifiers and numbers are the same in both.

Version 1.10 · 2026-11-12 The snippet and the public API are stable: a tag pasted today will work with every future version of the widget.

1. Snippet

One tag, no inline code, before </body>:

<script async src="https://cdn.flowhelp.ai/loader.js" data-key="pk_live_…"></script>
AttributeRequiredMeaning
data-keyyesthe agent's public key (pk_live_ + 32 hex characters), from the panel
data-localenoforced window language (buttons and messages): en, pl, de, fr, it (a region is fine too, for example de-AT); if omitted, auto or a language outside the list, the setting from the panel decides, then the browser language, and if none matches, English. The assistant replies in the visitor's language when it is one of these five, and otherwise in the window language
data-apinodev/staging only; production uses https://app.flowhelp.ai

The public key is visible in your HTML by design. It is not protected by secrecy but by the domain allowlist (below), the rate limits and the assistant's daily message cap.

2. Domain allowlist (Origin)

The widget works only on the domains listed in the panel (Settings → Installation → "Allowed domains"). Semantics:

Local addresses are not supported (since 2026-11-12): localhost, 127.0.0.1, private networks (10.…, 192.168.…) and reserved names (.local, .test) are on the list of addresses flowhelp does not allow for any customer. The panel won't save them and the widget won't load on them. An entry saved earlier is flagged under the list in the panel. Test on your real domain or on a test subdomain (for example https://test.sklep.pl); check the look and the answers in the preview in the panel (Appearance).

A page outside the allowlist: the browser blocks the call (the server answers 403 without CORS headers), the widget does not appear and logs an error in the console. It costs nothing.

3. The host page's Content-Security-Policy

If your page has a CSP, exactly two sources are needed (the only exception is your own avatar, see below):

script-src  … https://cdn.flowhelp.ai;
connect-src … https://app.flowhelp.ai;

The widget uses no inline JS, no eval, and no external resources beyond these two hosts. The exception is an avatar loaded from your own address (the "Avatar URL" field): if your CSP has img-src, it must include the host of that image (section 9). Styles live in a Shadow DOM (adoptedStyleSheets), so style-src does not need to change. The loader has a fixed path (5-minute cache); the core is addressed by hash, with integrity (SRI) and crossorigin.

4. Public API

Calls can be queued before the widget loads; they are executed in order:

window.flowhelp = window.flowhelp || [];
window.flowhelp.push(['open']);
window.flowhelp.push(['on', 'ready', () => console.log('widget ready')]);
MethodEffect
open / close / toggle / isOpenthe chat window
sendMessage(text)sends a message as the user
setLocale('pl'|'en'|'de'|'fr'|'it')window language; the assistant replies in the visitor's language when it is one of these five, and otherwise in the window language
on(event, cb) / off(event, cb)events: ready, open, close, message:sent, message:received, error
grantConsent() / revokeConsent()consent (see §6)
reset()clears the visitor identifier, the current conversation and the identity set by identify
identify({ userId, hash })identity of a signed-in user with an HMAC signature (§5); identify(null) = sign out
initno effect (the widget starts by itself)

Through the push queue methods return nothing, so isOpen makes sense only when called directly. After the ready event you can call the methods directly, for example window.flowhelp.open() or window.flowhelp.isOpen().

An unknown method logs a console warning and does not throw.

5. Signed-in users: identify() with an HMAC signature

If your site knows who is signed in, you can pass that to the widget. Conversations of such a user are labelled with their identifier in the panel.

The widget does not compute the signature and does not know the secret. Your backend computes the signature, using the assistant's HMAC secret (panel → Settings → Installation → "Secret for signing users (HMAC)"):

hash = hex( HMAC-SHA256( key = agent_secret, message = userId ) )

Node.js:

const { createHmac } = require('node:crypto');
const hash = createHmac('sha256', process.env.FLOWHELP_HMAC_SECRET).update(user.id).digest('hex');

PHP:

$hash = hash_hmac('sha256', $user->id, getenv('FLOWHELP_HMAC_SECRET'));

Python:

import hmac, hashlib
hash_ = hmac.new(SECRET.encode(), user_id.encode(), hashlib.sha256).hexdigest()

On the page (this can run before the widget loads; it goes into the queue):

window.flowhelp = window.flowhelp || [];
window.flowhelp.push(['identify', { userId: 'u_123', hash: '<64 hex characters>' }]);
// after sign-out:
window.flowhelp.push(['identify', null]);

Rules:

You choose the consent mode in the panel: Appearance → the "Privacy" tab → "Visitor consent".

window.flowhelp = window.flowhelp || [];
window.flowhelp.push(['grantConsent']);   // the visitor agreed
window.flowhelp.push(['revokeConsent']);  // the visitor withdrew consent

revokeConsent() ends the current conversation (clears the window), and in blocked mode hides the widget again. The visitor identifier stays; only reset() deletes it.

Consent is not remembered: after a page reload or a move to another page, deferred and blocked start over. In deferred the visitor clicks "I agree" again, and in blocked your page must call grantConsent() on every page load (a CMP usually remembers consent itself, so you can hook the call to its start). The visitor identifier is a UUID in localStorage (flowhelp:visitor:<key>; in sessionStorage when localStorage is blocked), with no cookies. The widget writes it only on the first interaction: the first message, a rating of an answer or a form submission. Loading a page with the widget writes nothing to the browser (not even an entry that checks whether storage is available). The first message always includes the AI disclaimer (EU AI Act Art. 50); it is editable in the panel and cannot be turned off.

As the controller of your site visitors' data, you inform them about the processing at the moment you collect it (GDPR Art. 13). In the panel (Appearance → the "Privacy" tab → "Privacy policy URL") you enter the address of your policy, and the widget shows a "Privacy policy" link in the language of the chat window (for example "Datenschutzerklärung" in German, "Politique de confidentialité" in French, "Informativa sulla privacy" in Italian):

The link opens in a new tab (target="_blank", rel="noopener noreferrer"). Only https:// addresses are accepted (up to 2000 characters); an empty field means no link. In blocked mode the widget has no consent banner of its own: your banner (CMP) shows the consent request and the link to your policy, and the widget adds the link only in the forms. The link is a plain hyperlink and loads nothing from your site, so it needs no CSP changes.

7. Limits and errors

SituationBehavior
20 messages / 5 min from one visitor429 RATE_LIMITED, the widget locks the input until the window ends and says how long to wait: in seconds, from one minute in minutes ("Try again in 5 min"), from one hour in hours
a second message before the previous answer has finished429 RATE_LIMITED (Retry-After: 5)
message allowance used up: the trial after 100 messages, a paid plan at 200% of its allowance429 QUOTA_EXCEEDED, the assistant is paused (below); the trial without Retry-After, a paid plan with Retry-After until the end of the billing period
the assistant's daily message limit is used up (safety cap): the greater of 1,000 and 10% of the message allowance per UTC day429 QUOTA_EXCEEDED with Retry-After until midnight UTC, the assistant is paused (below)
the agent stopped being published during a conversation403 FORBIDDEN, the assistant is paused (below)
the 14-day trial has ended403 FORBIDDEN on every widget route, the bootstrap included: the widget does not appear, the service is switched off (below)
assistant unpublished or paused (not in the ready status) when the page loads403 from the bootstrap, the widget does not appear
mismatched identify() signature403 before any answer, zero cost
question outside the knowledge basethe assistant searches again with different words; if there is still nothing, it says plainly what is missing, adds the closest fact with its source and suggests related questions, with no guessing

RATE_LIMITED is only ever the limit of one visitor, their network, or parallel messages, so its windows are short. A used-up allowance and the daily safety cap apply to the whole assistant: those are QUOTA_EXCEEDED, and the assistant is paused while contact with your business stays (below). The end of the trial is different: the service is switched off and the widget disappears from the page (below).

When the assistant is paused

The assistant may stop, but contact with your business does not. The visitor sees neither "Something went wrong" nor a countdown, and reads nothing about your plan, trial or message limit. They see the line "The assistant is unavailable right now.", and under it a way to reach your team:

The message field is then locked, with the hint "Use the contact option above", until someone on the team takes over the conversation. From the takeover on, the visitor writes as usual and their messages go to that person: a conversation taken over by a person goes through despite the pause. A closed card does not come back by itself every time the window opens; the pill under the line or the button with the form's title above the message field opens it again.

The bootstrap reports the pause in the behavior.assistantPaused field, without giving a reason: that is none of the visitor's business. The widget then shows this screen as soon as the window opens, instead of the welcome message and the suggested questions. A pause during a conversation (QUOTA_EXCEEDED or FORBIDDEN) looks the same: the visitor's question stays on screen, and the line takes the place of the answer. The error event of the public API (section 4) and the console entry with the error code work as before.

The widget checks by itself whether the assistant is back: when the window opens and when the visitor returns to the browser tab, at most once a minute. Once it is back (for example after a plan change, or at midnight UTC after the daily safety cap), the message field unlocks without a page reload and the "unavailable" line gives way to the welcome message; a line that took the place of an answer during a conversation stays in the history.

With no way to reach you, the widget does not appear. When the assistant is paused, nobody on your team is online, and both escalation and the contact form are off, the page shows neither the chat button nor the window, and the browser console has a short note for the site's developer. Exceptions: a conversation from the last 30 minutes (section 14) comes back to the window, and during a conversation the window never disappears. Every 10 minutes, and only while the tab is visible, the hidden widget checks whether the assistant is back or you have turned on a way to reach you, and then appears by itself. Advice: turn on the contact form (panel, Settings → Actions → "Enable the contact form"). Then, even with the assistant paused, visitors leave you their contact details instead of landing on a page without chat.

When the trial has ended

After the 14-day trial, if you have not chosen a plan, the service on your website is switched off, not paused: visitors see nothing at all, neither the chat button, nor the window, nor any message. Every widget route answers 403 FORBIDDEN with the message The trial has ended. Sign in to the flowhelp panel to choose a plan., and so does the bootstrap (GET /api/widget/bootstrap): the widget draws neither the chat button nor the window, and the browser console shows the same message with the error code. There is no contact mode: the contact form, the request for a person, live chat (including a conversation someone on your team has already taken over) and resuming a conversation after moving to another page stop working. The only exception is a visitor who had the chat window open at the very moment the trial ended: their next message is refused with the line "The assistant is unavailable right now.", and after a page reload the widget is gone.

Your data (knowledge, conversations, contact requests) stays in your dashboard: you can view and delete it, and export your conversations. Until your plan changes, the dashboard does not add or refresh knowledge (pages, files, notes, Q&A, "Refresh all websites"), does not take live chats, and does not answer test questions or suggest answers. The widget comes back by itself, from the next page view, as soon as we change your plan: you do not need to paste the snippet again. The dashboard shows a red banner on every page, and we email you about the switch-off the first time someone opens your site with the widget after the trial ends.

8. Formatting of answers (markdown)

The assistant's answers are rendered from markdown: paragraphs, bold, italic, ~~strikethrough~~, code and code blocks, lists (nested too), quotes, a horizontal rule, tables, links. # headings are rendered as bold paragraphs, not as <h1>, so they do not affect the structure of your page.

What the widget does not render, because model text never becomes HTML: raw HTML (shown as text), images (the description stays), links with a protocol other than http/https/mailto (they stay as text). Every link opens in a new tab with rel="noopener noreferrer".

Source links do not appear in the body of the answer. Instead of "[1][2]" in the middle of a sentence, a row of clickable links to subpages of your site is shown below the whole answer, preceded by the word "Sources"; each of them opens in a new tab. One subpage = one link, even if the answer drew on several of its fragments, and at most three links under one answer. Further hits are in the conversation in the panel.

Links appear only under an answer based on a web page from your knowledge. An answer based on a file, a note or a Q&A has no link, and neither has an answer that does not rely on your knowledge (a greeting, a clarifying question, an answer about the chat itself, section 15).

9. Appearance, theme, RTL, mobile

10. Compatibility with frameworks and CMSs

The widget works with WordPress (Elementor), Bootstrap, Tailwind and any page where you can paste a <script> tag. The styles of your page and of the widget do not mix: your page does not change how the widget looks, and the widget does not change how your page looks.

EnvironmentHow to paste
WordPress (Elementor)the snippet in the page footer (wp_footer), for example with a custom-code plugin
Bootstrap, Tailwindno changes, the tag before </body> is enough
A dir="rtl" pageheader and composer mirrored, screen corner unchanged

If your theme has global rules such as * { letter-spacing: 2px }, they have no effect (the widget has a full reset with !important on the host). The only things the widget needs from your page: two hosts in the CSP (section 3) and, with your own avatar, its host in img-src; your page's X-Frame-Options does not matter, because the widget is not an iframe.

11. How to check that the widget works

Open the page with the pasted tag: the chat button should appear in the corner. Whether the widget has loaded is also visible in the panel under Settings → Installation → "Widget status". If there is no button, look at the browser console: the error says whether the domain is missing from the list (section 2), whether the CSP blocks a host (section 3), or whether the assistant is unpublished or paused (section 7). You publish an unpublished assistant in the panel: Settings → Installation → "Publish the assistant". The same file served from a different origin (for example opened from disk) does not start the widget.

12. Answer ratings and the contact form

You turn both on in the panel; in the snippet nothing changes, and there is no API here to call from your page.

Thumbs under an answer

When the agent has rating collection turned on, every assistant answer has two buttons under it (👍/👎). To keep the conversation uncluttered, on a computer they appear when the pointer is over the answer (or when you reach them with the Tab key), and on a phone they are visible only under the latest answer; a rating you have given stays visible. Clicking the same one a second time undoes the rating. Ratings appear in the panel in Conversations (the "thumbs down" filter); this is the only quality signal that comes from your visitors.

Turning ratings off in the panel really turns them off: the buttons disappear, and a request sent anyway gets 403. A thumb requires neither cookie consent nor signing in. We attach it to the same conversation in which the answer was produced, and only that browser can rate it.

Contact form (lead) and "talk to a human"

The form appears according to the trigger set in the panel:

TriggerWhen the widget shows the form
after_messagesafter N assistant answers (N from the panel)
on_fallbackwhen the visitor asks for it (recommended; in the panel "when the visitor asks to get in touch"). The form never pops up on its own: a button with the form's title appears under the answer when the visitor writes that they want to get in touch, or the second time they get no answer. After every "don't know" the widget suggests related questions from your knowledge
manualnever on its own; there is a button with the form's title above the input field

The fields (labels, types, required flags; max 8) come from the panel. The form opens as a card in the conversation, under the message where it appeared; it covers and blocks nothing, so the visitor can keep writing to the assistant. Opened by a trigger, it does not take the cursor out of the message field; opened by a click, it puts the cursor in the first field. You close it with the cross or the Esc key. Closing it turns off the automatic trigger until the end of that conversation, so it does not come back after every message. After submission the visitor sees your thank-you text in the same card, and the cursor goes back to the message field. When the assistant is paused (section 7), the card opens by itself as soon as the window opens, whatever the trigger, because it is then the way to reach your team; with escalation on, the "talk to a human" form opens instead.

Escalation is the same form with an extra "Message" field. The widget does not keep it above the message field all the time: it has a small "person" icon in the header (always available, with your label as its name) and a pill under the answer that appears only when it is needed: at the second "don't know" in a conversation, or when the visitor writes that they want to talk to a person or get in touch (recognised in five languages, Polish also without diacritics). After a request is sent, the pill does not come back in that conversation. The conversation gets the "Escalated" status in Conversations, and Insights counts it as a conversation with a person involved (the "Handled without a human" metric).

If you entered the address of your privacy policy in the panel, both forms show a "Privacy policy" link above the "Send" button (§ 6).

Every submitted form goes to the panel and to email: the address from the action settings, and if there is none, the workspace notification address, and if there is none of those, the owner's email. The message contains the form fields and the last 10 messages of the conversation, so you can reply without opening the panel.

Limit: 5 forms per visitor per day (a UTC day). This is a safeguard for your inbox and for our sender reputation; it cannot be raised in the panel. A rejected form gets 429 with a message, and the content stays in the fields.

13. Widget languages and agent texts in other languages

The widget speaks English, Polish, German, French and Italian. The language is chosen in this order: the data-locale attribute in the snippet (or setLocale() from the page), then the "Widget language" setting in the panel (Appearance), and with "Automatic", the visitor's browser language. If none of them matches, the widget uses English. This language applies to the buttons and the messages, regardless of the language of your knowledge base. The assistant also answers in it when the visitor writes in a language outside these five or too briefly to tell ("ok"); when the visitor clearly writes in another of the five languages, the assistant answers in the visitor's language (section 15).

The texts you write yourself, that is the welcome message, the AI notice and the suggested questions, are in one language: the one you typed them in. In the panel (Appearance, the "Texts in other languages" section) you can give each of them a version for a specific language:

The texts of the contact form and of the talk-to-a-human button (section 12) work the same way: in the panel (Settings → Actions, "Texts in other languages") you can enter the heading, description, thank-you message and the label of every form field, and the button text, in each of the five languages. The widget takes them field by field, as above. A submitted form always has the same fields, whatever the visitor's language.

In Appearance → Texts you also set "Language of the main texts". That language's tab disappears from "Texts in other languages", and the panel tells you which visitors will see the main texts instead of their own, that is which languages have no welcome message.

14. Live chat

When someone on your team turns on "Take live chats" in the panel and has the panel open, the widget shows a strip under the header: "Anna from the team is online". Nothing changes in the snippet.

15. What the assistant knows about itself

About your company the assistant speaks only from your knowledge (the sources in the panel). When an answer is based on a web page, links to it stand below the whole answer (section 8). About itself and about the chat it knows from a short assistant card that we build automatically from the agent's settings, never from the content of your website. That is why questions like "Who are you?", "Which languages do you speak?", "Can I talk to a person?" or "Does anyone read this conversation?" get answers that match how the chat really works, even when your knowledge says nothing about it (or says something else, for example terms that list the language versions of a document).

What the assistant saysWhere it comes fromHow to change it
its name, and that it is an AI assistant, not a humanthe assistant name; without a name: "the AI assistant on this website"Appearance → "Assistant name". The fact that it is an AI cannot be turned off (AI Act, Art. 50)
which website it works onthe domain from the list of allowed domains (www. and subdomains are merged into the main domain); with several different domains: "this website"Settings → Installation → "Allowed domains"
where its knowledge comes frominformation shared by the company: pages, documents and the team's answers; it can be wrong, so binding matters (prices, contracts, orders, complaints) are worth confirming with youfixed
which languages it speaksthe widget works in English, Polish, German, French and Italian; the assistant answers in the visitor's language when it is one of these five, and otherwise in the language of the chat window (section 13); it understands other languages and never says it does not speak themAppearance → "Widget language", or data-locale in the snippet
how to reach a personthe agent's actions (section 12) and live chat (section 14): with escalation, the person icon in the header or a request in a message; with live chat, someone from the team joins when online; with the contact form, it says when the form appears. With none of these it says that this chat cannot hand the conversation over to a person, and gives your contact details only from your knowledgeSettings → Actions; "Take live chats"
what it will not doit does not place orders, take payments, change bookings, reset passwords, send emails or links, or see accounts, orders or customer data; it does not promise such actions and does not ask for login details or card numbers; it can explain how to do these things when your knowledge describes itfixed
what it remembersthe recent messages of this conversation, not earlier conversations; the conversation stays in the window for 2 days after the last message, also on other pages and after coming back, and “New conversation” in the header starts from scratchfixed (section 14, “Continuity”)
privacy of the conversationthe conversation is processed by an AI provider and saved, and your team sees it in the panel; it advises against sharing passwords, card numbers and health data; it gives the address of your privacy policyAppearance → "Privacy policy URL" (section 6). Without an address the assistant gives it only from your knowledge
what it runs on"flowhelp", but only when the chat window shows "Powered by flowhelp"; with the line hidden (Growth plan and above) it does not name the provider. It never names the AI modelAppearance → "Hide the 'Powered by flowhelp' line" (Growth plan and above). On Trial and Starter the line always shows; after a move to a plan below Growth it comes back by itself

Changes to the settings apply from the next message, with no change to the snippet. Every answer of the assistant counts as a regular message from your plan, including an answer from the card.

How the assistant handles a conversation:

16. Content the assistant should not read (data-flowhelp-skip)

The assistant learns from the content of your pages. If a page has a part that should stay out of its knowledge (an archived promotion, customer reviews with old prices, a sample conversation), add the data-flowhelp-skip attribute to its element:

<section class="archive" data-flowhelp-skip>
  …
</section>

The next time the page is read, the crawler skips that element and everything inside it; the rest of the page stays in the knowledge base. If almost nothing is left on the page after that, the assistant stops using the page altogether. Nothing changes for visitors or search engines. Content the assistant already knows disappears after the page is read again. The change takes effect at the next refresh of the page: the automatic one (weekly from the Starter plan) or the "Refresh" button next to the source in the panel → Knowledge (once a day; on the trial, once during the whole trial).