---
title: "PDF web server integration with cloud PDF viewer | Nutrient"
canonical_url: "https://www.nutrient.io/guides/document-engine/viewer/client-integration/web/"
md_url: "https://www.nutrient.io/guides/document-engine/viewer/client-integration/web.md"
last_updated: "2026-08-05T00:00:00.000Z"
description: "Learn how to integrate Nutrient Web SDK with Document Engine for PDF viewing. Complete guide covering JWT authentication, CORS configuration, framework integration (React, Vue, Angular), and deployment options, including CDN and self-hosting."
---

# PDF web server integration with cloud PDF viewer

To load a document in [Nutrient Web SDK](https://www.nutrient.io/guides/web.md) from Document Engine, call [`NutrientViewer.load()`](https://www.nutrient.io/api/web/PSPDFKit.html#.load) with a [configuration](https://www.nutrient.io/api/web/PSPDFKit.Configuration.html) object:

```js

NutrientViewer.load({
  container: "#nutrient-container",

  documentId: "<document_id>",
  authPayload: { jwt: "<jwt>" },
  instant: true,
  serverUrl: "https://<your_document_engine_instance>/"
});

```

The configuration options are:

- **`container`** — A CSS selector (e.g. `"#nutrient-container"`) or DOM element where you mount the viewer. The element must exist in the DOM before calling `load()`.

- **`documentId`** — The identifier of an existing document on Document Engine. See the [API reference](https://www.nutrient.io/api/reference/document-engine/upstream/#tag/Documents/operation/upload-document) for information on how to upload documents.

- **`authPayload`** — An object containing the JWT for authentication. See the guide on [generating a JWT](https://www.nutrient.io/guides/document-engine/viewer/client-authentication/generate-a-jwt.md) for details.

- **`instant`** — Whether to enable [Nutrient Instant](https://www.nutrient.io/guides/document-engine/viewer/real-time-collaboration.md) real-time collaboration.

- **`serverUrl`** — The URL of your Document Engine instance. See the [`serverUrl` configuration](#understanding-serverurl) section below.

## Understanding serverUrl

The `serverUrl` configuration tells the Web SDK where to find Document Engine.

The Web SDK can automatically infer the Document Engine URL from the base URL of the `nutrient-viewer.js` script tag. However, you must set `serverUrl` if you serve the script from a different location than Document Engine, such as a CDN or your bundled application.

| Scenario                           | `serverUrl` required?       |
| ---------------------------------- | --------------------------- |
| Script served from Document Engine | No — automatically inferred |
| Script served from CDN             | Yes                         |
| Script bundled in your application | Yes                         |

**Example — Serving from Document Engine (no `serverUrl` needed):**

```html

<script src="https://document-engine.example.com/nutrient-viewer.js"></script>
<script>
  NutrientViewer.load({
    container: "#viewer",

    documentId: "abc123",
    authPayload: { jwt: "..." }
    // `serverUrl` is inferred from the script location.
  });
</script>

```

**Example — Serving from CDN (`serverUrl` required):**

```html

<script src="https://cdn.cloud.nutrient.io/pspdfkit-web@1.22.0/nutrient-viewer.js"></script>
<script>
  NutrientViewer.load({
    container: "#viewer",

    documentId: "abc123",
    authPayload: { jwt: "..." },
    serverUrl: "https://document-engine.example.com/"
  });
</script>

```

## How to serve the Web SDK

This section outlines various options for serving Nutrient Web SDK for use as a Document Engine client in your web applications.

These options describe how to deliver the Web SDK JavaScript bundle. They don’t describe the service that stores or processes documents. [DWS Viewer API](https://www.nutrient.io/guides/dws-viewer/getting-started.md) uses the same Web SDK bundle with DWS session authentication. It can load the bundle from the CDN or from your own application assets. For a complete DWS integration, follow the [DWS Viewer API getting started](https://www.nutrient.io/guides/dws-viewer/getting-started.md) guide. For a standalone Web SDK setup with a publishable key, follow the [Web SDK quick start](https://www.nutrient.io/sdk/web/getting-started/quickstart.md) guide.

The **Serving from Document Engine** option below applies only to self-hosted Document Engine. DWS Viewer API doesn’t proxy the Web SDK bundle through a Document Engine instance.

### Serving from Document Engine

To load Nutrient Web SDK from Document Engine, load the main `nutrient-viewer.js` script like so:

```html

<script src="https://<document_engine_url>/nutrient-viewer.js"></script>

```

Document Engine proxies these requests to our Web SDK CDN served at `https://cdn.cloud.nutrient.io`.

However, this approach has limitations. We recommend the other options if:

1. You can’t or don’t want to provide access to our CDN, as Document Engine can’t proxy the files without it.

2. You wish to use a newer Web SDK version. Document Engine serves the version current at its own release, which might be older than the latest Web SDK release.

### Serving from CDN

We maintain a CDN with the Web SDK bundle at `https://cdn.cloud.nutrient.io`. Document Engine uses it (refer to the previous section), and customers can also use it directly.

To load Nutrient Web SDK from the CDN, load the main `nutrient-viewer.js` script like so:

```html

<script src="https://cdn.cloud.nutrient.io/pspdfkit-web@1.22.0/nutrient-viewer.js"></script>

```

### Serving manually

Finally, you can always serve the Web SDK manually within your applications. This has the benefit of working even offline or generally when you don’t want to or can’t provide access to our CDN.

Nutrient Web SDK ships as an [npm package](https://www.npmjs.com/package/@nutrient-sdk/viewer) that’s usually installed via a package manager:

### YARN

```yarn

yarn add @nutrient-sdk/viewer

```

### NPM

```npm

npm install --save @nutrient-sdk/viewer

```

Once it’s installed, you need to serve the contents of `/node_modules/@nutrient-sdk/viewer/dist` to your frontend. Multiple options for serving it depend on your environment.

The simplest option is to add it to the static assets of your application. You can then refer to it the same as you refer to any other script:

```html

<script src="https://<your_app_url>/static/nutrient-web/nutrient-viewer.js"></script>

```

To copy the Web SDK files to your project’s static directory, you can make use of [npm prepare script](https://docs.npmjs.com/cli/v9/using-npm/scripts#life-cycle-scripts):

```package.json

"scripts": {...
  "prepare": "mkdir -p./<your_projects_static_directory>/nutrient-web && cp -R./node_modules/@nutrient-sdk/viewer/dist/./public/nutrient-web/"
},

```

Make sure to replace the `<your_projects_static_directory>` placeholder with the actual directory for static files in your project.

## Framework integration

This section shows how to integrate the Web SDK with Document Engine in popular JavaScript frameworks.

### React

This component loads and unloads the viewer as the document, JWT, or server URL change:

```jsx

import { useEffect, useRef } from "react";

function PDFViewer({ documentId, jwt, serverUrl }) {
  const containerRef = useRef(null);

  useEffect(() => {
    const container = containerRef.current;
    let instance;

    (async () => {
      const NutrientViewer = (await import("@nutrient-sdk/viewer")).default;

      instance = await NutrientViewer.load({
        container,
        documentId,
        authPayload: { jwt },
        serverUrl,
      });
    })();

    return () => instance?.unload();
  }, [documentId, jwt, serverUrl]);

  return <div ref={containerRef} style={{ height: "100vh" }} />;
}

```

### Vue 3

This component loads the viewer on mount and unloads it when the component is destroyed:

```vue

<template>
  <div ref="container" style="height: 100vh"></div>
</template>

<script setup>
import { ref, onMounted, onUnmounted } from "vue";

const props = defineProps(["documentId", "jwt", "serverUrl"]);
const container = ref(null);
let instance = null;

onMounted(async () => {
  const NutrientViewer = (await import("@nutrient-sdk/viewer")).default;

  instance = await NutrientViewer.load({
    container: container.value,
    documentId: props.documentId,
    authPayload: { jwt: props.jwt },
    serverUrl: props.serverUrl,
  });
});

onUnmounted(() => instance?.unload());
</script>

```

### Angular

This component loads the viewer after the view initializes and unloads it on destroy:

```typescript

import {
  Component,
  ElementRef,
  ViewChild,
  AfterViewInit,
  OnDestroy,
  Input,
} from "@angular/core";

@Component({
  selector: "pdf-viewer",
  template: '<div #container style="height: 100vh"></div>',

})
export class PdfViewerComponent implements AfterViewInit, OnDestroy {
  @ViewChild("container") container!: ElementRef;
  @Input() documentId!: string;
  @Input() jwt!: string;
  @Input() serverUrl!: string;

  private instance: any;

  async ngAfterViewInit() {
    const NutrientViewer = (await import("@nutrient-sdk/viewer")).default;

    // Note: `ViewChild` is only available after view initialization.
    this.instance = await NutrientViewer.load({
      container: this.container.nativeElement,
      documentId: this.documentId,
      authPayload: { jwt: this.jwt },
      serverUrl: this.serverUrl,
    });
  }

  ngOnDestroy() {
    this.instance?.unload();
  }
}

```

**Container timing:** In frameworks like Angular, the container element may not be available immediately. Ensure you wait for the view to initialize (e.g. use `ngAfterViewInit` in Angular or `useEffect`/`onMounted` in React/Vue) before calling `NutrientViewer.load()`.

## Cross-origin configuration (CORS)

When your web application runs on a different domain than Document Engine, Cross-Origin Resource Sharing (CORS) must be configured via the JWT `allowed_origins` claim.

### Configuring allowed origins

Document Engine uses the `allowed_origins` claim in the JWT to determine which origins are permitted. When [generating a JWT](https://www.nutrient.io/guides/document-engine/viewer/client-authentication/generate-a-jwt.md), include the `allowed_origins` claim with your frontend domain(s):

```json

{
  "document_id": "abc123",
  "permissions": ["read-document"],
  "allowed_origins": ["https://your-app.com", "https://staging.your-app.com"]
}

```

Set `allowed_origins` to `"any"` to allow requests from any origin (not recommended for production).

### Common CORS errors

If you see an error like:

> Access to fetch at `https://document-engine.example.com/i/d/5/auth` from origin `https://your-app.com` has been blocked by CORS policy

Check that:

1. The JWT includes an `allowed_origins` claim with your frontend domain.

2. The protocol matches exactly (`http` vs. `https`).

3. The port is included if using a non-standard port.

Refer to the [JWT authentication](https://www.nutrient.io/guides/document-engine/viewer/client-authentication/generate-a-jwt.md) guide for details on configuring the `allowed_origins` claim.

## License and domain configuration

Document Engine validates that requests originate from licensed domains.

### Origin validation errors

If you see an error like:

> PSPDFKit Document Engine is not licensed for use from the origin `http://localhost:8080`

This means the requesting origin isn’t included in your license configuration.

**To resolve:**

- **Development** — Use a development license that includes `localhost`.

- **Production** — [Contact Support](https://support.nutrient.io/hc/en-us/requests/new) to add your production domains to your license.

Refer to the [domain configuration](https://www.nutrient.io/guides/document-engine/troubleshooting/license/domain-use-in-de.md) guide for more details.

## Differences from standalone mode

When using Document Engine instead of standalone Web SDK, some behaviors differ. These are outlined in the table below.

| Feature                 | Standalone                  | Document Engine                         |
| ----------------------- | --------------------------- | --------------------------------------- |
| Document source         | URL, `ArrayBuffer`, or file | `documentId` on server                  |
| Annotation storage      | Local or custom backend     | Server-managed                          |
| `instantJSON` parameter | Loads annotations from JSON | Ignored — annotations managed by server |
| `autoSaveMode`          | Controls local saving       | Controls syncing to Document Engine     |
| Document streaming      | Not available               | [Enabled by default](https://www.nutrient.io/guides/document-engine/viewer/streaming.md)         |

## Performance considerations

To improve load times within your application, we recommend using `preload` and `prefetch` in adequate scenarios. Preload `nutrient-viewer.js` on sites where you plan to display PDF content. Prefetch it on sites where no PDF content is visible.

The `preload` attribute of the `<link>` tag causes your browser to start downloading the resource earlier in the page lifecycle. This improves the overall feel of your site. The `prefetch` attribute fetches resources you predict a user is likely to request. See the [HTML specification](https://www.w3.org/TR/resource-hints/) for more information.

The implementation of these will depend on how you’re importing `nutrient-viewer.js` on your site/application. If your service is a regular website using vanilla JavaScript, add `<link rel="prefetch" href="path/to/nutrient-viewer.js">` or `<link rel="preload" href="path/to/nutrient-viewer.js">`. Place it within the `<head>` of the pages in question.

For websites and applications with modern frameworks that use webpack, use the `/* webpackPrefetch: true */` or `/* webpackPreload: true */` comments within your `import()`. Webpack will use this information to generate the appropriate `<link rel="prefetch"...>` or `<link rel="preload"...>` tags for you. See the [webpack documentation](https://webpack.js.org/guides/code-splitting/#prefetchingpreloading-modules) for more information.

## Version compatibility

Document Engine and Web SDK versions are generally compatible in server-backed mode, but some Web SDK features require a minimum Document Engine version. The table below lists known requirements for those feature and version combinations. It isn’t a complete compatibility matrix for current releases, so check the release notes for the specific versions you plan to deploy.

| Web SDK version | Incompatible Document Engine version | Affected functionality                                                                                                                                                                                                                                             |
| --------------- | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| 1.13–1.18       | Earlier than 1.12.0                  | `instance.exportPDF()`. These Document Engine versions require legacy query-parameter authentication for `/pdf`, which these Web SDK releases don’t send.                                                                                                          |
| 1.15 or later   | Earlier than 1.16.0                  | `SearchType.WORD_BASED` in server-backed mode.                                                                                                                                                                                                                     |
| 1.16 or later   | Earlier than 1.16.0                  | Removing password protection during server-backed PDF exports.                                                                                                                                                                                                     |
| 1.16 or later   | Earlier than 1.16.0                  | Removing annotation notes after an XFDF roundtrip.                                                                                                                                                                                                                 |
| 1.19 or later   | Earlier than 1.18.0                  | Document-defined annotation tab order in server-backed mode. Web SDK doesn’t request the unsupported endpoint and retains fallback tab ordering. `instance.getPageTabOrder()` reports that the Document Engine version doesn’t provide the document-defined order. |

When serving Web SDK from Document Engine, it provides the Web SDK version that shipped with that Document Engine release. When serving from the Nutrient CDN or bundling Web SDK manually, check the combinations above.

## Troubleshooting

This section covers common issues you may encounter when integrating Nutrient Web SDK with Document Engine, along with their solutions. For issues not listed here, refer to the related guides below or [contact Support](https://support.nutrient.io/hc/en-us/requests/new).

### Common errors

| Error                               | Cause                                         | Solution                                                                                                                                                |
| ----------------------------------- | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `container must be a valid element` | Element doesn’t exist when `load()` is called | Wait for DOM to be ready; use framework lifecycle hooks                                                                                                 |
| `Not licensed for origin`           | Domain not in license                         | Add domain to license; see [domain configuration](https://www.nutrient.io/guides/document-engine/troubleshooting/license/domain-use-in-de.md)                                                                                        |
| CORS errors                         | Cross-origin not configured                   | Add the `allowed_origins` claim to JWT                                                                                                                  |
| Connection timeout                  | Network or timeout configuration              | See the [504 response](https://www.nutrient.io/guides/document-engine/troubleshooting/errors-and-warnings/504-response.md) guide                                                                                                                 |
| Authentication failed               | Invalid or expired JWT                        | Regenerate JWT and verify claims (including `exp`). For expired tokens in long-running sessions, use runtime refresh with `onAuthFailed` + `setSession` |

To renew JWTs without recreating the viewer, refer to the [web client authentication and session renewal](https://www.nutrient.io/guides/web/viewer/client-authentication.md) guide.

### Related guides

- [Troubleshooting overview](https://www.nutrient.io/guides/document-engine/troubleshoot.md)

- [HTTPS setup](https://www.nutrient.io/guides/document-engine/troubleshooting/getting-started/https.md)

- [Document streaming](https://www.nutrient.io/guides/document-engine/viewer/streaming.md)

- [License troubleshooting](https://www.nutrient.io/guides/document-engine/troubleshooting/license/license-troubleshooting.md)