Perception guides
Uploads
Upload a file, then use it as a source for the Perception tools.
Upload a file once, then use it as a source for any Perception tool. The upload answers with a workspace_id and a file_id, which together name the file as a workspace_file source. Uploads are free and work at any balance.
Size limits
Files go straight from your client to the document service, never through Drex, so Drex puts no limit on their size. The document service caps each file by its type:
| File type | Largest file |
|---|---|
.csv, .parquet, .jsonl, .tsv, .txt | 1 GiB |
.json | 512 MiB |
| Everything else | 256 MiB |
How you upload can lower that:
- A one-request upload stops at 512 MiB, and a grant without a
pathat 256 MiB. Above that, use an upload session. - A grant's
max_bytescan cap it lower still. - A file over its limit is refused with
413 file_too_large.
With an SDK or the CLI
upload runs every step on this page for you and returns the file, ready to name as a source. Files of 32 MiB or more go up in parts through an upload session.
from nace_sdk import NaceClient
with NaceClient() as client:
uploaded = client.documents.upload("invoice.pdf", path="invoices/invoice.pdf")
job = client.documents.parse(uploaded.as_source())import { readFile } from "node:fs/promises";
import { NaceClient, workspaceFile } from "nace-sdk";
const client = new NaceClient();
const uploaded = await client.documents.upload(await readFile("invoice.pdf"), {
file_name: "invoice.pdf",
path: "invoices/invoice.pdf",
});
const job = await client.documents.parse({ source: workspaceFile(uploaded) });nace upload invoice.pdf --path invoices/invoice.pdf
# ws:<workspace_id>/<file_id>path defaults to the file's name. The CLI also uploads a local file you pass as a tool's SOURCE, at --upload-path (default: its name). upload and create_upload_session take on_conflict (--on-conflict in the CLI); see Same path again. The SDK and CLI pages list every parameter: Python, TypeScript, CLI.
One file
Ask for an upload grant, then send the file to the upload_url it returns, with its token:
curl https://console.nace.ai/v1/documents/upload-grants \
-H "Authorization: Bearer $NACE_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "path": "invoices/invoice.pdf", "ttl_seconds": 600 }'
# {"workspace_id":"...","upload_url":"https://.../v1/workspaces/.../files","token":"...","expires_at":"...","max_bytes":null}
curl "$UPLOAD_URL" \
-H "X-Upload-Token: $TOKEN" \
-F file=@invoice.pdf \
-F 'metadata={"path":"invoices/invoice.pdf"};type=application/json'| Grant field | What it does |
|---|---|
path | Pins the upload to this path. Leave it out to let the holder choose. |
max_bytes | The largest file the grant takes. null in the answer means the document service's cap for the file's type. |
total_size_bytes | The exact size the file must have. |
ttl_seconds | How long the grant works, up to 3600 (the default). |
metadata field | What it does |
|---|---|
path | Required. Where the file is stored: relative, not starting with /. |
total_size_bytes | The file's exact size. Required for a file of 32 MiB or more; the upload is refused unless the bytes match. |
on_conflict | reject (the default) or new_version. See Same path again. |
- The
metadatapart is required. - The upload's answer is a job that carries the file under
result.file.result.deduplicatedistruewhen the same file was already at that path. - Pass its
workspace_idandfile_idas the job's source:{"type": "workspace_file", "workspace_id": "...", "file_id": "..."}.
A grant works once. The CLI mints one with nace upload-grant and prints it as JSON.
Same path again
A path holds one file. Uploading to a path that's taken does one of three things:
- The same file again: the upload succeeds and returns the file already there, with
deduplicated: true. - A different file,
on_conflict: "reject"(the default): refused with codepath_conflict. A one-request upload answers409. An upload session's completion job fails with that code instead. - A different file,
on_conflict: "new_version": stored as the file's new current version, under the samefile_id. Jobs that read the file from then on get the new version.
To keep both files, upload the second one to another path.
Delete a file
DELETE /v1/documents/workspaces/{workspace_id}/files/{file_id} deletes a file you uploaded: every version of it, and everything derived from it, such as the workspace's parse outputs and search-index entries for it. It can't be undone. Jobs that already ran on the file keep their results until those jobs are deleted (Zero retention excepted). Pass the workspace_id and file_id the upload returned. Deleting is free and works at any balance.
curl -X DELETE "https://console.nace.ai/v1/documents/workspaces/$WORKSPACE_ID/files/$FILE_ID" \
-H "Authorization: Bearer $NACE_API_KEY"The answer waits while the deletion runs. 200 with "status": "deleted" means every trace is confirmed gone. 202 with "status": "deleting" means it is still running: send the same request again after Retry-After seconds to check on it. Another account's workspace is 404 workspace_not_found, and a file that isn't there is 404 file_not_found. In the rare case the file left the workspace but what was derived from it couldn't be confirmed gone, the answer is 500 file_delete_incomplete, and we finish it.
| Delete | Call |
|---|---|
| Python | documents.delete_file(workspace_id, file_id) |
| TypeScript | documents.deleteFile(workspaceId, fileId) |
| CLI | nace upload-delete ws:<workspace_id>/<file_id> |
| MCP | delete_uploaded_file(workspace_id, file_id) |
Files are kept in the account workspace until you delete them or your account. On zero retention, a grant or session without a workspace bound to your own storage is refused with 422 invalid_request.
To list, read, replace or delete uploaded files, and to ingest them so Search can find them, see Workspace files.
Large files
POST /v1/documents/upload-sessions with path and total_size_bytes opens a resumable upload. It also takes ttl_seconds (up to 5400, the default), on_conflict, and an Idempotency-Key header: the same key returns the same session, so a retry never opens a second one. Its answer gives session_token, chunk_size, total_parts and upload_session_url. Send each call below straight to the document service, with X-Upload-Token: <session_token>:
PUT {upload_session_url}/parts/{n}for each part, numbered from 0. Every part ischunk_sizebytes except a shorter last one. Sending a part again replaces it.POST {upload_session_url}/complete?wait_seconds=60with anIdempotency-Key. It assembles the file as a job, and answers with that job.- Until the job's
statusis final (succeeded,failedorcancelled), send step 2 again with the same key; a repeat answers at once with the job as it is now. Asucceededjob'sresult.filecarries theworkspace_idandfile_idto use as aworkspace_filesource. Afailedjob says why inerror, such aspath_conflict.
GET {upload_session_url} shows the session's status (open, completing, completed or aborted) and which parts arrived, so a client that dropped can send only the missing ones. DELETE {upload_session_url} aborts the upload. Sending complete again after the file is made answers with the same file.
| Step | Python | TypeScript |
|---|---|---|
| Open | documents.create_upload_session(path=..., total_size_bytes=...) | documents.createUploadSession({ path, total_size_bytes }) |
Send part n | documents.upload_part(session, n, data) | documents.uploadPart(session, n, data) |
| Finish | documents.complete_upload_session(session) | documents.completeUploadSession(session) |
| See what arrived | documents.get_upload_session(session) | documents.getUploadSession(session) |
| Abort | documents.abort_upload_session(session) | documents.abortUploadSession(session) |
The SDKs send a key with the open and with each completion, minted when you pass none, and wait up to about ten minutes for the file. The CLI opens a session with nace upload-session --path P --total-size-bytes N and prints it as JSON.