Authentication
Authentication endpoints for token exchange and installation configuration.
Feedback endpoints accept two kinds of credentials: a short-lived exchange token minted by the endpoints below
(for embedded, end-user-attributed feedback), or a workspace- or organization-scoped API key sent as
Authorization: Bearer <API-KEY-SECRET> (for automated/admin server-to-server access). See the
Authentication overview for the full API-key flow, required scopes, and attribution rules.
Get Installation Configuration
GET /api/v2/auth/config
Returns installation configuration including signing algorithm, expected claims, and rate limits. This endpoint is used by SDKs to configure JWT signing before calling the exchange endpoint.
curl "https://api.markup.io/api/v2/auth/config?publicKey=some+publicKey" \
-X GET \
-H "Authorization: Bearer <API-KEY-SECRET>" \
-H "Markup-API-Version: 2023-02-22" \
-H "Content-Type: application/json" {
"data": {
"installationId": "some installationId",
"expectedAud": "some expectedAud",
"expectedIss": "some expectedIss"
}
}Response Body AuthConfigResponse
| Option | Type | |
|---|---|---|
| installationId | string | |
| expectedAud | string | |
| expectedIss | string |
Request Query Params AuthConfigRequest
| Option | Type | |
|---|---|---|
| publicKey | string |
Exchange Token
POST /api/v2/auth/exchange
Exchanges a customer-signed JWT for a MarkUp exchange token.
The exchange token can then be used to authenticate subsequent API requests.
The optional markupRole claim on the customer JWT is the only input to the end user’s feedback role, and the accepted
values are project:viewer, project:contributor and project:owner. Omit the claim and the user gets
project:contributor; an unrecognized or non-string value is rejected with 400. MarkUp workspace membership never
influences this role, so a JWT email matching a workspace owner or organization admin does not elevate the end user.
The optional markupId claim binds the minted token to a single markup: every feedback, markup and upload endpoint then rejects a
request addressed at any other markup with 403, and the response echoes the bound markupId. It is validated at mint time — a markup
that is missing, deleted, or in another workspace is refused with the same 403, and a non-string or empty value with 400. Omit the
claim and the token stays workspace-scoped, behaving exactly as before.
curl "https://api.markup.io/api/v2/auth/exchange" \
-X POST \
-H "Authorization: Bearer <API-KEY-SECRET>" \
-H "Markup-API-Version: 2023-02-22" \
-H "Content-Type: application/json" \
--data '{
"token": "some token"
}'{
"data": {
"exchangeToken": "some exchangeToken",
"expiresAt": "2026-09-22T11:18:38.213Z",
"workspaceId": "some workspaceId",
"userId": "some userId",
"markupRole": {}
}
}Response Body ExchangeTokenResponse
| Option | Type | |
|---|---|---|
| exchangeToken | string | |
| expiresAt | Iso8601Timestamp | |
| workspaceId | string | |
| userId | string | |
| markupRole | FeedbackMarkupRole | |
| markupId | string |
Request Body ExchangeTokenRequest
| Option | Type | |
|---|---|---|
| token | string |
SDK Exchange Token
POST /api/v2/auth/sdk-exchange
Exchanges a verified Okta access token (from a Markup-website-authenticated
caller) for an SDK-scoped MarkUp exchange token. Used by the SDK silent-bridge and popup
sign-in flows so the SDK can call /v2 endpoints without requiring the host app to mint
a customer-signed JWT. allowExchangeToken: false ensures only Okta tokens are accepted —
an existing SDK exchange token MUST NOT be reusable to mint a fresh one (no infinite
token life via self-exchange).
curl "https://api.markup.io/api/v2/auth/sdk-exchange" \
-X POST \
-H "Authorization: Bearer <API-KEY-SECRET>" \
-H "Markup-API-Version: 2023-02-22" \
-H "Content-Type: application/json" \
--data '{
"publicKey": "some publicKey",
"markupId": "some markupId",
"sdkOrigin": "some sdkOrigin"
}'{
"data": {
"exchangeToken": "some exchangeToken",
"expiresAt": "2026-09-22T11:18:38.213Z",
"workspaceId": "some workspaceId",
"userId": "some userId",
"markupRole": {},
"validatedSdkOrigin": "some validatedSdkOrigin",
"name": "some name"
}
}Response Body - SdkExchangeResponse
Request Body SdkExchangeRequest
| Option | Type | |
|---|---|---|
| publicKey | string | |
| markupId | string | |
| sdkOrigin | string | |
| state optional | string |
SDK Exchange Result
GET /api/v2/auth/sdk-exchange/result
Retrieves the SDK session minted by a prior /auth/sdk-exchange, keyed by the
interactive sign-in state nonce. The SDK polls this after opening the sign-in popup. This
is the reliable delivery path for logged-out sign-in: the popup routes through an IdP whose
Cross-Origin-Opener-Policy severs window.opener, so the popup’s postMessage cannot be
relied on. Intentionally NOT behind isAuthenticated — the SDK holds no Okta/exchange token
at this point. Authorization is the unguessable state nonce plus re-verification of the
consumer context (publicKey / markupId / Origin) recorded at mint time; single-use + TTL.
curl "https://api.markup.io/api/v2/auth/sdk-exchange/result?state=some+state&publicKey=some+publicKey&markupId=some+markupId" \
-X GET \
-H "Authorization: Bearer <API-KEY-SECRET>" \
-H "Markup-API-Version: 2023-02-22" \
-H "Content-Type: application/json" {
"data": {
"exchangeToken": "some exchangeToken",
"expiresAt": "2026-09-22T11:18:38.213Z",
"workspaceId": "some workspaceId",
"userId": "some userId",
"markupRole": {},
"validatedSdkOrigin": "some validatedSdkOrigin",
"name": "some name"
}
}Response Body - SdkExchangeResponse
Request Query Params SdkExchangeResultRequest
| Option | Type | |
|---|---|---|
| state | string | |
| publicKey | string | |
| markupId | string |
Related types
SdkExchangeResponse
| Option | Type | |
|---|---|---|
| SdkExchangeResponse |