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
- In Settings → Single sign-on, create the secret, store it on your server as
UPVOICED_SSO_SECRETand the Audience asUPVOICED_WORKSPACE_ID, then turn on Accept SSO tokens from my app. - List your site under allowed origins. From any other site,
identifyfails withnetwork_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 }withid,name,emailandavatarUrl, or{ ok: false, error }.UpVoiced.userholds the current user ornull. - 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
identifywith{ 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 (
unreadevent).
| error | Meaning |
|---|---|
invalid_token | Not a compact JWT (or over 4 KB); nothing was sent. |
sso_token_invalid | Wrong secret, audience, expiry or a token used twice (see the SSO guide’s errors). |
sign_in_method_disabled | Single sign-on is off or has no secret. |
network_error | The API was unreachable, or the site is not an allowed origin. |
rate_limited | Too many attempts; try again later. |
api_unavailable | w.js was not loaded from cdn.upvoiced.com (for example a self-hosted copy). |