Skip to content
UpVoiced Docs

UpVoiced Docs · Widget

Identify signed-in users

People who are signed in to your app can vote, post and follow in the widget without the email code. Your server signs a token for the current user; the page hands it to UpVoiced.identify, which exchanges it for an UpVoiced session kept in memory.

This page covers the widget. The single sign-on guide explains the token’s claims, the same signer in Python, Ruby, PHP and Go, portal sign-in, secret rotation and every error code.

1. Turn on single sign-on

  1. In Settings → Single sign-on, create the secret, store it on your server as UPVOICED_SSO_SECRET and the Audience as UPVOICED_WORKSPACE_ID, then turn on Accept SSO tokens from my app.
  2. List your site under allowed origins. From any other site, identify fails with network_error.

2. Sign a token on your server

Never sign in the browser: the secret would be public. The signer below has no dependencies (Node.js 18+); each token is valid for 5 minutes and works once, so sign a new one for every identify.

// UpVoiced SSO: sign a token for the user who is signed in to YOUR app. Server-side only.
// Node.js 18+, no dependencies. Keep UPVOICED_SSO_SECRET out of browsers, logs and git.
import { createHmac, randomUUID } from 'node:crypto';

const base64url = (value) => Buffer.from(value).toString('base64url');

/**
 * @param {object} options
 * @param {string} options.secret UPVOICED_SSO_SECRET (Settings → Single sign-on)
 * @param {string} options.workspaceId the Audience shown next to the secret
 * @param {{ id: string | number, email?: string, name?: string, avatarUrl?: string }} options.user
 * @returns {string} a token valid for 5 minutes, usable once
 */
export function signUpVoicedToken({ secret, workspaceId, user }) {
  const now = Math.floor(Date.now() / 1000);
  const header = { alg: 'HS256', typ: 'JWT' };
  const payload = {
    aud: workspaceId,
    sub: String(user.id),
    email: user.email,
    name: user.name,
    avatar_url: user.avatarUrl,
    iat: now,
    exp: now + 300,
    jti: randomUUID(),
  };
  const signingInput = `${base64url(JSON.stringify(header))}.${base64url(JSON.stringify(payload))}`;
  const signature = createHmac('sha256', secret).update(signingInput).digest('base64url');
  return `${signingInput}.${signature}`;
}
// server.mjs (Express): a token for the user who is signed in to YOUR app.
import express from 'express';
import { signUpVoicedToken } from './sign-token.mjs';

const app = express();

// requireLogin is your own middleware: only signed-in users get a token.
app.get('/upvoiced/token', requireLogin, (req, res) => {
  const token = signUpVoicedToken({
    secret: process.env.UPVOICED_SSO_SECRET, // server-side only
    workspaceId: process.env.UPVOICED_WORKSPACE_ID,
    user: { id: req.user.id, email: req.user.email, name: req.user.name },
  });
  res.set('Cache-Control', 'no-store').json({ token });
});

Send email only if your app has verified it: UpVoiced uses it for notifications and to link an account that already used an email code (existing accounts).

3. Call UpVoiced.identify on the page

Run this on every page load where someone is signed in. The queue stub lets you call identify before w.js has loaded.

<script>
  window.UpVoiced = window.UpVoiced || { q: [] };
  ['identify', 'config', 'open', 'close', 'on', 'track'].forEach(function (m) {
    window.UpVoiced[m] =
      window.UpVoiced[m] ||
      function () {
        window.UpVoiced.q.push([m].concat([].slice.call(arguments)));
      };
  });
  // Only when someone is signed in to your app: ask your server for a fresh token.
  fetch('/upvoiced/token', { credentials: 'same-origin' })
    .then(function (response) {
      if (!response.ok) throw new Error('not signed in');
      return response.json();
    })
    .then(function (body) {
      UpVoiced.identify({ token: body.token });
    })
    .catch(function () {
      // Signed out: the widget asks for an email code when they vote or post.
    });
  UpVoiced.on('identify', function (result) {
    if (!result.ok) console.warn('UpVoiced sign-in failed:', result.error);
  });
</script>
<script async src="https://cdn.upvoiced.com/w.js" data-workspace="your-slug"></script>

In a single-page app, identify after your own sign-in completes and pass null when the user signs out:

// In a single-page app: identify after your own sign-in, sign out with yours.
async function identifyFeedbackUser() {
  const response = await fetch('/upvoiced/token', { credentials: 'same-origin' });
  if (!response.ok) return;
  const { token } = await response.json();
  const result = await window.UpVoiced.identify({ token });
  if (!result.ok) console.warn('UpVoiced sign-in failed:', result.error);
}

function signOutOfFeedback() {
  window.UpVoiced.identify(null); // forgets the in-memory session
}

How identify behaves

  • It returns a promise that never rejects and also emits identify: { ok: true, user } with id, name, email and avatarUrl, or { ok: false, error }. UpVoiced.user holds the current user or null.
  • The last call wins: a slower, earlier identify never replaces a newer one. A failed identify signs out the previous user rather than keep acting as them.
  • The session lives in memory for up to 12 hours: never in cookies or storage, and never shared with your page’s scripts. A reload needs a new token. When the session expires the widget emits identify with { ok: false, error: "session_expired" }.
  • After a successful identify the launcher shows a dot for unread changelog entries and status changes on posts the person follows or voted on (unread event).
identify errors
errorMeaning
invalid_tokenNot a compact JWT (or over 4 KB); nothing was sent.
sso_token_invalidWrong secret, audience, expiry or a token used twice (see the SSO guide’s errors).
sign_in_method_disabledSingle sign-on is off or has no secret.
network_errorThe API was unreachable, or the site is not an allowed origin.
rate_limitedToo many attempts; try again later.
api_unavailablew.js was not loaded from cdn.upvoiced.com (for example a self-hosted copy).