Gauge Support quick start
Wire your SaaS to Gauge Support in about 15 minutes: provision your users, embed the chat widget (or sign users into the portal), and receive verified webhooks.
The same flow is in the console at Developers → Support (/developers/support). There, every
snippet is already filled with your base URL and project id, and each step has a Check that
verifies it against your workspace. This page mirrors it.
| You need | Where |
|---|---|
| A support project | Support → Settings → Projects |
| Your API base URL | Developers → Support (production: https://app.gauge.chat/backend) |
| Node ≥18 or Python ≥3.9 or curl | — |
SDKs: npm install @gauge/support (zero dependencies, ESM + CJS, Node ≥18 and edge runtimes) and
pip install gauge-support (GaugeSupport / AsyncGaugeSupport over httpx, plus the identity
hash and webhook helpers; Python ≥3.9). Both expose the same resources (projects, customers,
tickets, sessions) with the same retry and Idempotency-Key rules.
Try it free first: the sandbox
Every workspace has one sandbox project, created the first time you open Developers →
Support or make a request with a test key. Pick Sandbox · test keys only in the project
picker and click Create test key: you get a pulse_k_test_ key that reaches the sandbox and
nothing else. Run every step below against it.
- It's free: it never counts toward your plan's projects, conversations, usage or analytics.
- Nothing leaves the building: emails are recorded but never sent, there is no SMS, voice or AI
spend, and webhooks arrive with
livemode: false. - A test key on a live project, or a live key on the sandbox, is 403 with
reasontest_key_live_project/live_key_sandbox_project. Sandbox customers are invisible to live keys, and live customers to test keys, so the two never mix. - Reset sandbox (on the sandbox panel, typed confirmation) deletes its customers, tickets, chats and articles. The project id, your test keys and your widget embed keep working.
Find the sandbox's id with gauge.projects.list() (the row with isSandbox: true; a test key
sees only that one). When you are ready, switch the picker to your real project and create a live
key there.
Step 1 · Create a project-pinned key (2 min)
In Developers → Support, choose the project and click Create support key. You get a live key that can reach only that project. Every other project, and any customer reachable only through one, answers 404. Choose:
- Sign customers in (
support-sessions:rw): needed for portal sign-in links (step 3b). - Erase customers (GDPR) (
support-erase:rw): leave it off unless your backend runs erasure.
support:rw is always included. *:rw never implies the two capability scopes. Store the key as
GAUGE_API_KEY in your server's secret manager. It is shown once.
Test keys (
pulse_k_test_) are for the sandbox: on the sandbox project the button reads Create test key, and that key works there and nowhere else. Live keys never reach the sandbox.
curl -s $BASE/v1/support/projects/$PROJECT_ID -H "Authorization: Bearer $GAUGE_API_KEY"import { GaugeSupport } from '@gauge/support';
export const gauge = new GaugeSupport({ apiKey: process.env.GAUGE_API_KEY!, baseUrl: BASE });
await gauge.projects.get(PROJECT_ID);import os
from gauge_support import GaugeSupport
gauge = GaugeSupport(os.environ["GAUGE_API_KEY"], base_url=BASE) # or AsyncGaugeSupport
gauge.projects.get(PROJECT_ID)Python mirrors every call below in snake_case with the API's camelCase bodies, e.g.
gauge.customers.upsert_by_ref(str(user.id), {"email": ..., "projectIds": [PROJECT_ID]}),
gauge.customers.sync(batch, idempotency_key=derive_idempotency_key("nightly", day, i)), and
for c in gauge.customers.list_updated_since(since): ... then pager.high_watermark.
Check passes when a live key pinned to this project has made a request (on the sandbox: when a test key has). A workspace-wide key works but shows a warning, because a leak would expose every project.
Step 2 · Provision and sync your users (4 min)
Address every user by your own stable user id (externalRef), never by email. The full rules
are in customer-sync.md.
On sign-up or change: upsert one user.
await gauge.customers.upsertByRef(user.id, {
email: user.email, name: user.name, locale: 'ar',
projectIds: [PROJECT_ID], // authoritative within your key's projects
metadata: { plan: user.plan }, // shown to agents; no secrets or regulated data
});
// → { outcome: 'created' | 'updated' | 'unchanged', customer }Batch: up to 500 per call. Each item stands alone; check every error.
import { deriveIdempotencyKey } from '@gauge/support';
const run = await gauge.customers.sync(batch, { idempotencyKey: await deriveIdempotencyKey('nightly', day, i) });The same Idempotency-Key with the same body replays the first answer, including the same
runId. The same key with a different body is a 409.
Nightly reconcile.
const pager = gauge.customers.listUpdatedSince(new Date(Date.parse(W) - 5 * 60_000));
for await (const c of pager) await repair(c);
await saveWatermark(pager.highWatermark); // exactly as returned; subtract the 5 min at READ timeDeactivate or block many users with gauge.customers.setStatus('deactivated', { externalRefs }).
deactivated makes the user read-only; blocked locks them out and drops live sessions.
Check passes when at least one customer holds active access to the project.
Step 3a · Embed the widget (4 min)
Once per project, provision a chat line, turn chat on, require your server's hash and allow
your origin. Then reveal the chat secret once and store it as GAUGE_CHAT_SECRET:
const p = await gauge.projects.get(PROJECT_ID);
await gauge.projects.update(PROJECT_ID, {
version: p.version, identityMode: 'hmac_verified', chatEnabled: true,
chatConnection: { mode: 'provision' }, chatAllowedOrigins: ['https://app.example.com'],
});
const { secret } = await gauge.projects.revealChatSecret(PROJECT_ID); // a second reveal is 409Per page view, on your server: sign the identity (v2: the user id and the time, so a leaked hash expires after 10 minutes).
import { signIdentity } from '@gauge/support';
const identity = await signIdentity(process.env.GAUGE_CHAT_SECRET!, user.id);
// { externalId, identityHash, identityTs } — identityTs is epoch SECONDSfrom gauge_support import sign_identity
identity = sign_identity(os.environ["GAUGE_CHAT_SECRET"], str(user.id))TS=$(date +%s)
printf '%s:%s' "$USER_ID" "$TS" | openssl dgst -sha256 -hmac "$GAUGE_CHAT_SECRET" | sed 's/^.* //'On the page: pin the live release with its integrity hash. The console fills in version
and integrity from https://widget.gauge.chat/v1/manifest.json (files['widget.js']).
<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: '<project chatConnectionId>',
visitorId: '{{ user.id }}',
identityHash: '{{ identity.identityHash }}',
identityTs: {{ identity.identityTs }},
supportMode: true,
});
});
</script>Or from a bundler: import { loadWidget } from '@gauge/support/browser' and call
loadWidget({ version, integrity, apiUrl, connectionId, visitorId, identityHash, identityTs, supportMode: true }).
It refuses to load a pinned version without a valid sha384- integrity, and an identityTs that
is not whole epoch seconds.
Your page's CSP needs script-src https://widget.gauge.chat and
connect-src https://app.gauge.chat wss://app.gauge.chat (details in
widget-hosting.md §4).
identityHash v2 is the default. The widget sends the hash in the
x-pulse-identityheader andexternalId+identityTsin thePOST /support/chat/sessionbody; the server accepts the timestamp within ±10 minutes. Because the hash is signed at page render, a page left open longer than that should re-render (or callPulseWebchat.initagain) before the visitor opens chat. Once every page sendsidentityTs, turn on the project'schatIdentityTsRequired(gauge.projects.update(id, { version, chatIdentityTsRequired: true })) so a v1 hash, which never expires, is refused. v1 (identityHash(secret, userId), noidentityTs) keeps working until then. Signing by hand? v2 ishex(HMAC-SHA256(secret, "v2\n" + userId + "\n" + identityTs)), deliberately notuserId:identityTs, so no v1 hash can ever pass as v2.
Check passes when the manifest is reachable and the project has chat on, a chat line, and
hmac_verified identity.
Step 3b · Or sign users into the portal or a WebView
Mint a single-use link per open (≤15 minutes; needs support-sessions:rw) and redirect to it.
Never cache or share it.
const { url } = await gauge.sessions.mint({ projectId: PROJECT_ID, externalRef: user.id, email: user.email, ttlSeconds: 300 });
return Response.redirect(url, 302);The project needs a usable portal address. A new project is given a platform address derived
from the deployment's portal template (<key>.support.gauge.chat on Gauge), and a custom domain
counts once verified under Support → Settings → Domains. With no usable address the mint answers
404 "No session link is available", the same answer as an unknown user, by design. If that
happens, check the project's Domains page first.
Step 4 · Receive and verify webhooks (4 min)
Add an endpoint in Developers → Webhooks subscribed to support.customer.* (and
support.ticket.* if you want tickets) and store its signing secret as GAUGE_WEBHOOK_SECRET.
Your receiver must:
- Verify the signature on the raw body.
x-pulse-signatureissha256=<hex HMAC-SHA256(secret,${x-pulse-timestamp}.${rawBody})>. During a rotation on our side it carries two comma-separated values, and any match passes. - Reject anything more than 5 minutes from your clock.
x-pulse-timestampis epoch milliseconds. - Dedupe on the body's
id. It is stable across retries and manual resends. Droplivemode: falsein production: it marks events about the sandbox project. - Answer 2xx within 10 s and do the work asynchronously. Failed deliveries retry at 30 s · 5 m · 1 h · 6 h.
// Next.js route handler (Node or Edge)
import { verifyWebhook } from '@gauge/support';
export async function POST(req: Request) {
const r = await verifyWebhook([process.env.GAUGE_WEBHOOK_SECRET!], await req.text(), req.headers);
if (!r.valid) return new Response(r.reason, { status: 400 });
if (await seen(r.event.id)) return new Response(null, { status: 200 });
await queue.add(r.event);
return new Response(null, { status: 200 });
}# FastAPI (Django and Flask recipes: packages/sdk-python/README.md)
from gauge_support import verify_webhook
@app.post("/gauge/webhooks")
async def gauge_webhook(request: Request):
r = verify_webhook([os.environ["GAUGE_WEBHOOK_SECRET"]], await request.body(), request.headers)
if not r.valid:
raise HTTPException(400, r.reason)
if not seen(r.event["id"]):
enqueue(r.event)
return {"ok": True}Hold both secrets in the list while you rotate. Comparison is constant-time, and every secret × signature pair is checked.
Check passes when an enabled endpoint hears support customer events and its last delivery succeeded. Use Send test event on the endpoint page to trigger one.
Errors you will meet
| HTTP | code / reason | Meaning |
|---|---|---|
| 401 | unauthorized | key missing, unknown, revoked or expired |
| 403 | forbidden / test_key_live_project | a pulse_k_test_ key naming a live project (test keys reach only the sandbox) |
| 403 | forbidden / live_key_sandbox_project | a live key naming the sandbox project |
| 403 | forbidden | the key lacks the scope or capability (support-sessions:rw, support-erase:rw) |
| 403 | customer_inactive | ticket for a blocked or deactivated customer |
| 404 | not_found | absent, another tenant's, or outside your key's projects (indistinguishable on purpose) |
| 409 | conflict | stale version, or an Idempotency-Key reused with a different body |
| 422 | email_required · email_in_use · email_unavailable | see customer-sync.md §2 |
| 429 | rate_limited | honour Retry-After (the SDK does) |
Both SDKs raise a typed class per status (Python: GaugeValidationError … GaugeConnectionError
with .status, .code, .reason). The Node SDK raises (GaugeValidationError, GaugeAuthenticationError,
GaugePermissionError, GaugeNotFoundError, GaugeConflictError, GaugeRateLimitError,
GaugeServerError, GaugeConnectionError) carrying status, code and reason. It retries
429, 5xx and network errors only where a repeat cannot double a write.
Verified end to end
On 2026-10-14 this flow ran against a local stack twice. First with a live pinned key (30
assertions, dx-e2e/transcript.txt). Then with a test key against the sandbox (41 assertions,
dx-e2e/transcript-sandbox.txt), using both SDKs: the sandbox is created on first visit; steps 1–4
pass with the test key in Node and in the Python client; identity v2 opens a widget session while
v1 and an 11-minute-old v2 hash are refused; every webhook arrives with livemode: false; the
ticket emails are recorded sent / sandbox and never handed to the transport; a test key on a
live project and a live key on the sandbox are both 403; and Reset sandbox deletes the test data
while the project, its id and its chat line survive. The transcripts are kept with the
support-embed programme notes.
Support API
Put Gauge's live chat, tickets and callbacks inside your own web or mobile app, signed in as your own users.
Quick start: chat in your website or web app (signed-in users)
About 15 minutes. You add one endpoint to your server and one snippet to your page. Gauge then knows which user is chatting and which Integration they are on.