Perception
Uploads
Uploads are free and work at any balance. See Uploads.
Upload a file
nace upload ./invoice.pdf --path invoices/2026/invoice.pdf
# ws:0f8b7c2e-.../3a91c5...| Flag | What it does |
|---|---|
--path PATH | The file's path in your workspace. Default: its file name. For a folder, the path its files go under: the folder's name by default, or / for the top of the workspace. |
--on-conflict {reject,new_version} | When a different file already has that path: reject (the default) fails with 409 path_conflict; new_version adds a version of that file. |
--workspace ID | The workspace to upload into. Without it, the file goes to the server as a source for the tools. |
-j, --jobs N | For a folder, how many files go up at once: 1 to 32 (default 8). |
--ingest | Ingest the uploaded files afterwards, as nace ingest does. Needs --workspace. See Upload and ingest. |
--async | With --ingest, print each ingestion's job id and return without waiting. Without --ingest, the command exits 2. |
--wait-timeout SECONDS | With --ingest, how long to wait for each ingestion (default 3600). Past it, the command exits 1, and the ingestion goes on: read it later with nace job <id> --wait. |
--json | Print the uploaded file's record (workspace_id, file_id, path and the rest) instead of its ws: source. For a folder or with --ingest, see JSON output. |
- Keep the file's extension in
--path. The document service types an upload from its first bytes, and from the extension when those don't settle it. - Only
--workspacepicks the workspace here.$NACE_WORKSPACE_IDand the workspacenace workspace-usesaved don't apply to uploads. - With
--on-conflict new_version, jobs on that file that haven't started yet read the new version.
What it prints
Stdout gets ws:<workspace_id>/<file_id>, ready to pass as SOURCE.
- The same file again: uploading the same bytes to the same path returns the file already there.
- Files of 32 MiB or more go up in parts, which changes two things:
- A path conflict ends with
error: Upload did not finish: ... (path_conflict: ...)instead of a409. It still exits1. - If the parts are still being assembled after about ten minutes, the command exits
1. Upload the same file to the same path again later to get it.
- A path conflict ends with
JSON output
For one file, --json prints the uploaded file's record. For a folder, or with --ingest, it prints one object with these fields:
| Field | What it holds |
|---|---|
uploaded | The uploaded files' records, each with its path and file_id. |
skipped | Each skipped file's path and error. |
failed | Each failed file's path and error. |
ingestions | With --ingest, each ingestion job. |
Upload a folder
nace upload ./fy25 --workspace <workspace_id> --ingest
# uploaded 2
# fy25/a.pdf
# fy25/q3/report.pdf
# ingested 2 | skipped 0 | failed 0 | 14 units
# ingested fy25/a.pdf
# ingested fy25/q3/report.pdfWhen FILE is a folder, nace upload uploads every file under it, -j at a time.
- Paths: each file keeps its path inside the folder, with
/between folders, under--path:./fy25/q3/report.pdfgoes tofy25/q3/report.pdf. - Left out: files and folders whose names start with
., such as.DS_Store,.gitand.env. A symlinked folder isn't followed. - Output: stdout gets
uploaded <n>, then each uploaded file's workspace path, indented two spaces, in path order.--jsongives each one'sfile_idtoo; pass it asws:<workspace_id>/<file_id>.
When a file can't be uploaded:
| What happened | What nace upload does |
|---|---|
The document service doesn't take its type (422 unsupported_file_type) | Skips it; that isn't a failure. When the uploads end, stderr lists the skipped files under warning: skipped <n> file(s) of a type the document service doesn't take:. |
| Any other error on that file | Prints error: <path>: <error> and goes on with the other files. When the uploads end, stderr shows failed <n> file(s): run the same command again to retry them. Running it again is safe: a file already uploaded comes back as it is. |
401, 403 or 404, which every file would get | Stops the run at once and exits 1. |
The command exits 0 when every file was uploaded or skipped, and 1 when any failed.
Upload and ingest
--ingest works for one file or a folder. It starts an ingestion of the uploaded files, waits for it, and prints its summary and file lines as nace ingest does.
- Batches: one ingestion per 1000 files, each batch its own block.
- A file that fails to ingest: as with
nace ingest, its batch addserror: <n> of <m> files failed to ingest. Every batch still runs, and then the command exits1. - A workspace is required: a file uploaded without a workspace can't be ingested, so without
--workspacethe command exits2before any upload. - A failed upload: nothing is ingested.
- Nothing to ingest: when every file of a folder was skipped, there is nothing to ingest: stderr says
error: no uploaded file ids to ingest, and the command exits1.
Upload grants
nace upload-grant mints a one-use token that lets another client, such as a browser, upload one file without your API key. It prints the grant as JSON, and a reminder on stderr to hand it only to that uploader.
| Flag | What it does |
|---|---|
--path PATH | Pin the workspace path the upload must use. |
--max-bytes N | The largest file the grant accepts. |
--total-size-bytes N | The exact size the file must have. |
--ttl-seconds N | How long the grant works, in seconds: at most 3600, the default. |
--workspace ID | The workspace the upload goes into; the grant works only for it. Without it, the upload goes to the server, as with nace upload. |
{
"workspace_id": "...",
"upload_url": "https://.../v1/workspaces/.../files",
"token": "...",
"expires_at": "...",
"max_bytes": null
}The uploader posts the file to upload_url with the token in X-Upload-Token, as in One file. The output is JSON with or without --json.
Upload sessions
nace upload-session opens a resumable upload in parts for a large file and prints it as JSON: session_id, session_token, chunk_size, total_parts, expires_at, upload_session_url and workspace_id. Another client then sends the parts and completes it, as in Large files: the CLI has no commands to send parts, complete, check or abort a session, and nace upload does all of that itself. Stderr reminds you that session_token writes into your workspace.
| Flag | What it does |
|---|---|
--path PATH | Required. The file's path in your workspace. |
--total-size-bytes N | Required. The file's exact size. |
--ttl-seconds N | How long the session accepts parts, in seconds: at most 5400, the default. |
--on-conflict {reject,new_version} | What happens when a different file already has that path, checked when the session completes. reject, the default, fails the completion job with path_conflict. new_version adds a version of that file. |
--idempotency-key KEY | The same key returns the same session. Without it, every run opens a new one. |
--workspace ID | The workspace the file goes into. Without it, the upload goes to the server, as with nace upload. |
Delete a file
nace upload-delete ws:<workspace_id>/<file_id> deletes a file you uploaded: every version of it and everything derived from it. It can't be undone. Pass the ws: source nace upload printed.
nace upload-delete ws:0f8b7c2e-.../3a91c5...
# deleted ws:0f8b7c2e-.../3a91c5...It waits while the deletion runs, then says on stderr:
deleted <source>once every trace is confirmed gone.still deleting <source>if it is still running. Run the command again to check on it.
Stdout stays empty. With --json, it prints the deletion as JSON: object, workspace_id, file_id and status.
| Error | When |
|---|---|
404 workspace_not_found | The workspace belongs to another account. |
404 file_not_found | The file isn't there. |