> ## Documentation Index
> Fetch the complete documentation index at: https://developer.box.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Build secure in-app document review with Box View

> Upload, render and review your key documents, all in your own app.

export const RelatedLinks = ({title, items = []}) => {
  const getBadgeClass = badge => {
    if (!badge) return "badge-default";
    const badgeType = badge.toLowerCase().replace(/\s+/g, "-");
    return `badge-${badge === "ガイド" ? "guide" : badgeType}`;
  };
  if (!items || items.length === 0) {
    return null;
  }
  return <div className="my-8">
      {}
      <h3 className="text-sm font-bold uppercase tracking-wider mb-4">{title}</h3>

      {}
      <div className="flex flex-col gap-3">
        {items.map((item, index) => <a key={index} href={item.href} className="py-2 px-3 rounded related_link hover:bg-[#f2f2f2] dark:hover:bg-[#111827] flex items-center gap-3 group no-underline hover:no-underline border-b-0">
            {}
            <span className={`px-2 py-1 rounded-full text-xs font-semibold uppercase tracking-wide flex-shrink-0 ${getBadgeClass(item.badge)}`}>
              {item.badge}
            </span>

            {}
            <span className="text-base">{item.label}</span>
          </a>)}
      </div>
    </div>;
};

export const SignupCTA = ({children}) => {
  return <div className="flex flex-wrap items-center gap-4 p-5 rounded-lg border border-gray-200 dark:border-gray-700 my-6" style={{
    background: "linear-gradient(135deg, rgba(0, 97, 213, 0.06), rgba(0, 97, 213, 0.02))"
  }}>
      <div className="flex-1 text-sm leading-relaxed text-gray-700 dark:text-gray-300" style={{
    minWidth: "280px"
  }}>
        {children}
      </div>
      <div className="flex flex-col items-center gap-2">
        <a href="https://account.box.com/signup/developer#ty9l3" className="signup-cta-button inline-flex items-center whitespace-nowrap px-5 py-2 text-sm font-semibold text-white no-underline">
          Get started for free
        </a>
        <a href="https://account.box.com/developers/console" className="signup-cta-login text-xs text-gray-500 dark:text-gray-400 no-underline whitespace-nowrap">
          Already have an account? Log in
        </a>
      </div>
    </div>;
};

export const Link = ({href, children, className, ...props}) => {
  const localizedHref = localizeLink(href);
  return <a href={localizedHref} className={className} {...props}>
      {children}
    </a>;
};

When you spend your day reading sensitive documents - tax returns, bank statements, pay stubs - you need to context-switch constantly: download a PDF, open it in a separate viewer, mark it up somewhere else, then return to the workbench to record a decision. Every shift is a chance to leak a document, lose a note, or stall an application.

This tutorial keeps the entire review inside your own application:

1. A loan workbench uploads each borrower document to Box through the secure upload API.
2. Box converts it into a high-fidelity HTML5 rendering
3. Your app requests a short-lived embed URL and drops it into an `<iframe>`.

Reviewers read and annotate the document in context. When you need a richer experience (in-context markup, a metadata and activity sidebar, or Box AI Q\&A), layer <Link href="/guides/embed/ui-elements/preview">Box Content Preview</Link> on the same underlying files in Box.

<Card title="Clone the working sample" href="https://github.com/box-community/box-secure-doc-review" icon="github" arrow="true">
  Prefer to start from running code? The complete app built in this tutorial is on GitHub. Clone it, add your Box credentials, and run.
</Card>

## What you are building

By the end of this tutorial, you have a working integration that:

* Uploads borrower documents to Box using the upload API, triggering automatic conversion for preview.
* Generates a short-lived, expiring embed URL for any uploaded file.
* Renders the document inside an `<iframe>` in your workbench.
* Mints downscoped, file-scoped tokens so the browser only ever holds a least-privilege credential.
* Upgrades the bare iframe to Box Content Preview so reviewers can annotate in context and use a richer review UI.

## Prerequisites

Before you start, make sure you have the following:

* A Box account with access to the Developer Console and admin rights to authorize a Platform App (a <Link href="https://account.box.com/signup/developer#ty9l3">free Box developer account</Link> works; consumer/Individual accounts do not). A paid <Link href="https://www.box.com/pricing">Box Enterprise account</Link> is required for the optional Box AI Q\&A layer.
* A Box <Link href="/guides/embed/box-view/setup">Platform App configured for Box View</Link> with **Client Credentials Grant (CCG)** authentication and **Application Access** set to **App Access Only**. This makes the app's Service Account the owner of every uploaded document.
* Python 3.11 or higher.
* The following scope enabled on your app:
  * Read and write all files and folders stored in Box
* The exact origin that serves your workbench front end (for example, `http://127.0.0.1:5000`) added to the **CORS allow-list** in the <Link href="https://cloud.app.box.com/developers/console">Developer Console</Link>. **This is required for the <Link href="/guides/tutorials/secure-in-app-document-review#add-in-context-annotations-with-box-content-preview">Box Content Preview annotation step</Link>**. Content Preview fails with a `403` until the origin is allow-listed.
* Your Box enterprise ID (found in the Developer Console under the account icon, **Copy Enterprise ID**).

<Note>
  This tutorial uses CCG authenticating as the **Service Account** (`enterprise_id`). Because the Service Account owns the files it uploads, no collaboration invites are required to preview them - a key simplification compared to integrations that operate on existing enterprise content.
</Note>

## How it works

Box View follows a three-stage flow:

1. **Upload.** Your backend uploads the borrower document to Box over the secure upload API. Content is stored with virus scanning and 256-bit encryption.
2. **Conversion.** On upload, Box automatically converts the file into HTML5-compatible assets that render crisply and responsively. Conversion happens once per file and the assets persist for as long as the file is stored.
3. **Embed.** Your backend requests an `expiring_embed_link` for the file and returns it to the front end, which places it in an `<iframe>`.

This solution uses three Box Platform capabilities:

| Component                      | Purpose                                                                           | API                                             |
| ------------------------------ | --------------------------------------------------------------------------------- | ----------------------------------------------- |
| **Upload API**                 | Store the borrower's tax return or statement in Box                               | `POST /2.0/files/content`                       |
| **Expiring embed link**        | Generate a short-lived preview URL for the `<iframe>`                             | `GET /2.0/files/:id?fields=expiring_embed_link` |
| **Token exchange (downscope)** | Mint a least-privilege, file-scoped token for Box Content Preview and annotations | `POST /oauth2/token`                            |

<Warning>
  The expiring embed link is valid for **about one minute** and is meant to be dropped into an `<iframe>` immediately after it is generated. Always generate it server-side, on demand, when the reviewer opens a document - never store it or send it ahead of time.
</Warning>

## Step-by-step process

<Steps>
  <Step title="Set up the development environment">
    1. Open your terminal and create a new project directory:

    ```bash theme={null}
    mkdir loan-review && cd loan-review
    ```

    2. Create and activate a Python virtual environment:

    ```bash theme={null}
    python3 -m venv .venv
    source .venv/bin/activate
    ```

    After activation, your terminal prompt shows `(.venv)` at the beginning. This confirms you are working inside the virtual environment.

    <Note>
      Every time you open a new terminal window or tab, you must re-activate the virtual environment by running `source .venv/bin/activate` from the project directory. If you see `ModuleNotFoundError` when running commands, it usually means the venv is not activated.
    </Note>

    3. Install the required packages:

    ```bash theme={null}
    pip install box-sdk-gen flask python-dotenv
    ```

    4. Create a `.env` file to store your credentials, then add the following content. Replace the placeholder values with your actual credentials from the Box Developer Console:

    ```bash theme={null}
    BOX_CLIENT_ID=your_client_id
    BOX_CLIENT_SECRET=your_client_secret
    BOX_ENTERPRISE_ID=your_enterprise_id
    LOAN_DOCS_FOLDER_ID=0
    ```

    <Note>
      `LOAN_DOCS_FOLDER_ID` defaults to `0`, the Service Account's root folder. That is fine for getting started. For a real deployment, create a dedicated folder (for example, one folder per loan application) and use its ID so documents stay organized.
    </Note>

    <Warning>
      Never commit `.env` files to version control. Add `.env` to your `.gitignore`.
    </Warning>

    <Note>
      **Understanding environment variables:** The `.env` file stores sensitive values (your actual credentials). Your Python code reads these values by referencing their **names** using `os.getenv("VARIABLE_NAME")`. For example, `os.getenv("BOX_CLIENT_ID")` looks up the value stored next to `BOX_CLIENT_ID=` in your `.env` file. When you copy the code in the following steps, keep the quoted variable names exactly as shown. Do not replace them with your actual credentials.
    </Note>
  </Step>

  <Step title="Authenticate the Box client">
    Create a new file called `box_client.py` in your project directory. Open the file and paste the following code:

    ```python theme={null}
    import os
    from dotenv import load_dotenv
    from box_sdk_gen import (
        BoxClient,
        BoxCCGAuth,
        CCGConfig,
    )

    load_dotenv()

    def get_box_client() -> BoxClient:
        config = CCGConfig(
            client_id=os.getenv("BOX_CLIENT_ID"),
            client_secret=os.getenv("BOX_CLIENT_SECRET"),
            enterprise_id=os.getenv("BOX_ENTERPRISE_ID"),
        )
        auth = BoxCCGAuth(config=config)
        return BoxClient(auth=auth)
    ```

    <Tip>
      Client Credentials Grant is recommended for server-to-server Box View use cases where the application's Service Account owns the content. For other authentication options, see <Link href="/guides/authentication/select">Select an authentication method</Link>.
    </Tip>
  </Step>

  <Step title="Upload a borrower document">
    Create a new file called `upload.py`. This function uploads a document to the loan documents folder and returns the new file ID. Uploading a document automatically triggers conversion, so the file is ready to preview moments later.

    ```python theme={null}
    import os
    from dotenv import load_dotenv
    from box_sdk_gen import (
        BoxClient,
        UploadFileAttributes,
        UploadFileAttributesParentField,
    )

    load_dotenv()

    def upload_document(client: BoxClient, file_path: str) -> str:
        folder_id = os.getenv("LOAN_DOCS_FOLDER_ID", "0")
        file_name = os.path.basename(file_path)

        with open(file_path, "rb") as file_stream:
            uploaded = client.uploads.upload_file(
                UploadFileAttributes(
                    name=file_name,
                    parent=UploadFileAttributesParentField(id=folder_id),
                ),
                file_stream,
            )

        file_id = uploaded.entries[0].id
        print(f"Uploaded {file_name} -> file ID {file_id}")
        return file_id
    ```

    <Note>
      For most document and image types, conversion is triggered automatically on upload. For video and 3D files, conversion is triggered on first preview instead. Either way, conversion runs only once per file and no explicit action is required from your code.
    </Note>
  </Step>

  <Step title="Generate the preview embed URL">
    Create a new file called `preview.py`. The `get_embed_url` function asks Box for the file's `expiring_embed_link` and returns the URL your front end will place in an `<iframe>`.

    ```python theme={null}
    from box_sdk_gen import BoxClient

    def get_embed_url(client: BoxClient, file_id: str) -> str:
        file = client.files.get_file_by_id(
            file_id,
            fields=["expiring_embed_link"],
        )

        if not file.expiring_embed_link:
            raise RuntimeError(
                "No embed link returned. The file may still be converting, "
                "or preview is not available for this file type."
            )

        return file.expiring_embed_link.url
    ```

    <Warning>
      Request `expiring_embed_link` only through the `fields` parameter, and use it right away. The link expires after about one minute by design - this is what keeps the embed secure.
    </Warning>
  </Step>

  <Step title="Mint a downscoped token for the browser">
    The bare iframe never needs a token, but Box Content Preview (added later) runs in the browser and does. Add a function to `preview.py` that exchanges the full Service Account token for a **downscoped**, file-scoped token. This token can only do what the scopes allow, on a single file, and is short-lived - safe to send to the browser.

    ```python theme={null}
    from box_sdk_gen import BoxClient

    PREVIEW_SCOPES = [
        "base_preview",
        "item_preview",
        "annotation_edit",
        "annotation_view_all",
        "item_download",
    ]

    def get_downscoped_token(client: BoxClient, file_id: str) -> dict:
        token = client.auth.downscope_token(
            PREVIEW_SCOPES,
            resource=f"https://api.box.com/2.0/files/{file_id}",
        )
        return {
            "access_token": token.access_token,
            "expires_in": token.expires_in,
        }
    ```

    The scopes map directly to review capabilities:

    | Scope                 | What it allows                                                       |
    | --------------------- | -------------------------------------------------------------------- |
    | `base_preview`        | Preview the file in Box Content Preview                              |
    | `item_preview`        | Preview the file (expiring embed link)                               |
    | `annotation_edit`     | Create, edit, and delete the reviewer's own annotations              |
    | `annotation_view_all` | View annotations from all reviewers on the file                      |
    | `item_download`       | Enable text selection and highlight annotations (and download/print) |

    <Warning>
      Never expose your client secret or a full access token in client-side code. Always downscope before sending a token to the browser. See <Link href="/guides/embed/box-view/best-practices#downscope-tokens">Box View best practices</Link>.
    </Warning>
  </Step>

  <Step title="Expose the backend endpoints">
    Create a new file called `app.py`. This Flask application is the backend your loan workbench calls. It exposes one endpoint to upload a document, one to fetch a fresh embed URL on demand, and one to mint a downscoped token.

    ```python theme={null}
    import os
    import tempfile

    from dotenv import load_dotenv
    from flask import Flask, request, jsonify, send_file

    from box_client import get_box_client
    from upload import upload_document
    from preview import get_embed_url, get_downscoped_token

    load_dotenv()
    app = Flask(__name__)

    @app.route("/api/documents", methods=["POST"])
    def upload():
        if "file" not in request.files:
            return jsonify({"error": "no file provided"}), 400

        incoming = request.files["file"]
        with tempfile.NamedTemporaryFile(
            delete=False, suffix=f"_{incoming.filename}"
        ) as tmp:
            incoming.save(tmp.name)
            temp_path = tmp.name

        try:
            client = get_box_client()
            file_id = upload_document(client, temp_path)
        finally:
            os.remove(temp_path)

        return jsonify({"file_id": file_id}), 201

    @app.route("/api/documents/<file_id>/embed", methods=["GET"])
    def embed(file_id):
        client = get_box_client()
        embed_url = get_embed_url(client, file_id)
        return jsonify({"embed_url": embed_url}), 200

    @app.route("/api/documents/<file_id>/token", methods=["GET"])
    def token(file_id):
        client = get_box_client()
        return jsonify(get_downscoped_token(client, file_id)), 200

    @app.route("/review/<file_id>")
    def review_page(file_id):
        return send_file("review.html")

    if __name__ == "__main__":
        app.run(port=5000, debug=True)
    ```

    <Note>
      The `/embed` endpoint creates the link fresh on every request. Because the link expires in about a minute, the front end should call this endpoint at the moment the reviewer opens a document, not ahead of time.
    </Note>

    <Warning>
      **Restart Flask whenever you add or change a route.** Flask loads your code once at startup, so a newly added route (like `/review/<file_id>`) returns `404 Not Found` until you stop the server with `Ctrl+C` and run `python app.py` again. Setting `debug=True` enables the auto-reloader so subsequent edits reload automatically - but the very first run after adding a route still requires a manual restart. Do not use `debug=True` in production.
    </Warning>

    <Warning>
      The `/review/<file_id>` route serves the review page **from the same origin as the backend** so the page's relative `fetch("/api/...")` calls reach your API. Do not open `review.html` by double-clicking it - opening it over the `file://` protocol means the relative `fetch` has no server to call, the iframe `src` is never set, and you see only the page header. You build `review.html` in the next step.
    </Warning>

    At this point, your project directory should contain the following files:

    ```
    loan-review/
    ├── .env
    ├── .venv/
    ├── app.py
    ├── box_client.py
    ├── preview.py
    └── upload.py
    ```
  </Step>

  <Step title="Render the document in an iframe">
    With the backend in place, the front end is simple: fetch the embed URL, then set it as the `src` of an `<iframe>`. This is the secure, in-app review surface - the reviewer never leaves your workbench.

    Create a file called `review.html` in the project root (next to `app.py`, so the `/review/<file_id>` route can serve it). The page reads the file ID from its own URL path, so you never hardcode it.

    ```html theme={null}
    <!DOCTYPE html>
    <html lang="en-US">
      <head>
        <meta charset="utf-8" />
        <title>Loan document review</title>
        <style>
          body { margin: 0; font-family: system-ui, sans-serif; }
          header { padding: 12px 20px; background: #f5f6f8; border-bottom: 1px solid #e0e0e0; }
          iframe { width: 100%; height: calc(100vh - 50px); border: 0; }
        </style>
      </head>
      <body>
        <header><strong>Loan application 4815</strong> &middot; Borrower tax return</header>
        <iframe id="doc" title="Borrower document"></iframe>

        <script>
          // The file ID is the last segment of the URL, e.g. /review/1234567890
          const fileId = window.location.pathname.split("/").pop();

          async function loadPreview() {
            const res = await fetch(`/api/documents/${fileId}/embed`);
            const { embed_url } = await res.json();
            document.getElementById("doc").src = embed_url;
          }

          loadPreview();
        </script>
      </body>
    </html>
    ```

    <Warning>
      Always reach this page through the backend at `http://127.0.0.1:5000/review/<file_id>` - never by double-clicking `review.html`. A double-clicked file opens over `file://`, where the relative `fetch("/api/...")` has no server to reach. The iframe `src` is then never set and you see only the header. The page and the API must share an origin.
    </Warning>

    <Tip>
      In production, the workbench app and the Box-facing backend typically live behind the same gateway (same origin), or you proxy `/api/*` to the backend, so these relative `fetch` calls resolve.
    </Tip>
  </Step>

  <Step title="Test the upload and embed flow">
    You can exercise the whole pipeline from the command line before wiring it into a UI.

    <Note>
      Make sure you are in the `loan-review` directory and the virtual environment is activated before running any commands:

      ```bash theme={null}
      cd ~/loan-review
      source .venv/bin/activate
      ```
    </Note>

    **1. Start the backend:**

    ```bash theme={null}
    python app.py
    ```

    You should see `Running on http://127.0.0.1:5000`. Leave this terminal running.

    **2. Upload a document.** In a second terminal, upload a sample tax return or statement (any PDF works):

    ```bash theme={null}
    curl -X POST http://127.0.0.1:5000/api/documents \
      -F "file=@sample-tax-return.pdf"
    ```

    You should get back a file ID:

    ```json theme={null}
    { "file_id": "1234567890" }
    ```

    **3. Fetch an embed URL** for that file ID:

    ```bash theme={null}
    curl http://127.0.0.1:5000/api/documents/1234567890/embed
    ```

    ```json theme={null}
    { "embed_url": "https://app.box.com/preview/expiring_embed/gvoct6FE!YT_X1Lau..." }
    ```

    **4. View it in the iframe.** With the backend still running, open the review page through the server, using your file ID as the last path segment:

    ```
    http://127.0.0.1:5000/review/1234567890
    ```

    The document renders inside the iframe. If instead you see only the `Loan application 4815` header, you most likely opened `review.html` by double-clicking it (the `file://` protocol) - open it through the URL above instead. As a separate sanity check, pasting the embed URL from step 3 directly into the browser within a minute also renders the document, proving conversion succeeded.
  </Step>
</Steps>

## Troubleshooting

<AccordionGroup>
  <Accordion title="ModuleNotFoundError: No module named '...'">
    Your virtual environment is not activated. Run `source .venv/bin/activate` from the project directory before running any `python` commands. Each new terminal tab needs its own activation.
  </Accordion>

  <Accordion title="invalid_client: The client credentials are invalid">
    Check your `.env` file:

    * Verify `BOX_CLIENT_ID` and `BOX_CLIENT_SECRET` match the values in Developer Console > your app > Configuration.
    * Confirm `BOX_ENTERPRISE_ID` is your enterprise ID (Developer Console > account icon > Copy Enterprise ID).
    * Ensure the app type is Client Credentials Grant and the app has been authorized.
  </Accordion>

  <Accordion title="unauthorized_client">
    The app has not been authorized by an admin yet. In the Developer Console, open the **Authorization** tab, click **Review and Submit**, and have your Box admin approve it in the Admin Console. See <Link href="/guides/authorization/platform-app-approval">Platform App approval</Link>.
  </Accordion>

  <Accordion title="No embed link returned / null expiring_embed_link">
    Conversion may still be in progress, or the file type does not support preview. Wait a few seconds after upload and retry. Confirm the uploaded file is a supported type - see the <Link href="/guides/representations/supported-file-types">supported file types</Link>.
  </Accordion>

  <Accordion title="404 Not Found when opening /review/<file_id>">
    Flask does not have the `/review/<file_id>` route registered. This almost always means you added the route but did not restart the server - Flask loads your code once at startup. Stop the server with `Ctrl+C` and run `python app.py` again. Also confirm `send_file` is imported (`from flask import Flask, request, jsonify, send_file`) and that the route exists in `app.py`. Running with `debug=True` enables auto-reload for future edits.
  </Accordion>

  <Accordion title="The page shows only the header, not the document">
    You opened `review.html` directly (over the `file://` protocol) instead of through the backend. The page's relative `fetch("/api/documents/.../embed")` call only works when the page is served from the same origin as the API. Start the backend and open `http://127.0.0.1:5000/review/<file_id>` instead of double-clicking the file. Open your browser's developer console to confirm - you will see the failed `fetch` request.
  </Accordion>

  <Accordion title="The iframe is blank or shows 'This shared file is no longer available'">
    The embed link has expired. Embed links last about one minute. Generate the link immediately before setting the iframe `src`, and request a new one each time the reviewer opens the document.
  </Accordion>

  <Accordion title="The annotate page returns 403 (Forbidden) even though the review page works">
    This is almost always a **CORS allow-list** issue. The iframe review page loads a Box-hosted URL and never calls the Box API from your origin, so it works without CORS. Box Content Preview, however, calls `api.box.com` **directly from the browser** - in your developer console you will see repeated `GET https://api.box.com/2.0/files/<id>?fields=...` requests returning `403`, thrown from `preview.js`.

    Fixes, in order:

    * Add the **exact** origin you open the page from to the CORS allow-list (Developer Console > your app > **Configuration** > **CORS Domains**), including scheme and port - for example `http://127.0.0.1:5000`. Remember that `127.0.0.1` and `localhost` are different origins; add whichever you use. Save and wait a minute or two, then restart and reload.
    * If it still `403`s, the downscoped token lacks permissions: confirm the **Read and write all files and folders** scope is enabled and that you **re-authorized** the app in the Admin Console afterward.
    * Confirm the file ID in the `/annotate/<file_id>` URL matches the file the token was minted for - the token is downscoped to a single file, so a mismatched ID also returns `403`.
  </Accordion>
</AccordionGroup>

## Add in-context annotations with Box Content Preview

The iframe embed is perfect for fast, read-only review. To let reviewers **mark up documents in context** - highlight a suspicious figure on a tax return, drop a point comment on a missing signature - render the same file with the <Link href="/guides/embed/ui-elements/preview">Box Content Preview</Link> UI Element instead of the bare iframe. Content Preview powers preview in the Box web app itself and runs entirely client-side, so annotations stay inside your workbench.

Content Preview needs a token in the browser. You already built the `/token` endpoint that returns a downscoped, file-scoped token with annotation scopes, so the front end never sees a full-privilege credential.

<Warning>
  **You must add your serving origin to the CORS allow-list before this step will work.** Box Content Preview makes API calls **directly from the browser to `api.box.com`**. Box rejects those calls with a `403 (Forbidden)` unless the page's origin is on your app's CORS allow-list (Developer Console > your app > **Configuration** > **CORS Domains**).

  Add the **exact** origin you open the page from, including the scheme and port - for example `http://127.0.0.1:5000`. Note that `http://127.0.0.1:5000` and `http://localhost:5000` are **different origins**: allow-list whichever you actually browse to (or add both). Changes can take a minute or two to take effect.
</Warning>

Create a file called `annotate.html` in the project root, then add a route in `app.py` to serve it from the same origin as the API - just like the iframe page:

```python theme={null}
@app.route("/annotate/<file_id>")
def annotate_page(file_id):
    return send_file("annotate.html")
```

```html theme={null}
<!DOCTYPE html>
<html lang="en-US">
  <head>
    <meta charset="utf-8" />
    <title>Loan document review</title>
    <link
      rel="stylesheet"
      href="https://cdn01.boxcdn.net/platform/preview/3.0.0/en-US/preview.css"
    />
    <script src="https://cdn01.boxcdn.net/platform/preview/3.0.0/en-US/preview.js"></script>
    <style>
      .preview-container { height: 100vh; width: 100%; }
    </style>
  </head>
  <body>
    <div class="preview-container"></div>

    <script>
      // The file ID is the last segment of the URL, e.g. /annotate/1234567890
      const fileId = window.location.pathname.split("/").pop();

      async function loadPreview() {
        const res = await fetch(`/api/documents/${fileId}/token`);
        const { access_token } = await res.json();

        const preview = new Box.Preview();
        preview.show(fileId, access_token, {
          container: ".preview-container",
          showAnnotations: true,
        });
      }

      loadPreview();
    </script>
  </body>
</html>
```

Open it the same way you opened the iframe page - through the backend, at `http://127.0.0.1:5000/annotate/<file_id>`, not by double-clicking the file.

When `showAnnotations` is `true` and the token carries the annotation scopes, reviewers see annotation controls in the preview header. Annotations are stored against the file in Box, so a second reviewer opening the same document sees the first reviewer's notes (granted `annotation_view_all`).

<Note>
  Box Annotations support **highlight comment**, **highlight only**, **draw**, and **point** annotation types. Point annotations work on both documents and images; highlight and draw annotations are document-only. Highlight annotations also require the `item_download` scope so the text layer is available. See the <Link href="/guides/embed/ui-elements/annotations">Annotations guide</Link> for the full matrix and for enabling real-time V4 annotations.
</Note>

<Warning>
  If the preview still returns a `403` **after** you have allow-listed the origin, the downscoped token is missing permissions. Confirm the **Read and write all files and folders** scope is enabled on your app, and that you **re-authorized** the app in the Admin Console after enabling it - CCG scope changes do not take effect until the app is re-approved. See the <Link href="/guides/embed/ui-elements/scopes">dedicated scopes</Link> guide.
</Warning>

## Layer a richer review UI

Once you are running Box Content Preview, you can progressively enrich the reviewer experience on the very same files - no re-upload, no second copy of the document.

<AccordionGroup>
  <Accordion title="Add a metadata and activity sidebar">
    Use the Content Preview element's sidebar to show file details, versions, and an activity feed alongside the document, so reviewers can see the document's history and discuss it without leaving the page. Enable annotations in the activity feed and the document view together for a connected review workflow. See <Link href="/guides/embed/ui-elements/sidebar">Content Sidebar</Link>.
  </Accordion>

  <Accordion title="Answer questions with Box AI in the previewer">
    Box AI for UI Elements adds document Q\&A and summaries directly in the Content Preview header. A reviewer can ask "What is the borrower's reported annual income?" and get a cited answer grounded in the document. Pass `hasHeader: true` and `contentAnswersProps` to `preview.show()`. See <Link href="/guides/embed/ui-elements/preview#box-ai-for-ui-elements">Box AI for UI Elements</Link>.
  </Accordion>

  <Accordion title="Review a full application as a collection">
    A loan application is rarely one file. Pass a `collection` of file IDs to `preview.show()` so reviewers can page through the tax return, W-2, and bank statements with navigation arrows inside one previewer.

    ```js theme={null}
    preview.show(fileId, access_token, {
      container: ".preview-container",
      collection: ["1234567890", "1234567891", "1234567892"],
      showAnnotations: true,
    });
    ```

    Mint the downscoped token with a resource and scopes that cover each file in the collection.
  </Accordion>

  <Accordion title="Tailor capabilities with scopes">
    Match the token's scopes to the reviewer's role. A junior reviewer might get `base_preview` + `annotation_view_self` + `annotation_edit` (sees only their own notes), while a senior underwriter gets `annotation_view_all`. After a decision is locked, drop the edit scopes to make the document effectively read-only. See <Link href="/guides/embed/ui-elements/preview#scopes">Content Preview scopes</Link>.
  </Accordion>
</AccordionGroup>

## Scaling to production

<AccordionGroup>
  <Accordion title="Organize documents per application">
    Instead of uploading everything to the Service Account root, create a folder per loan application and pass its ID as the upload parent. This keeps documents grouped, makes cleanup simple, and lets you apply <Link href="/guides/retention-policies/index">retention policies</Link> at the folder level for compliance.
  </Accordion>

  <Accordion title="Refresh tokens and embed links on demand">
    Embed links expire in about a minute and downscoped tokens are short-lived by design. Generate both server-side at the moment they are needed. For Content Preview, pass a <Link href="/guides/embed/ui-elements/preview#token-generator-function">token generator function</Link> instead of a static string so the previewer can fetch a fresh token whenever it needs one.
  </Accordion>

  <Accordion title="Keep credentials server-side">
    The client secret, enterprise ID, and full access token must never reach the browser. Store them in environment variables or a secrets manager, keep **Application Access** set to **App Access Only**, and restrict the CORS allow-list to the exact domains that run Box Content Preview. See <Link href="/guides/embed/box-view/best-practices">Box View best practices</Link>.
  </Accordion>

  <Accordion title="Audit the review with enterprise events">
    Use <Link href="/guides/events/enterprise-events/for-enterprise">enterprise events</Link> to track previews, downloads, and annotation activity across every loan document. This gives compliance teams an audit trail of who reviewed what, and when - without instrumenting your own app.
  </Accordion>
</AccordionGroup>

## Next steps

<CardGroup cols={2}>
  <Card title="Invoice intake automation" href="/guides/tutorials/invoice-intake" icon="file-invoice" arrow="true">
    Automate accounts payable with Box AI Extract and metadata.
  </Card>

  <Card title="Box View overview" href="/guides/embed/box-view/index" icon="eye" arrow="true">
    Learn how upload, conversion, and embed fit together.
  </Card>
</CardGroup>

<RelatedLinks
  title="RELATED GUIDES"
  items={[
{ label: translate("Box View setup"), href: "/guides/embed/box-view/setup", badge: "GUIDE" },
{ label: translate("Create file preview"), href: "/guides/embed/box-view/create-preview", badge: "GUIDE" },
{ label: translate("Content Preview UI Element"), href: "/guides/embed/ui-elements/preview", badge: "GUIDE" },
{ label: translate("Box Annotations"), href: "/guides/embed/ui-elements/annotations", badge: "GUIDE" },
{ label: translate("Downscope a token"), href: "/guides/authentication/tokens/downscope", badge: "GUIDE" }
]}
/>
