Developer docs

Put DeskLeap support inside your website and your apps

The chat widget, the identity and attribute signing, the event stream for your own badge, and the hosted page for desktop and mobile apps. The REST API for tickets, contacts and the knowledge base is documented separately at docs.deskleap.io.

Install the widget

Paste this before </body>. Your site ID is in the dashboard under Settings → Widget, where you can also restrict which domains may load it.

<script
  src="https://widget.deskleap.io/widget.iife.js"
  data-site-id="YOUR_SITE_ID"
  async
></script>

Using React or Next.js? There is a React component. On WordPress, use the plugin.

JavaScript API

The script exposes window.DeskLeap. With a script tag it starts by itself; to start it from code instead, leave the data-site-id tag out, load the script, and call DeskLeap.init() once.

DeskLeap.init({
  siteId: 'YOUR_SITE_ID',
  locale: 'ur-Latn',
  visitor: {
    name: user.name,
    email: user.email,
    userId: user.id,
    userHash: signed.userHash,             // from your server
    attributes: signed.attributes,         // from your server
    attributesHash: signed.attributesHash, // from your server
  },
});
DeskLeap.identify({ name, email, userId, userHash });   // after sign-in
DeskLeap.logout();                                    // on sign-out
DeskLeap.open();  DeskLeap.close();  DeskLeap.toggle();
DeskLeap.setLocale('ur');                             // switch language at runtime
DeskLeap.setContext({ current_screen: 'invoices' });  // app state, see Custom attributes
DeskLeap.getUnreadCount();                            // → number
DeskLeap.hideLauncher(); DeskLeap.showLauncher();     // e.g. on checkout, or behind your own Help button
const stop = DeskLeap.on('unread', ({ count }) => setBadge(count));
stop(); // or DeskLeap.off('unread', handler)

Identify signed-in users

Pass the signed-in user and your team sees their name and email on the conversation. Once the identity is verified (below), their chat history also follows them across browsers and devices. Turn on identity verification (Settings → Widget → Identity Verification) and DeskLeap only accepts an identity that carries a valid userHash: hex HMAC-SHA256 of the user's email, keyed with your identity secret. Compute it on your server.

// On YOUR server. Never ship the secret to a browser or an app.
const crypto = require('crypto');
const secret = process.env.DESKLEAP_IDENTITY_SECRET;

const userHash = crypto.createHmac('sha256', secret)
  .update(user.email)
  .digest('hex');
import hmac, hashlib, os
secret = os.environ["DESKLEAP_IDENTITY_SECRET"].encode()

user_hash = hmac.new(secret, user.email.encode(), hashlib.sha256).hexdigest()

With verification on, an identity without a valid hash is ignored, so nobody can open a chat as one of your customers by typing their email.

Users without an email (for example accounts that sign in with a phone number): sign the userId instead, userHash = HMAC-SHA256(secret, userId), and pass userId with it. A userId-signed identity is verified by its userId; an email sent beside it is not treated as verified. Cross-device history only ever follows a verified identity.

Custom attributes

Send facts about the user that your team needs on every conversation: tenant or shop ID, plan, which app they are in, its version, a device ID. They appear on the conversation in the dashboard, carry over to the ticket if the chat becomes one, and agents can search for them (search tenant_id:shop_8812, or just the value).

Attributes are set by your server and signed: attributesHash is hex HMAC-SHA256, with the same identity secret, of the attributes as JSON with the keys sorted and no whitespace. With identity verification on, attributes without a valid hash are dropped. Without it they are accepted and shown as not verified.

// Keys sorted, each "key":value as JSON, comma-separated, no whitespace.
// Built by hand: JSON.stringify always puts number-like keys ("9", "10") first,
// which would not match other languages.
function canonical(attrs) {
  return '{' + Object.keys(attrs).sort()
    .map((k) => JSON.stringify(k) + ':' + JSON.stringify(attrs[k]))
    .join(',') + '}';
}

const attributes = {
  user_id: user.id,          // binds the signature to this user
  tenant_id: 'shop_8812',
  plan: 'business',
  app: 'desktop',
  app_version: '1.1.5',
};
const attributesHash = crypto.createHmac('sha256', secret)
  .update(canonical(attributes))
  .digest('hex');
import json
attributes = {"user_id": user.id, "tenant_id": "shop_8812", "plan": "business", "app": "desktop", "app_version": "1.1.5"}
canonical = json.dumps(attributes, sort_keys=True, separators=(",", ":"), ensure_ascii=False)
attributes_hash = hmac.new(secret, canonical.encode("utf-8"), hashlib.sha256).hexdigest()

Pass them in visitor on init(), or later with DeskLeap.identify({ ..., attributes, attributesHash }). Limits: up to 25 keys; keys are letters, digits, _ . - and up to 40 characters; values are strings (up to 200 characters), whole numbers or true/false. Send versions, IDs and decimals as strings: languages format numbers like 1.0 differently, which changes the hash. Include the user's own ID (as above) so a signed set cannot be reused for someone else.

Context is for values that change on the device, like the screen the user is on: DeskLeap.setContext({ current_screen: 'invoices' }). It is never signed and is shown to your team as reported by the app. Setting a key to null removes it.

Events and unread count

Events: ready, open, close, unread { count }, message (a reply from your team arrived), handoff { agentsAvailable }, ticket (the visitor left a message for follow-up), error { reason } (the widget could not start, e.g. an unknown site ID or a domain that is not allowed) and attributes { accepted, verified, reason?, canonical? } (what happened to the attributes you sent — on a rejection, canonical is the exact string your hash must cover). Payloads never include message text. Replies that arrived while the page or app was closed are counted as unread the next time it opens.

// 1. In the page
DeskLeap.on('unread', ({ count }) => updateBadge(count));
DeskLeap.on('handoff', ({ agentsAvailable }) => { /* e.g. show your own call-back option */ });

// 2. Without a reference to DeskLeap
window.addEventListener('deskleap:unread', (e) => updateBadge(e.detail.count));

// 3. From a native host: a JSON string
//    {"source":"deskleap","event":"unread","data":{"count":2}}
//    React Native → WebView onMessage; an iframe parent → window "message".

Desktop and mobile apps

An app that cannot run a script tag on your own domain can open the hosted chat page. It is the same widget, full screen: always open, no launcher, no greeting bubble.

https://widget.deskleap.io/chat.html?siteId=YOUR_SITE_ID&locale=ur

Put only the site ID, locale and color (hex without #) in the URL. Pass identity and attributes in window.DeskLeapSettings, injected before the page runs, or afterwards as a message: { type: 'deskleap:identify', visitor }. Other messages: deskleap:context, deskleap:locale, deskleap:open, deskleap:logout. The hosted page works on sites with a domain restriction, and it cannot be framed by other websites.

React Native

import { WebView } from 'react-native-webview';
import { Linking } from 'react-native';

// identity comes from YOUR server, signed (see Identify signed-in users)
const settings = { locale: 'ur-Latn', visitor: signedVisitor };

<WebView
  source={{ uri: 'https://widget.deskleap.io/chat.html?siteId=YOUR_SITE_ID' }}
  injectedJavaScriptBeforeContentLoaded={
    `window.DeskLeapSettings = ${JSON.stringify(settings)}; true;`
  }
  onMessage={(e) => {
    const msg = JSON.parse(e.nativeEvent.data);
    if (msg.source === 'deskleap' && msg.event === 'unread') setBadge(msg.data.count);
  }}
  // links (WhatsApp, articles' external links) open outside the app
  setSupportMultipleWindows={false}
  onShouldStartLoadWithRequest={(req) => {
    if (req.url.startsWith('https://widget.deskleap.io/chat.html')) return true;
    Linking.openURL(req.url);
    return false;
  }}
/>

// Later, e.g. on a screen change:
webviewRef.current.postMessage(JSON.stringify({
  type: 'deskleap:context', context: { current_screen: 'invoices' },
}));

Electron

// main.js
const { BrowserWindow, shell, ipcMain } = require('electron');
const path = require('path');

function openSupport(settings /* signed on your server */) {
  const win = new BrowserWindow({
    width: 420, height: 680,
    webPreferences: {
      preload: path.join(__dirname, 'support-preload.js'),
      contextIsolation: true,
      additionalArguments: ['--deskleap=' + JSON.stringify(settings)],
    },
  });
  win.loadURL('https://widget.deskleap.io/chat.html?siteId=YOUR_SITE_ID');
  win.webContents.setWindowOpenHandler(({ url }) => {
    shell.openExternal(url);
    return { action: 'deny' };
  });
}

ipcMain.on('deskleap-unread', (_e, count) => app.setBadgeCount(count));
// support-preload.js
const { contextBridge, ipcRenderer } = require('electron');

const arg = process.argv.find((a) => a.startsWith('--deskleap='));
contextBridge.exposeInMainWorld('DeskLeapSettings', arg ? JSON.parse(arg.slice(11)) : {});

// In the hosted page the widget also posts its events to its own window,
// which a preload can hear under context isolation.
window.addEventListener('message', (e) => {
  if (typeof e.data !== 'string') return;
  let msg; try { msg = JSON.parse(e.data); } catch { return; }
  if (msg.source === 'deskleap' && msg.event === 'unread') {
    ipcRenderer.send('deskleap-unread', msg.data.count);
  }
});

When your team replies after the user has closed the app, the reply is also sent to the email address on the conversation, with a link to continue. Replying to that email does not reach the chat; the link does.

Languages and RTL

The widget is translated into English (en), Spanish (es), French (fr), German (de), Portuguese (pt), Arabic (ar), Urdu (ur) and Roman Urdu (ur-Latn). Arabic and Urdu are laid out right to left. By default it follows the browser language; browsers never report Roman Urdu, so set it with data-locale, locale on init(), or DeskLeap.setLocale().

Give each language its own greeting with data-greeting-<lang> (or greetings on init()). The greeting set in the dashboard is used for languages you don't list.

<script
  src="https://widget.deskleap.io/widget.iife.js"
  data-site-id="YOUR_SITE_ID"
  data-locale="ur-Latn"
  data-greeting="Hi! How can we help?"
  data-greeting-ur="السلام علیکم! ہم آپ کی کیا مدد کر سکتے ہیں؟"
  data-greeting-ur-latn="Assalam o Alaikum! Hum aap ki kya madad kar sakte hain?"
  async
></script>

You can also pick the widget language in the dashboard (Sites → your site → widget behaviour) without touching your embed code. A data-locale on the embed wins over the dashboard setting.

When nobody is online

When your AI Agent hands a chat to the team and nobody is online to take it, the widget asks for an email address straight away instead of keeping the visitor waiting, and the conversation becomes a ticket you can answer by email. Without a hand-off, a chat started while nobody is online waits up to a minute in case someone picks it up, then asks for the email; the visitor can skip the wait at any time. Outside business hours the visitor gets the message form before a chat starts, with your offline message.

If you add a WhatsApp number in Settings → Widget, that form also shows a button that opens a chat with your WhatsApp number. The conversation there happens in WhatsApp, not in DeskLeap. Your app can also listen for the handoff event and offer its own option.

Product details in the WhatsApp message

Turn on “Include the page the visitor is on” for your site (or data-whatsapp-include-page="true") and the WhatsApp message carries the page title and link. Report what is on screen and it carries that too:

DeskLeap.setContext({ product: 'Maroon Lawn Suit', color: 'Maroon', size: 'M' });

Recognised keys: product, color/colour, size, variant, sku, price. On phones, if your site has a sticky bottom bar, lift the chat button with data-mobile-offset or hide it and open the chat from your own button.

WhatsApp leads and orders

WhatsApp conversations stay in WhatsApp, but every click that starts one is counted: the widget’s WhatsApp button always, and your own WhatsApp links (a sticky bar, a product-page button) when you turn on “Count clicks on WhatsApp links on my site” in Sites → Widget behaviour. Each click records the page, the product you reported with setContext(), the device, and whether the visitor chatted first. See it under Reports → Leads & Orders.

To see which chats lead to orders, report the order on your confirmation page. value is in minor units (paisa, cents):

DeskLeap.trackConversion({ orderId: 'SP-1042', value: 590000, currency: 'PKR' });

Or send it from your server with an API key that has the conversions.write scope: POST https://api.deskleap.io/api/v1/conversions with { orderId, value, currency, email | visitorExternalId }. Server-sent orders are marked verified; browser-sent ones are not. An order is credited to the visitor’s latest chat, and latest WhatsApp click, within the 7 days before it. The Orders after chat panel under Reports → Leads & Orders shows the result.

Data attributes reference

data-site-idRequired. Your site ID (Settings → Widget).
data-localeen, es, fr, de, pt, ar, ur, ur-Latn. Default: the browser language.
data-greetingGreeting shown when a chat opens.
data-greeting-<lang>Greeting for one language, e.g. data-greeting-ur, data-greeting-ur-latn.
data-greeting-bubbleText of the small bubble beside the closed launcher (and data-greeting-bubble-<lang>).
data-show-greeting="false"Never show that bubble.
data-greeting-delayMilliseconds before the bubble appears (default 4000).
data-colorLauncher and header colour, e.g. #4f46e5.
data-positionbottom-right (default) or bottom-left.
data-mode="fullscreen"The chat fills the page, always open, no launcher. Used by the hosted page.
data-mode="helpcenter"Show your knowledge base inside the page instead of the chat: search, collections and articles, with links like ?article=<slug>. Add it as a second tag; your chat embed keeps working, and a “Chat with us” button opens it.
data-targetWith data-mode="helpcenter": CSS selector of the element to render into, e.g. #help. Default: right after the script tag.
data-mobile-offsetPhones: lift the chat button by this many px (0–200), above a sticky Buy Now / WhatsApp bar.
data-mobile-hide-launcher="true"Phones: no floating button; open the chat from your own button with DeskLeap.open().
data-hide-on-keyboard="false"Keep the button visible while a phone keyboard is open (it steps aside by default).
data-whatsapp-include-page="true"Add the page title and link, plus product / colour / size reported with setContext(), to the WhatsApp message.
data-track-whatsapp-links="true"Also count clicks on your own WhatsApp links (wa.me), not only the widget’s button.
data-exit-intent="false"Turn off the “before you go” offer on desktop.
data-exit-intent-excludeComma-separated paths where that offer never shows, e.g. /checkout,/cart.
data-visitor-name / -email / -user-id / -hashIdentify a signed-in user from server-rendered HTML.

REST API

Tickets, contacts, the knowledge base and webhooks are available over a REST API with API keys created in Settings → API Keys. The full reference is at docs.deskleap.io.

The platform is 100% free with unlimited human agents; an optional AI Agent add-on is $29/mo each. Questions? Create a free account and ask us in the chat.