---
title: "Authentication"
description: "How the cloud engine authenticates — doc-scoped JWTs, public share tokens, opening by token vs. by id vs. by share, and why scope lives server-side."
source: "https://www.cloudpdf.com/docs/engine/getting-started/authentication"
---

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

```ts
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.

```ts
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.

```ts
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.

```ts
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`](https://www.cloudpdf.com/docs/engine/core-concepts/security-and-access).

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

```ts
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 `EngineError`s: a `401` becomes
`Unauthenticated`, a `403` becomes `Forbidden`. See
[Async & errors](https://www.cloudpdf.com/docs/engine/core-concepts/async-and-errors) for the full list
and handling patterns.
