Add a full client ticket dashboard to any website with a single script tag.
Developer documentation · Plain HTML, Vue, React and site builders
The Support Portal lets your clients see and raise support tickets from your own website, without logging in to KanBird. You add one script to the site and KanBird provides the rest — the dashboard, the ticket form and the reply thread. Every ticket lands straight in your KanBird workspace.
Total, in-progress, solved and pending counts, the tickets being worked on right now, and the last 10 tickets.
Every ticket with its Ticket ID (like TIC000123). Search by Ticket ID, filter by date, product and status, and export to Excel.
Name, email, phone (optional), subject, product and message — with required fields clearly marked.
A status tracker (Received → In Progress → Resolved) and a reply thread between the client and your support team.
The script adds a hidden full-screen overlay to your page. When you call KanBirdSupportWidget.open(), the overlay shows a KanBird-hosted window inside an iframe. It lives in a Shadow DOM, so your site's CSS can't break the widget and the widget's CSS can't leak onto your page.
The script deliberately does not add its own launcher button. You choose where it opens from — a menu link, a button, a footer link — and call open() from there.
Two values connect a website to your KanBird account. Both are created in KanBird under Settings → Support Portal.
data-tenant value. Regenerating it later breaks every embed already live until its data-tenant is updated.data-client value.<script> tag with both values filled in. Send that snippet — and only that — to the website developer.| Value | Script attribute | Looks like | Scope |
|---|---|---|---|
| Support Portal key | data-tenant | YOUR_TENANT_KEY | One per KanBird account |
| Client code | data-client | cc_school_x7k2pq | One per organization |
In every example on this page, replace YOUR_TENANT_KEY and YOUR_CLIENT_CODE with your own values.
Paste this just before the closing </body> tag of your page:
<script src="https://app.kanbird.com/support-widget-embed.js"
data-tenant="YOUR_TENANT_KEY"
data-client="YOUR_CLIENT_CODE"></script>
Then open the widget from anything you like:
<a href="#" onclick="KanBirdSupportWidget.open(); return false;">Support</a>
That's the whole integration. The guides below show the same thing for common setups, including passing the logged-in visitor's name and email.
Pick the option that matches your website.
Works on any static or server-rendered page. A complete example:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>My website</title>
</head>
<body>
<nav>
<a href="#" id="support-link">Support</a>
</nav>
<!-- 1. Load the widget -->
<script src="https://app.kanbird.com/support-widget-embed.js"
data-tenant="YOUR_TENANT_KEY"
data-client="YOUR_CLIENT_CODE"></script>
<!-- 2. Open it from your own link -->
<script>
document.getElementById('support-link').addEventListener('click', function (e) {
e.preventDefault();
KanBirdSupportWidget.open();
});
</script>
</body>
</html>
If your site already knows who is signed in, pass their name and email so the New ticket form doesn't ask them to type it again:
var currentUser = { name: 'Karim Ahmed', email: 'karim@example.com' };
document.getElementById('support-link').addEventListener('click', function (e) {
e.preventDefault();
KanBirdSupportWidget.open({ name: currentUser.name, email: currentUser.email });
});
Load it as a normal script. Don't use type="module" on the embed tag — the widget reads its own data-tenant and data-client attributes, which browsers don't expose to module scripts.
In a single-page app the script must be added once, and only in the browser. This small composable loads it on demand and gives you an open() function to call from any component.
1. Add the composable
import { onMounted } from 'vue';
const WIDGET_URL = 'https://app.kanbird.com/support-widget-embed.js';
let loading = null;
// Adds the embed script once, no matter how many components ask for it.
export function loadSupportWidget({ tenant, client }) {
if (window.KanBirdSupportWidget) return Promise.resolve(window.KanBirdSupportWidget);
if (loading) return loading;
loading = new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = WIDGET_URL;
script.setAttribute('data-tenant', tenant);
script.setAttribute('data-client', client);
script.onload = () => {
if (window.KanBirdSupportWidget) resolve(window.KanBirdSupportWidget);
else { loading = null; reject(new Error('KanBird support widget did not initialise')); }
};
script.onerror = () => {
loading = null;
reject(new Error('Could not load the KanBird support widget'));
};
document.body.appendChild(script);
});
return loading;
}
export function useSupportWidget({ tenant, client }) {
// Preload after mount so the first click opens instantly.
onMounted(() => loadSupportWidget({ tenant, client }).catch(console.error));
async function open(user) {
const widget = await loadSupportWidget({ tenant, client });
widget.open(user);
}
return { open };
}
2. Use it in any component
<script setup>
import { useSupportWidget } from '@/composables/useSupportWidget';
// e.g. the user from your auth store — leave it out for anonymous visitors
const props = defineProps({ user: { type: Object, default: null } });
const { open } = useSupportWidget({
tenant: 'YOUR_TENANT_KEY',
client: 'YOUR_CLIENT_CODE',
});
</script>
<template>
<button type="button" @click="open({ name: props.user?.name, email: props.user?.email })">
Support
</button>
</template>
Nuxt: the composable only touches window and document inside onMounted and click handlers, so it is safe with server-side rendering.
Reuse loadSupportWidget from the file above and call it from a method:
<template>
<button type="button" @click="openSupport">Support</button>
</template>
<script>
import { loadSupportWidget } from '@/composables/useSupportWidget';
export default {
props: { user: { type: Object, default: null } },
methods: {
async openSupport() {
const widget = await loadSupportWidget({
tenant: 'YOUR_TENANT_KEY',
client: 'YOUR_CLIENT_CODE',
});
widget.open({
name: this.user && this.user.name,
email: this.user && this.user.email,
});
},
},
};
</script>
Same idea as Vue: load the script once in the browser, then call open() from a click handler.
1. Add the hook
import { useCallback, useEffect } from 'react';
const WIDGET_URL = 'https://app.kanbird.com/support-widget-embed.js';
let loading = null;
// Adds the embed script once — safe under React StrictMode's double effects.
function loadSupportWidget({ tenant, client }) {
if (window.KanBirdSupportWidget) return Promise.resolve(window.KanBirdSupportWidget);
if (loading) return loading;
loading = new Promise((resolve, reject) => {
const script = document.createElement('script');
script.src = WIDGET_URL;
script.setAttribute('data-tenant', tenant);
script.setAttribute('data-client', client);
script.onload = () => {
if (window.KanBirdSupportWidget) resolve(window.KanBirdSupportWidget);
else { loading = null; reject(new Error('KanBird support widget did not initialise')); }
};
script.onerror = () => {
loading = null;
reject(new Error('Could not load the KanBird support widget'));
};
document.body.appendChild(script);
});
return loading;
}
export function useSupportWidget({ tenant, client }) {
// Preload after mount so the first click opens instantly.
useEffect(() => {
loadSupportWidget({ tenant, client }).catch(console.error);
}, [tenant, client]);
return useCallback(async (user) => {
const widget = await loadSupportWidget({ tenant, client });
widget.open(user);
}, [tenant, client]);
}
2. Use it in a component
import { useSupportWidget } from '../hooks/useSupportWidget';
export default function SupportButton({ user }) {
const openSupport = useSupportWidget({
tenant: 'YOUR_TENANT_KEY',
client: 'YOUR_CLIENT_CODE',
});
return (
<button type="button" onClick={() => openSupport({ name: user?.name, email: user?.email })}>
Support
</button>
);
}
Next.js: put SupportButton in a Client Component by adding 'use client'; as the first line of the file. The hook only touches window and document inside effects and click handlers.
Any platform that lets you add custom code before the closing </body> tag can use the standard snippet. Look for a setting called footer code, custom code or scripts:
footer.php and paste it just before </body>.theme.liquid just before </body>.<script src="https://app.kanbird.com/support-widget-embed.js"
data-tenant="YOUR_TENANT_KEY"
data-client="YOUR_CLIENT_CODE"></script>
Then add a button or link wherever you want the entry point, using a Custom HTML block:
<button type="button" onclick="KanBirdSupportWidget.open()">Support</button>
Some builders strip onclick attributes or won't let you edit menu items. If that happens, use the Link or iframe option instead — it needs no JavaScript at all.
The widget is also a normal web page. You can link to it from any menu, or embed it in a page you control.
As a link — opens the portal in a new tab:
<a href="https://app.kanbird.com/support-widget?tenant=YOUR_TENANT_KEY&client=YOUR_CLIENT_CODE"
target="_blank" rel="noopener">Support portal</a>
As an iframe — shows it inside one of your own pages:
<iframe
src="https://app.kanbird.com/support-widget?tenant=YOUR_TENANT_KEY&client=YOUR_CLIENT_CODE"
title="Support portal"
style="width: 100%; height: 100vh; border: 0;">
</iframe>
To pre-fill the visitor's details, add name and email to the address, URL-encoded:
https://app.kanbird.com/support-widget?tenant=YOUR_TENANT_KEY&client=YOUR_CLIENT_CODE&name=Karim%20Ahmed&email=karim%40example.com
| Attribute | Required | Description |
|---|---|---|
data-tenant | Yes | The Support Portal key from Settings → Support Portal. |
data-client | Yes | The client code of the organization this website belongs to. |
Available as soon as the embed script has loaded.
| Method | Description |
|---|---|
open(user?) | Opens the full-screen support window. Optionally pass { name, email } to pre-fill the new-ticket form. |
close() | Closes the window. Visitors can also close it with the × in its top bar. |
toggle() | Opens the window if it's closed, closes it if it's open. |
KanBirdSupportWidget.open(); // anonymous visitor
KanBirdSupportWidget.open({ name: 'Karim Ahmed', email: 'karim@example.com' });
KanBirdSupportWidget.close();
KanBirdSupportWidget.toggle();
name and email only pre-fill the new-ticket form. They don't filter what the dashboard shows — see Security & privacy.
| Field | Required | Notes |
|---|---|---|
| Your name | Yes | Pre-filled when you pass name to open(). |
| Your email | Yes | Used to find or create the client's contact in KanBird. Pre-filled when you pass email. |
| Phone number | No | Digits, spaces and + - ( ) ., 6–30 characters. Saved on the contact if it has no number yet, and always recorded in the ticket's activity. |
| Subject | Yes | Becomes the ticket title. |
| Product | Yes | Must be a product assigned to the organization in KanBird. |
| Message | Yes | Becomes the ticket description. |
TIC000001, TIC000002, and so on.Tickets you create inside KanBird are internal by default. To show one to the client, tick Show this ticket to the client on the Support Portal when creating it, or switch on Visible to client on an existing ticket. If an older ticket doesn't have a Ticket ID yet, it gets one the moment it becomes visible.
Read this before you embed. The Support Portal key and client code sit in your page's source, so anyone who can view the page can copy them. They identify your organization — they are not passwords. Anyone holding them sees that organization's whole ticket feed, and can open any ticket to read its description and replies.
name / email is a convenience, not a login. It fills the form; it doesn't limit what the dashboard shows.| What you see | Likely cause | Fix |
|---|---|---|
KanBirdSupportWidget is not defined |
The script hasn't loaded yet, was blocked (ad blocker, content-security policy), or is loaded as type="module". |
Put the embed tag before the code that calls open(), load it as a normal script, and check the browser's Network tab. In Vue/React, use the composable/hook above, which waits for the script. |
| Console: missing data-tenant or missing data-client | The attribute is missing or misspelled, or the script is a module. | Copy the snippet again from Settings → Support Portal. |
| The window says Missing tenant/client code | You linked to the widget page without its query parameters. | Use the full URL from the Link or iframe guide. |
| Unknown or inactive client code, or an empty dashboard | The client code is mistyped or disabled, the Support Portal key was regenerated, or the portal is disabled. | Check Settings → Support Portal, re-enable it if needed, and update data-tenant / data-client. |
| Please select a product, or No products are assigned to your account yet | Product is required, and the organization has no products assigned. | Assign one or more products to the organization in KanBird. |
| Too many requests. Please try again in a minute. | The per-minute limit was reached (10 new tickets or 20 replies). | Wait a minute and retry. |
| The window opens blank, or the browser refuses to load it | Your site's Content-Security-Policy blocks KanBird. | Allow https://app.kanbird.com in both script-src and frame-src. |
| A ticket your team created isn't in the client's list | It's still internal. | Switch on Visible to client for that ticket in KanBird. |
| Two support windows, or the script loads twice | The snippet is being added more than once (common in single-page apps). | Add it once — the composable/hook above already guards against duplicates. |
Still stuck? Email info@kanbird.com and include your page URL and any console errors.