Authentication
Get a token and use it from the CLI, the API, and CI.
Every API call carries a bearer token. There are two ways to get one, and three places the CLI looks for one at runtime. This page covers both — pick the path that fits.
Token types
| Where it comes from | Format | Lifetime |
|---|---|---|
| Dashboard → Settings → API Tokens | pm_live_… | Long-lived; revoke from the dashboard |
| OAuth 2.1 device flow | Opaque bearer string | 90 days; auto-refreshed by the CLI when offline_access is granted |
Both forms are accepted in exactly the same Authorization: Bearer …
header — the API doesn't care which kind it is. The split exists
because dashboard tokens are mintable by humans (one click) while OAuth
tokens are how an agent or third-party tool gets delegated access
without seeing the user's password.
Scopes work the same for both: each token is
restricted to the scopes you granted at creation time, and the API
returns 403 forbidden if a request needs a scope the token lacks.
Getting a dashboard token
This is the fastest path. Use it for your own scripts, CI pipelines you control, and integrations where a human (you) will paste the token in.
- Open app.papermark.com, then Settings → API Tokens.
- Click Create token, name it, pick scopes (start with the smallest set that works — you can mint another with more scopes later), and choose Live or Test mode.
- Copy the token. It's shown exactly once — if you lose it, revoke it and mint a new one.
Getting an OAuth token (device flow)
Use this when you're building a CLI, a desktop integration, or any tool that ships to other people. The user authorizes once, your tool holds the token, and you never touch their password.
The CLI ships with the device-flow client baked in — running
papermark login triggers it:
$ papermark login
Open https://app.papermark.com/oauth/device in your browser.
Enter code: WDJB-MJHTYou can pass --scopes documents.read,links.write to narrow the
request (default is the union of all read scopes plus
offline_access). The CLI polls every few seconds until you approve
in the browser, then stores the access token + refresh token. Tokens
expire after 90 days; with offline_access the CLI refreshes silently
in the background.
Building your own client? Use OIDC discovery to find the device,
token, and registration endpoints — they live on the canonical app
host, not on api.papermark.com:
GET https://papermark.com/.well-known/openid-configurationThe response advertises device_authorization_endpoint,
token_endpoint, and registration_endpoint. Register a unique
public client_id of your own at the registration endpoint (open DCR,
RFC 7591) — don't reuse papermark-cli, that's reserved for the
official CLI. Then run the device flow per RFC 8628.
api.papermark.comis restricted to the/v1REST surface — the OAuth and discovery endpoints are not reachable on that host. The CLI handles this for you (it strips theapi.prefix to find the issuer); third-party clients should do the same.
Using the token
The CLI looks in three places, in order. The first one found wins:
PAPERMARK_TOKENenvironment variablePAPERMARK_CREDENTIALS_FILE— path to a JSON file like{"token": "pm_live_…", "apiUrl": "https://api.papermark.com"}- The config file (
~/.config/papermark/config.jsonon macOS/Linux, roaming AppData on Windows; XDG-aware)
This order is deliberate so a CI env var beats a developer's local
config without anyone editing files. Run papermark whoami to see
which source the active token is coming from.
Per-machine: the device flow
Run papermark login interactively. The token lands in the config
file at 0600 permissions.
Per-script: paste a token
papermark login --token pm_live_…Validates the token with one API call, then stores it. Same end state as the device flow, no browser.
CI: environment variable
export PAPERMARK_TOKEN=pm_live_…
papermark documents list --jsonGitHub Actions:
- run: papermark documents list --json
env:
PAPERMARK_TOKEN: ${{ secrets.PAPERMARK_TOKEN }}CI: credentials file
If your CI provider mounts secrets as files instead of env vars (Vault, some Kubernetes setups), point at the file:
export PAPERMARK_CREDENTIALS_FILE=/run/secrets/papermark.json
papermark documents listThe JSON shape is {"token": "...", "apiUrl": "..."} (apiUrl
optional).
CI: pipe a token via stdin
For setups where you'd rather not have the token sit in an env var or file at all (short-lived workers, secret-rotation scripts), pipe it in:
echo "pm_live_…" | papermark auth set --stdinThe token is validated and persisted to the config file in one step,
without a tempfile. The same command accepts a JSON object on stdin if
you also want to set apiUrl.
Rotating and revoking
- Dashboard tokens: revoke from Settings → API Tokens in the
dashboard. The next request returns
401 unauthorized. - OAuth tokens:
papermark logoutclears the local token and refresh token. To revoke server-side too, the user can disconnect the integration from the dashboard.
Troubleshooting
papermark doctor runs a sequence of checks: config file readable,
token present, API reachable, token still valid. Use it as the first
step when something stops working.