HelpStackDocs

Integrations

Widget Embed

Embed the HelpStack web chat widget on any website with a single script tag.

Quick facts#

Loaderhttps://helpstack.eu/widget.js
AuthNone — widget endpoints are public
TransportSocket.IO (primary) + REST fallback
Channel typeWEBSITE_CHAT
Rate limit~30 req/min/IP on config/FAQ/status; ~20 msg/min/visitor on the HTTP message fallback
Origin controlOptional per-channel domain allowlist (403 if not allowed)

The loader derives its baseUrl from its own <script src> origin, so the widget.js served from helpstack.eu automatically talks to HelpStack — there is nothing else to configure.

Embed snippet#

<script src="https://helpstack.eu/widget.js?id=CHANNEL_ID" async></script>

CHANNEL_ID is the id of your WEBSITE_CHAT channel. The loader auto-initializes from the ?id= query parameter — no extra JavaScript is required.

Where to get CHANNEL_ID#

In the dashboard go to Settings → Channels, open (or create) a Website Chat channel, and copy its id using the copy button shown next to the channel. That id is what you pass as ?id=CHANNEL_ID.

How the loader works#

  1. The script reads its own <script src> to determine:
    • baseUrl — the script's origin (e.g. https://helpstack.eu)
    • channelId — the ?id= query parameter
  2. It injects a floating chat bubble and an <iframe> pointing at:
    {baseUrl}/widget/{channelId}?visitorId={visitorId}
    
  3. The iframe opens a Socket.IO connection for real-time messaging, with an HTTP fallback when WebSocket is unavailable.

Visitor identity#

The loader generates a persistent visitor id stored in localStorage:

KeyValue format
chat_widget_visitor_idvisitor_<uuid>

The same visitorId is reused across page loads and sessions on that browser, so a returning visitor reconnects to their existing conversation. Clearing localStorage (or a different browser/device) produces a new visitor and a new conversation thread.

Programmatic API#

The loader exposes a global window.ChatWidget object:

window.ChatWidget.init('CHANNEL_ID'); // initialize manually (if not using ?id=)
window.ChatWidget.open();             // open the chat panel
window.ChatWidget.close();            // close the chat panel
window.ChatWidget.toggle();           // toggle open/closed

A command-queue helper is also available for the deferred form used in the snippet comment:

window.chat('init', 'CHANNEL_ID');

Identify who you are talking to at any point, so the AI does not have to ask:

window.ChatWidget.identify({ name: 'Ana Novak', email: 'ana@example.com' },
                           { plan: 'pro', account_id: '1234' });

Register a client-side agent tool so the AI can call code running on your page:

window.HelpStack('registerTool', 'highlight', async ({ section }) => {
  document.querySelector(`#${section}`)?.scrollIntoView({ behavior: 'smooth' });
  return { highlighted: section };
});

Whatever the handler returns is what the AI reads. Under the hood the widget iframe posts TOOL_EXECUTE to the parent window and waits for a TOOL_RESULT or TOOL_ERROR carrying the same callId; registerTool answers that for you. The widget stays silent for any tool name it has no handler for, so an existing message listener of your own keeps working alongside it. See Custom Agent Tools.

Appearance configuration#

Appearance is configured in the dashboard or over MCP (get_widget_config / update_widget_config, see MCP), not via the embed snippet. The widget fetches it from the server (GET /api/widget/[channelId]/config) and applies it via CSS variables. Verified configurable fields:

FieldNotes
primaryColorHex color. Default #247f7c (the brand teal)
positionbottom-right | bottom-left | top-right | top-left
mobilePositionSame enum as position, applied on mobile viewports
bubbleIconTypecustom or a preset
bubbleIconNameOne of: message, help, smile, headphone, zap, heart
bubbleOffsetXHorizontal offset of the bubble
bubbleOffsetYVertical offset of the bubble
greetingMessageGreeting text shown when the panel opens
avatarUrlAgent/brand avatar
showOnlineStatusWhether to display online/offline status
offlineMessageMessage shown when the channel is offline
productCardsShow product links in replies as cards. Default true. See Product cards

Public widget endpoints#

All endpoints are public (no auth) and IP/visitor rate-limited.

MethodPathPurpose
GET/api/widget/[channelId]/configWidget config (colors, position, greeting, avatar, status). ~30 req/min/IP
GET/api/widget/[channelId]/faqsUp to 5 FAQ chips for the greeting. ~30 req/min/IP
GET/api/widget/[channelId]/statusWhether the channel is currently online
POST/api/widget/[channelId]/uploadmultipart/form-data file upload
POST/api/widget/messageHTTP fallback to send a message when WebSocket is unavailable. ~20 msg/min/visitor

Upload response (POST /api/widget/[channelId]/upload):

{
  "storagePath": "...",
  "publicUrl": "...",
  "fileName": "...",
  "fileSize": 12345,
  "contentType": "image/png",
  "width": 800,
  "height": 600
}

width and height are present for images only.

Message fallback (POST /api/widget/message) — body:

{
  "channelId": "CHANNEL_ID",
  "visitorId": "visitor_<uuid>",
  "content": "Hello, I need help",
  "visitorInfo": {
    "userAgent": "...",
    "currentUrl": "https://example.com/pricing",
    "referrer": "https://google.com"
  }
}

visitorInfo is optional, and so is metadata (the same optional envelope the socket join accepts). The channel must be of type WEBSITE_CHAT or the request is rejected. On success the response envelope's data contains { messageId, conversationId, createdAt }. A visitor's conversation is created on their first message, so the first successful call is what makes conversationId exist; later calls from the same visitor reuse it.

Real-time events (Socket.IO)#

The iframe communicates with the server over Socket.IO.

Widget → server

EventPayload
joinchannelId, visitorId, visitorInfo { userAgent, currentUrl, referrer }
messageOutbound visitor message
typingVisitor typing indicator
heartbeatPresence keep-alive
tool:resultResult of a client-side tool execution
tool:errorError from a client-side tool execution

Server → widget

EventPayload
joinedconversationId, initial messages, widget config, faqs. A conversation is only created when the visitor sends their first message, so for a new visitor conversationId is null and messages is empty; a returning visitor with an open conversation gets its id and history. Do not depend on the id before the first message.
messageNew agent message
agent_typingAgent typing indicator
tool:executeRequest to run a client-side tool in the browser

Product cards#

When a reply links to a product page on your shop, the widget shows the link as a card: picture, name, price, stock, and an Add to cart button. It works like a link preview and needs no setup. AI replies, approved drafts and replies your team types all get cards.

Product cards under a chat reply, and the same card after tapping Add to cart
Product cards under a chat reply, and the same card after tapping Add to cart

Where the data comes from. When the reply is delivered, HelpStack opens the linked page and reads the product data your shop already publishes for search engines: schema.org Product structured data (JSON-LD or microdata), with the Open Graph tags (og:title, og:image, product:price:amount) as a fallback. WooCommerce prints this by default, and so do most Shopify themes. The card never comes from the reply text, so the AI cannot invent a price: a link that does not load, or a page that is not a product (a shipping page, a blog post), stays a plain link.

Connected Shopify stores are read through the Shopify Admin API instead of the page: price, stock and variants come straight from Shopify, and it also works on a development store behind its password.

Which links are opened. Only links on your own sites: the widget's domain allowlist, the page the visitor is on, and a connected Shopify or WooCommerce store. Every request goes through the same private-address protection as custom tools, redirects included. Pages are cached for an hour, so a popular product costs one request an hour.

Limits. At most 3 cards per reply. If your shop does not answer within a few seconds, the reply is sent without cards rather than late.

The buttons

A card has up to two buttons. The first one is:

SituationMain button
Out of stockOut of stock (disabled)
The visitor has to pick a size or colour firstChoose options: opens the product page
Your page can add it to the cartAdd to cart
OtherwiseView product

The second button is one of your own (see below) or View product. After Add to cart succeeds, the button changes to Added · View cart. Links to your shop open in the same tab, as any link on your shop would; the chat stays in the conversation after the page changes.

Shopify and WooCommerce work out of the box. On a Shopify storefront the widget adds the variant through the shop's own /cart/add.js; on WooCommerce it adds through the WooCommerce Store API, which block themes and classic themes both have (with the classic add-to-cart endpoint as a fallback), and tells the mini cart to refresh. A Shopify product with several variants shows Choose options unless the link points at one variant (?variant=123). The built-in cart only adds products of the shop the visitor is on.

Your own buttons

Register a handler with forProducts: true and it appears on every card. It receives the product:

<script>
  window.HelpStack = window.HelpStack || function () {
    (window.HelpStack.q = window.HelpStack.q || []).push(arguments);
  };

  window.HelpStack('registerAction', 'wishlist', async (product) => {
    // product: { url, title, productId, variantId, price, currency, platform, quantity }
    await myWishlist.add(product.productId);
    return { ok: true };
  }, { forProducts: true, label: { en: 'Save for later', sl: 'Shrani za pozneje' } });
</script>
Option
forProductstrue makes this a card button instead of a "Show me" button
labelButton text: a string, or one per language ({ en, sl, de, ... }), picked by the visitor's browser language. At most 40 characters
primarytrue puts your button first, before Add to cart

The name add_to_cart is special: it becomes the card's Add to cart button and replaces the built-in Shopify/WooCommerce one. Use it on any other shop system. Return { ok: true, cartUrl: '/cart' } to make View cart go to your cart page, or { ok: false, message } to show a failure under the card.

Handlers follow the same rules as action buttons: 10 seconds per tap, and a thrown error counts as a failure.

What the AI is told

In the web chat the AI is asked to include each recommended product's link, only links it got from a tool or the knowledge base, and at most 3 per reply. When the visitor taps a card button, the AI reads it on its next turn ("tapped Add to cart on the card for Trail bike 29" and it worked"), so it can follow up. Each tap is logged as a CLIENT_ACTION row in the tool logs, named after the button (add_to_cart, view_product, choose_options, view_cart, or yours).

Turning cards off

Channels → Appearance → Product cards, or update_widget_config with productCards: false. Links then stay plain links.

Domain allowlist#

A channel can restrict which origins are allowed to load the widget. When an allowlist is configured and the requesting origin is not on it, the server responds with 403. Configure the allowlist in the channel settings in the dashboard. Leave it empty to allow any origin.

Minimal example page#

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>HelpStack Widget Demo</title>
  </head>
  <body>
    <h1>My website</h1>
    <button onclick="window.ChatWidget.open()">Need help?</button>

    <!-- HelpStack widget -->
    <script
      src="https://helpstack.eu/widget.js?id=CHANNEL_ID"
      async
    ></script>
  </body>
</html>