Skip to Content
APIResourcesFeedback APIAuthentication

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.

GET /api/v2/auth/config
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"
200 OK
{ "data": { "installationId": "some installationId", "expectedAud": "some expectedAud", "expectedIss": "some expectedIss" } }

Response Body AuthConfigResponse

OptionType
installationId string
expectedAud string
expectedIss string

Request Query Params AuthConfigRequest

OptionType
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.

POST /api/v2/auth/exchange
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" }'
200 OK
{ "data": { "exchangeToken": "some exchangeToken", "expiresAt": "2026-09-22T11:18:38.213Z", "workspaceId": "some workspaceId", "userId": "some userId", "markupRole": {} } }

Response Body ExchangeTokenResponse

OptionType
exchangeToken string
expiresAt Iso8601Timestamp
workspaceId string
userId string
markupRole FeedbackMarkupRole
markupId string

Request Body ExchangeTokenRequest

OptionType
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).

POST /api/v2/auth/sdk-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" }'
200 OK
{ "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

OptionType
publicKey string
markupId string
sdkOrigin string
state optionalstring

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.

GET /api/v2/auth/sdk-exchange/result
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"
200 OK
{ "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

OptionType
state string
publicKey string
markupId string

SdkExchangeResponse

OptionType
SdkExchangeResponse
Last updated on