CloudPDF
DocsPricing
Start building

Authentication

The cloud engine authenticates every request with a bearer JWT. Your backend mints these tokens; the engine never holds your tenant secret and never signs anything itself. The CloudPDF server is the verifier of record — the SDK only decodes a token (unverified) to learn which document to route to.

Where the token comes from#

Set a token on the engine, on an individual open(), or both. A token may be a string or a function that returns a fresh one — the engine calls it on every request, so rotation is transparent.

const engine = createCloudEngine({
  baseUrl: 'https://pdf.your-app.com',
  token: () => getTenantToken(), // engine-level default bearer
});

Mint tokens on your server, never in the browser, and keep them short-lived. A doc-scoped token is pinned to a single document, so an exfiltrated token can’t be replayed against other documents.

Three ways to open#

The cloud engine accepts exactly three OpenInput shapes. Pick by where the credential comes from: your backend mints one (token), your frontend holds a tenant session (id), or there is no backend at all (share).

Open by token#

Use this when your backend mints doc-scoped JWTs — for example a logged-in reviewer authorized for exactly one document. The token’s doc_id claim identifies the document. Each open({ kind: 'token' }) is independent — one engine can hold many handles, each carrying its own per-document token.

const doc = await engine.open({
  kind: 'token',
  token: () => fetchDocToken(documentId),
});

The handle and all of its RPCs carry this token, not the engine-level one.

Open by id#

Use this when your frontend has a tenant session (for example a JWT minted at login by your auth backend) and just needs to open one of many documents the tenant owns.

const doc = await engine.open({
  kind: 'id',
  id: 'doc_proposal_2026',
  layerName: 'default', // optional; omit for the server default layer
  token: () => fetchTenantToken(), // optional per-open override
});

Without a per-open token, the engine uses the token supplied at construction.

Open by share token#

Use this for the no-backend embed flow: a public share token (shr_…) from the dashboard’s embed snippet. A share token is a reference to a stored grant on the server, not a credential — the engine exchanges it for a short-lived doc-scoped session JWT and silently re-exchanges near expiry, so revoking or editing the share retargets every embedded copy at the next renewal.

const engine = createCloudEngine({ baseUrl: 'https://pdf.your-app.com' });
const doc = await engine.open({
  kind: 'share',
  shareToken: 'shr_9f2k…',
  sharePassword: 'open-sesame', // only for passphrase-protected grants
});

sharePassword is the grant’s passphrase, checked at the exchange; password remains the PDF’s own encryption password, the same slot every other kind uses. A protected grant opened without its passphrase rejects with EngineErrorCode.SharePasswordRequired — prompt and retry.

The exchange endpoint checks the browser’s Origin header against the grant’s allowlist, and the capabilities the handle gets are the ones the grant carries. For custom flows (pre-exchange to inspect a docId, hand-rolled prompts), the primitives exchangeShareToken and shareSessionSource remain exported.

Scope and identity live on the server#

Authorization — what the caller may do (doc.text.copy, doc.annotate.read, annotations:update:self, …) and who they are — is carried inside the JWT and enforced by the server. The engine reads them from the token; it does not accept them as client options.

The local engine takes scope and identity through open() options because there’s no JWT involved. The cloud engine intentionally ignores those options — the token is the authority. This is what keeps the same SDK code portable between local and cloud. Read the resolved grants from doc.security.effectiveScope.

Anonymous engines#

If every open() supplies its own credential — a doc-scoped token or a public share token — the engine itself needs none: omit token at construction. Requests then go out without an Authorization header until a per-open credential is provided.

const engine = createCloudEngine({ baseUrl: 'https://pdf.your-app.com' });
const doc = await engine.open({ kind: 'share', shareToken: 'shr_9f2k…' });

What failures look like#

Auth problems surface as typed EngineErrors: a 401 becomes Unauthenticated, a 403 becomes Forbidden. See Async & errors for the full list and handling patterns.