Hopefully soon we will get an deactivate endpoint, until then you can use a hybrid approach for deactivation.
Tulip bulk user deactivation (hybrid)
Tulip’s API has no write route for deactivating users, so this uses a hybrid approach: the Users API resolves names to user ids, then a Playwright browser session drives the account-settings UI to do the actual deactivation. When a deactivate endpoint ships, the browser half can be dropped.

Prerequisites
- Node 18 or newer (the lookup uses the built-in
fetch). Check withnode -v. - A Tulip API token scoped to read the users route (e.g.
users:read). The deactivation runs through your logged-in browser session, so the token never needs write access. Save the key and secret somewhere safe.
Setup
1. Make a directory and add the script. Put deactivate-users.ts in a new folder and open a terminal there.
https://github.com/mellerbeck/deactivate-users/blob/main/deactivate-users.ts
2. Install the tooling.
bash
npm install -g tsxnpm install playwrightnpx playwright install chromium
3. Capture a logged-in session. This opens a browser; sign in to Tulip, then close the window. It writes your session to tulip-auth.json.
bash
npx playwright codegen --save-storage=tulip-auth.json https://your-instance.tulip.co
Treat tulip-auth.json like a password. It holds live session tokens. Do not commit or share it.
4. Set your credentials in the same terminal window (they do not persist between windows).
macOS / Linux:
bash
export TULIP_BASE=https://your-instance.tulip.coexport TULIP_API_KEY=your-token-keyexport TULIP_API_SECRET=your-token-secret
Windows Command Prompt (no quotes, no spaces around =):
cmd
set TULIP_BASE=https://your-instance.tulip.coset TULIP_API_KEY=your-token-keyset TULIP_API_SECRET=your-token-secret
5. List the users to deactivate. Create names.txt, one full name per line. Names must match what Tulip stores exactly. Blank lines and lines starting with # are ignored.
John DoeJane Doe# this line is a comment
6. Preview first. The dry run resolves every name and shows what would happen, without touching the browser.
bash
npx tsx deactivate-users.ts --file names.txt --dry-run
Each name comes back as OK (one exact match, queued), NOT FOUND, or AMBIGUOUS (more than one match, skipped so nothing wrong gets deactivated). Fix any names you care about before the real run.
7. Run it.
bash
npx tsx deactivate-users.ts --file names.txt
You can also pass names directly instead of a file:
bash
npx tsx deactivate-users.ts "John Doe" "Jane Doe"
How names are resolved
Each name in names.txt is matched against the Tulip Users API by exact full name, then gated by role. The dry run prints one of these outcomes per name:
- OK — exactly one eligible user. Queued.
- OK (xN) — several records that share one identical, non-empty badge id. These are treated as duplicate records for the same person, and all of them are queued. Records with differing or missing badges are never auto-grouped this way.
- NOT FOUND — no active user matches that exact name.
- SKIPPED — a name matched, but the user does not hold the target role.
- AMBIGUOUS — two or more eligible users that do not share a single badge id. Skipped entirely, so a genuine name collision between different people never deactivates the wrong account.
Run with --dry-run first and resolve any NOT FOUND / SKIPPED / AMBIGUOUS names before the real pass.
Notes
- Operators only by default. Only users with the
operatorrole are eligible; anyone else matching a name is reported as SKIPPED, not deactivated. The role is read from the user’sworkspaceRoleAssignmentsin the API response (no extra call), and a user counts as an operator if any of their workspace assignments is operator. To target a different role, setTULIP_ROLEto itsuserRoleId(for exampleshop-floor-operator). If your operators use a role variant rather than plainoperator, the dry run will surface them as SKIPPED so you can adjust. - Names must match exactly. Matching is on the stored full name. A middle name or spelling difference lands the row in NOT FOUND, which the dry run shows before anything happens.
- Watch the first runs. It runs headed by default. Once you trust it, set
HEADLESS=1to run unattended. - Session expiry. When
tulip-auth.jsongoes stale, the script detects the login redirect and tells you to re-run the codegen command from step 3. - Already-deactivated users do not appear in the default active-user search, so re-running a name lands it in NOT FOUND rather than erroring. That is expected.
- One failure does not stop the batch. Per-user errors are logged and the run continues; the final summary reports deactivated / failed / skipped counts.