Nutrient DWS Viewer API
The DWS Viewer API is Nutrient's cloud-hosted document viewing service. The flow is three
server-side steps: upload a document to Nutrient-managed storage (or reference an app-provided one),
mint a short-lived session JWT, and hand that JWT to the browser, where the Nutrient Web SDK renders
a full viewer backed by Nutrient infrastructure — storage, streaming, annotation sync, and
collaboration, with no viewing/storage servers to run. (You still need a small trusted backend
or serverless endpoint to mint the session token — the API key must never reach the browser;
this skill is exactly that server-side step.)
This skill covers the server-side half — upload and session mint, the part agents get wrong. The
one-line browser embed (NutrientViewer.load({ container, session })) is documented in
references/embed-guide.md; framework integration belongs to the nutrient-web-sdk skill.
When to use
- "Embed a PDF viewer for document review/approval in our SaaS app — I don't want to run servers."
- "Add annotation, forms, or signing to a document workflow without hosting infrastructure."
- "Mint a viewer session token / session JWT for the browser."
- "Add multi-user real-time annotation to a document, cloud-backed."
Three Nutrient products show documents in a browser. Pick the right one before writing code:
| Product |
Infra hosting |
Storage |
Persistence / collab |
Route here when |
| DWS Viewer API (this skill) |
Nutrient cloud |
Nutrient cloud or app-provided |
Built in |
Cloud-first; no server infra; collab fast |
Nutrient Web SDK (nutrient-web-sdk) |
Developer (browser WASM) |
Developer's server |
Via Document Engine |
Self-hosted/offline; @nutrient-sdk/viewer embed |
Document Engine (nutrient-document-engine) |
Developer's server |
Developer's DB |
Yes (server-side) |
Self-hosted persistence; custom auth; data residency |
- Route here (DWS Viewer API) when the user wants a cloud-backed viewer fast, does not want to
run servers, or needs annotation persistence / real-time collab out of the box.
- Route to
nutrient-web-sdk for the self-hosted WASM viewer, license keys, or
NutrientViewer.load() / @nutrient-sdk/viewer embed wiring without a DWS session.
- Route to
nutrient-document-engine for self-hosted annotation persistence or
data-residency / custom-auth backends.
The full comparison and routing rules live in references/viewer-decision-matrix.md (the single
source of truth for this disambiguation).
Setup
The DWS Viewer API is a separate product with its own API key — distinct from the Processor key
(NUTRIENT_API_KEY) and the Extraction key (NUTRIENT_EXTRACT_API_KEY). Do not reuse another
product's key here.
This skill does not silently fall back to NUTRIENT_API_KEY. The Viewer API's wrong-key
behavior is unconfirmed, so a silent fallback could mint a real browser session JWT against the
wrong product/tenant with no signal (a confused-deputy). If your tenant has migrated to a global
DWS key, opt in explicitly with --allow-global-key, which warns at the point of fallback.
Tool preference
scripts/viewer-session.py — the cloud-side operations (upload, session mint, combined).
references/embed-guide.md — the browser embed (NutrientViewer.load), CDN snippet, llms.txt
pointers, and the pointer to nutrient-web-sdk for framework recipes.
references/viewer-decision-matrix.md — full product decision matrix and routing rules.
Document modes
- DWS-managed — upload the document to Nutrient storage (
upload), then mint a session for its
document_id. Nutrient stores, streams, and (optionally) persists annotations. Counts against the
upload/storage quota; uploaded documents should be torn down when no longer needed (see Rules).
- App-provided — the developer streams their own file to the SDK in the browser; the backend
only mints a session with an empty body (
session --app-provided). No document is uploaded to
Nutrient storage, so nothing needs teardown — but each session still consumes session quota.
Operations
viewer-session.py — upload + session mint
# Upload a document to Nutrient storage; prints document_id
uv run scripts/viewer-session.py upload --file doc.pdf
# Mint a read-only session JWT for a DWS-managed document (write the token to a 0600 file)
uv run scripts/viewer-session.py session --document-id <id> --jwt-out session.jwt
# Elevate permissions explicitly (least privilege is the default — see Rules)
uv run scripts/viewer-session.py session --document-id <id> --allow-write --allow-download --jwt-out session.jwt
# One-shot: upload then mint (the common case and the live smoke path)
uv run scripts/viewer-session.py upload-and-session --file doc.pdf --jwt-out session.jwt
# Interactive only: echo the JWT to stdout instead of a file (opt-in; stdout is logged)
uv run scripts/viewer-session.py session --document-id <id> --print-jwt
# App-provided mode: mint a session with an empty body (no upload). See Rules — scope is
# UNVERIFIED and TTL is unbounded; do not use it as a default.
uv run scripts/viewer-session.py session --app-provided --jwt-out session.jwt
# Tear down a DWS-managed document when done (teardown)
uv run scripts/viewer-session.py delete --document-id <id>
- Upload:
POST https://api.nutrient.io/viewer/documents, binary body; response data.document_id.
- Session:
POST https://api.nutrient.io/viewer/sessions, body
{ allowed_documents: [{ document_id, permissions }], exp } (DWS-managed) or {} (app-provided);
response { jwt }. The JWT prints once to stdout for capture.
upload-and-session on partial failure (upload succeeds, session mint fails) attempts best-effort
cleanup — it deletes the just-uploaded document (DELETE /viewer/documents/{id}, confirmed live)
so a failed run does not orphan a document in cloud storage.
Rules
- The JWT needs an explicit sink. A session-minting command requires either
--jwt-out <file>
(written 0600) or --print-jwt. The default is the file — printing a browser bearer token to
stdout is opt-in because agent/CI transcripts capture stdout. The script errors before any billed
call if neither is given.
- Least privilege by default. A session grants
["read"] only. write and download
require explicit --allow-write / --allow-download (or an explicit --permissions list). A
minted JWT is a browser-facing bearer credential; defaulting to read-only limits the blast radius
of a leaked token.
- Short TTL. Default session expiry is 1 hour.
--expires-in can extend it up to a documented
ceiling (24h); the script warns above a short-TTL threshold. A leaked long-TTL JWT is an open
exfiltration window for as long as it is valid.
--app-provided is not least-privilege — don't default to it. It mints an empty-body session
whose authorization scope is unverified and whose TTL is not bounded by --expires-in
(the API applies its own). Use DWS-managed mode (--document-id) for any real session. Only reach
for --app-provided when you deliberately stream your own file to the browser SDK and have
independently confirmed the scope you get — not as a shortcut for smoke runs.
- Tear down uploaded documents. Every document created with
upload / upload-and-session
draws down a metered quota (Free tier: 100 docs / 500 MiB / 100 uploads per month) and carries
data-retention exposure. Delete it when done with viewer-session.py delete --document-id <id>.
Verify deletion by the document's absence from GET /viewer/documents (a GET-by-id always 404s
and is not a liveness signal).
- Fail fast on a missing key or missing required args, with a message naming
NUTRIENT_DWS_VIEWER_API_KEY.
- If a future
nutrient-dws client adds Viewer methods, the script can swap its httpx calls for
them; today it uses httpx directly (the SDK targets Processor/Extraction).
Security Hardening Addendum
- Never store
NUTRIENT_DWS_VIEWER_API_KEY in committed files. Use process env injection at runtime
(shell/export, secrets manager, or host env).
- Never expose the session JWT in client-side source bundles or commit it. It is a bearer
credential granting document access until
exp. Leaking it is equivalent to leaking the API key.
- The script prints the JWT once to stdout for interactive capture and never logs it at
debug/verbose level. stdout is not an inherently safe sink — it is captured into agent
transcripts and CI build logs. For automation, write the JWT to a
0600 file or an env-export
rather than raw stdout, and do not retain CI logs from runs that mint real JWTs.
- The script redacts the API key from any error body before printing, and never sets
httpx verify=False.
- Revocation: no session-revocation endpoint is confirmed. A leaked JWT can only be contained by
rotating the underlying API key and waiting out the current sessions'
exp. Keep TTLs short
for high-sensitivity documents.
- App-provided scope: an empty-body (
--app-provided) session's authorization scope is
unconfirmed — the least-privilege permissions list does not attach to that path. The script
warns when --app-provided is used.
Documentation
Reference map
references/viewer-decision-matrix.md — full Viewer API vs Web SDK vs Document Engine matrix and
routing rules; the single source of truth for the disambiguation. Also records the confirmed
delete/teardown contract.
references/embed-guide.md — browser embed (NutrientViewer.load), CDN snippet, llms.txt
pointers, and the pointer to nutrient-web-sdk for framework recipes.
- Sibling
nutrient-web-sdk/SKILL.md — the self-hosted WASM viewer and @nutrient-sdk/viewer embed.
1---2name: dws-viewer-api3description: Upload documents to Nutrient-managed cloud storage and mint browser viewer session JWTs via the Nutrient DWS Viewer API (cloud-hosted document viewing). Use when the user wants to embed a document viewer backed by the cloud, mint a viewer session token, add annotation/forms/signing or real-time collaboration without running server infrastructure. Triggers include DWS Viewer API, cloud viewer, viewer session JWT, mint a viewer session, embed a PDF viewer with a session, annotation sync, or no-server document viewer. Not for the self-hosted Web SDK (`@nutrient-sdk/viewer` / `NutrientViewer.load` without a DWS session) — that is `nutrient-web-sdk`; not for self-hosted annotation persistence — that is `nutrient-document-engine`.4license: MIT5---67# Nutrient DWS Viewer API89The DWS Viewer API is Nutrient's **cloud-hosted** document viewing service. The flow is three10server-side steps: upload a document to Nutrient-managed storage (or reference an app-provided one),11mint a short-lived session JWT, and hand that JWT to the browser, where the Nutrient Web SDK renders12a full viewer backed by Nutrient infrastructure — storage, streaming, annotation sync, and13collaboration, with no viewing/storage servers to run. (You still need a small trusted backend14or serverless endpoint to mint the session token — the API key must never reach the browser;15this skill is exactly that server-side step.)1617This skill covers the **server-side half** — upload and session mint, the part agents get wrong. The18one-line browser embed (`NutrientViewer.load({ container, session })`) is documented in19`references/embed-guide.md`; framework integration belongs to the `nutrient-web-sdk` skill.2021## When to use2223- "Embed a PDF viewer for document review/approval in our SaaS app — I don't want to run servers."24- "Add annotation, forms, or signing to a document workflow without hosting infrastructure."25- "Mint a viewer session token / session JWT for the browser."26- "Add multi-user real-time annotation to a document, cloud-backed."2728Three Nutrient products show documents in a browser. Pick the right one **before** writing code:2930| Product | Infra hosting | Storage | Persistence / collab | Route here when |31|---|---|---|---|---|32| **DWS Viewer API** (this skill) | Nutrient cloud | Nutrient cloud or app-provided | Built in | Cloud-first; no server infra; collab fast |33| **Nutrient Web SDK** (`nutrient-web-sdk`) | Developer (browser WASM) | Developer's server | Via Document Engine | Self-hosted/offline; `@nutrient-sdk/viewer` embed |34| **Document Engine** (`nutrient-document-engine`) | Developer's server | Developer's DB | Yes (server-side) | Self-hosted persistence; custom auth; data residency |3536- **Route here (DWS Viewer API)** when the user wants a cloud-backed viewer fast, does not want to37 run servers, or needs annotation persistence / real-time collab out of the box.38- **Route to `nutrient-web-sdk`** for the self-hosted WASM viewer, license keys, or39 `NutrientViewer.load()` / `@nutrient-sdk/viewer` embed wiring **without** a DWS session.40- **Route to `nutrient-document-engine`** for self-hosted annotation persistence or41 data-residency / custom-auth backends.4243The full comparison and routing rules live in `references/viewer-decision-matrix.md` (the single44source of truth for this disambiguation).4546## Setup4748The DWS Viewer API is a **separate product** with its own API key — distinct from the Processor key49(`NUTRIENT_API_KEY`) and the Extraction key (`NUTRIENT_EXTRACT_API_KEY`). Do **not** reuse another50product's key here.5152- Get a DWS Viewer API key at <https://dashboard.nutrient.io/>.53- Export it as `NUTRIENT_DWS_VIEWER_API_KEY`:54 ```bash55 export NUTRIENT_DWS_VIEWER_API_KEY="pdf_live_..."56 ```57- Run from the directory containing this SKILL.md:58 ```bash59 cd <directory containing this SKILL.md> && uv run scripts/viewer-session.py --help60 ```6162This skill does **not** silently fall back to `NUTRIENT_API_KEY`. The Viewer API's wrong-key63behavior is unconfirmed, so a silent fallback could mint a real browser session JWT against the64wrong product/tenant with no signal (a confused-deputy). If your tenant has migrated to a global65DWS key, opt in explicitly with `--allow-global-key`, which warns at the point of fallback.6667## Tool preference6869- `scripts/viewer-session.py` — the cloud-side operations (upload, session mint, combined).70- `references/embed-guide.md` — the browser embed (`NutrientViewer.load`), CDN snippet, llms.txt71 pointers, and the pointer to `nutrient-web-sdk` for framework recipes.72- `references/viewer-decision-matrix.md` — full product decision matrix and routing rules.7374## Document modes7576- **DWS-managed** — upload the document to Nutrient storage (`upload`), then mint a session for its77 `document_id`. Nutrient stores, streams, and (optionally) persists annotations. Counts against the78 upload/storage quota; uploaded documents should be torn down when no longer needed (see Rules).79- **App-provided** — the developer streams their own file to the SDK in the browser; the backend80 only mints a session with an empty body (`session --app-provided`). No document is uploaded to81 Nutrient storage, so nothing needs teardown — but each session still consumes session quota.8283## Operations8485### `viewer-session.py` — upload + session mint8687```bash88# Upload a document to Nutrient storage; prints document_id89uv run scripts/viewer-session.py upload --file doc.pdf9091# Mint a read-only session JWT for a DWS-managed document (write the token to a 0600 file)92uv run scripts/viewer-session.py session --document-id <id> --jwt-out session.jwt9394# Elevate permissions explicitly (least privilege is the default — see Rules)95uv run scripts/viewer-session.py session --document-id <id> --allow-write --allow-download --jwt-out session.jwt9697# One-shot: upload then mint (the common case and the live smoke path)98uv run scripts/viewer-session.py upload-and-session --file doc.pdf --jwt-out session.jwt99100# Interactive only: echo the JWT to stdout instead of a file (opt-in; stdout is logged)101uv run scripts/viewer-session.py session --document-id <id> --print-jwt102103# App-provided mode: mint a session with an empty body (no upload). See Rules — scope is104# UNVERIFIED and TTL is unbounded; do not use it as a default.105uv run scripts/viewer-session.py session --app-provided --jwt-out session.jwt106107# Tear down a DWS-managed document when done (teardown)108uv run scripts/viewer-session.py delete --document-id <id>109```110111- Upload: `POST https://api.nutrient.io/viewer/documents`, binary body; response `data.document_id`.112- Session: `POST https://api.nutrient.io/viewer/sessions`, body113 `{ allowed_documents: [{ document_id, permissions }], exp }` (DWS-managed) or `{}` (app-provided);114 response `{ jwt }`. The JWT prints **once** to stdout for capture.115- `upload-and-session` on partial failure (upload succeeds, session mint fails) attempts best-effort116 cleanup — it deletes the just-uploaded document (`DELETE /viewer/documents/{id}`, confirmed live)117 so a failed run does not orphan a document in cloud storage.118119## Rules120121- **The JWT needs an explicit sink.** A session-minting command requires either `--jwt-out <file>`122 (written `0600`) or `--print-jwt`. The default is the file — printing a browser bearer token to123 stdout is opt-in because agent/CI transcripts capture stdout. The script errors before any billed124 call if neither is given.125- **Least privilege by default.** A session grants **`["read"]`** only. `write` and `download`126 require explicit `--allow-write` / `--allow-download` (or an explicit `--permissions` list). A127 minted JWT is a browser-facing bearer credential; defaulting to read-only limits the blast radius128 of a leaked token.129- **Short TTL.** Default session expiry is 1 hour. `--expires-in` can extend it up to a documented130 ceiling (24h); the script warns above a short-TTL threshold. A leaked long-TTL JWT is an open131 exfiltration window for as long as it is valid.132- **`--app-provided` is not least-privilege — don't default to it.** It mints an empty-body session133 whose authorization scope is **unverified** and whose TTL is **not bounded** by `--expires-in`134 (the API applies its own). Use DWS-managed mode (`--document-id`) for any real session. Only reach135 for `--app-provided` when you deliberately stream your own file to the browser SDK and have136 independently confirmed the scope you get — not as a shortcut for smoke runs.137- **Tear down uploaded documents.** Every document created with `upload` / `upload-and-session`138 draws down a metered quota (Free tier: 100 docs / 500 MiB / 100 uploads per month) and carries139 data-retention exposure. Delete it when done with `viewer-session.py delete --document-id <id>`.140 Verify deletion by the document's absence from `GET /viewer/documents` (a GET-by-id always 404s141 and is not a liveness signal).142- **Fail fast** on a missing key or missing required args, with a message naming143 `NUTRIENT_DWS_VIEWER_API_KEY`.144- If a future `nutrient-dws` client adds Viewer methods, the script can swap its `httpx` calls for145 them; today it uses `httpx` directly (the SDK targets Processor/Extraction).146147## Security Hardening Addendum148149- Never store `NUTRIENT_DWS_VIEWER_API_KEY` in committed files. Use process env injection at runtime150 (shell/export, secrets manager, or host env).151- **Never expose the session JWT in client-side source bundles or commit it.** It is a bearer152 credential granting document access until `exp`. Leaking it is equivalent to leaking the API key.153- The script prints the JWT **once to stdout** for interactive capture and never logs it at154 debug/verbose level. stdout is **not** an inherently safe sink — it is captured into agent155 transcripts and CI build logs. For automation, write the JWT to a `0600` file or an env-export156 rather than raw stdout, and do not retain CI logs from runs that mint real JWTs.157- The script redacts the API key from any error body before printing, and never sets158 `httpx verify=False`.159- **Revocation:** no session-revocation endpoint is confirmed. A leaked JWT can only be contained by160 **rotating the underlying API key** and waiting out the current sessions' `exp`. Keep TTLs short161 for high-sensitivity documents.162- **App-provided scope:** an empty-body (`--app-provided`) session's authorization scope is163 unconfirmed — the least-privilege `permissions` list does not attach to that path. The script164 warns when `--app-provided` is used.165166## Documentation167168- Viewer API guides (llms.txt): <https://www.nutrient.io/guides/dws-viewer/llms.txt>169- Cross-product index: <https://www.nutrient.io/llms.txt>170- Product overview: <https://www.nutrient.io/api/viewer-api/>171172## Reference map173174- `references/viewer-decision-matrix.md` — full Viewer API vs Web SDK vs Document Engine matrix and175 routing rules; the single source of truth for the disambiguation. Also records the confirmed176 delete/teardown contract.177- `references/embed-guide.md` — browser embed (`NutrientViewer.load`), CDN snippet, llms.txt178 pointers, and the pointer to `nutrient-web-sdk` for framework recipes.179- Sibling `nutrient-web-sdk/SKILL.md` — the self-hosted WASM viewer and `@nutrient-sdk/viewer` embed.