PapermarkDocs

Authentication

OAuth 2.1 and bearer tokens for the REST API.

The REST API accepts two kinds of credentials in exactly the same Authorization: Bearer … header:

  • Dashboard tokens (pm_live_…) — minted by a human in the dashboard
  • OAuth 2.1 access tokens — issued via the device authorization grant (RFC 8628) for delegated access

The API never inspects which kind it is — both flow through the same auth middleware. If you just want to call the API from your own script, use a dashboard token. If you're building a tool that ships to other people, use OAuth so they don't paste credentials into your software.

The reader-friendly version of this page lives at /docs/authentication. What follows is the spec.

Bearer header

Authorization: Bearer pm_live_AbCdEfGh…

Whitespace and casing in the scheme are tolerant (bearer works), but the Authorization header name is required. Cookies, query params, and ?api_key= are not accepted.

Dashboard tokens

PropertyValue
Formatpm_live_<32+ chars>
Where to mintDashboard → Settings → API Tokens
LifetimeLong-lived; no expiry. Revoke from the dashboard.
ScopesSet at creation time; immutable. Mint a new token if you need different scopes.
Storage on the serverSHA-256 hashed (with NEXTAUTH_SECRET salt). The token is shown to you exactly once; the dashboard only ever displays a partial.

Treat tokens as secrets. A leaked pm_live_… is equivalent to a leaked password scoped to whatever you granted.

OAuth 2.1 device flow

For desktop CLIs, MCP clients, and any tool where the user lives in a terminal (no browser to redirect through), use the device authorization grant (RFC 8628).

Discovery

OAuth and discovery do not live on api.papermark.com — that host is restricted to the /v1 REST surface. The OIDC issuer is the canonical app host. Discover endpoints there:

GET https://papermark.com/.well-known/openid-configuration

The response includes issuer, device_authorization_endpoint, token_endpoint, and the supported scopes_supported / grant_types_supported. Cache the document and use whatever URLs it advertises — don't hard-code endpoint paths.

Client

The Papermark CLI ships with a built-in public client ID papermark-cli (no client secret, device flow + refresh only). That ID is reserved for the official CLI — don't reuse it.

For third-party clients, register your own public client at the registration_endpoint advertised by discovery (open Dynamic Client Registration, RFC 7591):

curl -X POST "$REGISTRATION_ENDPOINT" \
  -H "Content-Type: application/json" \
  -d '{
    "client_name": "Your App Name",
    "application_type": "native",
    "token_endpoint_auth_method": "none",
    "grant_types": [
      "urn:ietf:params:oauth:grant-type:device_code",
      "refresh_token"
    ],
    "redirect_uris": []
  }'

The response includes a unique client_id and a registration_access_token you can use to update or delete the registration later. Use the new client_id in every device-flow request below.

Flow

  1. Discover the endpoints (once, then cache):

    curl https://papermark.com/.well-known/openid-configuration
  2. Request a device code at the discovered device_authorization_endpoint:

    curl -X POST "$DEVICE_AUTHORIZATION_ENDPOINT" \
      -d "client_id=$CLIENT_ID" \
      -d "scope=documents.read offline_access"

    Response:

    {
      "device_code": "…",
      "user_code": "WDJB-MJHT",
      "verification_uri": "https://app.papermark.com/oauth/device",
      "verification_uri_complete": "https://app.papermark.com/oauth/device?user_code=WDJB-MJHT",
      "expires_in": 600,
      "interval": 5
    }
  3. Tell the user. Print the verification_uri and the user_code, or open verification_uri_complete in their browser. They have expires_in seconds (10 minutes) to approve.

  4. Poll the token endpoint every interval seconds (5):

    curl -X POST "$TOKEN_ENDPOINT" \
      -d "grant_type=urn:ietf:params:oauth:grant-type:device_code" \
      -d "client_id=$CLIENT_ID" \
      -d "device_code=…"

    While the user hasn't approved yet, you'll get {"error": "authorization_pending"}. Once they do:

    {
      "access_token": "…",
      "refresh_token": "…",
      "token_type": "Bearer",
      "expires_in": 7776000,
      "scope": "documents.read offline_access"
    }

    Access tokens are valid for 90 days.

  5. Refresh when the access token nears expiry, if you requested offline_access:

    curl -X POST "$TOKEN_ENDPOINT" \
      -d "grant_type=refresh_token" \
      -d "client_id=$CLIENT_ID" \
      -d "refresh_token=…"

The CLI does discovery + steps 2–5 automatically — you only see the URL and the code.

Scopes

Both token types carry a list of scopes. Each endpoint declares the scope it requires; a request with a token missing that scope returns 403 forbidden. There is no implicit scope hierarchydocuments.write does not imply documents.read. Grant both if your tool needs both. The full list is on Scopes.

What goes wrong

CodeMeaningFix
401 unauthorizedNo Authorization header, malformed value, unknown token, or token revokedConfirm header shape and that the token still exists
403 forbiddenToken is valid but lacks the required scope, or isn't authorized for the teamMint a new token with the right scopes; or pick a different team
429 rate_limit_exceededToken is making too many calls per minuteSee Rate limits

Full error envelope and code list at Errors.

On this page