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
| Field | Type | Description |
|---|---|---|
email | string | Used for contact deduplication across sessions/devices |
name | string | Displayed in agent sidebar and chat list |
userId | string | Alternative to email for visitor ID resolution |
userHash | string | HMAC-SHA256 of userId using your signing secret. Required when identity verification is enabled. |
company | object | Displayed 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:
| Field | Type | Display |
|---|---|---|
name | string | Shown as company header with avatar initial |
domain / website | string | Clickable link below company name |
mrr / arr / revenue / ltv | number | Formatted as currency ($X,XXX) |
createdAt / signed_up | string | Formatted as localized date |
| any other field | any | Shown as key-value row |
email or userId triggers contact resolution (deduplication across sessions). Syncs across tabs via localStorage.
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 = {}.
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' });
| Param | Type | Required | Description |
|---|---|---|---|
event | string | Yes | Event name (max 100 chars) |
metadata | object | No | Arbitrary key-value metadata |
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');
| Param | Type | Required | Description |
|---|---|---|---|
tag | string | Yes | Tag name (max 50 chars, lowercased automatically) |
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();
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
| Property | Type | Description |
|---|---|---|
helpr.chat.state | string | Current state: hidden, toast, panel, or enlarged |
helpr.chat.isOpen | boolean | true 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');
}
});
| Param | Type | Description |
|---|---|---|
newState | string | The state the widget just transitioned to |
previousState | string | The state the widget was in before the change |
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);
| Param | Type | Description |
|---|---|---|
width | number | string | null | Panel width. Numbers are treated as pixels. Strings support px, %, vh, vw, em, rem, vmin, vmax. Pass null to reset. |
height | number | string | null | Panel 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);
| Param | Type | Description |
|---|---|---|
width | number | string | null | Enlarged panel width. Numbers are pixels; strings support CSS units. Pass null to reset. |
height | number | string | null | Enlarged 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 });
| Param | Type | Required | Description |
|---|---|---|---|
target | string | Element | Yes | CSS selector or DOM element to embed into |
options.width | number | No | Container width in pixels (sets inline style on target) |
options.height | number | No | Container 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 }]);
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);
| Param | Type | Description |
|---|---|---|
selector | string | Element | CSS selector or DOM element to use as the chat launcher. Clicking it toggles the chat panel. |
false | boolean | Hides the default bubble. Chat can only be opened via helpr.chat.open() or declarative triggers. |
true | boolean | Restores 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']);
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>
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>
| Attribute | Default | Description |
|---|---|---|
data-panel-width | 370px | Default panel width. Supports CSS units: 420, 100%, 80vw |
data-panel-height | 520px | Default panel height. Same unit support as width. |
data-panel-width-large | 500px | Enlarged panel width. Same unit support. |
data-panel-height-large | 700px | Enlarged panel height. Same unit support. |
data-position | bottom-right | Widget position: bottom-right or bottom-left |
data-group | — | Route chats to a specific agent group. Use the group's public_id from Settings → Groups. See Group Routing. |
data-target | — | CSS selector for inline embed container. When set, the chat panel renders inside the target element instead of as a floating widget. |
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>
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
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:
helpr.setCards()— Push cards from your frontend JavaScript- 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 works | You call setCards() from your frontend — cards are pushed via WebSocket | Helpr calls your server endpoint when an agent opens the chat |
| Setup | One line of JS — no server endpoint needed | Build an HTTPS endpoint, configure in Settings |
| Data visible to visitor? | Yes — data passes through the browser | No — server-to-server only |
| Real-time updates | Instant — call setCards() again anytime | Refreshed when agent opens chat (60s cache) |
| Trust level | Shown with "unverified" badge unless identity verified | Always trusted (server-to-server) |
| Best for | Non-sensitive context: current page state, feature flags, plan info, quick links | Sensitive data: orders, billing, PII, internal notes |
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://andhttps://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
- Build an HTTPS endpoint that accepts POST requests and returns card JSON
- Configure the endpoint URL and signing secret in Settings → Data API
- Enable Require identity verification in Settings → Data API, and pass
userHashviahelpr.identify()to authenticate visitor sessions - Identify visitors with a reference field:
helpr.identify({ userId: 'cust_123', userHash: '<HMAC>' }) - When an agent opens a chat, Helpr calls your endpoint with a signed request
- Your endpoint returns
{ "cards": [...] }— rendered in the sidebar
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.'
}
]
});
});
Card Types
Both setCards() and the Data API use the same card schema. Every card has a type and title.
| Type | Required Fields | Description |
|---|---|---|
key-value | title, items[] | List of label/value pairs. Values can be strings or { text, href } link objects. |
table | title, 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. |
list | title, items[] | Simple ordered list of strings. |
text | title, body | Free-form text block (max 1000 chars). |
link | title, url | Renders 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
{
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
| Order | Date | Total | Status |
|---|---|---|---|
| #1234 | Apr 15 | $99.00 | Shipped |
| #1230 | Apr 10 | $49.00 | Delivered |
| #1225 | Apr 3 | $29.99 | Delivered |
| $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', '']
}
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.
| Order | Total | Status |
|---|---|---|
| #1234 | $99.00 | Shipped |
| #1230 | $49.00 | Delivered |
| Order | Reason |
|---|---|
| #1220 | Damaged |
{
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']
]
}
]
}
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
- Upgraded to Enterprise
- Changed password
- Added payment method
{
type: 'list',
title: 'Recent Activity',
items: ['Upgraded to Enterprise', 'Changed password', 'Added payment method']
}
text
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
{
type: 'link',
title: 'View in Admin Panel',
url: 'https://admin.yourapp.com/users/cust_123'
}
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:
| Category | Labels |
|---|---|
| Identity | Name, Email, Phone, UID, ID, User ID |
| Location | Location, Country, Address, Timezone, Language |
| Account | Status, Role, Plan, Subscription, 2FA, Seats |
| Billing | MRR, Revenue, Total, Payment, Card, Next billing |
| Commerce | Order, Orders, Shipping |
| Organization | Company, Organization, Assigned to |
| Time | Created, Joined, Since, Expires, Last seen, Local time |
| Device | System, OS, Browser, Device, Screen, IP |
| Activity | Messages, Total chats, Active, Tags, Notes |
| Navigation | Referrer, 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:
| Limit | Value |
|---|---|
| Max cards | 10 |
| Total payload size | 64 KB (entire cards array, serialized) |
| Max columns per table | 5 |
| Max rows per table | 20 |
| Max sub-tables per card | 5 |
| Max items per key-value / list | 20 |
| Title length | 100 characters |
| Value / cell length | 200 characters |
| Text body length | 1,000 characters |
| URLs | http:// and https:// only |
| Data API cache TTL | 60 seconds per visitor |
| Data API timeout | 5 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..."
}
| Field | Description |
|---|---|
ref | The value of the configured reference field from identify() |
ref_field | Which field was used (e.g. userId, email) |
visitor_id | Helpr's internal visitor ID |
visitor_name | Name from identify(), if set |
visitor_email | Email from identify(), if set |
timestamp | Unix timestamp of the request |
request_id | Unique 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
- Enable Require identity verification in Settings → Data API
- On your server, compute
HMAC-SHA256(userId, signingSecret) - Pass it as
userHashinhelpr.identify() - 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
});
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.
| Viewport | Behavior |
|---|---|
| > 480px | Standard 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 |
setSize() or data attributes are overridden by the mobile breakpoint to ensure usability on small screens.
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;
| Directive | Value | Why |
|---|---|---|
script-src | https://helpr.so | Loads the widget script and hashed bundle |
connect-src | https://helpr.so wss://ws.helpr.so wss://assist-cdn.helpr.so wss://assist.helpr.so | API 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-src | https://helpr.so data: | Image attachments shared in chat and local image previews before upload |
media-src | https://helpr.so | Audio 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
localStorageand 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:
| Field | Description |
|---|---|
url | Current page URL (sensitive params stripped) |
title | Document title |
referrer | Original referrer URL |
ip, city, region, country | Geo-location (resolved server-side from IP) |
os, osVer | Operating system and version |
browser, brVer | Browser name and version |
deviceType | desktop, mobile, or tablet |
screen | Screen resolution (WxH) |
language | Browser language |
timezone | IANA timezone string |
utm | UTM parameters (source, medium, campaign, term, content) |
landing | First page URL of the session |
pageHistory | Last 50 pages visited with timestamps |
Data Persistence
| Data Type | Storage | TTL |
|---|---|---|
| Identity | visitor_identity.identity | Permanent |
| Custom Data | visitor_identity.custom | Permanent — set a key to null via setCustom() to delete |
| Tags | visitor_tags table | Permanent |
| SDK Cards | visitor_identity.sdk_cards | Permanent (replaced on each setCards call) |
| Events | visitor_events table | Permanent |
| Contact | visitor_contacts table | Permanent (deduplicated by email) |
| Page History | In-memory (WS server) | Session-scoped |
| Device / Geo | In-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>