Dokumentation

flowhelp widget embed: customer guide (contract v1)

English translation of WIDGET.md. The Polish file docs/WIDGET.md is the source of truth; if the two ever differ, the Polish one wins.

Version 1.3 · 2026-09-22 · Source of truth: packages/config/src/widget-embed.ts and DECYZJE.md ADR-026 (embedding), ADR-027 (markdown), ADR-028 (identify), ADR-029 (ratings, contact form, preview in the panel), ADR-031 (source links outside the answer text), ADR-032 (action row, form as a card, one subpage = one link). Everything in this guide is frozen: a snippet pasted today will keep working 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 UI language (pl/en/de/fr/it); if omitted, the agent configuration or the browser decides
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 agent's daily credit cap.

2. Domain allowlist (Origin)

The widget works only on the domains listed in the agent configuration. Semantics:

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:

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. 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 gotowy')]);
MethodEffect
open / close / toggle / isOpenthe chat window
sendMessage(text)sends a message as the user
setLocale('pl'|'en'|'de'|'fr'|'it')UI language
setContext(obj)page context (v0: stored, not used yet)
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)

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, and with the "require verified identity" option turned on, anonymous visitors cannot write.

The widget does not compute the signature and does not know the secret. Your backend computes the signature, using the agent's HMAC secret (panel → Settings → Widget; until Stage 4 we hand it over to you):

hash = hex( HMAC-SHA256( key = sekret_agenta, 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 znaki hex>' }]);
// po wylogowaniu:
window.flowhelp.push(['identify', null]);

Rules:

consentMode from the agent configuration:

Consent is not persisted between visits (v0). 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 → "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 visitor (default; configurable per agent)429, the widget locks the input for Retry-After seconds and shows a message
a second message before the previous answer has finished429 (Retry-After: 5)
the agent's daily credit limit is used up429 until midnight UTC
agent paused / not in the ready status403, the widget does not appear
the agent requires identity and the page did not call identify()the message is not sent; the message "Sign in on this site to use the chat." is shown
mismatched identify() signature403 before any answer, zero cost
question outside the knowledge basethe answer "I don't have that information in my knowledge base." with no guessing

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 (ADR-027): 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 specific subpages of your site is shown below the answer, preceded by the word "Sources" (ADR-031); each of them also opens in a new tab. One subpage = one link, even if the answer drew on several of its fragments (ADR-032), and at most three links under one answer. Further hits are in the conversation in the panel, but under the answer they would be a wall of pills instead of a footnote. The link between a sentence and its source does not disappear: the same mechanism still computes it, it just does not clutter the text.

9. Appearance, theme, RTL, mobile

10. Compatibility with frameworks and CMSs (verified 2026-09-14)

A style leak test in both directions (docs/qa/etap3-zamkniecie-2026-09-14/): two runs of the same page (without the widget and with it), comparing probes of the host and of the widget elements, z-index, mobile at 390 px, zero console errors.

EnvironmentResult
Bootstrap 5.3 (navbar fixed-top, footer z-1050, aggressive global stylesheet)0 differences in the host probes, widget styles untouched
Tailwind (Play CDN, preflight)as above
WordPress 7.1 + Elementor 4.2.4 + Hello Elementor 3.5.1the widget loads through a snippet in wp_footer, Elementor and host styles unchanged, zero console errors
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 (§3); your page's X-Frame-Options does not matter, because the widget is not an iframe.

11. Embed test

QA page: https://flowhelp.ai/qa/widget-embed-test.html (code: docs/qa/). It checks that the loader loads the core, the window opens from the queue, the answer has a citation, and the host styles stay untouched. The same file served from a different origin does not start the widget.

12. Answer ratings and the contact form (ADR-029)

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 (👍/👎). Clicking the same one a second time undoes the rating. Ratings appear in the panel in Conversations (the "thumbs down" filter) and in Insights; 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_fallbackright after an "I don't have that information" answer, that is, where the assistant did not help
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 above the conversation (ADR-032): it dims what is under it and takes over the keyboard; 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, with a "Back to the chat" button (then tapping the backdrop also closes the card).

Escalation is the same form with an extra "Message" field and its own button in the action row (you set its text in the panel; the button wraps a long label, so you do not need to shorten it). The conversation then gets an "escalated" mark, which feeds the deflection metric in Insights.

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. Preview in the panel (widget-preview.js)

The "Appearance" section of the panel shows the same widget that runs on your site; it is not a separate mockup. The preview also carries the name from the "Agent name" field: change the name and you immediately see the header that the visitor will see (together with the "AI" badge). Step 3 of onboarding shows the same name. The panel loads https://cdn.flowhelp.ai/widget-preview.js, which is a second entry point of the same code, and mounts the widget in the form's frame instead of in the screen corner.

What differs from the widget on your site:

The preview is not part of the embed contract: it is a panel tool, and you should not paste it onto your site, because it does not talk to your agent. If the preview script does not load within 5 s (panel CSP, CDN), the panel shows a static card with the color and the greeting, and the form keeps working.

14. Widget languages and agent texts in other languages (ADR-041)

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, the messages and the assistant's answers, regardless of the language of your knowledge base.

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 contact form and the talk-to-a-human button (section 12) have a single set of texts for now, in the language you typed them in the panel.