helpr docs

Developer Docs

JavaScript SDK, chat widget API, data integration, and deployment guides.

Installation

Add the widget script to your page:

<script>
  window.helpr = window.helpr || { _q: [] };
</script>
<script src="https://helpr.so/w.js" data-team="YOUR_TEAM_ID" async></script>

Commands can be queued before the script loads:

window.helpr._q.push(['identify', { email: '[email protected]', name: 'Jane' }]);
window.helpr._q.push(['track', 'page_viewed', { section: 'pricing' }]);

helpr.identify(traits)

Set the visitor's identity. Merges with existing identity data.

helpr.identify({
  email: '[email protected]',
  name: 'Jane Smith',
  userId: 'usr_123',
  company: {
    name: 'Acme Inc',
    domain: 'acme.com',
    plan: 'enterprise',
    mrr: 4900,
    seats: 25,
    industry: 'SaaS',
    createdAt: '2024-01-15'
  }
});

Reserved fields

FieldTypeDescription
emailstringUsed for contact deduplication across sessions/devices
namestringDisplayed in agent sidebar and chat list
userIdstringAlternative to email for visitor ID resolution
userHashstringHMAC-SHA256 of userId using your signing secret. Required when identity verification is enabled.
companyobjectDisplayed as a dedicated "Company" section in the agent sidebar

All other fields are stored as arbitrary identity attributes and displayed in the agent's "Identity" sidebar section.

Company object

When company is an object, it gets its own sidebar section with structured display:

FieldTypeDisplay
namestringShown as company header with avatar initial
domain / websitestringClickable link below company name
mrr / arr / revenue / ltvnumberFormatted as currency ($X,XXX)
createdAt / signed_upstringFormatted as localized date
any other fieldanyShown as key-value row
Behavior: Merges incrementally — does not overwrite unset fields. Setting email or userId triggers contact resolution (deduplication across sessions). Syncs across tabs via localStorage.
Visibility: Identity data is available on both the Visitors list (filterable, searchable) and the agent chat sidebar. Because it's transmitted via the browser, avoid sending sensitive data here — use the Data API for server-side-only data.

helpr.setCustom(data)

Set custom data attributes for the visitor. Merges with existing custom data.

helpr.setCustom({
  subscription: 'enterprise',
  mrr: 4900,
  trialEnds: '2026-05-01',
  features: ['sso', 'api', 'webhooks']
});

Deleting traits

Set a key to null to remove it. The null value is persisted and the trait will no longer appear in the sidebar.

// Remove a single trait
helpr.setCustom({ trialEnds: null });

// Replace the entire custom object (removes all previous traits)
helpr.custom = { subscription: 'pro' };

Persistence

Custom data persists across sessions and page reloads via localStorage and is synced to the server on every heartbeat. It survives browser restarts. Data is only cleared by calling helpr.reset() or by replacing the entire object with helpr.custom = {}.

Visibility: Custom data is available on both the Visitors list (filterable) and the agent chat sidebar. This data is visible in the visitor's browser source. For sensitive data (orders, billing, PII), use the Data API instead.

helpr.track(event, metadata?)

Track a user event. Events appear in the agent's "Events" timeline in real-time.

helpr.track('plan_upgraded', { from: 'free', to: 'pro', mrr: 49 });
helpr.track('feature_used', { feature: 'export', format: 'csv' });
helpr.track('checkout_started');
helpr.track('error_encountered', { code: 'PAYMENT_FAILED', page: '/checkout' });
ParamTypeRequiredDescription
eventstringYesEvent name (max 100 chars)
metadataobjectNoArbitrary key-value metadata
Events are sent immediately via WebSocket and persisted with millisecond timestamps. Up to 30 most recent events are loaded when an agent opens a chat. New events appear in real-time.

helpr.tag(tag)

Add a tag to the current visitor. Tags are displayed in the agent sidebar and chat list.

helpr.tag('vip');
helpr.tag('trial-expiring');
helpr.tag('enterprise');
ParamTypeRequiredDescription
tagstringYesTag name (max 50 chars, lowercased automatically)
Tags are deduplicated, persist via localStorage, and can be added/removed by agents from the sidebar. Cleared on helpr.reset().

helpr.untag(tag)

Remove a tag from the current visitor.

helpr.untag('trial-expiring');

helpr.custom getter / setter

Direct property access for custom data.

// Set (replaces entire custom object)
helpr.custom = { plan: 'pro', seats: 5 };

// Get
console.log(helpr.custom.plan); // 'pro'

helpr.update()

Force an immediate heartbeat to sync current state with the server.

helpr.update();

helpr.reset()

Clear all identity, custom data, and tags. Generate a new anonymous visitor ID and reconnect.

// Call on logout
helpr.reset();
Clears localStorage, generates a new random visitor ID, and reconnects with fresh state.

helpr.chat.open()

Open the chat panel programmatically. Creates the widget if it hasn't been initialized yet.

helpr.chat.open();
// Open chat when a CTA button is clicked
document.getElementById('contact-btn').addEventListener('click', function() {
    helpr.chat.open();
});

helpr.chat.close()

Close the chat widget completely. The widget is hidden and the visitor's dismiss state is saved.

helpr.chat.close();

helpr.chat.toggle()

Toggle the chat panel open or closed. If the widget is hidden or minimized to a toast, it opens the full panel. If the panel is open, it closes.

helpr.chat.toggle();

helpr.chat.minimize()

Minimize the chat panel to a compact toast notification showing the latest message. The visitor can click the toast to reopen the full panel.

helpr.chat.minimize();

helpr.chat.expand()

Expand the chat panel to the larger size. Use helpr.chat.setLargeSize() to customize the enlarged dimensions.

helpr.chat.expand();

helpr.chat.state getter

Get the current widget state as a string.

console.log(helpr.chat.state); // 'hidden', 'toast', 'panel', or 'enlarged'
console.log(helpr.chat.isOpen); // true or false
PropertyTypeDescription
helpr.chat.statestringCurrent state: hidden, toast, panel, or enlarged
helpr.chat.isOpenbooleantrue when the widget is visible (any state except hidden)

helpr.chat.unreadCount getter

Get the number of unread messages. Use this to render your own badge or notification indicator.

// Show a badge on your nav element
var count = helpr.chat.unreadCount;
if (count > 0) {
    myBadge.textContent = count;
    myBadge.style.display = 'block';
}

// React to changes
helpr.chat.onStateChange(function() {
    updateBadge(helpr.chat.unreadCount);
});

helpr.chat.onStateChange(callback)

Register a callback that fires whenever the widget state changes.

helpr.chat.onStateChange(function(newState, previousState) {
    console.log('Chat changed from', previousState, 'to', newState);

    if (newState === 'panel') {
        analytics.track('chat_opened');
    }
});
ParamTypeDescription
newStatestringThe state the widget just transitioned to
previousStatestringThe state the widget was in before the change
Callbacks are called synchronously in the order they were registered. Exceptions are caught and silently ignored.

helpr.chat.setSize(width, height)

Set custom dimensions for the chat panel at runtime. Accepts numbers (pixels) or CSS unit strings.

// Pixels (number or string)
helpr.chat.setSize(420, 600);

// CSS units
helpr.chat.setSize('100%', '80vh');

// Reset to defaults
helpr.chat.setSize(null, null);
ParamTypeDescription
widthnumber | string | nullPanel width. Numbers are treated as pixels. Strings support px, %, vh, vw, em, rem, vmin, vmax. Pass null to reset.
heightnumber | string | nullPanel height. Same format as width. Pass null to reset.

helpr.chat.setLargeSize(width, height)

Set custom dimensions for the enlarged (expanded) chat panel. Defaults are 500×700. Same unit support as setSize().

// Custom enlarged size
helpr.chat.setLargeSize(600, 800);
helpr.chat.setLargeSize('90vw', '90vh');

// Reset to defaults
helpr.chat.setLargeSize(null, null);
ParamTypeDescription
widthnumber | string | nullEnlarged panel width. Numbers are pixels; strings support CSS units. Pass null to reset.
heightnumber | string | nullEnlarged panel height. Same format as width. Pass null to reset.

helpr.chat.embed(target, options?)

Embed the chat panel directly into a container element on your page instead of using the default floating widget. The panel fills the container and hides minimize/close/resize controls.

// Embed into a div
helpr.chat.embed('#support-chat');

// With custom dimensions
helpr.chat.embed('#support-chat', { width: 420, height: 600 });

// Target a DOM element directly
var el = document.getElementById('chat-container');
helpr.chat.embed(el, { width: 500, height: 700 });
ParamTypeRequiredDescription
targetstring | ElementYesCSS selector or DOM element to embed into
options.widthnumberNoContainer width in pixels (sets inline style on target)
options.heightnumberNoContainer height in pixels (sets inline style on target)

Using the script attribute

For static pages, use the data-target attribute to auto-embed on load:

<div id="support-chat" style="width: 400px; height: 600px;"></div>

<script src="https://helpr.so/w.js"
  data-team="YOUR_TEAM_ID"
  data-target="#support-chat"
  async></script>

Pre-load queue

window.helpr._q.push(['chat.embed', '#support-chat', { width: 420, height: 600 }]);
In embedded mode the chat panel always stays visible — close and minimize actions are disabled. The panel renders without box-shadow and uses a subtle border to blend into your page layout. Style the container element (padding, border-radius, background) to match your design.

helpr.chat.trigger(selector | boolean)

Control the floating chat bubble. Use a custom element as the chat launcher, hide the bubble entirely, or restore it.

// Use your own button — hides the default bubble
helpr.chat.trigger('#my-chat-button');

// Hide the bubble completely (open chat programmatically)
helpr.chat.trigger(false);

// Restore the default bubble
helpr.chat.trigger(true);
ParamTypeDescription
selectorstring | ElementCSS selector or DOM element to use as the chat launcher. Clicking it toggles the chat panel.
falsebooleanHides the default bubble. Chat can only be opened via helpr.chat.open() or declarative triggers.
truebooleanRestores the default bubble.

Unread badge on custom triggers

When using a custom trigger element, Helpr sets a data-helpr-unread attribute with the current unread count. Use CSS to style your own badge:

#my-chat-button { position: relative; }

#my-chat-button[data-helpr-unread="0"]::after { display: none; }

#my-chat-button::after {
  content: attr(data-helpr-unread);
  position: absolute;
  top: -4px;
  right: -4px;
  min-width: 18px;
  height: 18px;
  border-radius: 9px;
  background: #ef4444;
  color: #fff;
  font-size: 11px;
  font-weight: 700;
  display: flex;
  align-items: center;
  justify-content: center;
}

Pre-load queue

window.helpr._q.push(['chat.trigger', '#my-chat-button']);
The chat.trigger() setting overrides the team-level "Chat bubble" setting in the dashboard. This lets admins set a default (shown/hidden) while developers control behavior per-page.

Declarative Triggers

Control the chat widget without JavaScript using HTML attributes or anchor links.

Data attributes

Add a data-helpr-* attribute to any clickable element:

<!-- Open chat -->
<button data-helpr-open>Chat with us</button>

<!-- Close chat -->
<button data-helpr-close>Close chat</button>

<!-- Toggle open/close -->
<button data-helpr-toggle>Toggle chat</button>

Anchor links

Use #helpr-* hash links — works on any element:

<a href="#helpr-open">Chat with us</a>
<a href="#helpr-close">Close</a>
<a href="#helpr-toggle">Toggle chat</a>
Both methods prevent the default click behavior (no page scroll or hash change). They work on any element — buttons, links, divs, images, etc.

Script Configuration Attributes

Customize the widget's appearance by adding data-* attributes to the script tag:

<script src="https://helpr.so/w.js"
  data-team="YOUR_TEAM_ID"
  data-panel-width="420"
  data-panel-height="600"
  data-panel-width-large="600"
  data-panel-height-large="800"
  data-position="bottom-left"
  data-group="billing"
  data-target="#support-chat"
  async></script>
AttributeDefaultDescription
data-panel-width370pxDefault panel width. Supports CSS units: 420, 100%, 80vw
data-panel-height520pxDefault panel height. Same unit support as width.
data-panel-width-large500pxEnlarged panel width. Same unit support.
data-panel-height-large700pxEnlarged panel height. Same unit support.
data-positionbottom-rightWidget position: bottom-right or bottom-left
data-groupRoute chats to a specific agent group. Use the group's public_id from Settings → Groups. See Group Routing.
data-targetCSS selector for inline embed container. When set, the chat panel renders inside the target element instead of as a floating widget.
Script attributes set initial values. Use helpr.chat.setSize() and helpr.chat.setLargeSize() to change dimensions at runtime.

Group Routing

Groups let you organize agents into departments (Sales, Billing, Engineering, etc.) and route incoming chats accordingly. There are two ways to use groups with the widget:

Hardcoded routing with data-group

Add data-group to the script tag to route all chats from that page to a specific group. Only agents in that group appear online, and new chats are assigned to the group.

<!-- Billing page — route to billing team -->
<script src="https://helpr.so/w.js"
  data-team="YOUR_TEAM_ID"
  data-group="billing"
  async></script>

<!-- Sales page — route to sales team -->
<script src="https://helpr.so/w.js"
  data-team="YOUR_TEAM_ID"
  data-group="sales"
  async></script>
The data-group value is the group's public_id, visible in Settings → Integration or Settings → Groups. If the group doesn't exist or has no members, the widget falls back to showing all agents.

Department picker

Instead of hardcoding a group, you can let visitors choose a department before chatting. Enable the Department picker in Settings → Widget → Features.

When enabled, the widget shows a list of your groups when the visitor opens the chat panel. After they pick a department:

  • Only agents from that group appear in the avatar carousel
  • The group name is shown above the welcome message
  • New chats are assigned to the selected group
  • A back arrow in the header lets the visitor change their selection before sending a message
The department picker only appears when no data-group is set on the script tag. If data-group is present, it takes priority and the picker is skipped. The picker requires at least one group with active members.

Online detection

When a group is selected (via data-group or the picker), online status is scoped to that group. The widget shows as offline if no agents in that group are connected — even if other team agents are online.

Sidebar Cards

Sidebar cards let you display rich, structured data alongside a visitor's chat — account info, orders, subscription details, internal notes, quick links, and more. Each card renders as a collapsible section in the agent sidebar that can be reordered and hidden per-agent.

There are two ways to push cards into the sidebar:

  1. helpr.setCards() — Push cards from your frontend JavaScript
  2. Data API — Helpr calls your server endpoint and pulls cards server-to-server

Both methods use the exact same card format and render identically. You can use one or both — cards from both sources appear together in the sidebar.

SDK vs Data API — Which to Use

helpr.setCards()Data API
How it worksYou call setCards() from your frontend — cards are pushed via WebSocketHelpr calls your server endpoint when an agent opens the chat
SetupOne line of JS — no server endpoint neededBuild an HTTPS endpoint, configure in Settings
Data visible to visitor?Yes — data passes through the browserNo — server-to-server only
Real-time updatesInstant — call setCards() again anytimeRefreshed when agent opens chat (60s cache)
Trust levelShown with "unverified" badge unless identity verifiedAlways trusted (server-to-server)
Best forNon-sensitive context: current page state, feature flags, plan info, quick linksSensitive data: orders, billing, PII, internal notes
Use both together: Push quick context from the frontend with setCards() for instant loading, and use the Data API for sensitive data that shouldn't touch the browser. Both appear in the same sidebar seamlessly.

helpr.setCards(cards) — Frontend

Push structured cards to the agent sidebar directly from your JavaScript. No server endpoint required.

helpr.setCards([
  {
    type: 'key-value',
    title: 'Account',
    items: [
      { label: 'Plan', value: 'Enterprise' },
      { label: 'MRR', value: { text: '$4,900/mo', href: 'https://dashboard.stripe.com/customers/cus_123' } },
      { label: 'Status', value: 'Active' },
      { label: 'Seats', value: '12 / 25' }
    ]
  },
  {
    type: 'link',
    title: 'Open in Admin Panel',
    url: 'https://admin.yourapp.com/users/cust_123'
  }
]);

Updating cards

Call setCards() again to replace all cards. Changes push to agents in real-time — no page refresh needed.

// Update cards when context changes (e.g. user starts checkout)
helpr.setCards([
  { type: 'text', title: 'Status', body: 'Checkout in progress — step 3 of 4' },
  { type: 'key-value', title: 'Cart', items: [
    { label: 'Items', value: '3' },
    { label: 'Total', value: '$149.00' }
  ]}
]);

Clearing cards

helpr.setCards([]);

Pre-load queue

Cards can be queued before the script loads:

window.helpr._q.push(['setCards', [
  { type: 'key-value', title: 'Subscription', items: [
    { label: 'Plan', value: currentUser.plan },
    { label: 'Renewal', value: currentUser.renewalDate }
  ]}
]]);

Security

Since setCards runs in the browser, data can be spoofed. Helpr protects against this:

  • All values are sanitized server-side (HTML stripped, URLs restricted to http:// and https:// only)
  • Cards from identity-verified sessions are shown as trusted (no badge)
  • Cards from unverified sessions display an "unverified" badge so agents know the data may not be trustworthy

To remove the badge, enable identity verification and pass a valid userHash.

Data API — Server-side

The Data API lets you serve cards from your backend. When an agent opens a chat, Helpr calls your endpoint server-to-server with a signed request. Your endpoint returns card data — the visitor's browser never sees it.

Setup

  1. Build an HTTPS endpoint that accepts POST requests and returns card JSON
  2. Configure the endpoint URL and signing secret in Settings → Data API
  3. Enable Require identity verification in Settings → Data API, and pass userHash via helpr.identify() to authenticate visitor sessions
  4. Identify visitors with a reference field: helpr.identify({ userId: 'cust_123', userHash: '<HMAC>' })
  5. When an agent opens a chat, Helpr calls your endpoint with a signed request
  6. Your endpoint returns { "cards": [...] } — rendered in the sidebar
Don't skip step 3. Without identity verification, any visitor can spoof a userId in the browser console and pull another user's card data. See Identity Verification for implementation details.

Example endpoint (Node.js / Express)

app.post('/api/helpr-sidebar', async (req, res) => {
  // 1. Verify the request signature (see Data API Reference below)
  if (!verifyHelprRequest(req, process.env.HELPR_SECRET)) {
    return res.status(401).json({ error: 'Invalid signature' });
  }

  // 2. Look up the user
  const user = await db.users.findOne({ id: req.body.ref });
  if (!user) return res.json({ cards: [] });

  // 3. Return cards
  res.json({
    cards: [
      {
        type: 'key-value',
        title: 'Subscription',
        items: [
          { label: 'Plan', value: user.plan },
          { label: 'MRR', value: { text: '$' + user.mrr, href: 'https://stripe.com/customers/' + user.stripeId } },
          { label: 'Status', value: user.status }
        ]
      },
      {
        type: 'table',
        title: 'Recent Orders',
        columns: ['Order', 'Date', 'Total'],
        rows: user.recentOrders.map(o => ({
          href: 'https://admin.shop.com/orders/' + o.id,
          cells: ['#' + o.id, o.date, '$' + o.total]
        }))
      },
      {
        type: 'text',
        title: 'Internal Note',
        body: user.supportNote || 'No notes.'
      }
    ]
  });
});
Cards are cached for 60 seconds per visitor. Agents can force a refresh from the sidebar. Full request/response format and security details are in the Data API Reference section below.

Card Types

Both setCards() and the Data API use the same card schema. Every card has a type and title.

TypeRequired FieldsDescription
key-valuetitle, items[]List of label/value pairs. Values can be strings or { text, href } link objects.
tabletitle, columns[], rows[]Tabular data with sticky headers. Rows: plain arrays or { href, cells } for clickable rows. Cells: strings or { text, href } for per-cell links. Optional footer[] for a sticky bottom row. Use tables[] instead of columns/rows to render multiple tables in a single card.
listtitle, items[]Simple ordered list of strings.
texttitle, bodyFree-form text block (max 1000 chars).
linktitle, urlRenders as a clickable button that opens in a new tab.

How Cards Render

Each card appears as a collapsible section in the agent sidebar with a gray header bar and a toggle chevron. Here's what each type looks like:

key-value

Subscription
PlanEnterprise
StatusActive
Seats12 / 25
{
  type: 'key-value',
  title: 'Subscription',
  items: [
    { label: 'Plan', value: 'Enterprise' },
    { label: 'MRR', value: { text: '$4,900/mo', href: 'https://stripe.com/...' } },
    { label: 'Status', value: 'Active' },
    { label: 'Seats', value: '12 / 25' }
  ]
}

table

Recent Orders
OrderDateTotalStatus
#1234Apr 15$99.00Shipped
#1230Apr 10$49.00Delivered
#1225Apr 3$29.99Delivered
$177.99
{
  type: 'table',
  title: 'Recent Orders',
  columns: ['Order', 'Date', 'Total', 'Status'],
  rows: [
    { href: 'https://admin.shop.com/orders/1234', cells: ['#1234', 'Apr 15', '$99.00', 'Shipped'] },
    ['#1230', 'Apr 10', '$49.00', 'Delivered'],
    ['#1225', 'Apr 3', '$29.99', 'Delivered']
  ],
  footer: ['', '', '$177.99', '']
}
The footer row sticks to the bottom of the card when the table scrolls — useful for totals, summaries, or counts.

Row links vs. cell links

You can make an entire row clickable, or link individual cells:

rows: [
  // Entire row is clickable (highlights on hover, opens href)
  { href: 'https://admin.shop.com/orders/1234', cells: ['#1234', 'Apr 15', '$99.00'] },

  // Individual cell is a link (only that cell is clickable)
  ['#1230', 'Apr 10', { text: '$49.00', href: 'https://stripe.com/payments/pi_xyz' }],

  // Plain row, no links
  ['#1225', 'Apr 3', '$29.99']
]

Cell values can be a plain string or a { text, href } object. When href is set, the cell renders as a clickable link. This works in both rows and footer.

Multiple tables in one card

To group related tables under a single collapsible card, use the tables array instead of top-level columns/rows. Each entry in tables is a full table object with its own title, columns, rows, and optional footer. The card-level title becomes the collapsible header.

Customer Activity
Orders
OrderTotalStatus
#1234$99.00Shipped
#1230$49.00Delivered
Returns
OrderReason
#1220Damaged
{
  type: 'table',
  title: 'Customer Activity',
  tables: [
    {
      title: 'Orders',
      columns: ['Order', 'Total', 'Status'],
      rows: [
        ['#1234', '$99.00', 'Shipped'],
        ['#1230', '$49.00', 'Delivered']
      ]
    },
    {
      title: 'Returns',
      columns: ['Order', 'Reason'],
      rows: [
        ['#1220', 'Damaged']
      ]
    }
  ]
}
Each sub-table supports the same features as a standalone table: footer, clickable rows via href, and per-cell links. The top-level columns/rows fields are ignored when tables is present. Max 5 sub-tables per card.

list

Recent Activity
  1. Upgraded to Enterprise
  2. Changed password
  3. Added payment method
{
  type: 'list',
  title: 'Recent Activity',
  items: ['Upgraded to Enterprise', 'Changed password', 'Added payment method']
}

text

Internal Note

VIP customer. Escalate billing issues to finance team. Has custom SLA with 1-hour response time.

{
  type: 'text',
  title: 'Internal Note',
  body: 'VIP customer. Escalate billing issues to finance team. Has custom SLA with 1-hour response time.'
}

link

View in Admin Panel
{
  type: 'link',
  title: 'View in Admin Panel',
  url: 'https://admin.yourapp.com/users/cust_123'
}
All links open in a new tab with rel="noopener noreferrer" — the target site will not see Helpr as the referrer.

Label Icons

In key-value cards, certain label names automatically display an icon with a tooltip instead of text. Use these exact labels to get built-in icons:

CategoryLabels
IdentityName, Email, Phone, UID, ID, User ID
LocationLocation, Country, Address, Timezone, Language
AccountStatus, Role, Plan, Subscription, 2FA, Seats
BillingMRR, Revenue, Total, Payment, Card, Next billing
CommerceOrder, Orders, Shipping
OrganizationCompany, Organization, Assigned to
TimeCreated, Joined, Since, Expires, Last seen, Local time
DeviceSystem, OS, Browser, Device, Screen, IP
ActivityMessages, Total chats, Active, Tags, Notes
NavigationReferrer, Landing

Labels are case-sensitive. To force a recognized label to show as text instead of an icon, set "icon": false:

{ label: 'Email', value: '[email protected]' }           // shows icon
{ label: 'Email', value: '[email protected]', icon: false } // shows "Email" text
{ label: 'Custom field', value: 'some value' }            // always text

Limits

These limits apply to both setCards() and Data API responses:

LimitValue
Max cards10
Total payload size64 KB (entire cards array, serialized)
Max columns per table5
Max rows per table20
Max sub-tables per card5
Max items per key-value / list20
Title length100 characters
Value / cell length200 characters
Text body length1,000 characters
URLshttp:// and https:// only
Data API cache TTL60 seconds per visitor
Data API timeout5 seconds

Data API — Request Format

When an agent opens a chat, Helpr sends a signed POST request to your configured endpoint.

POST https://yourapp.com/api/helpr-sidebar
Content-Type: application/json
X-Helpr-Signature: sha256=<hmac_hex_digest>
X-Helpr-Timestamp: 1714000000
X-Helpr-Team: <your_widget_public_key>
X-Helpr-Request-Id: <unique_request_id>

Body

{
  "ref": "cust_123",
  "ref_field": "userId",
  "visitor_id": "abc123def456",
  "visitor_name": "Jane Smith",
  "visitor_email": "[email protected]",
  "timestamp": 1714000000,
  "request_id": "a1b2c3d4e5f6..."
}
FieldDescription
refThe value of the configured reference field from identify()
ref_fieldWhich field was used (e.g. userId, email)
visitor_idHelpr's internal visitor ID
visitor_nameName from identify(), if set
visitor_emailEmail from identify(), if set
timestampUnix timestamp of the request
request_idUnique nonce — use to detect replayed requests

Response

Return a JSON object with a cards array:

{ "cards": [ { type: "key-value", ... }, { type: "table", ... } ] }

Signature Verification

Every Data API request is signed with HMAC-SHA256. Always verify the signature before returning data.

Node.js

const crypto = require('crypto');

function verifyHelprRequest(req, secret) {
  const signature = req.headers['x-helpr-signature'];
  const timestamp = parseInt(req.headers['x-helpr-timestamp'], 10);

  // Reject requests older than 5 minutes
  if (Math.abs(Date.now() / 1000 - timestamp) > 300) return false;

  const expected = 'sha256=' + crypto
    .createHmac('sha256', secret)
    .update(timestamp + '.' + req.rawBody)
    .digest('hex');

  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  );
}

PHP

function verifyHelprRequest(string $secret): bool {
    $signature = $_SERVER['HTTP_X_HELPR_SIGNATURE'] ?? '';
    $timestamp = (int) ($_SERVER['HTTP_X_HELPR_TIMESTAMP'] ?? 0);

    if (abs(time() - $timestamp) > 300) return false;

    $body = file_get_contents('php://input');
    $expected = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $body, $secret);

    return hash_equals($expected, $signature);
}

Python

import hmac, hashlib, time

def verify_helpr_request(body: bytes, headers: dict, secret: str) -> bool:
    signature = headers.get('x-helpr-signature', '')
    timestamp = int(headers.get('x-helpr-timestamp', '0'))

    if abs(time.time() - timestamp) > 300:
        return False

    expected = 'sha256=' + hmac.new(
        secret.encode(), f'{timestamp}.'.encode() + body, hashlib.sha256
    ).hexdigest()

    return hmac.compare_digest(expected, signature)

Identity Verification

Identity verification prevents visitors from spoofing another user's identity in the browser console. When enabled:

  • The Data API will not return cards for unverified visitors
  • setCards() data from unverified visitors shows an "unverified" badge

How it works

  1. Enable Require identity verification in Settings → Data API
  2. On your server, compute HMAC-SHA256(userId, signingSecret)
  3. Pass it as userHash in helpr.identify()
  4. Helpr verifies server-side — only verified identities get full access

Generating the hash

Node.js

const userHash = crypto
  .createHmac('sha256', process.env.HELPR_SIGNING_SECRET)
  .update(userId)
  .digest('hex');

PHP

$userHash = hash_hmac('sha256', $userId, $signingSecret);

Python

user_hash = hmac.new(
    signing_secret.encode(), user_id.encode(), hashlib.sha256
).hexdigest()

Frontend

helpr.identify({
  userId: 'cust_123',
  userHash: 'a1b2c3d4...'  // generated server-side, NEVER in client JS
});
Important: The userHash must be generated on your server. Never compute it in client-side JavaScript — that would expose your signing secret.

Single Page App Integration

Helpr automatically tracks page views via heartbeat, but SPA route changes happen without a full page reload. Call helpr.update() after navigation to sync the visitor's current URL immediately.

React / React Router

import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';

function App() {
    const location = useLocation();

    useEffect(() => {
        if (window.helpr) helpr.update();
    }, [location]);

    return <Outlet />;
}

Next.js (App Router)

'use client';
import { usePathname } from 'next/navigation';
import { useEffect } from 'react';

export function HelprSync() {
    const pathname = usePathname();

    useEffect(() => {
        if (window.helpr) helpr.update();
    }, [pathname]);

    return null;
}

Vue Router

// In your router setup
router.afterEach(() => {
    if (window.helpr) helpr.update();
});

Handling Logout

Call helpr.reset() when a user logs out to clear their identity and start a fresh visitor session. On the next login, call helpr.identify() again.

function handleLogout() {
    helpr.reset();
    // ... your logout logic
}

function handleLogin(user) {
    helpr.identify({
        email: user.email,
        name: user.name,
        userHash: user.helprHash
    });
}

Mobile Behavior

The widget automatically adapts to small screens. No additional configuration is needed.

ViewportBehavior
> 480pxStandard floating panel with configurable dimensions
≤ 480px (normal)Full width, nearly full height with a top margin for the status bar
≤ 480px (enlarged)True fullscreen — 100vw × 100vh, no border radius
Custom sizes set via setSize() or data attributes are overridden by the mobile breakpoint to ensure usability on small screens.
Building a mobile app? See the Mobile SDK reference for React Native, Expo, native chat, visual assist, WebView capture, visitor identity, and page metadata.

Security & CSP

If your site uses a Content Security Policy, add the following directives to allow the widget to load and connect:

Content-Security-Policy:
  script-src  https://helpr.so;
  connect-src https://helpr.so wss://ws.helpr.so wss://assist-cdn.helpr.so wss://assist.helpr.so;
  style-src   'unsafe-inline';
  img-src     https://helpr.so data:;
  media-src   https://helpr.so;
DirectiveValueWhy
script-srchttps://helpr.soLoads the widget script and hashed bundle
connect-srchttps://helpr.so wss://ws.helpr.so wss://assist-cdn.helpr.so wss://assist.helpr.soAPI requests (HTTPS), chat WebSocket, and Visual Assist WebSocket
style-src'unsafe-inline'Widget styles injected inside a closed Shadow DOM (isolated from your page)
img-srchttps://helpr.so data:Image attachments shared in chat and local image previews before upload
media-srchttps://helpr.soAudio and video attachments shared in chat

img-src and media-src are only needed if file attachments or voice messages are enabled in your widget settings. If you disable both, you can skip these directives.

Permissions Policy

If your site sets a Permissions-Policy header and you want visitors to send voice messages, make sure microphone access is allowed:

Permissions-Policy: microphone=(self)

If your site doesn't set this header (most don't), microphone access is allowed by default and no changes are needed. If the policy blocks microphone access, the voice message button will not appear in the widget.

Shadow DOM Isolation

The widget renders inside a closed Shadow DOM. Your page styles cannot affect the widget, and widget styles cannot leak into your page. This also means the widget's inline styles are invisible to your CSP — they only exist within the shadow boundary.

Data Privacy

  • Visitor identity data (identify()) is transmitted over HTTPS and stored server-side.
  • A visitor ID is stored in localStorage and a first-party cookie for session continuity.
  • Chat messages are stored in localStorage (last 100 messages) for offline resilience.
  • No third-party cookies, trackers, or analytics are loaded by the widget.

Identity Verification

To prevent visitors from impersonating others, enable identity verification by passing a server-generated userHash with helpr.identify(). See Identity Verification for setup details.

Pre-load Queue

Queue commands before the widget script loads. They execute in order once the script initializes.

window.helpr = window.helpr || { _q: [] };

window.helpr._q.push(['identify', { email: '[email protected]', name: 'Jane' }]);
window.helpr._q.push(['setCustom', { plan: 'pro' }]);
window.helpr._q.push(['setCards', [{ type: 'text', title: 'Status', body: 'Trial day 3 of 14' }]]);
window.helpr._q.push(['track', 'page_loaded', { section: 'dashboard' }]);
window.helpr._q.push(['tag', 'enterprise']);
window.helpr._q.push(['chat.open']);
window.helpr._q.push(['reset']);

Supported commands: identify, setCustom, setCards, track, tag, untag, reset, chat.open, chat.close, chat.toggle, chat.embed

Auto-Captured Data

The widget automatically captures and sends the following without any code:

FieldDescription
urlCurrent page URL (sensitive params stripped)
titleDocument title
referrerOriginal referrer URL
ip, city, region, countryGeo-location (resolved server-side from IP)
os, osVerOperating system and version
browser, brVerBrowser name and version
deviceTypedesktop, mobile, or tablet
screenScreen resolution (WxH)
languageBrowser language
timezoneIANA timezone string
utmUTM parameters (source, medium, campaign, term, content)
landingFirst page URL of the session
pageHistoryLast 50 pages visited with timestamps

Data Persistence

Data TypeStorageTTL
Identityvisitor_identity.identityPermanent
Custom Datavisitor_identity.customPermanent — set a key to null via setCustom() to delete
Tagsvisitor_tags tablePermanent
SDK Cardsvisitor_identity.sdk_cardsPermanent (replaced on each setCards call)
Eventsvisitor_events tablePermanent
Contactvisitor_contacts tablePermanent (deduplicated by email)
Page HistoryIn-memory (WS server)Session-scoped
Device / GeoIn-memory (WS server)Session-scoped

Full Integration Example

<script>
  window.helpr = window.helpr || { _q: [] };

  // Identify user after login
  window.helpr._q.push(['identify', {
    email: currentUser.email,
    name: currentUser.fullName,
    userId: currentUser.id,
    company: {
      name: currentUser.company.name,
      domain: currentUser.company.domain,
      plan: currentUser.company.plan,
      mrr: currentUser.company.mrr,
      seats: currentUser.company.seatCount
    }
  }]);

  // Set contextual data
  window.helpr._q.push(['setCustom', {
    region: currentUser.region,
    role: currentUser.role
  }]);

  // Tag based on account attributes
  if (currentUser.company.mrr > 1000) {
    window.helpr._q.push(['tag', 'high-value']);
  }

  // Pass structured cards to the agent sidebar
  window.helpr._q.push(['setCards', [
    { type: 'key-value', title: 'Subscription', items: [
      { label: 'Plan', value: currentUser.plan },
      { label: 'Seats', value: String(currentUser.seats) + ' / ' + String(currentUser.seatLimit) }
    ]},
    { type: 'link', title: 'Open in Admin', url: 'https://admin.yourapp.com/users/' + currentUser.id }
  ]]);
</script>
<script src="https://helpr.so/w.js" data-team="team_abc123" async></script>

<script>
  // Track events throughout the app
  document.getElementById('upgrade-btn').addEventListener('click', function() {
    helpr.track('upgrade_clicked', { currentPlan: 'free', page: location.pathname });
  });

  // Track after async operations
  async function exportData(format) {
    await api.export(format);
    helpr.track('data_exported', { format: format, rows: data.length });
  }

  // On logout
  function logout() {
    helpr.reset();
    window.location = '/login';
  }
</script>