Identify users

Connect widget activity to real users so feedback is always attributed correctly.

JM
James Morton
Written By James MortonLast updated 11 days ago

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, and name directly 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: "[email protected]",
  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

Note: 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: "[email protected]", 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: "[email protected]", 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.

Warning: 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

<Tabs> <Tab title="Next.js"> ```ts // 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 }); }

</Tab>
<Tab title="Express">
```js
// routes/widget-token.js
const jwt = require("jsonwebtoken");

router.get("/widget-token", requireAuth, (req, res) => {
  const token = jwt.sign(
    {
      sub: req.user.id,
      email: req.user.email,
      name: req.user.name,
    },
    process.env.QUACKBACK_WIDGET_SECRET,
    { algorithm: "HS256", expiresIn: "5m" }
  );

  res.json({ token });
});

</Tab> <Tab title="Django"> ```python # views.py import jwt import time from django.conf import settings from django.http import JsonResponse from django.contrib.auth.decorators import login_required

@login_required def widget_token(request): payload = { "sub": str(request.user.id), "email": request.user.email, "name": request.user.get_full_name(), "exp": int(time.time()) + 300, # 5 minutes } token = jwt.encode( payload, settings.QUACKBACK_WIDGET_SECRET, algorithm="HS256", ) return JsonResponse({"token": token})

</Tab>
<Tab title="Rails">
```ruby
# app/controllers/widget_tokens_controller.rb
require "jwt"

class WidgetTokensController < ApplicationController
  before_action :authenticate_user!

  def show
    payload = {
      sub: current_user.id.to_s,
      email: current_user.email,
      name: current_user.name,
      exp: 5.minutes.from_now.to_i,
    }
    token = JWT.encode(payload, ENV["QUACKBACK_WIDGET_SECRET"], "HS256")
    render json: { token: }
  end
end

</Tab> <Tab title="Laravel"> ```php // app/Http/Controllers/WidgetTokenController.php use Firebase\JWT\JWT;

class WidgetTokenController extends Controller { public function show(Request $request) { $this->middleware('auth');

    $payload = [
        'sub'   => (string) $request->user()->id,
        'email' => $request->user()->email,
        'name'  => $request->user()->name,
        'exp'   => time() + 300, // 5 minutes
    ];

    $token = JWT::encode($payload, config('services.quackback.widget_secret'), 'HS256');

    return response()->json(['token' => $token]);
}

}

</Tab>
</Tabs>

### Pass the token to the widget

Fetch the token from your endpoint and pass it as `ssoToken`.

```js
const { token } = await fetch("/widget-token").then((r) => r.json());

Quackback("identify", { ssoToken: token });

Tip: 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: "[email protected]",
  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.

Note: 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:

  1. Go to Admin → Settings → Widget and click Regenerate Secret.
  2. Deploy updated server-side token signing with the new secret.
  3. The old secret is invalidated instantly — users will see an auth error until your server is updated.

Warning: 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.



Sign portal users in from the widget

If you run a private portal, you can use the widget identity as a sign-in for the portal too — visitors who are already known to the widget don't need to sign in again to open the full portal.

Two things have to be true:

  1. Verified identity only is on (Admin → Settings → Widget). The widget is signing JWTs, not just capturing emails.
  2. Widget sign-in is on for the portal (Admin → Settings → Security → Authentication → Portal access).

When both are on, visitors who follow a "View full portal" link from the widget are signed in automatically via a one-time token. Email-capture mode (unverified) does not grant portal access — there's no cryptographic guarantee of identity in that flow.

See Control portal access for the admin-side toggle and the full grant rules.


Next steps

Was this helpful?

Your feedback shapes what we write next.