Quincer AI Docs Home Support Start free

Connect

Visitor sign-in

Verify who the visitor is on your platform before the AI calls any secure custom tool. Required for “What’s my balance?”, “Show my order”, “Cancel my subscription” — anything tied to a specific user account. Public tools (“track this package by tracking number”) work without sign-in.

i

Visitor sign-in is the foundation; the actual gating of which tools require it lives on each Custom tool (separate feature). You configure the verification here; you mark individual tools as “requires sign-in” on the Custom tools page.

When you need this

Two example flows side-by-side:

ScenarioSign-in required?
Visitor asks “where is order #ABC123?” (anyone can pass an order number) No
Visitor asks “what’s my account balance?” (must be the right person) Yes
Visitor asks “cancel my latest subscription” Yes
Visitor asks “what are your business hours?” No (knowledge-base answer)

How it works (architecture)

  1. Visitor signs in on your platform via your existing flow (email/password, SSO, Google, whatever).
  2. Your platform produces a signed token proving who the visitor is — either a standard JWT signed with public-key crypto (recommended) or a JWT signed with a shared HMAC secret.
  3. Your page calls ChatWidget.identify({ token }) after sign-in. The widget attaches the token to every chat request.
  4. Our chat backend verifies the token against the keys you registered on the Visitor sign-in config page.
  5. Verified identity is attached to the request context. Any custom tool marked “requires sign-in” can now run and gets the verified user id automatically.

We never store your visitor passwords. We only verify a token your auth system already issued.

Mode 1 — JWT + JWKS (recommended)

Your platform issues JWTs and exposes a JWKS endpoint with the public keys. We pull the keys, cache them for 15 minutes, and verify token signatures locally on every chat request.

You configure this on the Visitor sign-in page (Integrations → Back to Integrations links back to it). In the panel below, the top Mode card is a single choice — Disabled, JWT + JWKS, or HMAC shared secret. Picking JWT + JWKS reveals the JWKS endpoint card, where the one required field is the JWKS URL. The optional Required claims card below it (Issuer / Audience) and Claim mapping are covered later on this page. Click Save at the bottom to store it.

Mode
Pick how your platform identifies the visitor to the widget
Disabled
No verification. Secure tools cannot be called. Public tools (e.g. “track this order by ID”) still work without sign-in.
JWT + JWKS Recommended
Your platform issues signed JWTs (Auth0, Cognito, Clerk, or anything that serves a JWKS endpoint).
HMAC shared secret
Your backend signs a JWT with HS256 + a shared secret. Simpler if you don’t already have public-key infrastructure.

The Mode card — a single pick; JWT + JWKS is the recommended choice.

JWKS endpoint
Cached 15 min — rotation is automatic on next fetch
JWKS URL
https://your-domain.auth0.com/.well-known/jwks.json
Save

The JWKS endpoint card — paste the URL that serves your platform’s public signing keys.

Auth0 example

  1. In our dashboard: Integrations → Visitor sign-in → JWT + JWKS.
  2. JWKS URL: https://YOUR_DOMAIN.auth0.com/.well-known/jwks.json
  3. Issuer (optional): https://YOUR_DOMAIN.auth0.com/
  4. Audience (optional): the API identifier you set up in Auth0.
  5. Save.

On your page, after Auth0 login completes:

const auth0 = await createAuth0Client({ ... });
const token = await auth0.getTokenSilently();
ChatWidget.identify({ token });

Amazon Cognito example

Use the User Pool ID’s discovery doc — Cognito serves a standards-compliant JWKS at:

https://cognito-idp.{REGION}.amazonaws.com/{USER_POOL_ID}/.well-known/jwks.json

Issuer: https://cognito-idp.{REGION}.amazonaws.com/{USER_POOL_ID}.

Clerk / Firebase Auth / generic OIDC

Same pattern. Any provider that serves a JWKS works. Common endpoints:

Key rotation

Keys cache for 15 min. After you rotate keys, click the “Refresh JWKS + verify” button on the Test card to force an immediate refresh. Production traffic refreshes automatically on the next chat request after the cache expires.

Mode 2 — HMAC shared secret

Simpler if you don’t already have public-key infrastructure. Your backend signs a JWT with HS256 + a shared secret we store encrypted at rest. Less recommended than JWT/JWKS only because a leaked secret is a one-step path to forged identities — rotate it if you suspect compromise.

Setup

Pick HMAC shared secret in the Mode card and the Shared secret card appears. In the panel below, the Generate button next to the Secret field creates a random 32-byte value (base64) — copy it into your backend’s secrets manager and sign your JWTs with it using HS256. Once a secret is stored, the card shows a green Secret is configured (hidden) row with a Clear button, and the field re-labels to Replace secret so you can rotate it without ever seeing the old value (we never echo it back). The example sign code under the field is the exact jose call your backend runs. Save to store it encrypted at rest.

Shared secret
Stored encrypted at rest — sign your JWTs with HS256 using this same secret
✓ Secret is configured (hidden)Clear
Replace secret
Leave blank to keep the current secret
Generate

Example sign code (Node, jose): new SignJWT({sub, email}).setProtectedHeader({alg:'HS256'}).setExpirationTime('1h').sign(secretBytes)

Save

The Shared secret card — once set, it masks to “configured (hidden)” with a Clear button; Generate mints a fresh 32-byte secret.

  1. In our dashboard: Integrations → Visitor sign-in → HMAC shared secret.
  2. Click Generate to create a random 32-byte secret. Copy it to your backend’s secrets manager.
  3. Save.

Node example (sign in your backend)

Use the same jose library we use for verification:

import { SignJWT } from "jose";

const secret = new TextEncoder().encode(process.env.QUINCER_VISITOR_SECRET);

// Call this after your user signs in.
async function tokenForUser(user) {
  return new SignJWT({
    email: user.email,
    name: user.name,
    plan: user.plan,
  })
    .setProtectedHeader({ alg: "HS256" })
    .setSubject(user.id)          // → ends up as the canonical user id
    .setIssuedAt()
    .setExpirationTime("1h")       // short-lived; rotate on refresh
    .sign(secret);
}

Pass the token to the widget

On the page where your user lands after signing in:

<script>
  // Server-rendered: include the token in your HTML response.
  ChatWidget.identify({ token: "{{ JWT_FROM_BACKEND }}" });
</script>

Or fetch the token from your own /api/me/quincer-token endpoint after sign-in.

If you want the widget itself to prompt the visitor to sign in when a secure tool is needed, register a Sign-in URL in the config. The widget opens this URL in a new tab when an unauthenticated visitor asks for a secure tool; your sign-in page posts the token back when login succeeds.

Your sign-in page

// after login completes:
const token = await getQuincerTokenForCurrentUser();
window.opener.postMessage(
  { type: "quincer-identify", token },
  // Match the origin where the widget is embedded.
  "https://your-customer-site.com"
);
window.close();

The widget verifies the message origin matches the configured Sign-in URL before accepting the token, so a random tab can’t inject one.

Claim mapping

The model only sees what you tell it to see. The verifier extracts standard JWT claims by default (everything except iss/sub/aud/exp/iat/nbf/jti) and passes them to tools that need identity. To rename or restrict that, fill in the Claim mapping JSON. Example:

{
  "userId": "sub",
  "email": "email",
  "tier": "https://acme.example/tier",
  "accountId": "https://acme.example/account_id"
}

Now a custom tool that lists requiredClaims: ["userId", "tier"] works as expected, even though the JWT uses Auth0’s namespaced custom-claim convention.

Testing your setup

The config page has a built-in Test a token card:

In the panel below, paste a real token into the box and click Verify. In JWT mode a second Refresh JWKS + verify button drops the cached keys and refetches before verifying — use it right after rotating keys. A pass renders a green Verified result showing the resolved sub, the source (jwt or hmac), and the exact Claims the AI tools will see; a failure shows the reason and message instead.

Test a token
Run a real token through the verifier and see what the AI would see
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1c2VyXzg0MiIsImVtYWlsIjoiYXZhQGFjbWUuY29tIiwidGllciI6InBybyJ9...
Verify ↻ Refresh JWKS + verify
Verified
sub: user_842 · source: jwt
Claims (what the AI sees): { "email": "ava@acme.com", "tier": "pro" }

The Test a token card — a pass shows the resolved sub, source, and the claims your tools receive.

  1. Sign in on your platform and grab a real token (browser dev tools, your backend, your auth provider’s dashboard).
  2. Paste it into the Test card and click Verify.
  3. You’ll see either a green “Verified” result with the resolved identity (this is what the AI tools see), or a clear failure reason and message.

Use this to iterate on the JWKS URL, the issuer/audience values, and the claim mapping without bouncing through your full sign-in flow.

Common failure reasons

ReasonWhat it meansFix
missing-token No Authorization: Bearer header on the request. Call ChatWidget.identify({ token }) before the visitor sends a message.
config-incomplete You picked a mode but didn’t finish setup. JWT mode needs a JWKS URL; HMAC needs a secret.
expired The token’s exp is past. Refresh the token in your sign-in flow and re-call identify.
bad-signature Token signed with a different key than we have. Rotated keys? Click Refresh JWKS + verify. HMAC mismatch? Re-copy the secret to your backend.
bad-claim iss / aud enforced and the token doesn’t match; or no sub claim. Check the issuer/audience values, or stop enforcing them. Every token must have sub — we use it as the stable user id.
jwks-fetch-failed We couldn’t reach the JWKS URL. Confirm the URL is public (no auth required) and serves JSON.

Security notes

!

Your visitor session expiration is yours to control. We honor whatever exp your JWT carries. Don’t issue 1-year tokens — a leaked one is valid for that whole window.