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.
The voice widget requires microphone permissions and must be served over HTTPS (or localhost). Browsers block microphone access on insecure origins.

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 mic button 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 btn = document.createElement('button');
  btn.id = 'trillet-voice-btn';
  btn.innerHTML = `<svg width="22" height="22" fill="white" viewBox="0 0 24 24"><path d="M12 14c1.66 0 3-1.34 3-3V5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3zm-1-9c0-.55.45-1 1-1s1 .45 1 1v6c0 .55-.45 1-1 1s-1-.45-1-1V5zm6 6c0 2.76-2.24 5-5 5s-5-2.24-5-5H5c0 3.53 2.61 6.43 6 6.92V21h2v-3.08c3.39-.49 6-3.39 6-6.92h-2z"/></svg>`;
  btn.style.cssText = 'position:fixed;bottom:24px;right:24px;width:52px;height:52px;border-radius:12px;background:#0066ff;border:1px solid rgba(0,102,255,0.2);cursor:pointer;display:flex;align-items:center;justify-content:center;box-shadow:0 1px 3px rgba(0,0,0,0.08);z-index:9999;transition:all 0.1s ease-in-out;';
  document.body.appendChild(btn);

  let agent = null;
  let isActive = false;

  btn.addEventListener('click', async () => {
    if (isActive) { agent?.endCall(); return; }

    btn.style.background = '#d1d5db';
    btn.disabled = true;

    try {
      agent = new TrilletAgent({ workspaceId: WORKSPACE_ID, agentId: AGENT_ID, mode: 'voice' });

      agent.on('connected', () => {
        isActive = true;
        btn.disabled = false;
        btn.style.background = '#ef4444';
        btn.style.borderColor = 'rgba(239,68,68,0.2)';
        btn.innerHTML = `<svg width="20" height="20" fill="white" viewBox="0 0 24 24"><path d="M12 9c-1.6 0-3.15.25-4.6.72v3.1c0 .39-.23.74-.56.9-.98.49-1.87 1.12-2.66 1.85-.18.18-.43.28-.7.28-.28 0-.53-.11-.71-.29L.29 13.08a.956.956 0 010-1.36C3.55 8.67 7.56 7 12 7s8.45 1.67 11.71 4.72c.18.18.29.43.29.71 0 .28-.11.53-.29.71l-2.48 2.48c-.18.18-.43.29-.71.29-.27 0-.52-.1-.7-.28-.79-.73-1.68-1.36-2.66-1.85a.993.993 0 01-.56-.9v-3.1C15.15 9.25 13.6 9 12 9z"/></svg>`;
      });

      agent.on('disconnected', () => {
        isActive = false;
        btn.style.background = '#0066ff';
        btn.style.borderColor = 'rgba(0,102,255,0.2)';
        btn.innerHTML = `<svg width="22" height="22" fill="white" viewBox="0 0 24 24"><path d="M12 14c1.66 0 3-1.34 3-3V5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3zm-1-9c0-.55.45-1 1-1s1 .45 1 1v6c0 .55-.45 1-1 1s-1-.45-1-1V5zm6 6c0 2.76-2.24 5-5 5s-5-2.24-5-5H5c0 3.53 2.61 6.43 6 6.92V21h2v-3.08c3.39-.49 6-3.39 6-6.92h-2z"/></svg>`;
        btn.disabled = false;
      });

      agent.on('error', () => {
        btn.style.background = '#0066ff';
        btn.style.borderColor = 'rgba(0,102,255,0.2)';
        btn.innerHTML = `<svg width="22" height="22" fill="white" viewBox="0 0 24 24"><path d="M12 14c1.66 0 3-1.34 3-3V5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3zm-1-9c0-.55.45-1 1-1s1 .45 1 1v6c0 .55-.45 1-1 1s-1-.45-1-1V5zm6 6c0 2.76-2.24 5-5 5s-5-2.24-5-5H5c0 3.53 2.61 6.43 6 6.92V21h2v-3.08c3.39-.49 6-3.39 6-6.92h-2z"/></svg>`;
        btn.disabled = false;
        isActive = false;
      });

      await agent.startPublicCall();
    } catch (err) {
      btn.style.background = '#0066ff';
      btn.style.borderColor = 'rgba(0,102,255,0.2)';
      btn.innerHTML = `<svg width="22" height="22" fill="white" viewBox="0 0 24 24"><path d="M12 14c1.66 0 3-1.34 3-3V5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3zm-1-9c0-.55.45-1 1-1s1 .45 1 1v6c0 .55-.45 1-1 1s-1-.45-1-1V5zm6 6c0 2.76-2.24 5-5 5s-5-2.24-5-5H5c0 3.53 2.61 6.43 6 6.92V21h2v-3.08c3.39-.49 6-3.39 6-6.92h-2z"/></svg>`;
      btn.disabled = false;
    }
  });
</script>
A polished voice widget with mute/unmute, end call, audio visualizer, and live transcript display.
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Trillet Voice Widget</title>
  <style>
    * { box-sizing: border-box; }

    #trillet-v-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-v-bubble:hover { background: #0052cc; }
    #trillet-v-bubble svg { width: 22px; height: 22px; fill: white; }

    #trillet-v-panel {
      position: fixed;
      bottom: 88px;
      right: 24px;
      width: 340px;
      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-v-panel.open { display: flex; animation: trillet-v-in 0.15s cubic-bezier(0.16,1,0.3,1); }

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

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

    .trillet-v-transcript {
      max-height: 180px;
      overflow-y: auto;
      padding: 12px 16px;
      display: flex;
      flex-direction: column;
      gap: 6px;
      background: #f9fafb;
    }
    .trillet-v-msg {
      max-width: 85%;
      padding: 7px 11px;
      border-radius: 8px;
      font-size: 12px;
      line-height: 1.4;
    }
    .trillet-v-msg.user {
      background: #0066ff; color: #fff; align-self: flex-end;
    }
    .trillet-v-msg.assistant {
      background: #fff; color: #111827; align-self: flex-start;
      border: 1px solid #e5e7eb;
    }
    .trillet-v-msg.system {
      background: #f3f4f6; color: #6b7280;
      align-self: center; font-size: 11px; padding: 3px 8px; border-radius: 4px;
    }

    .trillet-v-controls {
      padding: 20px;
      background: #fff;
      border-top: 1px solid #e5e7eb;
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 14px;
    }

    .trillet-v-vis {
      display: flex; align-items: center; justify-content: center;
      gap: 3px; height: 32px;
    }
    .trillet-v-vis .bar {
      width: 3px; height: 6px;
      background: #0066ff; border-radius: 2px;
      transition: height 0.1s;
    }
    .trillet-v-vis.active .bar {
      animation: trillet-pulse 0.8s infinite ease-in-out;
    }
    .trillet-v-vis.active .bar:nth-child(1) { animation-delay: 0s; }
    .trillet-v-vis.active .bar:nth-child(2) { animation-delay: 0.1s; }
    .trillet-v-vis.active .bar:nth-child(3) { animation-delay: 0.2s; }
    .trillet-v-vis.active .bar:nth-child(4) { animation-delay: 0.3s; }
    .trillet-v-vis.active .bar:nth-child(5) { animation-delay: 0.4s; }
    .trillet-v-vis.active .bar:nth-child(6) { animation-delay: 0.3s; }
    .trillet-v-vis.active .bar:nth-child(7) { animation-delay: 0.2s; }
    @keyframes trillet-pulse {
      0%, 100% { height: 6px; }
      50% { height: 22px; }
    }

    .trillet-v-label {
      font-size: 12px; color: #6b7280; font-weight: 500;
    }

    .trillet-v-btns {
      display: flex; align-items: center; gap: 12px;
    }
    .trillet-v-mic {
      width: 48px; height: 48px; border-radius: 12px; border: none;
      cursor: pointer; display: flex; align-items: center; justify-content: center;
      transition: all 0.1s;
    }
    .trillet-v-mic.idle {
      background: #0066ff; color: white;
      border: 1px solid rgba(0, 102, 255, 0.2);
      box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
    }
    .trillet-v-mic.idle:hover { background: #0052cc; }
    .trillet-v-mic.active {
      background: #0066ff; color: white;
      border: 1px solid rgba(0, 102, 255, 0.2);
    }
    .trillet-v-mic.muted {
      background: #fef2f2; color: #ef4444;
      border: 1px solid rgba(239, 68, 68, 0.15);
    }
    .trillet-v-mic svg { width: 20px; height: 20px; }

    .trillet-v-end {
      width: 40px; height: 40px; border-radius: 12px; border: none;
      background: #ef4444; color: white; cursor: pointer;
      border: 1px solid rgba(239, 68, 68, 0.2);
      display: flex; align-items: center; justify-content: center;
      transition: background 0.1s;
    }
    .trillet-v-end:hover { background: #dc2626; }
    .trillet-v-end svg { width: 18px; height: 18px; }

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

  <!-- Paste everything below into your website -->
  <button id="trillet-v-bubble">
    <svg viewBox="0 0 24 24"><path d="M12 14c1.66 0 3-1.34 3-3V5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3zm-1-9c0-.55.45-1 1-1s1 .45 1 1v6c0 .55-.45 1-1 1s-1-.45-1-1V5zm6 6c0 2.76-2.24 5-5 5s-5-2.24-5-5H5c0 3.53 2.61 6.43 6 6.92V21h2v-3.08c3.39-.49 6-3.39 6-6.92h-2z"/></svg>
  </button>

  <div id="trillet-v-panel">
    <div class="trillet-v-header">
      <div>
        <h3>Voice Assistant</h3>
        <div class="status">
          <div class="status-dot" id="trillet-v-dot"></div>
          <span id="trillet-v-status-text">Ready</span>
        </div>
      </div>
      <button class="trillet-v-close" id="trillet-v-close">&#x2715;</button>
    </div>
    <div class="trillet-v-transcript" id="trillet-v-transcript"></div>
    <div class="trillet-v-controls">
      <div class="trillet-v-vis" id="trillet-v-vis">
        <div class="bar"></div><div class="bar"></div><div class="bar"></div>
        <div class="bar"></div>
        <div class="bar"></div><div class="bar"></div><div class="bar"></div>
      </div>
      <div class="trillet-v-label" id="trillet-v-label">Tap the mic to start</div>
      <div class="trillet-v-btns" id="trillet-v-btns">
        <button class="trillet-v-mic idle" id="trillet-v-mic">
          <svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 14c1.66 0 3-1.34 3-3V5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3zm-1-9c0-.55.45-1 1-1s1 .45 1 1v6c0 .55-.45 1-1 1s-1-.45-1-1V5zm6 6c0 2.76-2.24 5-5 5s-5-2.24-5-5H5c0 3.53 2.61 6.43 6 6.92V21h2v-3.08c3.39-.49 6-3.39 6-6.92h-2z"/></svg>
        </button>
      </div>
    </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-v-bubble');
    const panel = $('trillet-v-panel');
    const micSvg = `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 14c1.66 0 3-1.34 3-3V5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3zm-1-9c0-.55.45-1 1-1s1 .45 1 1v6c0 .55-.45 1-1 1s-1-.45-1-1V5zm6 6c0 2.76-2.24 5-5 5s-5-2.24-5-5H5c0 3.53 2.61 6.43 6 6.92V21h2v-3.08c3.39-.49 6-3.39 6-6.92h-2z"/></svg>`;
    const micMutedSvg = `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M19 11h-1.7c0 .74-.16 1.43-.43 2.05l1.23 1.23c.56-.98.9-2.09.9-3.28zm-4.02.17c0-.06.02-.11.02-.17V5c0-1.66-1.34-3-3-3S9 3.34 9 5v.18l5.98 5.99zM4.27 3L3 4.27l6.01 6.01V11c0 1.66 1.33 3 2.99 3 .22 0 .44-.03.65-.08l1.66 1.66c-.71.33-1.5.52-2.31.52-2.76 0-5.3-2.1-5.3-5.1H5c0 3.41 2.72 6.23 6 6.72V21h2v-3.28c.91-.13 1.77-.45 2.54-.9L19.73 21 21 19.73 4.27 3z"/></svg>`;
    const hangupSvg = `<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 9c-1.6 0-3.15.25-4.6.72v3.1c0 .39-.23.74-.56.9-.98.49-1.87 1.12-2.66 1.85-.18.18-.43.28-.7.28-.28 0-.53-.11-.71-.29L.29 13.08a.956.956 0 010-1.36C3.55 8.67 7.56 7 12 7s8.45 1.67 11.71 4.72c.18.18.29.43.29.71 0 .28-.11.53-.29.71l-2.48 2.48c-.18.18-.43.29-.71.29-.27 0-.52-.1-.7-.28-.79-.73-1.68-1.36-2.66-1.85a.993.993 0 01-.56-.9v-3.1C15.15 9.25 13.6 9 12 9z"/></svg>`;

    let agent = null;
    let isActive = false;
    let isMuted = false;
    let pollInterval = null;
    const displayed = new Set();

    bubble.addEventListener('click', () => panel.classList.toggle('open'));
    $('trillet-v-close').addEventListener('click', () => panel.classList.remove('open'));

    $('trillet-v-mic').addEventListener('click', () => {
      if (!isActive) connectAgent();
      else toggleMute();
    });

    async function connectAgent() {
      setStatus('Connecting...', false);
      $('trillet-v-label').textContent = 'Connecting...';
      $('trillet-v-mic').disabled = true;

      try {
        agent = new TrilletAgent({
          workspaceId: WORKSPACE_ID,
          agentId: AGENT_ID,
          mode: 'voice',
        });

        agent.on('connected', () => {
          isActive = true;
          isMuted = false;
          setStatus('Connected', true);
          $('trillet-v-label').textContent = 'Listening...';
          $('trillet-v-vis').classList.add('active');
          $('trillet-v-mic').disabled = false;
          showActiveControls();
          addMsg('Voice call connected', 'system');
          startPolling();
        });

        agent.on('disconnected', () => {
          isActive = false;
          setStatus('Disconnected', false);
          $('trillet-v-label').textContent = 'Call ended';
          $('trillet-v-vis').classList.remove('active');
          showIdleControls();
          stopPolling();
          addMsg('Call ended', 'system');
        });

        agent.on('error', () => {
          addMsg('An error occurred', 'system');
          setStatus('Error', false);
        });

        agent.on('assistantStartedSpeaking', () => {
          $('trillet-v-label').textContent = 'Agent speaking...';
        });

        agent.on('assistantStoppedSpeaking', () => {
          $('trillet-v-label').textContent = isMuted ? 'Muted' : 'Listening...';
        });

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

        await agent.startPublicCall();
      } catch (err) {
        setStatus('Failed', false);
        $('trillet-v-label').textContent = 'Failed to connect';
        $('trillet-v-mic').disabled = false;
        showIdleControls();
      }
    }

    function toggleMute() {
      isMuted = !isMuted;
      try { agent?.toggleMicrophone?.(!isMuted); } catch (e) {}
      const mic = $('trillet-v-mic');
      if (isMuted) {
        mic.className = 'trillet-v-mic muted';
        mic.innerHTML = micMutedSvg;
        $('trillet-v-label').textContent = 'Muted';
      } else {
        mic.className = 'trillet-v-mic active';
        mic.innerHTML = micSvg;
        $('trillet-v-label').textContent = 'Listening...';
      }
    }

    function endCall() { try { agent?.endCall(); } catch (e) {} }

    function showActiveControls() {
      $('trillet-v-btns').innerHTML = `
        <button class="trillet-v-end" id="trillet-v-end">${hangupSvg}</button>
        <button class="trillet-v-mic active" id="trillet-v-mic">${micSvg}</button>
      `;
      $('trillet-v-end').addEventListener('click', endCall);
      $('trillet-v-mic').addEventListener('click', toggleMute);
    }

    function showIdleControls() {
      $('trillet-v-btns').innerHTML = `
        <button class="trillet-v-mic idle" id="trillet-v-mic">${micSvg}</button>
      `;
      $('trillet-v-mic').addEventListener('click', () => {
        if (!isActive) connectAgent();
        else toggleMute();
      });
    }

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

    function stopPolling() {
      if (pollInterval) { clearInterval(pollInterval); pollInterval = null; }
    }

    function setStatus(text, online) {
      $('trillet-v-status-text').textContent = text;
      $('trillet-v-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-v-msg ' + type;
      el.textContent = text;
      const container = $('trillet-v-transcript');
      container.appendChild(el);
      container.scrollTop = container.scrollHeight;
    }
  </script>

</body>
</html>

Events Reference

EventDescription
connectedVoice call connected, agent is live
disconnectedCall ended
errorAn error occurred
assistantStartedSpeakingAgent is speaking
assistantStoppedSpeakingAgent stopped speaking
transcriptTranscript of speech (user or assistant)
agent.on('assistantStartedSpeaking', () => {
  console.log('Agent is talking...');
});