Docs
Dashboard
Guides / SessionsBeta

File uploads and downloads

The shared /workspace disk: browser downloads, files you upload for the browser, reading, writing and downloading files and whole folders, also after the session has stopped.

Every session has a disk at /workspace (the session’s workspacePath): 10 GiB with a shell, 5 GiB for a browser-only session. When it is full, writes fail with “No space left on device”; delete files to make room. The browser, the shell and the API all see the same files:

  • browser downloads land in /workspace/downloads, whichever client triggers them;
  • files you upload through the API are there for the browser to use in a file input;
  • the shell starts in /workspace and can read and write it;
  • the files API lists, reads, writes and deletes files in it, and downloads a whole folder as one archive.

That is what lets an agent download a CSV in the browser, analyse it with Python and hand the result back, without the file ever leaving the machine. The workspace is saved when the session stops, and comes back when you resume it.

Files work on every kind of session: a browser only, a shell only, or both. See the three kinds of session. Reading, listing and downloading files also work after the session has stopped (see below).

Paths in the files API are relative to /workspace (downloads/report.csv), or absolute paths inside it (/workspace/downloads/report.csv). Paths outside the workspace are rejected with 403 invalid_path.

Downloads

Whatever triggers a download (Playwright, Puppeteer, the actions API, the AI agent or a click in the live view), the file lands in /workspace/downloads. Chrome’s unfinished .crdownload files are ignored until the download is done. Wait for the file, then read it, process it in the shell, or copy it to your own machine. A shell-only session has no browser, so nothing lands there by itself: fetch files in the shell with curl or wget instead.

# Answers once a finished file matches, or 408 after timeoutMs.curl -G https://api.boxline.dev/v1/sessions/$SESSION_ID/files/wait \  -H "x-api-key: $BOXLINE_API_KEY" \  --data-urlencode "pattern=downloads/*.csv" \  --data-urlencode "timeoutMs=30000"curl -G https://api.boxline.dev/v1/sessions/$SESSION_ID/files \  -H "x-api-key: $BOXLINE_API_KEY" \  --data-urlencode "path=downloads/report.csv" \  -o report.csv

To reuse the browser’s login from the shell, export its cookies.

Upload a file for the browser

To give a website a file, for example in an upload form, put the file in the workspace first, then set the form’s file input to it with the upload action (it needs a session with a browser).

  1. PUT /v1/sessions/:id/files?path= with the file’s bytes as the body. Folders are created for you, and the file appears only once it is fully written.
  2. { "action": "upload", "selector": "input[type=file]", "path": "/workspace/report.pdf" } sets the file input. The path is inside the session.
curl -X PUT "https://api.boxline.dev/v1/sessions/$SESSION_ID/files?path=report.pdf" \  -H "x-api-key: $BOXLINE_API_KEY" \  -H "content-type: application/octet-stream" \  --data-binary @report.pdfcurl -X POST https://api.boxline.dev/v1/sessions/$SESSION_ID/browser/actions \  -H "x-api-key: $BOXLINE_API_KEY" \  -H "content-type: application/json" \  -d '{"actions": [        {"action": "upload", "selector": "input[type=file]", "path": "/workspace/report.pdf"},        {"action": "click", "selector": "button[type=submit]"}      ]}'

With Playwright, page.setInputFiles() sends a file from your own computer instead, which works too.

Planned shape, may change

Coming soon: a session.files.upload() helper in the SDKs, plain-English steps that understand “upload report.pdf”, and a size limit per upload that depends on your plan.

Read a file

GET /v1/sessions/:id/files?path= returns the file’s bytes. It always comes as a download (application/octet-stream, as an attachment), never shown as a web page, because the files come from a machine that runs untrusted code. It also works after the session has stopped: see Files of a stopped session.

curl -G https://api.boxline.dev/v1/sessions/$SESSION_ID/files \  -H "x-api-key: $BOXLINE_API_KEY" \  --data-urlencode "path=summary.csv" \  -o summary.csv

List a folder

Add list=1, or point path at a folder. It works on a stopped session too.

curl -G https://api.boxline.dev/v1/sessions/$SESSION_ID/files \  -H "x-api-key: $BOXLINE_API_KEY" \  --data-urlencode "path=downloads" \  --data-urlencode "list=1"
Response
{  "path": "/workspace/downloads",  "entries": [    { "name": "report.csv", "type": "file", "size": 48213, "mtime": "2026-09-28T12:01:07.000Z" }  ]}

type is "file", "dir" or "other". On a stopped session a link is listed as "symlink".

Write a file

PUT /v1/sessions/:id/files?path=. The request body is the raw file contents. The response is { path, size }. The session must be running.

Shell
curl -X PUT "https://api.boxline.dev/v1/sessions/$SESSION_ID/files?path=summarise.py" \  -H "x-api-key: $BOXLINE_API_KEY" \  --data-binary @summarise.py

Delete a file

DELETE /v1/sessions/:id/files?path= deletes a file or a folder, and returns 204. The session must be running.

Shell
curl -X DELETE "https://api.boxline.dev/v1/sessions/$SESSION_ID/files?path=summary.csv" \  -H "x-api-key: $BOXLINE_API_KEY"

Wait for a file

GET /v1/sessions/:id/files/wait?pattern=&timeoutMs=

Downloads finish in their own time. Instead of polling, ask the session to answer once a file matching a glob exists and has stopped growing. The glob applies to the last part of the path, relative to /workspace, for example downloads/*.csv. The response is { path, size } for the newest match, with an absolute path such as /workspace/downloads/report.csv. If nothing matches in time (default 30 seconds, at most 10 minutes), the request fails with 408. The session must be running.

Download a folder

GET /v1/sessions/:id/files/archive?path= returns a folder and everything in it as one .tar.gz: everything an agent made, in one call. path is the folder, and the whole workspace when you leave it out. Like every file, it comes as a download (application/octet-stream, as an attachment). It works on a running session and on a stopped one.

curl -G https://api.boxline.dev/v1/sessions/$SESSION_ID/files/archive \  -H "x-api-key: $BOXLINE_API_KEY" \  --data-urlencode "path=output" \  -o output.tar.gztar -xzf output.tar.gz
  • .boxline/ at the top of the folder is left out. A link is stored as a link and never followed.
  • At most 1 GiB and 100,000 entries. A bigger folder is refused before any byte is sent.
  • Only two archives of one session can be made at a time.
StatusCodeWhen
400not_a_directorypath is a file. Read it with Read a file.
403 or 400invalid_pathThe path is outside the workspace, or goes through a link.
404not_foundThere is no such folder.
413archive_too_largeOver 1 GiB or 100,000 entries. Download smaller folders, or single files.
429archive_busyTwo archives of this session are already being made. Try again when one is done.
409nothing_savedA stopped session that saved no workspace (see below).
409machine_too_oldThe session runs on a machine from before a platform update. Start a new session.

Every field is in the API reference: Download a folder.

Files of a stopped session

A stopped session keeps its workspace until it is deleted (see How long they are kept). Three calls read it without resuming the session and without starting a machine, so no machine time is billed:

  • reading a file: GET /v1/sessions/:id/files?path=;
  • listing a folder: the same call with list=1;
  • downloading a folder: GET /v1/sessions/:id/files/archive?path=.
curl -X POST https://api.boxline.dev/v1/sessions/$SESSION_ID/stop -H "x-api-key: $BOXLINE_API_KEY"# No machine runs now, and the files are still there.curl -G https://api.boxline.dev/v1/sessions/$SESSION_ID/files \  -H "x-api-key: $BOXLINE_API_KEY" \  --data-urlencode "path=output/total.txt"

They answer from the workspace the session saved: the copy made when it stopped, or, after a lost machine, its last checkpoint (checkpointAt on the session), which is the same copy a resume restores.

CallOn a stopped session
Read a file, list a folder, download a folderWorks, read-only, from the saved workspace.
Upload a file, delete a file, wait for a file409 session_not_running: resume the session first.
  • If the session saved no workspace (a machine lost before its first checkpoint, or a session that stopped without saving), the read answers 409 nothing_saved. A resume answers the same.
  • A deleted session’s files are gone: 409 session_not_running.
  • .boxline/ is left out. A link is listed as a link and never followed, and an entry that is not a regular file cannot be downloaded.
  • One request scans at most 1 GiB of the saved workspace, uncompressed (413 payload_too_largebeyond that). For a file deep in a very large workspace, download its folder with the archive call.
  • These reads count against 60 a minute per project.

From the command line

Shell
boxline files ls $SESSION_ID downloadsboxline files get $SESSION_ID downloads/report.csv     # saves report.csv here ("-" prints it)boxline files get $SESSION_ID output                   # a folder is saved as a .tar.gzboxline files put $SESSION_ID data/input.csv ./input.csvboxline files rm $SESSION_ID data/input.csv

See the CLI.

With the SDKs

NodePythonReturns
session.files.read(path)session.files.read(path)The bytes
session.files.readText(path)session.files.read_text(path)The text
session.files.archive(path)session.files.archive(path)The folder as one .tar.gz (the bytes)
session.files.write(path, data)session.files.write(path, data){ path, size }
session.files.list(path = ".")session.files.list(path=".")Node: { path, entries }; Python: the entries
session.files.delete(path)session.files.delete(path)Nothing
session.files.waitFor(pattern, timeoutMs = 30000)session.files.wait_for(pattern, timeout_ms=30000){ path, size }

These docs describe a beta API. Endpoints, fields and SDK methods may change before general availability; breaking changes will be listed in the changelog.