Skip to main content

Prerequisites

1

Get your Workspace ID

In the Trillet Portal sidebar, click the workspace name dropdown at the top. Your Workspace ID is displayed next to each workspace - click to copy.
2

Get your Agent ID

Navigate to Call Flows in the sidebar. Each call flow card displays its Agent ID - copy it from the card.
3

Whitelist your domain

Go to Settings → Domain and add the domain where you’ll embed the widget (e.g. https://yoursite.com). This is required to avoid CORS errors. Allow up to 24 hours for propagation. Skip this if you’re using a whitelabelled custom domain.
4

Enable public access

Open your call flow, go to the Settings tab, and toggle Public Access on. Without this, startPublicCall() will be rejected.
Never expose your API key in client-side code. The widget uses startPublicCall() which only requires the Workspace ID and Agent ID.

Integration

Choose between a minimal quick-start snippet or a complete styled widget, then pick your framework.
Paste this before the closing </body> tag. A chat bubble appears in the bottom-right corner.
<script type="module">
  import { TrilletAgent } from 'https://cdn.jsdelivr.net/npm/@trillet-ai/web-sdk/+esm';

  const WORKSPACE_ID = 'your-workspace-id';
  const AGENT_ID = 'your-agent-id';

  const container = document.createElement('div');
  container.innerHTML = `
    <div id="trillet-chat-bubble" style="position:fixed;bottom:24px;right:24px;width:52px;height:52px;background:#0066ff;border-radius:12px;cursor:pointer;display:flex;align-items:center;justify-content:center;box-shadow:0 1px 3px rgba(0,0,0,0.08);z-index:9999;border:1px solid rgba(0,102,255,0.2);transition:all 0.1s ease-in-out;">
      <svg width="22" height="22" fill="white" viewBox="0 0 24 24"><path d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 14H6l-2 2V4h16v12z"/></svg>
    </div>
  `;
  document.body.appendChild(container);

  const bubble = document.getElementById('trillet-chat-bubble');
  let chatWindow = null;
  let agent = null;
  let isConnected = false;

  bubble.addEventListener('click', () => {
    if (chatWindow) {
      chatWindow.style.display = chatWindow.style.display === 'none' ? 'flex' : 'none';
      return;
    }
    openChat();
  });

  function openChat() {
    chatWindow = document.createElement('div');
    chatWindow.style.cssText = 'position:fixed;bottom:88px;right:24px;width:380px;height:520px;background:#fff;border-radius:12px;border:1px solid #e5e7eb;box-shadow:0 4px 12px -1px rgba(0,0,0,0.05);display:flex;flex-direction:column;overflow:hidden;z-index:9999;font-family:-apple-system,BlinkMacSystemFont,system-ui,sans-serif;';
    chatWindow.innerHTML = `
      <div style="background:#fff;border-bottom:1px solid #e5e7eb;padding:14px 16px;display:flex;justify-content:space-between;align-items:center;">
        <div><div style="font-weight:600;font-size:14px;color:#111827;">Assistant</div><div id="trillet-status" style="font-size:12px;color:#6b7280;">Connecting...</div></div>
        <button id="trillet-close" style="background:none;border:none;color:#9ca3af;cursor:pointer;font-size:16px;padding:4px;">&#x2715;</button>
      </div>
      <div id="trillet-messages" style="flex:1;overflow-y:auto;padding:16px;display:flex;flex-direction:column;gap:8px;background:#f9fafb;"></div>
      <div style="padding:12px;border-top:1px solid #e5e7eb;display:flex;gap:8px;">
        <input id="trillet-input" type="text" placeholder="Type a message..." disabled style="flex:1;padding:9px 14px;border:1px solid #e5e7eb;border-radius:8px;font-size:13px;outline:none;color:#111827;background:#fff;transition:border-color 0.2s;">
        <button id="trillet-send" disabled style="padding:9px 14px;border:none;border-radius:8px;background:#0066ff;color:#fff;font-size:13px;font-weight:500;cursor:pointer;transition:background 0.1s;">Send</button>
      </div>
    `;
    document.body.appendChild(chatWindow);

    document.getElementById('trillet-close').onclick = () => chatWindow.style.display = 'none';
    document.getElementById('trillet-input').onkeypress = (e) => { if (e.key === 'Enter') sendMessage(); };
    document.getElementById('trillet-send').onclick = sendMessage;

    connectAgent();
  }

  async function connectAgent() {
    try {
      agent = new TrilletAgent({ workspaceId: WORKSPACE_ID, agentId: AGENT_ID, mode: 'text' });

      agent.on('connected', () => {
        isConnected = true;
        document.getElementById('trillet-status').textContent = 'Online';
        document.getElementById('trillet-status').style.color = '#10b981';
        document.getElementById('trillet-input').disabled = false;
        document.getElementById('trillet-send').disabled = false;
        document.getElementById('trillet-input').focus();
      });

      agent.on('disconnected', () => {
        isConnected = false;
        document.getElementById('trillet-status').textContent = 'Offline';
        document.getElementById('trillet-status').style.color = '#6b7280';
        document.getElementById('trillet-input').disabled = true;
        document.getElementById('trillet-send').disabled = true;
      });

      agent.on('error', () => addMsg('Something went wrong. Please try again.', 'system'));

      ['message', 'transcript', 'transcriptUpdate'].forEach(evt => {
        agent.on(evt, (data) => {
          let text = typeof data === 'string' ? data : data?.text || data?.content || data?.message;
          if (data?.role && data.role !== 'assistant') return;
          if (data?.isFinal === false) return;
          if (text) addMsg(text, 'assistant');
        });
      });

      await agent.startPublicCall();
    } catch (err) {
      document.getElementById('trillet-status').textContent = 'Failed to connect';
      addMsg('Could not connect. Please refresh and try again.', 'system');
    }
  }

  function sendMessage() {
    const input = document.getElementById('trillet-input');
    const text = input.value.trim();
    if (!text || !isConnected) return;
    addMsg(text, 'user');
    input.value = '';
    try {
      agent.sendTextMessage?.(text) || agent.sendText?.(text) || agent.sendMessage?.(text);
    } catch (e) {
      addMsg('Failed to send message.', 'system');
    }
  }

  const displayed = new Set();
  function addMsg(text, type) {
    const key = type + ':' + text;
    if (displayed.has(key)) return;
    displayed.add(key);
    const el = document.createElement('div');
    const isUser = type === 'user';
    el.style.cssText = `max-width:80%;padding:9px 13px;border-radius:10px;font-size:13px;line-height:1.5;${
      isUser ? 'background:#0066ff;color:#fff;align-self:flex-end;'
      : type === 'system' ? 'background:#f3f4f6;color:#6b7280;align-self:center;font-size:12px;padding:6px 12px;'
      : 'background:#fff;color:#111827;align-self:flex-start;border:1px solid #e5e7eb;'
    }`;
    el.textContent = text;
    const container = document.getElementById('trillet-messages');
    container.appendChild(el);
    container.scrollTop = container.scrollHeight;
  }
</script>
A polished chat widget with status indicators, typing animation, and transcript polling. Self-contained HTML file you can use directly or hand off.
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Trillet Chat Widget</title>
  <style>
    * { box-sizing: border-box; }

    #trillet-bubble {
      position: fixed;
      bottom: 24px;
      right: 24px;
      width: 52px;
      height: 52px;
      background: #0066ff;
      border-radius: 12px;
      cursor: pointer;
      display: flex;
      align-items: center;
      justify-content: center;
      box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
      border: 1px solid rgba(0, 102, 255, 0.2);
      z-index: 9999;
      transition: all 0.1s ease-in-out;
    }
    #trillet-bubble:hover { background: #0052cc; }
    #trillet-bubble svg { width: 22px; height: 22px; fill: white; }

    #trillet-panel {
      position: fixed;
      bottom: 88px;
      right: 24px;
      width: 380px;
      height: 540px;
      background: #fff;
      border-radius: 12px;
      border: 1px solid #e5e7eb;
      box-shadow: 0 4px 12px -1px rgba(0, 0, 0, 0.05);
      display: none;
      flex-direction: column;
      overflow: hidden;
      z-index: 9999;
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
    }
    #trillet-panel.open { display: flex; animation: trillet-in 0.15s cubic-bezier(0.16,1,0.3,1); }

    @keyframes trillet-in {
      from { opacity: 0; transform: translateY(8px); }
      to { opacity: 1; transform: translateY(0); }
    }

    .trillet-header {
      padding: 14px 16px;
      border-bottom: 1px solid #e5e7eb;
      display: flex;
      justify-content: space-between;
      align-items: center;
      background: #fff;
    }
    .trillet-header h3 { font-size: 14px; font-weight: 600; margin: 0; color: #111827; }
    .trillet-header .status {
      font-size: 12px;
      color: #6b7280;
      display: flex;
      align-items: center;
      gap: 5px;
      margin-top: 1px;
    }
    .trillet-header .status-dot {
      width: 6px; height: 6px;
      border-radius: 50%;
      background: #d1d5db;
    }
    .trillet-header .status-dot.online { background: #10b981; }
    .trillet-close {
      background: none;
      border: none;
      color: #9ca3af;
      cursor: pointer;
      font-size: 16px;
      padding: 4px;
      line-height: 1;
      transition: color 0.1s;
    }
    .trillet-close:hover { color: #6b7280; }

    .trillet-messages {
      flex: 1;
      overflow-y: auto;
      padding: 16px;
      display: flex;
      flex-direction: column;
      gap: 8px;
      background: #f9fafb;
    }
    .trillet-msg {
      max-width: 82%;
      padding: 9px 13px;
      border-radius: 10px;
      font-size: 13px;
      line-height: 1.5;
    }
    .trillet-msg.user {
      background: #0066ff;
      color: #fff;
      align-self: flex-end;
    }
    .trillet-msg.assistant {
      background: #fff;
      color: #111827;
      align-self: flex-start;
      border: 1px solid #e5e7eb;
    }
    .trillet-msg.system {
      background: #f3f4f6;
      color: #6b7280;
      align-self: center;
      font-size: 11px;
      padding: 4px 10px;
      border-radius: 6px;
    }

    .trillet-typing {
      display: flex;
      gap: 4px;
      padding: 10px 14px;
      background: #fff;
      border: 1px solid #e5e7eb;
      border-radius: 10px;
      align-self: flex-start;
    }
    .trillet-typing span {
      width: 5px; height: 5px;
      background: #9ca3af;
      border-radius: 50%;
      animation: trillet-dot 1.4s infinite ease-in-out;
    }
    .trillet-typing span:nth-child(2) { animation-delay: 0.2s; }
    .trillet-typing span:nth-child(3) { animation-delay: 0.4s; }
    @keyframes trillet-dot {
      0%, 60%, 100% { transform: translateY(0); opacity: 0.4; }
      30% { transform: translateY(-3px); opacity: 1; }
    }

    .trillet-input-area {
      padding: 12px;
      border-top: 1px solid #e5e7eb;
      display: flex;
      gap: 8px;
      background: #fff;
    }
    .trillet-input-area input {
      flex: 1;
      padding: 9px 14px;
      border: 1px solid #e5e7eb;
      border-radius: 8px;
      font-size: 13px;
      outline: none;
      color: #111827;
      transition: border-color 0.2s;
    }
    .trillet-input-area input:focus { border-color: #0066ff; }
    .trillet-input-area input:disabled { background: #f9fafb; color: #9ca3af; }
    .trillet-send {
      padding: 9px 14px;
      border: none;
      border-radius: 8px;
      background: #0066ff;
      color: #fff;
      cursor: pointer;
      font-size: 13px;
      font-weight: 500;
      transition: background 0.1s;
    }
    .trillet-send:hover:not(:disabled) { background: #0052cc; }
    .trillet-send:disabled { background: #d1d5db; cursor: not-allowed; }

    @media (max-width: 480px) {
      #trillet-panel {
        width: calc(100vw - 24px);
        height: calc(100vh - 140px);
        right: 12px;
        bottom: 88px;
      }
    }
  </style>
</head>
<body>

  <!-- Paste everything below into your website -->
  <button id="trillet-bubble">
    <svg viewBox="0 0 24 24"><path d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 14H6l-2 2V4h16v12z"/></svg>
  </button>

  <div id="trillet-panel">
    <div class="trillet-header">
      <div>
        <h3>Assistant</h3>
        <div class="status">
          <div class="status-dot" id="trillet-dot"></div>
          <span id="trillet-status-text">Ready</span>
        </div>
      </div>
      <button class="trillet-close" id="trillet-close">&#x2715;</button>
    </div>
    <div class="trillet-messages" id="trillet-messages"></div>
    <div class="trillet-input-area">
      <input type="text" id="trillet-input" placeholder="Type a message..." disabled>
      <button class="trillet-send" id="trillet-send" disabled>Send</button>
    </div>
  </div>

  <script type="module">
    import { TrilletAgent } from 'https://cdn.jsdelivr.net/npm/@trillet-ai/web-sdk/+esm';

    // ── REPLACE THESE WITH YOUR VALUES ──
    const WORKSPACE_ID = 'your-workspace-id';
    const AGENT_ID = 'your-agent-id';
    // ─────────────────────────────────────

    const $ = (id) => document.getElementById(id);
    const bubble = $('trillet-bubble');
    const panel = $('trillet-panel');

    let agent = null;
    let isConnected = false;
    let hasConnected = false;
    const displayed = new Set();

    bubble.addEventListener('click', () => {
      panel.classList.toggle('open');
      if (!hasConnected) {
        hasConnected = true;
        connectAgent();
      }
    });

    $('trillet-close').addEventListener('click', () => panel.classList.remove('open'));
    $('trillet-input').addEventListener('keypress', (e) => { if (e.key === 'Enter') sendMessage(); });
    $('trillet-send').addEventListener('click', sendMessage);

    async function connectAgent() {
      setStatus('Connecting...', false);
      try {
        agent = new TrilletAgent({
          workspaceId: WORKSPACE_ID,
          agentId: AGENT_ID,
          mode: 'text',
        });

        agent.on('connected', () => {
          isConnected = true;
          setStatus('Online', true);
          $('trillet-input').disabled = false;
          $('trillet-send').disabled = false;
          $('trillet-input').focus();
          startPolling();
        });

        agent.on('disconnected', () => {
          isConnected = false;
          setStatus('Offline', false);
          $('trillet-input').disabled = true;
          $('trillet-send').disabled = true;
          addMsg('Session ended.', 'system');
        });

        agent.on('error', () => addMsg('Something went wrong. Please try again.', 'system'));

        ['message', 'transcript', 'transcriptUpdate', 'response'].forEach(evt => {
          agent.on(evt, (data) => {
            hideTyping();
            let text = typeof data === 'string' ? data : data?.text || data?.content || data?.message;
            if (data?.role && data.role !== 'assistant') return;
            if (data?.isFinal === false) return;
            if (text) addMsg(text, 'assistant');
          });
        });

        agent.on('assistantStartedSpeaking', showTyping);
        agent.on('assistantStoppedSpeaking', hideTyping);

        await agent.startPublicCall();
      } catch (err) {
        setStatus('Failed', false);
        addMsg('Could not connect. Please refresh and try again.', 'system');
      }
    }

    let pollInterval = null;
    function startPolling() {
      let lastCount = 0;
      pollInterval = setInterval(() => {
        if (!agent || !isConnected) return;
        try {
          const transcripts = agent.getTranscripts?.() || [];
          for (let i = lastCount; i < transcripts.length; i++) {
            const t = transcripts[i];
            if (t.role === 'assistant' && t.text) { hideTyping(); addMsg(t.text, 'assistant'); }
          }
          lastCount = transcripts.length;
        } catch (e) {}
      }, 500);
    }

    function sendMessage() {
      const input = $('trillet-input');
      const text = input.value.trim();
      if (!text || !isConnected) return;
      addMsg(text, 'user');
      input.value = '';
      showTyping();
      try {
        if (agent.sendTextMessage) agent.sendTextMessage(text);
        else if (agent.sendText) agent.sendText(text);
        else if (agent.sendMessage) agent.sendMessage(text);
      } catch (e) {
        hideTyping();
        addMsg('Failed to send message.', 'system');
      }
    }

    function setStatus(text, online) {
      $('trillet-status-text').textContent = text;
      $('trillet-dot').classList.toggle('online', online);
    }

    function addMsg(text, type) {
      if (!text?.trim()) return;
      const key = type + ':' + text;
      if (displayed.has(key)) return;
      displayed.add(key);
      const el = document.createElement('div');
      el.className = 'trillet-msg ' + type;
      el.textContent = text;
      $('trillet-messages').appendChild(el);
      $('trillet-messages').scrollTop = $('trillet-messages').scrollHeight;
    }

    function showTyping() {
      if (document.getElementById('trillet-typing')) return;
      const el = document.createElement('div');
      el.className = 'trillet-typing';
      el.id = 'trillet-typing';
      el.innerHTML = '<span></span><span></span><span></span>';
      $('trillet-messages').appendChild(el);
      $('trillet-messages').scrollTop = $('trillet-messages').scrollHeight;
    }

    function hideTyping() {
      document.getElementById('trillet-typing')?.remove();
    }
  </script>

</body>
</html>

Events Reference

The SDK emits these events you can listen to:
EventDescription
connectedAgent is connected and ready
disconnectedSession ended
errorAn error occurred
messageNew message received
transcriptTranscript update received
assistantStartedSpeakingAgent begins responding
assistantStoppedSpeakingAgent finished responding
agent.on('connected', (details) => {
  console.log('Connected:', details.callId);
});