Linking widget activity to real users gives you accurate attribution in the dashboard, and lets users see their own votes, comments, and submissions when they open the widget.
The widget loads anonymously as soon as Quackback("init") runs — no identify call is required to make it appear. You call identify when you know who the user is, so their activity is attributed to their account instead of to an anonymous session.
Two ways to identify a known user:
- With their details — the simplest option. Pass
id,email, andnamedirectly from the client. - With a signed token — sign a short-lived JWT on your server and pass it as
ssoToken. Use this in production so nobody can impersonate another account.
If you never call identify, the widget stays anonymous and prompts for an email inline the first time the user posts.
Identify logged-in users
Call identify after login and any time the user's session changes.
Quackback("identify", {
id: "user_123",
email: "ada@example.com",
name: "Ada Lovelace",
});| Field | Type | Required | Notes |
|---|---|---|---|
id | string | Yes | Stable, unique ID from your system |
email | string | Yes | Used for notifications and dedup |
name | string | No | Display name shown in the dashboard |
Unverified calls trust the client, so a determined user could spoof someone else's identity. For production, turn on Verified identity only and use ssoToken — see below.
Anonymous sessions
Do nothing. The widget loads anonymously after init. If the visitor submits feedback or votes, a session is created on the fly and the widget prompts for an email inline at submission time.
Quackback("init", { placement: "right" });
// That's it — no identify call needed for anonymous visitors.Anonymous is the default state; there's no { anonymous: true } flag to pass. If they later log in and you call identify, everything they did anonymously moves across to their account automatically.
Bundle identity into init
If you already know the user when the widget loads, pass identity directly to init and skip the separate identify call:
Quackback("init", {
placement: "right",
identity: { id: "user_123", email: "ada@example.com", name: "Ada" },
});The identity key accepts the same shapes as the standalone call — { id, email, name } or { ssoToken }. Omit it entirely for anonymous, or if you'll call Quackback("identify", ...) later (for example, once async auth resolves).
Switch users mid-session
Call identify again with a different user any time the signed-in user changes — account switching, post-login transitions, or profile updates. The widget picks up the new identity and subsequent activity is attributed correctly.
// User A signs out, User B signs in:
Quackback("identify", { id: "user_B", email: "bob@example.com", name: "Bob" });No need to call logout in between — the new identify call replaces the previous session.
Clear identity on logout
Call logout to clear the current identity. The widget stays visible — just in an anonymous state, same as it started. Put this in your logout handler.
Quackback("logout");If the widget panel is open, it closes automatically.
Verified identity with JWT
Unverified calls and inline email capture both trust the client, so a determined user could post feedback as a different account. A signed token removes that possibility — Quackback only accepts identities your backend has vouched for.
Turn on Verified identity only in Admin → Settings → Widget and copy the Widget Secret. Once it's on, unverified identify calls and the inline email prompt are both rejected — only a valid token from your server will identify a user.
Never expose the widget secret in client-side code. Sign tokens on your server only.
How it works
Your server signs a JWT with HS256 using the widget secret. The widget sends ssoToken to Quackback, which verifies the signature before accepting the identity claims.
JWT claims
| Claim | Required | Notes |
|---|---|---|
sub | Yes | User's stable ID |
email | Yes | User's email address |
name | No | Display name |
avatarUrl | No | URL of the user's avatar image |
exp | Recommended | Unix timestamp; use ~5 minutes from now |
| custom | No | Any additional key-value pairs (see below) |
Server-side signing examples
// app/api/widget-token/route.ts
import { SignJWT } from "jose";
import { getServerSession } from "next-auth";
const secret = new TextEncoder().encode(process.env.QUACKBACK_WIDGET_SECRET);
export async function GET() {
const session = await getServerSession();
if (!session?.user) {
return new Response("Unauthorized", { status: 401 });
}
const token = await new SignJWT({
sub: session.user.id,
email: session.user.email,
name: session.user.name,
})
.setProtectedHeader({ alg: "HS256" })
.setExpirationTime("5m")
.sign(secret);
return Response.json({ token });
}Pass the token to the widget
Fetch the token from your endpoint and pass it as ssoToken.
const { token } = await fetch("/widget-token").then((r) => r.json());
Quackback("identify", { ssoToken: token });Fetch a fresh token on each login or page load. Tokens expire quickly by design — this limits the blast radius if one is intercepted.
Custom attributes
You can pass extra fields like plan, company, or mrr alongside the standard ones and they'll show up on the user's profile. This works for both unverified calls and signed tokens.
With an unverified identify call:
Quackback("identify", {
id: "user_123",
email: "ada@example.com",
name: "Ada Lovelace",
plan: "pro",
company: "Acme Corp",
mrr: 299,
});Or as extra claims inside a signed JWT:
// Server-side
const payload = {
sub: user.id,
email: user.email,
name: user.name,
plan: "pro",
company: "Acme Corp",
mrr: 299,
};Attributes are validated against the ones you've configured. Anything that doesn't match a configured attribute is ignored, not rejected — so you can pass extra data without worrying about breaking the call.
Set up your attributes in Admin → Settings → User Attributes first so nothing gets silently dropped.
Rotate the secret
If your secret is ever exposed, rotate it immediately:
- Go to Admin → Settings → Widget and click Regenerate Secret.
- Deploy updated server-side token signing with the new secret.
- The old secret is invalidated instantly — users will see an auth error until your server is updated.
Rotate and deploy atomically. Any lag between invalidating the old secret and deploying the new signing code will cause identify calls to fail for active users.
Next steps
- Custom attributes — configure which attributes appear in the dashboard
- Widget configuration — full list of widget init options
- Install the widget — embed snippet and SDK command reference