Your website
Widget JavaScript API
Drive the chat widget from your own page: open and close it, set opening lines, hear what is said, register tools and read campaigns.
Control the widget
The widget on your site answers to one function, bidi(command, ...). Open it from your own button, greet each page in its own words, hear what is said, and see which campaign brought the visitor. It runs inside your page only: nothing is sent between windows, and it never hands your page the conversation's secret.
1. Install it with the queue
Paste the one-line stub before your embed code. Calls you make before the widget has loaded wait in a queue and run in order once it is ready. If your page already uses the name bidi for something else, the widget leaves it alone and answers as bidiWidget: use that name in the stub and in your calls.
<script>
window.bidi = window.bidi || Object.assign(function () { window.bidi.q.push(arguments); }, { q: [] });
</script>
<script src="https://www.bidii.ai/widget.js" data-bidi-bot="pk_YOUR_KEY" async></script>2. Open, close, toggle
Wire these to your own buttons and links. Opening moves keyboard focus into the chat; closing returns it to the launcher.
bidi('open'); // open the chat
bidi('close'); // minimise it
bidi('toggle'); // open it if it is closed, close it if it is open
document.querySelector('#chat-with-us').addEventListener('click', function () {
bidi('open');
});3. Opening lines and suggested questions for each page
setInitialMessages replaces the greeting on this page with up to five lines of your own, 500 characters each. They show once per conversation: a visitor who has already started chatting keeps the lines the conversation began with. setSuggestedMessages replaces the question chips with up to four of your own; pass an empty list to show none, or null to go back to the ones from your dashboard.
bidi('setInitialMessages', [
'Looking at villas? I can check viewing times for you.',
'Tell me the area you like.'
]);
bidi('setSuggestedMessages', ['When can I view a villa?', 'Is financing available?']);4. Events
on(event, handler) calls your function with a small object. user-message and assistant-message carry the text, and assistant-message also says who wrote it, the assistant or your team (from). open and close fire when the panel does. handoff fires once, when the conversation is passed to a person. off(event, handler) stops listening. A handler that throws is reported in the console and never breaks the chat.
bidi('on', 'user-message', function (event) {
console.log('The visitor wrote:', event.text);
});
bidi('on', 'assistant-message', function (event) {
console.log(event.from, 'answered:', event.text); // from: "assistant" or "team"
});
bidi('on', 'open', function () { /* the panel opened */ });
bidi('on', 'close', function () { /* the panel was minimised */ });
bidi('on', 'handoff', function (event) {
console.log('Passed to a person:', event.state); // "handoff_requested" or "human_active"
});5. Tell the widget who is signed in
Call identify after your own sign-in, with a token your server signed (a JWT, HS256), or with the user's id and its HMAC-SHA256 hash plus an optional email and name. Call identify(null) when they sign out.
// A token your server signed (JWT, HS256):
bidi('identify', { token: 'HEADER.PAYLOAD.SIGNATURE' });
// Or the user's id and its HMAC-SHA256 hash, computed on your server:
bidi('identify', {
user_id: 'u_42',
user_hash: 'THE_64_CHARACTER_HEX_HASH',
email: 'sara@example.ae',
name: 'Sara'
});
bidi('identify', null); // they signed outThe widget sends the call to bidi, which checks it against your assistant's identity secret: a visitor who verifies becomes a verified contact, with memory across visits. Until the assistant has a secret, what your page says is shown to your team as an unverified claim and trusted by nothing. The recipe is on its own page: Identity verification
6. Register tools your page can run
registerTools takes a list of tools: a snake_case name, a description, the parameters as a JSON schema and a handler function. It answers with the names it holds. A handler runs when the assistant calls a client tool of the same name that you declared in the dashboard's Actions tab: it receives the arguments, has 10 seconds to return plain data, and what it returns reaches the assistant as untrusted data. A tool you did not declare is never called.
bidi('registerTools', [{
name: 'open_cart',
description: 'Open the cart on this page.',
parameters: { type: 'object', properties: {} },
handler: function () { openCart(); return { ok: true }; }
}]);
// Called once the widget has loaded, it answers ['open_cart'].7. Campaigns: UTM and referrer
Nothing to call. The widget reads utm_source, utm_medium, utm_campaign, utm_term and utm_content from the address of the first page of the visit, and the site that sent the visitor (host and path only, never the query string). Both are kept with the conversation. You see them in the conversation's header, as columns in the leads CSV and at the end of the conversation export (CSV and JSON), as utm and referrer on a conversation and on a lead in the API (/api/v2/conversations, /api/v2/leads), and under data.utm and data.referrer in lead.created, lead.updated and lead.status_changed. Values are cut at 120 characters, and no other address parameter is read.
"data": {
"name": "Mariam",
"contact": "050 123 4567",
"utm": {
"utm_source": "instagram",
"utm_medium": "paid_social",
"utm_campaign": "eid-2026"
},
"referrer": "https://l.instagram.com/"
}