---
title: "Authentication"
description: "The three CloudPDF credentials — API token, tenant JWT, and document JWT — how each is obtained, and what it can reach."
source: "https://www.cloudpdf.com/docs/api-reference/authentication"
---

# Authentication

CloudPDF has three credentials. The rule that explains all of them:
**authority mints only downward, and never back up.**

```
API token       the deployment root   ·  your server env
   │ mints
   ▼
Tenant JWT      one tenant's subtree  ·  your backend
   │ mints
   ▼
Document JWT    one document, scoped  ·  the browser
```

Each rung can create the rung below it and can do strictly less than the rung
above. A document JWT can never call an admin route; a tenant JWT can never
leave its own tenant. Only the bottom rung is ever allowed to reach a browser.

## Where you enter the ladder

### Managed SaaS

On managed CloudPDF the API token is the **platform's** credential and your
account is one tenant. You enter at the second rung: your [dashboard](https://app.cloudpdf.com) API key
*is* a tenant token, already scoped to your account. Everything below the
second rung — minting document tokens, capabilities, revocation — works
exactly as documented here, with your API key as the `token`.

### Self-hosted

You operate the deployment, so you hold the top rung: the API token
configured through `CLOUDPDF_API_AUTH_TOKENS`. You can call anything with
it directly, or mint tenant tokens so each of your services can only reach
its own tenant.

- **API token** — The deployment's static root credential (CLOUDPDF\_API\_AUTH\_TOKENS), valid on every surface.
- **Tenant token** — Delegated tenant JWT, valid only under its own /v1/tenants/\{tenantId}/ subtree — the path tenant must equal the token's tenant\_id. Doc-scoped viewer tokens are rejected on every admin route.
- **Document token** — Doc-scoped JWT, valid only on the /v1/docs/\{docId} subtree it names, gated by the capability scopes it carries (each operation's x-required-capability).

## The API token

A static secret configured on the deployment through `CLOUDPDF_API_AUTH_TOKENS`
(comma-separated, so you can rotate by adding the new value, deploying, then
removing the old one). It is the deployment root: valid on every surface, it
passes every scope and capability check, and it is the only credential that can
mint a tenant token. On managed CloudPDF it belongs to the platform — knowing
the rung above you exists is part of the model, but you never hold it.

Construct the client with your credential (keep both values in server-side
configuration, never in a browser bundle):

**TypeScript**

```typescript
import { CloudPDFClient } from "@cloudpdf/sdk";

const client = new CloudPDFClient({
    baseUrl: "https://yourhost.com/path/to/api",
    token: "<token>",
});
```

**Python**

```python
from cloudpdf import CloudPDFClient

client = CloudPDFClient(
    token="<token>",
    base_url="https://yourhost.com/path/to/api",
)
```

**PHP**

```php
use CloudPDF\CloudPDFClient;

$client = new CloudPDFClient(
    token: '<token>',
    options: ['baseUrl' => 'https://yourhost.com/path/to/api'],
);
```

**.NET**

```csharp
using CloudPDF;

var client = new CloudPDFClient(
    "<token>",
    new ClientOptions { BaseUrl = "https://yourhost.com/path/to/api" }
);
```

**Go**

```go
import (
    client "github.com/embedpdf/cloudpdf-sdk-go/v3/client"
    option "github.com/embedpdf/cloudpdf-sdk-go/v3/option"
)

client := client.NewClient(
    option.WithToken("<token>"),
    option.WithBaseURL("https://yourhost.com/path/to/api"),
)
```

**Java**

```java
import com.cloudpdf.api.CloudPDFClient;

CloudPDFClient client = CloudPDFClient
    .builder()
    .token("<token>")
    .url("https://yourhost.com/path/to/api")
    .build();
```

**Ruby**

```ruby
require "cloudpdf"

client = CloudPDF::Client.new(
  token: "<token>",
  base_url: "https://yourhost.com/path/to/api"
)
```

> The API token is a root credential. Keep it in server-side configuration only — never in a
> browser bundle, a mobile app, or a repository. Anything holding it can read and delete every
> document in every tenant.

## Tenant tokens

A tenant JWT represents a principal inside **one** tenant. It carries a `scope`
array naming the tenant-level operations it may perform, and the server rejects
it on any path whose `tenantId` is not its own `tenant_id`.

On managed CloudPDF this is what your dashboard API key is. On a self-hosted
deployment, mint one with the API token via
[issue token](https://www.cloudpdf.com/docs/api-reference/tokens/issue) using `kind: "tenant"` — that
request is rejected for every other credential, which is the "downward" rule
doing its job. Use `scope: ["*"]` for a full tenant administrator.

The available scopes:

- `docs.create` — [Commit upload](https://www.cloudpdf.com/docs/api-reference/documents/commit), [Upload document through the origin](https://www.cloudpdf.com/docs/api-reference/documents/upload-proxy), [Import document](https://www.cloudpdf.com/docs/api-reference/documents/import-from), [Initialize upload](https://www.cloudpdf.com/docs/api-reference/documents/init)
- `docs.delete` — [Delete document](https://www.cloudpdf.com/docs/api-reference/documents/delete)
- `docs.read` — [List documents](https://www.cloudpdf.com/docs/api-reference/documents/list), [Get document](https://www.cloudpdf.com/docs/api-reference/documents/get), [Download document](https://www.cloudpdf.com/docs/api-reference/documents/download), [Get thumbnail](https://www.cloudpdf.com/docs/api-reference/documents/thumbnail), [Tenant usage](https://www.cloudpdf.com/docs/api-reference/tenants/usage)
- `shares.manage` — [List shares](https://www.cloudpdf.com/docs/api-reference/shares/list), [Create share](https://www.cloudpdf.com/docs/api-reference/shares/create), [Get share](https://www.cloudpdf.com/docs/api-reference/shares/get), [Update share](https://www.cloudpdf.com/docs/api-reference/shares/update), [Revoke share](https://www.cloudpdf.com/docs/api-reference/shares/delete)
- `tokens.issue-doc` — [Issue token](https://www.cloudpdf.com/docs/api-reference/tokens/issue)
- `tokens.revoke` — [Revoke token](https://www.cloudpdf.com/docs/api-reference/tokens/revoke)

## Document tokens

A document JWT is the only credential built to leave your infrastructure. It is
pinned to a single `doc_id`, carries **capabilities** rather than tenant scopes,
and is typically minted for minutes rather than hours — so an exfiltrated token
cannot be replayed against another document.

Mint one from your backend for each user session:

**TypeScript**

```typescript
import { CloudPDFClient } from "@cloudpdf/sdk";

const client = new CloudPDFClient({
  baseUrl: "https://yourhost.com/path/to/api",
  token: "<token>",
});

await client.tokens.issue({
  tenantId: "tenantId",
  body: {
    kind: "doc",
    sub: "sub",
    docId: "docId",
    scope: ["scope"],
    expiresIn: 1
  }
});
```

**Python**

```python
from cloudpdf import CloudPDFClient
from cloudpdf import CloudPDFClient, TokensIssueRequest_Doc

client = CloudPDFClient(
    token="<token>",
    base_url="https://yourhost.com/path/to/api",
)

client.tokens.issue(
    tenant_id="tenantId",
    request=TokensIssueRequest_Doc(
        sub="sub",
        doc_id="docId",
        scope=[
            "scope"
        ],
        expires_in=1,
    ),
)
```

**PHP**

```php
use CloudPDF\CloudPDFClient;

$client = new CloudPDFClient(
    token: '<token>',
    options: ['baseUrl' => 'https://yourhost.com/path/to/api'],
);

$client->tokens->issue(
    'tenantId',
    new IssueTokensRequest([
        'body' => TokensIssueRequest::doc(new TokensIssueRequestDoc([
            'sub' => 'sub',
            'docId' => 'docId',
            'scope' => [
                'scope',
            ],
            'expiresIn' => 1,
        ])),
    ]),
);
```

**.NET**

```csharp
using CloudPDF;

var client = new CloudPDFClient(
    "<token>",
    new ClientOptions { BaseUrl = "https://yourhost.com/path/to/api" }
);

await client.Tokens.IssueAsync(
    new IssueTokensRequest
    {
        TenantId = "tenantId",
        Body = new TokensIssueRequest(
            new TokensIssueRequest.Doc(
                new TokensIssueRequestDoc
                {
                    Sub = "sub",
                    DocId = "docId",
                    Scope = new List<string>() { "scope" },
                    ExpiresIn = 1,
                }
            )
        ),
    }
);
```

**Go**

```go
import (
    "context"

    cloudpdf "github.com/embedpdf/cloudpdf-sdk-go/v3"
    client "github.com/embedpdf/cloudpdf-sdk-go/v3/client"
    option "github.com/embedpdf/cloudpdf-sdk-go/v3/option"
)

client := client.NewClient(
    option.WithToken("<token>"),
    option.WithBaseURL("https://yourhost.com/path/to/api"),
)

request := &cloudpdf.IssueTokensRequest{
    TenantID: "tenantId",
    Body: &cloudpdf.TokensIssueRequest{
        Doc: &cloudpdf.TokensIssueRequestDoc{
            Sub: "sub",
            DocID: "docId",
            Scope: []string{
                "scope",
            },
            ExpiresIn: 1,
        },
    },
}
client.Tokens.Issue(
    context.TODO(),
    request,
)
```

**Java**

```java
import com.cloudpdf.api.CloudPDFClient;

CloudPDFClient client = CloudPDFClient
    .builder()
    .token("<token>")
    .url("https://yourhost.com/path/to/api")
    .build();

client.tokens().issue(
    "tenantId",
    IssueTokensRequest
        .builder()
        .body(
            TokensIssueRequest.doc(
                TokensIssueRequestDoc
                    .builder()
                    .sub("sub")
                    .docId("docId")
                    .expiresIn(1)
                    .scope(
                        Arrays.asList("scope")
                    )
                    .build()
            )
        )
        .build()
);
```

**Ruby**

```ruby
require "cloudpdf"

client = CloudPDF::Client.new(
  token: "<token>",
  base_url: "https://yourhost.com/path/to/api"
)

client.tokens.issue(tenant_id: "tenantId")
```

That token is what the viewer opens with — see
[Engine: getting started](https://www.cloudpdf.com/docs/engine/getting-started).

### Capabilities

Each capability unlocks specific operations. Grant the narrowest set that lets
the user do their job:

- `doc.annotate.modify` — [Create annotation](https://www.cloudpdf.com/docs/api-reference/document-operations/annotations/create), [Update annotation](https://www.cloudpdf.com/docs/api-reference/document-operations/annotations/update), [Delete annotation](https://www.cloudpdf.com/docs/api-reference/document-operations/annotations/delete), [Flatten pages](https://www.cloudpdf.com/docs/api-reference/document-operations/pages/flatten), [Apply redactions](https://www.cloudpdf.com/docs/api-reference/document-operations/redactions/apply)
- `doc.annotate.read` — [List annotations](https://www.cloudpdf.com/docs/api-reference/document-operations/annotations/list)
- `doc.download` — [Download PDF](https://www.cloudpdf.com/docs/api-reference/document-operations/download), [Extract pages](https://www.cloudpdf.com/docs/api-reference/document-operations/pages/extract)
- `doc.forms.fill` — [Import form data](https://www.cloudpdf.com/docs/api-reference/document-operations/forms/import-data), [Reset form field](https://www.cloudpdf.com/docs/api-reference/document-operations/forms/reset), [Set form value](https://www.cloudpdf.com/docs/api-reference/document-operations/forms/set-value)
- `doc.forms.read` — [Get form snapshot](https://www.cloudpdf.com/docs/api-reference/document-operations/forms/get), [Export form data](https://www.cloudpdf.com/docs/api-reference/document-operations/forms/export-data)
- `doc.open` — [Open document](https://www.cloudpdf.com/docs/api-reference/document-operations/head), [Get manifest](https://www.cloudpdf.com/docs/api-reference/document-operations/manifest), [Get metadata](https://www.cloudpdf.com/docs/api-reference/document-operations/metadata/get)
- `doc.pages.assemble` — [Delete pages](https://www.cloudpdf.com/docs/api-reference/document-operations/pages/delete), [Insert pages from a PDF](https://www.cloudpdf.com/docs/api-reference/document-operations/pages/insert), [Insert blank pages](https://www.cloudpdf.com/docs/api-reference/document-operations/pages/insert-blank), [Move pages](https://www.cloudpdf.com/docs/api-reference/document-operations/pages/move), [Rotate pages](https://www.cloudpdf.com/docs/api-reference/document-operations/pages/rotate)
- `doc.pages.modify` — [Flatten pages](https://www.cloudpdf.com/docs/api-reference/document-operations/pages/flatten), [Apply redactions](https://www.cloudpdf.com/docs/api-reference/document-operations/redactions/apply)
- `doc.redact` — [Apply redactions](https://www.cloudpdf.com/docs/api-reference/document-operations/redactions/apply)
- `doc.render` — [Render page](https://www.cloudpdf.com/docs/api-reference/document-operations/render)
- `doc.text.copy` — [Extract page text](https://www.cloudpdf.com/docs/api-reference/document-operations/text)

A read-only viewer usually needs `doc.open` and `doc.render`; add
`doc.annotate.read` to display existing annotations, `doc.annotate.modify` to
let the user create them, and `doc.download` only if they may take the file.

## The trust boundary

The important property: **your application decides, CloudPDF enforces.**

1. A user opens a document in your app.
2. Your backend applies *your* authorization rules — roles, sharing, billing state.
3. Only then does it mint a document token carrying exactly the capabilities
   that decision allows.
4. The browser receives the token and opens the document.

CloudPDF never sees your user model. It enforces what the token says, so the
token is where your policy is expressed. Mint per user, per document, per
session — never one long-lived token reused across users.

## Signing modes

On managed CloudPDF, signing is the platform's concern — mint through the API
and skip this section. On a self-hosted deployment, how tokens are produced
depends on how the server verifies them:

- **HS256 (shared secret)** — the deployment holds `CLOUDPDF_JWT_SECRET` and can
  sign, so the [issue token](https://www.cloudpdf.com/docs/api-reference/tokens/issue) operation is
  mounted and your backend can mint through the API as shown above. The secret
  must be at least 32 bytes on a production license.
- **Asymmetric (public key or JWKS)** — the deployment only *verifies*, holding a
  public key or a JWKS URL. It cannot sign, so the issue endpoint is not
  mounted: your backend mints tokens itself with its own private key and
  publishes the matching public key.

In both modes the claims are identical. A token carries `sub`, `tenant_id`,
`iat`, `exp`, a `scope` array, and — for document tokens — `doc_id`. Include a
`jti` if you want to be able to revoke it.

## Revocation

Tokens are short-lived by design, but you can end a session early with
[revoke token](https://www.cloudpdf.com/docs/api-reference/tokens/revoke) using the `jti` returned at
issue time. Live viewer sessions drop on their next heartbeat.

`tokens.issue-doc` and `tokens.revoke` are deliberately separate scopes: a leaked
issuing capability creates unauthorized access, while a leaked revoke capability
kills sessions. Different blast radii deserve different grants.

## What the errors mean

- **401** — the credential is missing, malformed, expired, or its signature does
  not verify.
- **403** — the credential is valid but insufficient: wrong tenant, missing
  tenant scope, or missing document capability. Each operation page lists what
  it requires.
- **422 `DocPasswordRequired`** — the document is encrypted and needs
  `X-Document-Password`. That header is accepted only with the API token; viewer
  document tokens use the SDK's password-session flow instead.
