# How to Migrate Box Folder Ownership from a Service Account to Users Programmatically [TOC] GitHub source: [https://github.com/lbakyl/box-folder-owner-migration](https://github.com/lbakyl/box-folder-owner-migration) This is the sequel to my earlier post on [programmatically creating home folders for new users on Box](https://bachelor-tech.com/how-to-programmatically-create-home-folders-for-new-users-on-box-with-azure). That post solved the problem going forward, for new hires. This one solves it looking backward, for everyone who already has a folder sitting under a shared admin or service account from before the automation existed. ## The Problem If you've ever inherited a Box tenant, you've probably seen this pattern: someone, at some point, needed to get new employees a working folder quickly. The fastest way to do that was to create it under an admin or service account and share it with the employee as an editor. It works. It also means that admin account (and anyone with sufficient rights on it) has standing access to every one of those folders, indefinitely, whether or not anyone remembers why. Fixing this by hand in the Box UI, one folder at a time, doesn't scale past a handful of people, and doing it wrong on a folder with real data in it is not a mistake you want to discover after the fact. ## Why Ownership Transfer, Not a Copy The good news: Box's ownership transfer is a metadata operation, not a data operation. Changing who owns a folder doesn't move, copy, or touch a single byte inside it. It just updates a pointer. That means a folder with 150 GB in it transfers exactly as fast as an empty one, and there's no realistic scenario where this corrupts or loses data. The risk in this kind of migration is entirely about *access*: making sure the right person ends up owning it and nobody unintended keeps standing access, not about the data itself. ## The Approach 1. **Always dry-run first.** Before changing anything, read the folder's current owner and every collaborator on it, and report what *would* happen. Nothing changes until that's reviewed and confirmed. 2. **Add the target user as a collaborator**, if they aren't one already. 3. **Upgrade their collaboration to owner.** Box transfers ownership and automatically demotes the previous owner to editor on that folder as a side effect. This is the actual mechanism; there's no separate "transfer ownership" button in the API, just a role change. 4. **Remove every other standing collaborator**, except anyone explicitly named to keep. 5. **Rename the folder** to something friendlier than a folder ID. Full source, ready to run: [github.com/lbakyl/box-folder-owner-migration](https://github.com/lbakyl/box-folder-owner-migration). ## Setup ### 1. Prerequisites - Node.js 22 and npm. - Azure Functions Core Tools v4: `npm install -g azure-functions-core-tools@4`. This runs the tool locally as a real Azure Functions host (the same runtime a deployed Function App would use), just on your own machine. Nothing here needs an actual Azure subscription. - Clone the repo and run `npm install`. - Copy `local.settings.json.example` to `local.settings.json` as-is. No values need filling in for this one; it just gives the local Functions host somewhere to start. ### 2. Authenticate as the folder's current owner This is the part worth getting right, and it's a deliberate design choice, not an accident: **the tool authenticates as the account that currently owns the folders**, using a proper OAuth sign-in, rather than as some all-powerful service account that can impersonate anyone. Why does it matter which one? Because only a folder's actual current owner can hand it over to someone else. I confirmed this the hard way against Box's own API. A co-owner, or an admin with broader rights but no direct relationship to that specific folder, gets a flat 403 trying to change ownership. There's no way around that with more permissions; it has to be the real owner. The tempting shortcut here is to give a service account the "Make API calls using the as-user header" scope, which lets it act as any user in the enterprise on demand. Don't. That's the exact standing power this whole migration exists to remove. Trading one all-access admin account for one all-access automation credential isn't a fix; it's a rebrand of the same problem. - Go to [account.box.com/developers/console](https://account.box.com/developers/console), signed in **as the account that owns the folders**. - Create a Custom App → User Authentication (OAuth 2.0). - Add redirect URI `http://localhost:8765/callback`. - Scopes: read/write all files and folders, and Manage Users (needed for the storage-limit check and for looking up user IDs by email). Leave "as-user header" and "generate user access tokens" off. - Have an admin authorize the app (Admin Console → Platform Apps). - Sign in once: ```bash node tools/boxLogin.js ``` It prints a URL. Open it in a private browser window, sign in as the folder owner, approve. The login is stored in `~/.box-migration` on your machine (never in the project folder), and refreshes itself automatically for up to 60 days after its last use. Run `node tools/boxLogin.js --logout` when you're done. ## Running It Start the local server: ```bash func start ``` Find a folder ID (from its URL in the Box web UI) and the target user's Box ID (`node tools/lookupUsers.js someone@example.com`), then dry-run it: ```bash curl -X POST http://localhost:7071/api/migrateHomeFolder \ -H "Content-Type: application/json" \ -d '{"folderId": "123456789", "userId": "987654321", "userName": "Jane Doe"}' ``` That returns a report (current owner, who'd be removed, storage check, whether the folder needs to be relocated first) and changes nothing. Once it looks right, add `confirm: true` to actually run it, and run the same dry-run request one more time afterward: a `404 not_found` response means the old owner can no longer see the folder at all, which is the actual proof it worked, not just a hopeful assumption. Full commands, including how to keep specific collaborators, are in the [repo's README](https://github.com/lbakyl/box-folder-owner-migration#3-running-it). ## Gotchas Worth Knowing Before You Run This at Scale ### Inherited access isn't the same as direct access, and Box's API blends them together This is the one that actually matters most, and it's not obvious from the API docs. When you ask Box for a folder's collaborator list, it hands back both collaborations created directly on that folder *and* access inherited from a parent folder, in the same list, with no obvious flag telling you which is which at a glance (the parent's ID is buried in the entry's `item` field). That distinction bit me directly on a real migration: I transferred ownership of a folder that had its own direct collaborator, confirmed the transfer worked, and moved on, except three other people still had full access to it afterward, because their access had never been on that folder at all. It came from a *parent* folder several levels up (a shared departmental structure), and transferring ownership of the child folder does nothing to that inherited grant. The folder had a new owner and the exact same standing access problem I was trying to fix. The fix has two parts, and both matter: only ever touch a collaboration after confirming, by ID, straight from Box, that it actually belongs to the folder you're working on (not a parent), and if a folder has *any* inherited access on it at all, move it out of that parent tree as part of the migration, not only when it happens to block something else. Ownership transfer alone does not sever inherited access; only physically relocating the folder out of the shared structure does. ### A failed request can have partially succeeded Box processes some folder operations in the background, and a large or recently-touched folder can return `409 operation_blocked_temporary` on the next request. The trap here is assuming that error means nothing happened. It doesn't necessarily. An earlier step in that same request (commonly, moving the folder out of its parent) can have already gone through, leaving the folder in a safe but incomplete state: no longer nested in the shared structure, but not yet owned by the new person either. Re-checking the folder's actual state after an error, rather than trusting the error message alone, saved me from either assuming a completed step needed redoing, or assuming an incomplete one was done. ### Storage limits are per-user, and they include what's about to land on them A user's own Box storage limit counts everything they own, and transferring them a large folder counts against it immediately. If their limit is too small, the upgrade fails with `403 storage_limit_exceeded`. Worth checking before the transfer, not discovering after it's halfway done, which the tool does automatically as part of the dry run. ## Wrap-Up The pattern here generalizes past Box: any platform where "who owns this" and "who can currently reach it" get conflated over time is a candidate for this same shape of fix: dry run everything, verify by proving the old owner genuinely can't reach it anymore rather than trusting a success message, and be paranoid about anything inherited rather than set explicitly. Full source: [github.com/lbakyl/box-folder-owner-migration](https://github.com/lbakyl/box-folder-owner-migration)