Workspaces
Files and ingestion
List, read, replace and delete the files in a workspace, and ingest them so search can find them.
Every file you upload lives in a workspace, at a path you choose. The routes on this page all sit under /v1/workspaces/{workspace_id}. Listing, reading, replacing and deleting files is free and works at any balance.
- Add files with Uploads, passing the workspace's
workspace_id. An upload without one goes to the server instead, and isn't part of a workspace. In the dashboard, see Workspaces. - Use a file in a tool job as a
workspace_filesource, straight away, when your account owns the workspace. A file in a workspace you were invited into from another account is refused with422 invalid_source. - Search only finds a file once it's ingested. See Search.
In the CLI, every command here takes --workspace ID:
- Without it, the CLI uses
$NACE_WORKSPACE_ID, then the workspacenace workspace-usesaved. With none, the command exits with a usage error that says how to pick one. See Workspaces. nace workspace-fileandnace workspace-file-deletealso take the file asws:<workspace_id>/<file_id>, which names its workspace too.
List files
curl "https://console.nace.ai/v1/workspaces/$WORKSPACE_ID/files?path_prefix=reports/&ingestion_status=stale" \
-H "Authorization: Bearer $NACE_API_KEY"| Query | CLI flag | What it does |
|---|---|---|
path_prefix | --path-prefix | Only files whose path starts with this. |
ingestion_status | --status | Only files in these statuses. Repeat it for more than one. |
categories | --category | Only files in these categories, set by ingestion. Repeatable. |
file_types | --file-type | Only these extensions, such as pdf or xlsx. Repeatable. |
modified_after, modified_before | --modified-after, --modified-before | Only files whose updated_at falls after or before this time. |
limit | --limit | 1 to 200, default 50. |
cursor | --cursor | The previous page's next_cursor. Keep going until it's null. |
The CLI also takes --all, for every page, and --json, to print the page as JSON.
Each page has:
| Field | What it holds |
|---|---|
items | The workspace files on this page. |
next_cursor | Pass it as cursor for the next page. null on the last one. |
total_count | Every file that matches, across all pages. |
coverage.restricted_candidates | How many files the workspace's access labels kept from you. |
coverage.labels_required | The labels that would show you those files. |
coverage.request_access_hint | A hint, as text, on how to get access to them. |
When nothing was kept from you, as in a workspace without labels, the coverage fields are 0, [] and null.
Read a file
GET .../files/{file_id} returns the file plus download_url and preview_url: links to its bytes that work without your API key until download_url_expires_at, 15 minutes on. download_url saves the file; preview_url opens it in a browser. A file that doesn't exist, or was deleted, is 404 file_not_found.
curl https://console.nace.ai/v1/workspaces/$WORKSPACE_ID/files/$FILE_ID \
-H "Authorization: Bearer $NACE_API_KEY"Metadata
GET .../files/{file_id}/metadata (files.metadata, --metadata) returns what ingestion learned about the file. Most of it is empty until the file is ingested.
| Field | What it does |
|---|---|
file | The file. |
lane | How it was read: document, table, media or json. |
page_count, sheet_names, duration_ms | Pages of a document, sheets of a workbook, length of audio or video. null when they don't apply. |
summary, keywords | A summary and keywords for the whole file. |
documents, sheets, media | Per-document, per-sheet or per-recording detail: category, summary, keywords, page range or size. |
outputs | What ingestion produced from the file. See Outputs. |
representations | Whether each is current: search_index and organized_files. Read this to know the file is searchable now. |
last_error | Why the last ingestion failed, or null. |
Versions
GET .../files/{file_id}/versions (files.versions, --versions) pages through the file's versions, newest first, with limit and cursor as above. Only the current version's bytes are kept. Each version has:
| Field | What it holds |
|---|---|
version, is_current | Its number, and whether it's the current one. |
size_bytes | Its size. |
observed_hash, ingested_hash | The hash of its bytes, and of the bytes last ingested. |
created_at, ingested_at | When it was uploaded, and when it was ingested. |
replaced_at | When a newer version replaced it. null on the current one. |
Outputs
Ingestion turns a file into outputs, listed in its metadata's outputs. Each has output_id, kind, path, generation (the file version it came from) and created_at. Its kind is one of:
kind | What it is |
|---|---|
derived_file | A file stored in the workspace, such as a document's Markdown, or a recording's transcript or frames. |
opensearch_doc | A search-index entry, with nothing to download. |
Read one output: GET .../files/{file_id}/outputs/{output_id} (files.output) returns a derived_file with download_url, preview_url and download_url_expires_at. Any other kind is 422 invalid_request.
List the files in a folder output, such as a recording's frames: GET .../files/{file_id}/outputs/{output_id}/members (files.output_members, outputMembers in TypeScript). It pages with offset and limit (1 to 200, default 50). A single-file output has no members. It returns:
| Field | What it holds |
|---|---|
members | The files, each with path, file_name, size_bytes, download_url and preview_url. |
total_count | How many files the output holds. |
download_url_expires_at | When the links stop working. |
Replace a file
PUT .../files/{file_id} gives the file new bytes, at the same path.
- The body is multipart, like an upload: the new bytes in
file, and optionally ametadatapart withlabels, which replaces them ({}clears them). A body that isn'tmultipart/form-datais415 unsupported_media_type. - The answer is
200with afile_replacejob that has already succeeded, withresult.fileandresult.previous_version. - Up to about 4.5 MB. For a larger file, upload it to the same path instead, as below.
- Not retried: Python's
files.replacesends it once unless you passretry, since a retry after the first one landed would add a second version.
curl -X PUT "https://console.nace.ai/v1/workspaces/$WORKSPACE_ID/files/$FILE_ID" \
-H "Authorization: Bearer $NACE_API_KEY" \
-F file=@report-v2.pdfAfter a replace:
- A file that was ingested becomes
staleuntil you ingest it again; one that wasn't staysdiscovered. - Jobs that read the file from then on get the new version.
Uploading to the same path with on_conflict: "new_version" does the same; see Same path again. Use that for a file over about 4.5 MB, and in the CLI, which has no replace command.
Delete a file
DELETE .../files/{file_id} removes the file, its outputs and its search-index entries, and cancels ingestions still running on it. Deleting can't be undone.
- The answer is
202with afile_deletejob, or200when it finishes withinwait_seconds. - Waiting and retries: like ingestion, it takes
?wait_seconds(capped at 60) and anIdempotency-Key. It has no body.
curl -X DELETE "https://console.nace.ai/v1/workspaces/$WORKSPACE_ID/files/$FILE_ID" \
-H "Authorization: Bearer $NACE_API_KEY"The job's result has file_id, the counts of what was deleted (outputs_deleted, index_documents_deleted), jobs_cancelled and verified, which is true once every trace is confirmed gone.
Ingest files
Pricing to be announced.
Ingestion reads your files and builds what search needs: the search index, metadata and outputs. It's the only call that makes files searchable. Upload a batch first, then ingest it in one run.
curl https://console.nace.ai/v1/workspaces/$WORKSPACE_ID/ingestions \
-H "Authorization: Bearer $NACE_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: ingest-reports-2026-10" \
-d '{ "selection": { "type": "path_prefix", "path_prefix": "reports/", "stale_only": true } }'| API body | SDK | CLI | What it does |
|---|---|---|---|
selection with file_ids | file_ids | --file-id, repeatable | Ingest these files: 1 to 1000 ids. |
selection with path_prefix | path_prefix | --path-prefix | Ingest the files under this folder. "" is the whole workspace. |
selection.stale_only | stale_only | --stale-only | With a path prefix, only files that changed since they were last ingested. |
reingest_unchanged | reingest_unchanged | --reingest-unchanged | Rebuild files that are already ingested and unchanged. Off by default, so those are skipped. |
- In the API,
selectionis required:{"type": "file_ids", "file_ids": [...]}or{"type": "path_prefix", "path_prefix": "reports/"}. - In the SDKs and the CLI, pass file ids or a path prefix, not both. With neither, the whole workspace is ingested.
What comes back:
- The answer is
202with aningestionjob, or200when it finishes withinwait_seconds. Read it as in Jobs and results. - Waiting and retries: it takes
?wait_seconds=N(0 to 60; more waits 60) and anIdempotency-Key. Retry identical inputs with the same key to get the original job; use a new key for a changed selection. Changed inputs are not a supported retry: they may replay an earlier job or be rejected. - One bad file doesn't fail the run: the job succeeds, and its result reports each file.
| Result field | What it holds |
|---|---|
outcomes | One per file: file_id, path, status (ingested, skipped_unchanged, skipped_empty or failed), lane, units and, when it failed, error. |
files_ingested, files_skipped, files_empty, files_failed | How many files had each outcome. skipped_empty is a valid file with no rows, such as an empty Parquet file. |
units_total | The units ingested, across every file. |
Search reads only what ingestion built, so it misses any file that isn't ingested.
Ingestion status
Each file's ingestion_status says where it stands, and the workspace's stats count them in files_by_ingestion_status, so you can tell what needs ingesting without listing every file.
| Status | What it means |
|---|---|
discovered | Uploaded, never ingested. |
queued, ingesting | An ingestion is on its way. |
ingested | Searchable. |
stale | Changed since it was ingested. Ingest again, with stale_only. |
expired | The derived content is no longer available. Ingest again to make it searchable. |
failed | The last ingestion failed; see last_error in its metadata. |
not_required | Valid, but holds nothing to index, such as an empty Parquet file. |
The workspace file object
| Field | What it holds |
|---|---|
file_id, workspace_id | The file's id and its workspace. Together they make a workspace_file source, or ws:<workspace_id>/<file_id> in the CLI. |
path, file_name, file_type | Where it is in the workspace, its name and its extension. |
size_bytes | The current version's size. |
version | Starts at 1, and goes up with each replacement. |
observed_hash, ingested_hash | The hash of the current bytes, and of the bytes last ingested (null if never). |
ingestion_status, ingested_at | Where it stands (see Ingestion status) and when it was last ingested. |
category | The category ingestion gave it, or null. |
access_label, access_label_source | Its access label, or null when the workspace has none. |
expires_at | Legacy expiry timestamp, or null. Standard files and ingested content stay until explicit deletion. |
labels | Your own key-value metadata, stored as given. |
created_at, updated_at | When it was uploaded and last changed. |