Event handling
Everything that happens in the chat fires an event: the app loading, messages going back and forth, popups opening. Subscribe with on(), unsubscribe with off().
on() and off()
const chat = new Chatoshi({
partnerKey: 'your-partner-key',
mode: 'popup'
});
chat.on('app:ready', () => {
console.log('Chat is ready');
});
chat.on('message:received', (data) => {
console.log('AI responded in chat', data.chatId);
});
chat.on('popup:open', () => {
console.log('Popup opened');
});
off() removes a specific callback, or all listeners for an event when called without one:
const handler = () => console.log('opened');
chat.on('popup:open', handler);
chat.off('popup:open', handler);
chat.off('popup:open');
Both methods return the instance, so calls chain:
chat
.on('app:ready', onReady)
.on('popup:open', onOpen);
Events
Lifecycle
| Event | Payload | When |
|---|---|---|
| app:ready | null | The chat app finished loading. Messages queued earlier are delivered now. |
Messages
| Event | Payload | When |
|---|---|---|
| message:sent | { chatId, personality, characterCount } | The user submitted a message |
| message:received | { chatId, personality } | The AI response fully arrived |
| session:new:response | { requestId?, success, sessionId?, error? } | The app acknowledged a newConversation() call |
| message:send:response | { requestId?, success, sessionId?, error? } | The app acknowledged a sendMessage() call |
Widgets
| Event | Payload | When |
|---|---|---|
| popup:open, drawer:open | { firstOpened: boolean } | The popup or drawer opened |
| popup:close, drawer:close | none | The popup or drawer closed |
Sending messages from code
sendMessage() and newConversation() are the code-side counterpart of the message events:
chat.newConversation('What is the Bitcoin price today?', 'req-1');
chat.on('session:new:response', (data) => {
if (data.requestId === 'req-1' && data.success) {
console.log('New session:', data.sessionId);
}
});
Calls made before app:ready are queued and flushed once the chat loads, so you don't need to wait yourself.
Examples
Engagement tracking
let messages = 0;
chat.on('message:sent', () => {
messages += 1;
});
chat.on('popup:close', () => {
if (messages > 0) {
analytics.track('chat_session', { messages });
}
messages = 0;
});
Unread badge
const badge = document.querySelector('.chat-badge');
chat.on('message:received', () => {
const popup = chat.getPopup();
if (popup && !popup.isOpen()) {
badge?.classList.add('visible');
}
});
chat.on('popup:open', () => {
badge?.classList.remove('visible');
});
Core mode protocol (postMessage)
When you embed the chat directly in an iframe, without the SDK, you communicate with it via postMessage. This is the same protocol the SDK uses internally.
<iframe
id="chatoshi"
src="https://aichat.chatoshi.ai/?embed_mode=core&intent_id=widget-001"
style="width: 100%; height: 600px; border: none;"
></iframe>
Messages you send
Every message has this shape:
{
source: 'chatoshi-sdk';
intentId: string;
eventName: string;
data?: object;
}
intentId must match the intent_id URL parameter of the iframe.
| Event | Data | Effect |
|---|---|---|
| session:new | { message?, requestId? } | Creates a new session, optionally sends the first message |
| message:send | { message, requestId? } | Sends a message to the current session |
| layout:sidebar | { visible: boolean } | Shows or hides the sidebar (core mode only) |
const iframe = document.getElementById('chatoshi');
iframe.contentWindow.postMessage({
source: 'chatoshi-sdk',
intentId: 'widget-001',
eventName: 'session:new',
data: { message: 'What is the Bitcoin price?', requestId: 'req-1' }
}, '*');
Events you receive
Events from the chat come back with source: 'aptachat':
{
source: 'aptachat';
eventName: string;
timestamp: number;
intentId?: string;
data: any;
}
| Event | Payload | When |
|---|---|---|
| app:ready | null | The chat finished initializing |
| message:sent | { chatId, personality, characterCount } | The user submitted a message |
| message:received | { chatId, personality } | The AI response fully arrived |
| session:new:response | { requestId?, success, sessionId?, error? } | Reply to session:new |
| message:send:response | { requestId?, success, sessionId?, error? } | Reply to message:send |
window.addEventListener('message', (event) => {
if (event.data?.source !== 'aptachat') return;
switch (event.data.eventName) {
case 'app:ready':
console.log('Chat is ready');
break;
case 'message:received':
console.log('AI responded:', event.data.data);
break;
case 'session:new:response':
if (!event.data.data.success) {
console.error('Session failed:', event.data.data.error);
}
break;
}
});
Complete example
<iframe
id="chatoshi"
src="https://aichat.chatoshi.ai/?embed_mode=core&intent_id=widget-001"
style="width: 100%; height: 600px; border: none;"
></iframe>
<script>
const iframe = document.getElementById('chatoshi');
window.addEventListener('message', (event) => {
if (event.data?.source !== 'aptachat') return;
if (event.data.eventName === 'app:ready') {
iframe.contentWindow.postMessage({
source: 'chatoshi-sdk',
intentId: 'widget-001',
eventName: 'session:new',
data: { message: 'Hello!' }
}, '*');
}
if (event.data.eventName === 'message:received') {
console.log('AI responded');
}
});
</script>
Cleanup
destroy() removes all listeners along with the chat itself:
chat.destroy();
On long-lived pages that create and recreate chats, destroy old instances before making new ones; keep one instance at a time.