1. Docs
  2. Install

Install in Vue & Nuxt

Add the TalkingDot messenger to a Vue 3 app built with Vite, or to a Nuxt 3 site. Load it once at the app root, identify signed-in users, and let it follow router changes automatically.

Before you start: copy your workspace key from Settings → Install. It's the 16-character data-key value. The key is public, so it can live in VITE_TALKINGDOT_KEY or Nuxt's public runtime config. Your workspace secret (Settings → Security) must stay on the server.

Vue 3 (Vite): quickest option

Paste the snippet into index.html in your project root, just before </body>. It loads once per page load, outside Vue, and nothing else is needed.

index.html
  <body>
    <div id="app"></div>
    <script type="module" src="/src/main.ts"></script>

    <script src="https://talkingdot.com/widget.js" data-key="YOUR_WORKSPACE_KEY" async></script>
  </body>

Vue 3 (Vite): load from main.ts

To keep the key in an environment variable, load the script from code instead. This helper adds widget.js only once, and queues calls made before the script has loaded:

src/talkingdot.ts
type TalkingDotFn = ((...args: any[]) => any) & { q?: unknown[][] };

declare global {
  interface Window {
    TalkingDot?: TalkingDotFn;
    TalkingDotSettings?: Record<string, unknown>;
  }
}

function api(): TalkingDotFn {
  if (!window.TalkingDot) {
    const q: unknown[][] = [];
    window.TalkingDot = Object.assign((...args: unknown[]) => { q.push(args); }, { q });
  }
  return window.TalkingDot;
}

export function talkingdot(...args: unknown[]): unknown {
  return typeof window === 'undefined' ? undefined : api()(...args);
}

export function loadTalkingDot(key: string): void {
  if (typeof window === 'undefined' || document.getElementById('talkingdot-js')) return;
  api();
  const s = document.createElement('script');
  s.id = 'talkingdot-js';
  s.async = true;
  s.src = 'https://talkingdot.com/widget.js';
  s.setAttribute('data-key', key);
  document.head.appendChild(s);
}
src/main.ts
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
import { loadTalkingDot } from './talkingdot';

createApp(App).use(router).mount('#app');
loadTalkingDot(import.meta.env.VITE_TALKINGDOT_KEY);

You can also call loadTalkingDot() from onMounted in App.vue. Either way, call it from the root once, not from individual route components.

Nuxt 3

Add the key to your public runtime config. You can set it with the NUXT_PUBLIC_TALKINGDOT_KEY environment variable:

nuxt.config.ts
export default defineNuxtConfig({
  runtimeConfig: {
    talkingdotSecret: '',               // server only (NUXT_TALKINGDOT_SECRET), used for user_hash
    public: { talkingdotKey: '' },      // NUXT_PUBLIC_TALKINGDOT_KEY
  },
});

Option A: useHead in app.vue

app.vue
<script setup lang="ts">
const config = useRuntimeConfig();

useHead({
  script: [
    { src: 'https://talkingdot.com/widget.js', 'data-key': config.public.talkingdotKey, async: true },
  ],
});
</script>

<template>
  <NuxtLayout>
    <NuxtPage />
  </NuxtLayout>
</template>

Because app.vue wraps every page, the tag is rendered once and stays loaded as visitors navigate.

Option B: a client-only plugin

A .client.ts plugin runs only in the browser. It also gives you $talkingdot() everywhere, with calls queued until the script loads:

plugins/talkingdot.client.ts
export default defineNuxtPlugin(() => {
  const key = useRuntimeConfig().public.talkingdotKey as string;
  const w = window as any;

  w.TalkingDot = w.TalkingDot || function () { (w.TalkingDot.q = w.TalkingDot.q || []).push(arguments); };
  if (!document.getElementById('talkingdot-js')) {
    const s = document.createElement('script');
    s.id = 'talkingdot-js';
    s.async = true;
    s.src = 'https://talkingdot.com/widget.js';
    s.setAttribute('data-key', key);
    document.head.appendChild(s);
  }

  return { provide: { talkingdot: (...args: unknown[]) => w.TalkingDot(...args) } };
});

Use either Option A or Option B, not both.

Identify signed-in users

Watch your auth state. Call update when a user is known, and shutdown after a real sign-out so the next person on that browser starts a fresh, anonymous session. The user_hash must come from your server. See Identity verification.

App.vue
<script setup lang="ts">
import { watch } from 'vue';
import { useAuthStore } from '@/stores/auth'; // your own store (Pinia, etc.)
import { talkingdot } from '@/talkingdot';         // Nuxt: const { $talkingdot } = useNuxtApp()

const auth = useAuthStore();
const FLAG = 'talkingdot_identified';

watch(
  () => (auth.ready ? auth.user : undefined), // undefined while the auth check is loading
  (user) => {
    if (user === undefined) return;
    if (user) {
      talkingdot('update', { user_id: String(user.id), email: user.email, name: user.name, user_hash: user.talkingdotHash });
      localStorage.setItem(FLAG, '1');
    } else if (localStorage.getItem(FLAG)) {
      localStorage.removeItem(FLAG);
      talkingdot('shutdown');
    }
  },
  { immediate: true },
);
</script>

In Nuxt, compute the hash in a server route, so talkingdotSecret never leaves the server, and return it with the current user:

server/api/me.get.ts
import { createHmac } from 'node:crypto';

export default defineEventHandler(async (event) => {
  const user = await getUserFromSession(event); // your own auth
  if (!user) return null;
  const hash = createHmac('sha256', useRuntimeConfig(event).talkingdotSecret)
    .update(String(user.id))
    .digest('hex');
  return { id: user.id, email: user.email, name: user.name, talkingdotHash: hash };
});

Router changes

Nothing to add. The messenger watches the History API and hash changes, so every Vue Router and Nuxt navigation is recorded as a page view. Page rules and proactive messages are checked again on each route. Don't add router hooks that reload the script or call shutdown on navigation.

Verify it works

  1. Open your app in a private window. The launcher bubble appears in the bottom-right corner, or on whichever side you chose.
  2. Navigate between a few routes, then send a test message. In the Inbox, the contact sidebar shows the pages you visited.
  3. Sign in, then sign out. Your user's details appear on the conversation. After signing out, the messenger starts an empty, anonymous session.

Troubleshooting

  • TalkingDot: missing or invalid data-key: the key variable is empty. Vite only exposes VITE_ variables, and Nuxt reads NUXT_PUBLIC_TALKINGDOT_KEY at runtime. Restart the dev server after changing .env.
  • Hydration warnings or window is not defined: browser-only code ran on the server. Keep it in onMounted, a .client.ts plugin, or behind import.meta.client.
  • Nothing on localhost or preview deploys: if you set a domain allow-list in Settings → Security, add localhost and your preview domains.
  • widget.js downloads twice: the snippet is in both index.html and your code, or you used both Nuxt options. Keep one. The second copy is ignored, but it still costs a download.

Next steps

Last updated October 4, 2026 Something unclear? Tell us