Support APIQuick starts by stack

Quick start: Next.js (App Router)

Next.js 14+ with route handlers. About 15 minutes to a verified chat message.

npm install @gauge/support
# .env.local (server only: no NEXT_PUBLIC_ prefix, so none of these reach the browser)
GAUGE_APP_ID=<app id>
GAUGE_IDENTITY_SECRET=<identity secret>
GAUGE_WEBHOOK_SECRET=<whsec_…>

1. Token endpoint: app/gauge-token/route.js

currentUser() is your auth (NextAuth's auth(), Clerk's currentUser(), your session read).

import { mintIntegrationToken } from '@gauge/support';
import { currentUser } from '@/lib/auth';

export const dynamic = 'force-dynamic';

export async function GET() {
  const user = await currentUser();
  if (!user) return Response.json({ error: 'sign in first' }, { status: 401 });
  const token = await mintIntegrationToken(process.env.GAUGE_IDENTITY_SECRET, {
    appId: process.env.GAUGE_APP_ID,
    userId: String(user.id),
    integration: 'customer',
    name: user.name,
    email: user.email,
  });
  return Response.json({ token }, { headers: { 'cache-control': 'no-store' } });
}

2. The widget (your page)

<script src="https://widget.gauge.chat/v1/<version>/widget.js"
  integrity="sha384-<integrity>" crossorigin="anonymous" async></script>
<script>
  window.addEventListener('load', function () {
    window.PulseWebchat.init({
      apiUrl: 'https://app.gauge.chat/backend',
      connectionId: '<chat connection id>',
      supportMode: true,
      integration: 'customer',
      getToken: function () { // called on every open, so the token is always fresh
        return fetch('/gauge-token', { credentials: 'include' })
          .then(function (r) { return r.json(); })
          .then(function (j) { return j.token; });
      },
    });
  });
</script>

CSP: script-src https://widget.gauge.chat and connect-src https://app.gauge.chat wss://app.gauge.chat. Signed-out visitors: leave out getToken; they chat anonymously where the Integration allows it. Check my site (Developers → Support) fetches your page and tells you if the snippet, its integrity and the connection id are right.

In a React tree, load it once from your root layout with next/script (strategy="afterInteractive") and call PulseWebchat.init in its onLoad.

3. Webhook: app/gauge/webhooks/route.js

Read the body with request.text(): the signature covers the exact bytes, so never request.json() first.

import { verifyWebhook } from '@gauge/support';

const handled = new Set(); // use your database in production: ids repeat on retries and resends

export async function POST(request) {
  const raw = await request.text();
  const result = await verifyWebhook(process.env.GAUGE_WEBHOOK_SECRET, raw, request.headers);
  if (!result.valid) return new Response(result.reason, { status: 400 });
  const event = result.event;
  if (!handled.has(event.id)) {
    handled.add(event.id);
    console.info('gauge', event.event, event.id, event.livemode ? 'live' : 'sandbox');
  }
  return new Response(null, { status: 200 });
}

4. Check it

  1. Sign in to your site as a test user (in the sandbox: Seed test customers, then sign in as test_customer_1), open the widget and send "hello". In the console the conversation shows the user's name and Verified. Not verified? Paste the JSON from /gauge-token into Developers → Support → Token checker: it names the rule that failed.
  2. Developers → Webhooks → your endpoint → Send test event, pick support.chat.message_created: the result says Delivered · 200 and your log shows the event id. Change one byte of the secret in your env and send again: your handler answers 400 bad_signature.

Server was down? Replay failed on the same page resends everything that did not land.

On this page