Skip to Content
SDKAuthentication

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:

Session resolution order
onTokenNeeded → cached session → silent SSO → interactive sign-in

If 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

  1. The SDK calls your onTokenNeeded callback.
  2. Your backend signs a short-lived JWT with the private key for your SDK installation (RS256).
  3. onTokenNeeded returns that JWT to the SDK.
  4. The SDK exchanges it via POST /api/v2/auth/exchange for a short-lived MarkUp session token, and uses that for subsequent API calls.
onTokenNeeded
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.

OptionType
iss string

Issuer. Must match the expectedIss returned by GET /api/v2/auth/config.

aud string

Audience. Must match the expectedAud returned by GET /api/v2/auth/config.

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 optionalstring

Display name shown in the toolbar and on the user's comments.

email optionalstring

User email.

avatarUrl optionalstring

URL of the user's avatar image.

markupRole optionalstring

Feedback role for this user, e.g. project:viewer or project:contributor.

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:

GET /api/v2/auth/config
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"
200 OK
{ "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:

server: /api/markup-token
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.

SDK-managed sign-in
const markup = MarkUpSDK.init({ publicKey: "<YOUR_PUBLIC_KEY>", markupId: "<YOUR_MARKUP_ID>" }); markup.render();

How it works

  1. 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.
  2. 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:

SDKErrorCodeMeaning
POPUP_BLOCKEDThe browser blocked the sign-in popup (window.open failed).
POPUP_CLOSEDThe user closed the popup before completing sign-in.
WORKSPACE_FORBIDDENThe signed-in user isn’t a member of the MarkUp’s workspace.
SDK_SIGNIN_FAILEDSign-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:

ValueBehavior
"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.
Persist the session
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 in init, 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