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
| Property | Value |
|---|---|
| Format | pm_live_<32+ chars> |
| Where to mint | Dashboard → Settings → API Tokens |
| Lifetime | Long-lived; no expiry. Revoke from the dashboard. |
| Scopes | Set at creation time; immutable. Mint a new token if you need different scopes. |
| Storage on the server | SHA-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-configurationThe 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
-
Discover the endpoints (once, then cache):
curl https://papermark.com/.well-known/openid-configuration -
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 } -
Tell the user. Print the
verification_uriand theuser_code, or openverification_uri_completein their browser. They haveexpires_inseconds (10 minutes) to approve. -
Poll the token endpoint every
intervalseconds (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.
-
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 hierarchy —
documents.write does not imply documents.read. Grant both if your
tool needs both. The full list is on
Scopes.
What goes wrong
| Code | Meaning | Fix |
|---|---|---|
401 unauthorized | No Authorization header, malformed value, unknown token, or token revoked | Confirm header shape and that the token still exists |
403 forbidden | Token is valid but lacks the required scope, or isn't authorized for the team | Mint a new token with the right scopes; or pick a different team |
429 rate_limit_exceeded | Token is making too many calls per minute | See Rate limits |
Full error envelope and code list at Errors.