---
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-05-14T16:53:43.816Z"
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, pass the document’s ID, the JSON Web Token (JWT) for authentication, and information as to whether or not Instant real-time collaboration should be enabled in the [configuration](https://www.nutrient.io/api/web/PSPDFKit.Configuration.html) object passed to [`NutrientViewer.load()`](https://www.nutrient.io/api/web/PSPDFKit.html#.load):

```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 the viewer will be mounted. 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 [Nutrient Instant](https://www.nutrient.io/guides/document-engine/viewer/real-time-collaboration.md) real-time collaboration should be enabled.

- **`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, if you serve the script from a different location than Document Engine (such as from CDN or bundled in your application), you must explicitly set `serverUrl`.

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

### 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 this requests to our Web SDK CDN served at `https://cdn.cloud.nutrient.io`.

However, there are limitations to this approach, and it’s recommended to use the other options if:

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

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

### Serving from CDN

We maintain a CDN with the Web SDK bundle at `https://cdn.cloud.nutrient.io`. It’s used by Document Engine (refer to the previous section), and it’s also available to be used by our customers.

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.14.0/nutrient-viewer.js"></script>

```

### Serving manually

Finally, you can always serve the Web SDK manually within your applications. This has a 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 serve the contents of `/node_modules/@nutrient-sdk/viewer/dist` to your frontend. There are multiple options for serving it that 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

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

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

```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, 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: preloading `nutrient-viewer.js` for sites on which you wish to display PDF content using Nutrient, and prefetching on sites on which no PDF content is visible.

The `preload` attribute of the `<link>` tag causes your browser to start downloading the resource of the `<link>` tag earlier in the lifecycle of your page, which will improve the overall feel of your site, while the `prefetch` attribute is used to fetch the 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, you can add `<link rel="prefetch" href="path/to/nutrient-viewer.js">` or `<link rel="preload" href="path/to/nutrient-viewer.js">` within the `<head>` of the pages in question.

For websites and applications with modern frameworks that use webpack for bundling, this can be done using 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

For best results, use compatible versions of Document Engine and Web SDK:

- When serving Web SDK from Document Engine, versions are automatically matched.

- When serving from CDN or bundling manually, ensure your Web SDK version is compatible with your Document Engine version.

- Check the [requirements guide](https://www.nutrient.io/guides/document-engine/about/requirements.md) for minimum version compatibility.

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