- A loan workbench uploads each borrower document to Box through the secure upload API.
- Box converts it into a high-fidelity HTML5 rendering
- Your app requests a short-lived embed URL and drops it into an
<iframe>.
Clone the working sample
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 works; consumer/Individual accounts do not). A paid is required for the optional Box AI Q&A layer.
- A Box 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 . This is required for the . Content Preview fails with a403until the origin is allow-listed. - Your Box enterprise ID (found in the Developer Console under the account icon, Copy Enterprise ID).
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.How it works
Box View follows a three-stage flow:- Upload. Your backend uploads the borrower document to Box over the secure upload API. Content is stored with virus scanning and 256-bit encryption.
- 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.
- Embed. Your backend requests an
expiring_embed_linkfor the file and returns it to the front end, which places it in an<iframe>.
Step-by-step process
Set up the development environment
- Open your terminal and create a new project directory:
- Create and activate a Python virtual environment:
(.venv) at the beginning. This confirms you are working inside the virtual environment.source .venv/bin/activate from the project directory. If you see ModuleNotFoundError when running commands, it usually means the venv is not activated.- Install the required packages:
- Create a
.envfile to store your credentials, then add the following content. Replace the placeholder values with your actual credentials from the Box Developer Console:
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..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.Authenticate the Box client
box_client.py in your project directory. Open the file and paste the following code:Upload a borrower document
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.Generate the preview embed URL
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>.Mint a downscoped token for the browser
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.Expose the backend endpoints
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./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.Render the document in an iframe
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.Test the upload and embed flow
loan-review directory and the virtual environment is activated before running any commands: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):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.Troubleshooting
ModuleNotFoundError: No module named '...'
ModuleNotFoundError: No module named '...'
source .venv/bin/activate from the project directory before running any python commands. Each new terminal tab needs its own activation.invalid_client: The client credentials are invalid
invalid_client: The client credentials are invalid
.env file:- Verify
BOX_CLIENT_IDandBOX_CLIENT_SECRETmatch the values in Developer Console > your app > Configuration. - Confirm
BOX_ENTERPRISE_IDis your enterprise ID (Developer Console > account icon > Copy Enterprise ID). - Ensure the app type is Client Credentials Grant and the app has been authorized.
No embed link returned / null expiring_embed_link
No embed link returned / null expiring_embed_link
404 Not Found when opening /review/<file_id>
404 Not Found when opening /review/<file_id>
/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.The page shows only the header, not the document
The page shows only the header, not the document
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.The annotate page returns 403 (Forbidden) even though the review page works
The annotate page returns 403 (Forbidden) even though the review page works
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 that127.0.0.1andlocalhostare different origins; add whichever you use. Save and wait a minute or two, then restart and reload. - If it still
403s, 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 returns403.
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 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.
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:
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).
item_download scope so the text layer is available. See the for the full matrix and for enabling real-time V4 annotations.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.Answer questions with Box AI in the previewer
Answer questions with Box AI in the previewer
hasHeader: true and contentAnswersProps to preview.show(). See .Review a full application as a collection
Review a full application as a collection
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.Tailor capabilities with scopes
Tailor capabilities with scopes
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 .Scaling to production
Organize documents per application
Organize documents per application
Refresh tokens and embed links on demand
Refresh tokens and embed links on demand
Keep credentials server-side
Keep credentials server-side
Audit the review with enterprise events
Audit the review with enterprise events
