---
title: Box
description: Box via the official typed SDK. Translates virtual keys into nested folders under a configurable rootFolderId.
---

## Installation

`box-typescript-sdk-gen` is an optional peer dependency of `files-sdk` - install alongside the SDK so the adapter's imports resolve at runtime.

```package-install
files-sdk box-typescript-sdk-gen
```

## Usage

Box via the official `box-typescript-sdk-gen` SDK. Box files live by ID, not by path, so the adapter walks `rootFolderId` and translates virtual keys (`docs/a.txt`) into nested Box subfolders, auto-creating intermediate folders on `upload()`. Five auth shapes (pre-built client, developer token, OAuth refresh-token, Client Credentials Grant, JWT server auth) cover scripts, user apps, and enterprise installs - token lifecycle is handled by the SDK's built-in `Authentication` classes.

```ts lineNumbers
import { Files } from "files-sdk";
import { box } from "files-sdk/box";

// Server-side: Client Credentials Grant (recommended for backend services).
// The SDK manages access-token lifetime internally - no manual refresh
// bookkeeping in the adapter.
const files = new Files({
  adapter: box({
    ccg: {
      clientId: process.env.BOX_CLIENT_ID!,
      clientSecret: process.env.BOX_CLIENT_SECRET!,
      enterpriseId: process.env.BOX_ENTERPRISE_ID!,
    },
    rootFolderId: process.env.BOX_ROOT_FOLDER_ID, // defaults to "0" (account root)
    // publicByDefault: true → upload() also calls addShareLinkToFile and
    //                       url() returns the link's download_url.
  }),
});

// Other auth shapes the adapter accepts:
//   developerToken: process.env.BOX_DEVELOPER_TOKEN  // dev-console token
//   oauth: { clientId, clientSecret, refreshToken, tokenStorage } // user-app flow
//   jwt:   { configJsonString }                     // JWT server auth
//   client: yourBoxClient                           // pre-built escape hatch
```

### OAuth refresh tokens

Box refresh tokens are single-use. Each refresh returns a new refresh token and invalidates the old one, so the `refreshToken` you configured stops working after the adapter's first refresh. By default the rotated token lives only in the instance's memory. After a restart, or in a second instance started with the same value, every call then fails with `Unauthorized` (`invalid_grant`).

Pass `oauth.tokenStorage` to keep the rotated tokens somewhere durable and shared, such as your database or a KV store. The adapter seeds the storage with `refreshToken` only when it holds no token yet; after that, the stored token wins. Concurrent refreshes within one instance share a single exchange, so a burst of calls doesn't spend the same token twice.

```ts lineNumbers
const files = new Files({
  adapter: box({
    oauth: {
      clientId: process.env.BOX_CLIENT_ID!,
      clientSecret: process.env.BOX_CLIENT_SECRET!,
      refreshToken: process.env.BOX_REFRESH_TOKEN!, // first-run seed only
      tokenStorage: {
        get: async () => (await kv.get("box-oauth-token")) ?? undefined,
        store: async (token) => {
          await kv.set("box-oauth-token", token);
        },
        clear: async () => {
          await kv.delete("box-oauth-token");
        },
      },
    },
  }),
});
```

:::tip
For servers and background jobs, prefer `ccg` or `jwt`. They authenticate the app itself, so there is no user token to store or rotate.
:::

## Options

<AutoTypeTable
  path="../../packages/files-sdk/src/box/index.ts"
  name="BoxAdapterOptions"
/>

## Limitations

`list()` is not recursive - it lists one folder: the one the `prefix` points into (the part up to its last `/`, resolved under `rootFolderId`). For deep enumeration, drop to `raw.folders.getFolderItems` and recurse manually.

Box addresses everything by ID, so the adapter caches the folder and file IDs it resolves from your keys. When a cached ID turns out to be gone (another process deleted the key and created it again), the call evicts it, resolves the path once more, and retries, instead of failing with `NotFound`.

## Compatibility

| Method | Status | Notes |
| --- | :-: | --- |
| `upload` | ⚠️ | Two-stage: walks/creates parent folders by ID under `rootFolderId`, then `uploads.uploadFile` (≤50 MB) or `chunkedUploads.uploadBigFile` (>50 MB). Re-uploads against existing leaf names upload a new version (overwrite) through `uploadFileVersion`, or a chunked upload session for the existing file above 50 MB. Stream bodies are buffered up-front - Box's upload manager takes a Node `Readable`, not a Web stream. User `metadata` and `cacheControl` throw - Box exposes file metadata via classifications and metadata templates; drop to `raw.fileMetadata.*` if you need it. Pause/resume via `control` is in-process only — the chunked-upload commit needs a whole-file digest, so a token cannot resume in a new process. |
| `download` | ⚠️ | Resolves the file ID, then fetches `getDownloadFileUrl` for both buffered and streaming reads - the SDK's native `downloadFile` returns a Node `Readable` that's awkward to expose isomorphically, so the adapter routes through standard HTTP, which gives a `ReadableStream` body. |
| `delete` | ✅ | A folder key is a no-op, like a missing key. |
| `list` | ⚠️ | Lists one folder, no recursion: the folder the `prefix` points into (the part up to its last `/`, resolved under `rootFolderId` - `photos/` lists `photos`), with the rest of the prefix matched client-side against child names within the page. Keys come back in full (`photos/cover.jpg`). Without a delimiter, subfolders are skipped; with `delimiter: '/'` they come back as `prefixes` (`photos/2024/`) that you can pass back as the next `prefix`. A prefix into a missing folder lists nothing. Pagination uses Box's marker-based paging (the opaque marker is the cursor), so large folders page past Box's 10,000-item offset cap. |
| `search` | ⚠️ | Built on `listAll` — inherits this adapter's `list` behavior above. Client-side key match (glob, regex, substring, exact). |
| `head` | ⚠️ | Box doesn't store user-supplied content types on file content - `head()` returns a type inferred from the filename extension (or `application/octet-stream` when unknown). `size`, `etag`, and `lastModified` come from `getFileById`. |
| `exists` | ✅ |  |
| `copy` | ✅ | Overwrites an existing destination file like the other adapters: the adapter deletes the destination, then copies (the old file stays recoverable in Box's trash). A folder at the destination, or the same file under different casing, is `Conflict`; a folder as the source is `NotFound`. `move` behaves the same. |
| `url` | ⚠️ | Default mints a signed download URL via `getDownloadFileUrl` - Box controls the TTL server-side, so `expiresIn` is accepted for API symmetry but is not honoured. With `publicByDefault: true`, `upload()` calls `addShareLinkToFile` (open access) and `url()` returns the link's `download_url`. With `publicBaseUrl`, returns `<publicBaseUrl>/<key>`. In both modes a plain `url(key)` returns the permanent link (`publicUrl` is `true`); an explicit `expiresIn` still gets the tokenized URL. `responseContentDisposition` always throws - Box's URLs have no Content-Disposition override. |
| `signedUploadUrl` | ❌ | Throws - Box uploads require a multipart POST with both an `attributes` JSON part and the file bytes part, which fits neither the SDK's PUT-with-headers nor S3-style POST-with-form-fields shape. Use `upload()` server-side, or Box's UI Elements / Content Uploader for browser flows. |
