Authentication
Before the SDK can load or post comments it needs a user session. You choose how that session is established by whether you pass onTokenNeeded to MarkUpSDK.init():
- Bring your own token (
onTokenNeeded) — authenticate your end users with your own backend. Use this when your app already knows who the user is. - Sign in with MarkUp (omit
onTokenNeeded) — let the SDK sign users into their own MarkUp accounts.
When render() runs, the SDK resolves a session in this order:
onTokenNeeded → cached session → silent SSO → interactive sign-inIf you provide onTokenNeeded, it wins. Otherwise the SDK reuses a cached session, then tries a silent single-sign-on, and finally falls back to an interactive Sign in button.
Mode A — Bring your own token
This is the right mode when your application has already authenticated the user and you want their MarkUp comments attributed to that same identity.
How it works
- The SDK calls your
onTokenNeededcallback. - Your backend signs a short-lived JWT with the private key for your SDK installation (RS256).
onTokenNeededreturns that JWT to the SDK.- The SDK exchanges it via
POST /api/v2/auth/exchangefor a short-lived MarkUp session token, and uses that for subsequent API calls.
const markup = MarkUpSDK.init({
publicKey: "<YOUR_PUBLIC_KEY>",
markupId: "<YOUR_MARKUP_ID>",
onTokenNeeded: async () => {
// Call your own backend, which signs and returns a JWT.
const res = await fetch("/api/markup-token");
const {token} = await res.json();
return token;
}
});JWT claims
Your backend must sign a JWT with these claims. Signatures are verified with RS256 against the public key registered on your installation — symmetric (HS256) signing is rejected.
| Option | Type | |
|---|---|---|
| iss | string | Issuer. Must match the |
| aud | string | Audience. Must match the |
| sub | string | Subject — a stable identifier for the end user in your system. Becomes the external user identity in MarkUp. |
| exp | number | Expiration time, in Unix seconds. Keep it short-lived (e.g. a few minutes). |
| iat | number | Issued-at time, in Unix seconds. |
| jti | string | Unique token ID (nonce). Used for replay protection — generate a fresh value per token. |
| name optional | string | Display name shown in the toolbar and on the user's comments. |
| email optional | string | User email. |
| avatarUrl optional | string | URL of the user's avatar image. |
| markupRole optional | string | Feedback role for this user, e.g. |
Get the expected issuer and audience
The iss and aud your token must carry are defined by your installation. Fetch them from the config endpoint using your publicKey:
curl "https://api.markup.io/api/v2/auth/config?publicKey=<YOUR_PUBLIC_KEY>" \
-X GET \
-H "Markup-API-Version: 2023-02-22" \
-H "Content-Type: application/json"{
"data": {
"installationId": "...",
"expectedIss": "https://api.markup.io",
"expectedAud": "https://api.markup.io/v2/auth/exchange"
}
}Sign the token on your backend
A minimal Node.js endpoint that signs a token for the current user:
import fs from "node:fs";
import {randomUUID} from "node:crypto";
import jwt from "jsonwebtoken";
const PRIVATE_KEY = fs.readFileSync("private.pem", "utf8");
// From GET /api/v2/auth/config for your installation:
const ISSUER = "https://api.markup.io";
const AUDIENCE = "https://api.markup.io/v2/auth/exchange";
export function createMarkupToken(user: {id: string; name: string; email: string}) {
const now = Math.floor(Date.now() / 1000);
return jwt.sign(
{
iss: ISSUER,
aud: AUDIENCE,
sub: user.id,
iat: now,
exp: now + 5 * 60, // 5 minutes
jti: randomUUID(),
name: user.name,
email: user.email
},
PRIVATE_KEY,
{algorithm: "RS256"}
);
}Sign tokens only on your server, behind your own authentication. Never ship the private key to the browser, and never let an unauthenticated caller mint a token for an arbitrary
sub.
For the raw endpoint reference (/auth/config and /auth/exchange), see the Feedback authentication API docs.
Mode B — Sign in with MarkUp
Omit onTokenNeeded and the SDK signs users into their own MarkUp accounts. This is the fastest way to get started and needs no backend.
const markup = MarkUpSDK.init({
publicKey: "<YOUR_PUBLIC_KEY>",
markupId: "<YOUR_MARKUP_ID>"
});
markup.render();How it works
- Silent SSO — on
render()the SDK makes a single silent attempt through a hidden, origin-locked iframe. If the user already has a MarkUp session, they’re signed in with no interaction. - Interactive sign-in — if the silent attempt fails, the toolbar shows a Sign in button. Clicking it opens a MarkUp sign-in popup; on success the SDK loads data automatically.
The signed-in user must be a member of the MarkUp’s workspace. A user who signs in but isn’t a member is rejected.
Allowed origins are required for this flow. Sign in with MarkUp only mints a session for an origin listed in your installation’s allowed origins, matched against the origin where your app (and the SDK) runs. An installation with no origins configured — or one created with Allow all origins — can’t use this flow; authenticate with a backend-signed token instead.
Sign-in errors
Interactive sign-in can reject with a MarkUpSDKError whose code is one of:
SDKErrorCode | Meaning |
|---|---|
POPUP_BLOCKED | The browser blocked the sign-in popup (window.open failed). |
POPUP_CLOSED | The user closed the popup before completing sign-in. |
WORKSPACE_FORBIDDEN | The signed-in user isn’t a member of the MarkUp’s workspace. |
SDK_SIGNIN_FAILED | Sign-in failed for another reason. |
Sessions
Once resolved, a session is cached and refreshed automatically before it expires, so users don’t have to re-authenticate on every page load. Control where the session is persisted with the sessionStorage option:
| Value | Behavior |
|---|---|
"memory" (default) | Session lives only for the lifetime of the page. |
"sessionStorage" | Session persists for the browser tab/session. |
"localStorage" | Session persists across tabs and reloads. |
MarkUpSDK.init({
publicKey: "<YOUR_PUBLIC_KEY>",
markupId: "<YOUR_MARKUP_ID>",
sessionStorage: "localStorage"
});Origins are fixed at build time
The MarkUp API origin and the sign-in website origin are frozen into the SDK bundle at build time (from
MARKUP_API_URL/MARKUP_APP_URL). There is no runtime URL option ininit, and the website origin is the only origin the SDK accepts sign-in messages from. This keeps the sign-in handshake locked to MarkUp’s own domains. (This is separate from your installation’s allowed origins, which are the origins of your app.)
Next steps
- Configuration — all
initandrenderoptions. - Create an SDK installation — generate the key pair
onTokenNeededsigns with. - API reference — error classes and
SDKErrorCodevalues.