---
title: "classify"
method: POST
path: "/v1/classify"
tags: ["document-operations"]
---

# classify

`POST /v1/classify`

## Request

- Base URL: `https://ndi-api.nace.ai` (the document declares no server; this is the origin it was published from)
- URL: `https://ndi-api.nace.ai/v1/classify`
- Auth: API key in header `X-API-Key`
- TypeScript: <https://skmtc.dev/nace/apis/ndi-service/docs/v1/classify/post.ts> (a function with typed inputs; its response is checked against the schema)

Classify one document against caller-provided classes.

Cost class: **job** (model inference per document; billed on completion).

The canonical p0 input is one document, matching the routing use case and
common industry shape.

Returns ``202`` immediately unless ``?wait_seconds=N`` is set; the result
carries ranked label units for the requested document or page granularity.

Args:
    body: Classify parameters: one source, classes, unknown policy, and
        output options.
    caller: Authenticated tenant.
    session: Database session.
    wait_seconds: Bounded inline wait.
    idempotency_key: Idempotent replay key.
    response: Injected by FastAPI for status-code management.

Returns:
    A :class:`~ndi_service.app.platform_api.results.Job`.

## Query parameters

- `wait_seconds` integer: Bounded inline wait. 0 (the default) returns 202 immediately. Above 0 the route returns 200 with a terminal job if the work finished in time, and 202 with the still-running job if it did not. Either way, read `status`.

## Headers

- `Idempotency-Key` string, nullable: Replaying a request with the same key returns the original job with 200, never a second job. The key is kept with the job for replay and never cleared, so it must not contain personal data.

## Request body

- ClassifyRequest: Body for canonical ``POST /v1/classify``. The canonical p0 surface classifies one document against caller-provided classes. Internally this still adapts onto the shared categorize workflow.
  - `name` string, nullable: Optional display name. Public Parse, Split, Classify, Extract, and Ground runs use a stable server default when blank. The console calls a named run a 'project'.
  - `project_id` string, uuid, nullable: Caller-minted grouping id; jobs submitted together share it. Not a server-side resource.
  - `output_workspace_id` string, uuid, nullable: Workspace for retained result artifacts. Defaults to the unique source workspace, or a reused Parse's output workspace. Required for ambiguous destinations involving customer storage. Does not change source access.
  - `corpus_sources` WorkspaceFileSource[]: Workspace files visible in the run tray, including files not submitted by this request.
    - `type` 'workspace_file'
    - `workspace_id` string, uuid, required: Workspace that owns the file.
    - `file_id` string, uuid, required: File id from GET /v1/workspaces/{workspace_id}/files.
  - `parse_mode` 'low' | 'medium' | 'high': How much parsing effort to use: Low uses native OCR, Medium routes to the best parser, and High adds page verification and correction.
  - `parse` ParseSettings: Settings for the Parse that runs before Split, Classify, or Extract. A subset of the ``POST /v1/parse`` body with the same field names and nested models. Options that only shape the Parse response (``output``, ``chunking``) and options the runtime refuses (``quality``, ``ocr``) are not accepted here. A setting the file type cannot use is accepted and reported in the result's warnings.
    - `figures` ParseSettingsFigures: How figure regions are kept in the Parse behind an operation.
      - `mode` 'omit' | 'include' | 'describe', required: omit drops figure regions from the Parse this operation reads. include and describe both keep them and are the default: the operation reads the same Parse for either.
    - `diagrams` ParseDiagramsOptions
      - `mode` 'omit' | 'mermaid': Whether flowchart-like figures stay as images or are rewritten as Mermaid fences.
    - `spreadsheet` ParseSpreadsheetOptions: Workbook-specific Parse controls for spreadsheet sources. Defaults preserve the pre-control behavior: every sheet/row/column is included and formula *source text* is omitted (cached values still appear).
      - `sheets` string[], nullable: Optional sheet-name filter. When set, only listed sheets are parsed, in workbook order.
      - `include_hidden_sheets` boolean: When false, skip sheets marked hidden in the workbook.
      - `include_hidden_rows` boolean: When false, skip rows marked hidden.
      - `include_hidden_columns` boolean: When false, skip columns marked hidden.
      - `include_formulas` boolean: When true, include formula source text. Cached calculated values are always present.
    - `page_ranges` PageRange[], nullable: One-based inclusive pages to read, as page_ranges on Parse. On an operation with its own page_ranges the two must agree. With a parse_result source it keeps only those pages of the reused Parse.
      - `start` integer, required: First page of the range, 1-based and inclusive.
      - `end` integer, required: Last page of the range, 1-based and inclusive.
    - `parse_mode` 'high' | 'medium' | 'low', nullable: Parsing effort for the Parse before this operation, as parse_mode on Parse. Omit to use the request's top-level parse_mode, which defaults to low.
  - `source` union, required: The document to classify. A parse_result reuses the stored text of a succeeded Parse job.
    - UploadSource: Bytes staged through ``POST /v1/uploads``. Retained until explicit deletion, except under Zero retention.
      - `type` 'upload'
      - `upload_id` string, uuid, required: Id returned by POST /v1/uploads. Ordinary bytes remain until their consuming jobs or tenant are deleted.
    - UrlSource: A public or presigned HTTPS location we fetch server-side.
      - `type` 'url'
      - `url` string, required: HTTPS URL the service fetches.
      - `file_name` string, required: Filename to record for the fetched bytes.
    - WorkspaceFileSource: A file already in a workspace. The single seam between the two contexts: a granular operation may read a workspace file and still write nothing back. The file need **not** be ingested — granular operations read source bytes, not representations.
      - `type` 'workspace_file'
      - `workspace_id` string, uuid, required: Workspace that owns the file.
      - `file_id` string, uuid, required: File id from GET /v1/workspaces/{workspace_id}/files.
    - ParseResultSource: A prior Parse job whose parsed artifact can be reused by downstream methods.
      - `type` 'parse_result'
      - `job_id` string, uuid, required: Succeeded parse job whose artifact should be reused.
  - `context` ClassifyContext: Caller-supplied context about the document being classified. Today this carries only the audited entity's name. Direction-sensitive categories (``supplier_invoice`` vs ``sales_invoice``, ``customer_purchase_order`` vs ``purchase_order``, and similar pairs across the taxonomy) read the same document two different ways depending on which side of it is the audited entity, and the model is never told that otherwise. Naming it here lets the decision read the document's own buyer/supplier or issuer/recipient cues against the right side.
    - `entity_name` string, nullable: The audited entity's name, exactly as it would appear on its own letterhead or as the counterparty on a document it did not issue. Omit when the caller does not know it or it does not apply; direction-sensitive categories are then decided without the hint, as they always have been.
    - `entity_aliases` string[]: Other names the audited entity is known by in its documents (trading names, former names, abbreviations).
  - `classes` ClassifyClass[], required: Caller-defined classes to score the document against.
    - `id` string, required: Stable caller-defined id returned on labels. Not the reserved other group.
    - `label` string, required: Human-readable class name shown in results.
    - `description` string: What belongs in this class. Used as model guidance.
    - `criteria` string[]: Optional extra rules the model should apply when scoring this class.
    - `subclasses` ClassifySubclass[]: Optional finer labels nested under this class.
      - `id` string, required
      - `label` string, required
      - `description` string
      - `criteria` string[]
  - `granularity` 'document' | 'page': Score the whole document once, or return one unit per selected page.
  - `page_ranges` PageRange[], nullable: One-based inclusive page ranges to classify. Omit to use the whole document. Not supported for spreadsheets (.xlsx, .xls, .xlsm, .csv, .parquet).
    - `start` integer, required: First page of the range, 1-based and inclusive.
    - `end` integer, required: Last page of the range, 1-based and inclusive.
  - `unknown_policy` 'allow' | 'force_best': allow may mark a unit unknown. force_best commits an ambiguous unit to one of your classes instead of abstaining. A blank or unreadable page the model sees is assigned the closest listed class — the closed decode vocabulary has no page-state tokens. A unit that could not be parsed is still returned unknown, and so is an empty spreadsheet tab. Pages never read by any model because of document size are also returned unknown and disclosed as classification_unread_pages.
  - `output` ClassifyOutputOptions
    - `max_alternatives` integer: How many ranked labels to return per unit, including the winner.
    - `include_reason` boolean: When true, each label may include a short reason string.
    - `delivery` 'progressive' | 'complete': Progressive publishes labels on the running job before the reason finishes. Complete waits for reasons (or their failure) before any result. Only meaningful when include_reason is true. Does not change the final result.

## Response `202`

Successful Response

- union
  - ClassifyJob
    - `job_id` string, uuid, required: Server-minted id. Poll GET /v1/jobs/{job_id} or pass wait_seconds on create.
    - `kind` 'classify'
    - `status` 'queued' | 'running' | 'succeeded' | 'failed' | 'cancelled', required: ``cancelled`` is a first-class terminal status, not a ``failed`` variant. Conflating them makes error-rate metrics lie and forces every client to string-match an error code.
    - `name` string, nullable: Display name (the console's 'project'). Public Parse, Split, Classify, Extract, and Ground runs supply a stable server default when blank; citation-generated Ground jobs are excluded. mutable via PATCH /v1/jobs/{job_id}.
    - `project_id` string, uuid, nullable: Caller-minted grouping id shared by jobs submitted together. Filterable on GET /v1/jobs.
    - `workspace_id` string, uuid, nullable: The workspace this job reads. Null for a document operation whose source is an upload, a URL, or a prior parse result.
    - `idempotency_key` string, nullable: Key that was replayed to return this job. Absent when the create call had no Idempotency-Key.
    - `force` boolean, nullable: The request's own `force` flag, echoed regardless of outcome. Present for kinds whose request carries one (e.g. kg_build, where a true value cleared the durable store before the job ran) so a caller can tell that apart from a build that failed leaving the prior store untouched — `result` alone cannot, since it is absent on failure either way.
    - `source` union: What this job read, for the document operations that take a single `source` (parse, split, classify, extract, ground); null for every other kind. Together with `workspace_id` this is what lets a caller re-open the document a past run was about — both `workspace_file` and `upload` sources remain until explicit deletion. Read from the stored request, so it rides the same retention path as `query` and `result`: null once `result_state` leaves `available`.
      - UploadSource: Bytes staged through ``POST /v1/uploads``. Retained until explicit deletion, except under Zero retention.
        - `type` 'upload'
        - `upload_id` string, uuid, required: Id returned by POST /v1/uploads. Ordinary bytes remain until their consuming jobs or tenant are deleted.
      - UrlSource: A public or presigned HTTPS location we fetch server-side.
        - `type` 'url'
        - `url` string, required: HTTPS URL the service fetches.
        - `file_name` string, required: Filename to record for the fetched bytes.
      - WorkspaceFileSource: A file already in a workspace. The single seam between the two contexts: a granular operation may read a workspace file and still write nothing back. The file need **not** be ingested — granular operations read source bytes, not representations.
        - `type` 'workspace_file'
        - `workspace_id` string, uuid, required: Workspace that owns the file.
        - `file_id` string, uuid, required: File id from GET /v1/workspaces/{workspace_id}/files.
      - ParseResultSource: A prior Parse job whose parsed artifact can be reused by downstream methods.
        - `type` 'parse_result'
        - `job_id` string, uuid, required: Succeeded parse job whose artifact should be reused.
    - `query` string, nullable: The question this job was asked, for the search kinds that carry one (intelligent_search, qa_file, qa_tables, filtered_search); null for every other kind. Read from the stored request, so it rides the same retention path as `result`: null once `result_state` leaves `available`. Narrow by design — the frozen access context never reaches the wire. File scope is summarised separately as `query_scope`.
    - `query_scope` union: Where this search ran, summarised for history UIs: the whole workspace, one file path, one folder prefix, or a count of files. Null for non-query jobs, filtered-search page continuations, and once `result_state` leaves `available`. Never lists multiple filenames, never the raw request, and never the frozen API-key id.
      - WorkspaceQueryScope: The question ran against the whole workspace.
        - `type` 'workspace'
      - FileQueryScope: The question ran against one workspace-relative file.
        - `type` 'file'
        - `path` string, required
      - PathPrefixQueryScope: The question ran against one folder subtree.
        - `type` 'path_prefix'
        - `prefix` string, required
      - FilesQueryScope: The question ran against several files; only the count is public.
        - `type` 'files'
        - `count` integer, required
    - `effort` string, nullable: How hard this run worked, for the search kinds that carry it. A deep-search job (kind deep_search_v2) created before the effort knob was removed echoes its requested level: 'low', 'medium', or 'high'; newer deep-search jobs, which all run one configuration, read null. An intelligent_search row carries an internal selector instead: 'fast' is fact-search, 'automatic' is automatic-search, and 'deep' (or the legacy 'balanced') is a deep-search or console QA chat job created before the 2026-09 consolidation — those two are not distinguishable by this field. Null for every other kind. Read from the stored request, so it rides the same retention path as `query` and `result`: null once `result_state` leaves `available`.
    - `surface` 'deep_search' | 'qa' | 'workbook', nullable: Which console history this deep-search job belongs to. `deep_search` is the public route and the Deep Search page; `qa` and `workbook` are the console chat skins over the same agent. It does not pick the implementation. Null for fact-search and every non-deep-search kind. Jobs stored before this field existed echo `deep_search`. Read from the stored payload, so it rides the same retention path as `query`.
    - `progress` JobProgress: Absent until the job is ``running``.
      - `units_total` integer, nullable
      - `units_done` integer
      - `items_total` integer, nullable: Files, for batch kinds.
      - `items_done` integer
      - `items_failed` integer: Quarantined after exhausting retries, for batch kinds. Excluded from items_done.
      - `stage` string, nullable: Current pipeline stage, for display only.
      - `knowledge_graph` KnowledgeGraphBuildProgress: Where a ``kg_build`` is, beyond what ``stage`` alone can say. A graph build runs for minutes to hours through a fixed sequence of phases, and ``stage`` names only the current one. Turning that into "how far along" otherwise means hard-coding the build engine's phase order in every client — an order that lives in another package and is free to gain a phase. So the position is reported rather than inferred. The counters are deliberately not a percentage. The phases are wildly unequal (extraction is ~60% of a build, and its per-document cost varies 25x with how dense the documents are), so weighting them is a presentation decision, made against measurements, that belongs to whoever is drawing the bar.
        - `phase_index` integer, required: 0-based position of the current phase in the build's fixed run order; equals phase_count when done.
        - `phase_count` integer, required: How many phases a build passes through before it is done. Currently 9.
        - `countable` boolean, required: Whether items_total/items_done describe this phase. False for the phases that are a single step, which have no unit to count — draw those as indeterminate rather than as a bar stuck at zero.
        - `documents` integer, nullable: Documents registered from the corpus, once the registering phase has run.
        - `mentions` integer, nullable: Mentions extracted across the corpus, once the validating phase has run.
        - `step` string, nullable: The span in flight inside a single-step phase, reported by the running step itself. Null when the phase counts its own units, or before the step has reported one.
        - `step_done` integer, nullable: Units of `step` finished, when it has countable units.
        - `step_total` integer, nullable: Units `step` has to get through, when that is known.
      - `routing` AutomaticSearchRouting
        - `selected` 'fact' | 'agent' | 'deep' | 'filtered', required: The route the router chose, the one the request's `mode` named, or `fact` when nothing was searched: the workspace had no ingested files, or the query asked for nothing that a document contains.
        - `final` 'fact' | 'agent' | 'deep' | 'filtered', required: The pipeline that produced the returned output. It differs from `selected` only in `auto` mode, when the router chose fact search, fact search could not settle the question, and the agent answered instead.
        - `reason` string, required: The router's explanation of its decision, or why no router ran.
        - `router_model` string, required: The model used to choose the search pipeline; `none` when no router ran.
        - `router` union: Which router decided, and what it measured: an LLM (its explanation is ``reason``), NaceDM (its probability and decision point), or `requested` when no router ran: the request's `mode` was `fact` or `agent`, or the workspace had no ingested files. Results routed before this field existed read as the LLM.
          - AutomaticLlmRouter: An LLM chose the route. Its explanation is the routing's ``reason``.
            - `kind` 'llm'
          - AutomaticNaceDmRouter: NaceDM, the decision model, chose the route by scoring how likely the query needs the agent.
            - `kind` 'nacedm'
            - `p_agent` number, nullable, required: NaceDM's probability that the query needs the investigating agent rather than one fact-search pass; 1 - p_agent is the probability that fact search settles it. Null when the request was too long for NaceDM to score, which routes to the agent.
            - `decision_point` number, required: The agent runs when ``p_agent`` is at or above this; fact search runs below it.
          - AutomaticRequestedRouter: No router ran: the request's ``mode`` named the route, or the workspace had no ingested files.
            - `kind` 'requested'
        - `router_wall_ms` integer, nullable: Wall time spent selecting the initial route.
        - `fact_wall_ms` integer, nullable: Wall time spent running fact search, when selected.
        - `delegated_wall_ms` integer, nullable: Wall time spent preparing and running the native deep or filtered job.
    - `result` ClassifyResult
      - `result_type` 'classify'
      - `reason_status` 'pending' | 'complete' | 'failed' | 'not_requested': Pending means labels are available while the top-label reason is still running. Complete means the top-label reason finished. Failed means labels stand and the top-label reason did not finish. A runner-up reason that does not finish leaves that label empty and does not by itself fail the job. Reasons that finished are kept. Not_requested means include_reason was false. Always read the job status for cancellation or failure. Default complete so stored results from before this field validate unchanged.
      - `labels_at` string, date-time, nullable: When NDI published this job's labels, as an ISO 8601 UTC timestamp, on the same clock as started_at and finished_at. Time to labels is labels_at minus created_at. Reason time is finished_at minus labels_at. Those gaps mean the same thing on every lane. Null on a cache replay, and when labels and the reason arrived together.
      - `units` ClassifyUnit[], required: One scored unit per document, or per page when granularity is page.
        - `granularity` 'document' | 'page': Whether this unit scores the whole document or one page range.
        - `page_range` ClassifyPageRange
          - `start` integer, required: First page of this unit, 1-based and inclusive.
          - `end` integer, required: Last page of this unit, 1-based and inclusive.
        - `unknown` boolean, required: True when no requested class won this unit — including reserved other and pages never read by any model — regardless of unknown_policy. False when a requested class won, even if some pages were other.
        - `labels` ClassifyLabel[], required: Ranked class assignments for this unit, best first.
          - `class_id` string, required: Id of this label. One of the request classes, or the reserved id other when no requested class matched this slice. other can be the top label when the unit is unknown, or a ranked alternative when some pages did not match. Join request classes on this only when the id is in your classes list.
          - `subclass_id` string, nullable: Subclass id when the assignment is a nested class. Null for a top-level class.
          - `label` string, required: Display name of that class. Copied from the request for a caller-defined class_id; a freeform name when class_id is other.
          - `rank` integer, required: 1-based position in this unit's labels after classes are sorted by score. 1 is the assigned (or top) class; later ranks are alternatives kept by output.max_alternatives.
          - `reason` string, nullable: Short model explanation for this label, only when output.include_reason is true. For the top label, why that class was chosen. For a later rank on an abstained unit, why that class was close but not the pick.
          - `confidence` number, nullable: DREX's probability for this label, averaged over the menu orders, when DREX decided it. A document label shared by several segments is the page-weighted mean of the probability recorded on each segment, when every label in the unit has one. Labels are ordered by page coverage. Confidence is per label, so a lower rank can carry a higher confidence. An abstained unit's top label is that mean. Its runner-up labels carry DREX's probability for that runner-up option. When any other label in the unit lacks a DREX probability, every such label uses the share of the unit (pages or sheets) assigned that class. Runner-up labels on an abstained unit do not use that share.
      - `unknown_units` integer: Count of units where no requested class won (declined).
      - `pages_read_fraction` number: Fraction of expected pages that entered at least one model prompt, from 0 to 1. Equals 1.0 when no pages were unread; otherwise 1 minus unread over expected.
      - `warnings` string[]: Parse settings this run accepted but did not apply, each naming the setting and why, for example a page selection on a file with no pages. Left out when empty.
      - `disclosures` union[]: Structured signals about how this result was produced. Empty when all behaviours matched what the caller would assume without them.
        - union
          - ClassificationGapFillDisclosure: Pages left unclassified by the model were assigned a fallback label. The classification still succeeded. Gap-filled pages should be treated as lower-confidence assignments rather than absences. One disclosure is emitted per distinct fallback label; ``filled_pages`` in that disclosure are exactly the pages that received ``fallback_label``.
            - `code` 'classification_gap_fill', required
            - `filled_pages` integer[], required: Exact 1-based pages assigned a fallback label because the model left them unclassified.
            - `fallback_label` string, required: The classification label applied to every page in filled_pages.
          - ClassificationUnreadPagesDisclosure: Pages never entered any model prompt and are therefore unknown. The classification still succeeded. These pages were dropped by the document-size batch or token cap, so no model produced a label for them. They carry the reserved unknown label regardless of unknown_policy and must not be read as gap-fill (the model never saw them).
            - `code` 'classification_unread_pages', required
            - `unread_pages` integer[], required: Exact 1-based pages that never entered any model prompt and are returned unknown because no model produced a label for them.
          - ExtractionNullOnSuccessDisclosure: Schema-required fields returned null because the document did not contain them. The job succeeded. Null means the document was examined and the field was absent — not that extraction failed or was skipped.
            - `code` 'extraction_null_on_success', required
            - `field_paths` string[], required: JSON paths of schema-required fields that were absent in the source document and returned as null.
          - ParseVendorProvenanceDisclosure: The file was processed by a vendor other than the router's first choice. The parse result is valid. The vendor name is published so a caller can account for differing output quality or formatting between vendors.
            - `code` 'parse_vendor_provenance', required
            - `engine` 'native_ocr' | 'document_ocr' | 'layout_ocr', required: Stable public identity for the parse engine that produced a result.
            - `fallback_depth` integer, required: Number of earlier engines that failed before this engine produced the result.
          - ParseFidelityDisclosure: An oversized parse used a bounded fidelity mode without dropping extracted text.
            - `code` 'parse_fidelity', required
            - `mode` 'reduced_resolution' | 'reduced_layout', required: Rendering or layout adjustment applied to fit the processing memory budget.
            - `pages` integer[], required: Exact 1-based source pages affected when physical pages are available; empty when the bounded format has no reliable physical-page map.
          - ContentTruncatedDisclosure: A user-visible content field was truncated before being included in the result. The result is otherwise complete. ``unit`` states how both size fields should be interpreted.
            - `code` 'content_truncated', required
            - `target` 'classification_input' | 'document_content' | 'spreadsheet_region' | 'extraction_array', required: Processing area where content was reduced before producing a result.
            - `unit` 'characters' | 'tokens' | 'rows' | 'columns' | 'cells' | 'items', required: Unit used to quantify a content reduction.
            - `original_size` integer, required: Size of the content before the reduction was applied.
            - `retained_size` integer, required: Size of the content retained after the reduction was applied.
          - SourceIncompleteDisclosure: The source file ended mid-document, so the result covers only the part that arrived. The job succeeded on the bytes that were present, and a lane that can read a partial file still returns everything it could read. Without this disclosure that output is indistinguishable from a complete document, which is the dangerous reading: a short result looks like a short source. No original size is published because an incomplete file carries no record of how long it should have been. ``received_bytes`` is what a caller can compare against the size it believes it sent.
            - `code` 'source_incomplete', required
            - `received_bytes` integer, required: Size of the file as received, whose final record ended before the record did.
    - `result_state` 'available' | 'expired' | 'not_retained' | 'cancelled' | 'storage_unavailable': Why ``Job.result`` is null. ``available`` with a null result means the job has not succeeded yet; ``expired`` means its content was removed; ``not_retained`` means the client is provisioned ``zero_retention`` and already had its one chance to read it; ``cancelled`` means the job, or the root job it ran for, was cancelled. A cancelled job serves no results (it is not charged, including delivered parts) — not its own progressive values, and not a delegated child's result. It is a read state only, derived on the wire; no row stores it. ``storage_unavailable`` withholds a retained result while its workspace binding refuses access; ``Job.result_error`` explains the current refusal. A late poll returns the job with a state, never a 404 — "this expired" and "this never existed" are a scheduling problem and a bug respectively, and a 404 makes them identical.
    - `result_error` Error: The error object, per spec 12 §1.8.
      - `code` 'storage_connection_not_found' | 'storage_connection_in_use' | 'storage_limit_exceeded' | 'storage_conflict' | 'storage_auth_required' | 'storage_unavailable' | 'scan_required' | 'scan_rejected' | 'scan_unavailable' | 'invalid_request' | 'unsupported_file_type' | 'file_too_large' | 'invalid_schema' | 'invalid_sql' | 'invalid_path_prefix' | 'command_not_permitted' | 'batch_too_large' | 'domain_validation_failed' | 'default_label_required' | 'wait_required_for_zero_retention' | 'ledger_decision_invalid' | 'tenant_required' | 'partner_required' | 'workspace_not_found' | 'file_not_found' | 'job_not_found' | 'upload_not_found' | 'node_not_found' | 'schema_not_found' | 'domain_not_found' | 'session_not_found' | 'access_label_not_found' | 'ledger_understanding_not_found' | 'ledger_table_not_found' | 'tenant_not_found' | 'path_conflict' | 'schema_version_conflict' | 'access_label_name_taken' | 'workspace_name_taken' | 'access_label_in_use' | 'upload_expired' | 'file_not_ingested' | 'workspace_deleting' | 'workspace_not_deletable' | 'ingestion_in_progress' | 'job_not_cancellable' | 'confirmation_required' | 'domain_not_published' | 'domain_in_use' | 'reserved_domain_slug' | 'kg_not_built' | 'kg_build_in_progress' | 'ledger_understanding_in_progress' | 'ledger_understanding_version_outdated' | 'session_busy' | 'tenant_limit_reached' | 'range_not_satisfiable' | 'result_expired' | 'access_denied' | 'tenant_disabled' | 'access_backstop_violation' | 'access_mode_not_implemented' | 'rate_limited' | 'concurrency_limit_reached' | 'quota_exceeded' | 'corrupt_file' | 'zero_byte_file' | 'encrypted_file' | 'csv_dialect_ambiguous' | 'csv_parse_failed' | 'parse_failed' | 'ocr_failed' | 'conversion_failed' | 'sheetless_workbook' | 'unplayable_media' | 'unclassified_pages' | 'job_cancelled' | 'job_timeout' | 'upstream_unavailable' | 'internal_error' | 'unauthorized' | 'not_implemented', required: Every error the ``/v1`` surface can return. Clients must treat this as an **open** enum: a new member is an additive change (spec 12 §9), and a client that crashes on an unknown code makes every future error a breaking change.
      - `message` string, required: Human- and model-readable; says what to do differently.
      - `detail` object, nullable: Structured payload, when the code carries one.
      - `retryable` boolean, required: Whether retrying the identical request could succeed.
      - `request_id` string, nullable: Per-request correlation id; matches the ``X-Request-Id`` header. Null only when the error was minted outside any HTTP request, e.g. by a worker.
    - `error` Error: The error object, per spec 12 §1.8.
      - `code` 'storage_connection_not_found' | 'storage_connection_in_use' | 'storage_limit_exceeded' | 'storage_conflict' | 'storage_auth_required' | 'storage_unavailable' | 'scan_required' | 'scan_rejected' | 'scan_unavailable' | 'invalid_request' | 'unsupported_file_type' | 'file_too_large' | 'invalid_schema' | 'invalid_sql' | 'invalid_path_prefix' | 'command_not_permitted' | 'batch_too_large' | 'domain_validation_failed' | 'default_label_required' | 'wait_required_for_zero_retention' | 'ledger_decision_invalid' | 'tenant_required' | 'partner_required' | 'workspace_not_found' | 'file_not_found' | 'job_not_found' | 'upload_not_found' | 'node_not_found' | 'schema_not_found' | 'domain_not_found' | 'session_not_found' | 'access_label_not_found' | 'ledger_understanding_not_found' | 'ledger_table_not_found' | 'tenant_not_found' | 'path_conflict' | 'schema_version_conflict' | 'access_label_name_taken' | 'workspace_name_taken' | 'access_label_in_use' | 'upload_expired' | 'file_not_ingested' | 'workspace_deleting' | 'workspace_not_deletable' | 'ingestion_in_progress' | 'job_not_cancellable' | 'confirmation_required' | 'domain_not_published' | 'domain_in_use' | 'reserved_domain_slug' | 'kg_not_built' | 'kg_build_in_progress' | 'ledger_understanding_in_progress' | 'ledger_understanding_version_outdated' | 'session_busy' | 'tenant_limit_reached' | 'range_not_satisfiable' | 'result_expired' | 'access_denied' | 'tenant_disabled' | 'access_backstop_violation' | 'access_mode_not_implemented' | 'rate_limited' | 'concurrency_limit_reached' | 'quota_exceeded' | 'corrupt_file' | 'zero_byte_file' | 'encrypted_file' | 'csv_dialect_ambiguous' | 'csv_parse_failed' | 'parse_failed' | 'ocr_failed' | 'conversion_failed' | 'sheetless_workbook' | 'unplayable_media' | 'unclassified_pages' | 'job_cancelled' | 'job_timeout' | 'upstream_unavailable' | 'internal_error' | 'unauthorized' | 'not_implemented', required: Every error the ``/v1`` surface can return. Clients must treat this as an **open** enum: a new member is an additive change (spec 12 §9), and a client that crashes on an unknown code makes every future error a breaking change.
      - `message` string, required: Human- and model-readable; says what to do differently.
      - `detail` object, nullable: Structured payload, when the code carries one.
      - `retryable` boolean, required: Whether retrying the identical request could succeed.
      - `request_id` string, nullable: Per-request correlation id; matches the ``X-Request-Id`` header. Null only when the error was minted outside any HTTP request, e.g. by a worker.
    - `units` integer: Measured work in legacy credit units, retained even when a failed or cancelled job is not billed. Kept for compatibility: `credits` is the authoritative billed amount.
    - `credits` integer, nullable: Final credits billed for this job, priced once under `credit_schedule_version`. Null until `usage_final` is true. Never changes afterwards, including after payload expiry or deletion. Failed and cancelled-before-completion jobs finalize with 0 credits, including delivered parts; ones finalized before that policy keep their credits. Always null on a stage job that Automatic Search delegated: its work is billed on the parent job.
    - `usage_final` boolean: True once `credits` is final and will not change. False while the job is queued or running, and until any cleanup of its delegated stages has been accounted for. A failed tool job (read_file, run_sql, hybrid_search, grep, file_metadata, folder_metadata) created with an Idempotency-Key stays false for one hour after it fails, because a retry with the same key re-runs it; after that hour it is final and the retry returns the stored error.
    - `credit_schedule_version` integer, nullable: Version of the credit schedule `credits` were priced under. Null until `usage_final` is true.
    - `usage_finalized_at` string, date-time, nullable: When `credits` became final. Null until `usage_final` is true.
    - `cost` JobCost: An immutable USD price independent of the legacy credit schedule.
      - `currency` 'USD'
      - `pricing_version` string, required: The dollar price-sheet version pinned when this job was accepted.
      - `total_usd` string, required: Sum of the component amounts in USD, serialized as a decimal string.
      - `lines` CostLine[], required: The independently priced Parse and operation components, including paid reuse.
        - `operation` 'parse' | 'split' | 'classify' | 'extract' | 'ground', required: The work this dollar amount pays for.
        - `quantity` integer, required: The number of page-equivalents or Ground targets priced by this line.
        - `unit` 'page' | 'item', required: The quantity's billing unit: page-equivalent or Ground target.
        - `execution` 'fresh' | 'reused', required: The price tier: normal work or verified, billing-eligible tenant reuse; not provider token caching.
        - `unit_price_usd` string, required: USD per unit under the job's pinned price sheet, serialized as a decimal string.
        - `amount_usd` string, required: This line's USD amount: quantity multiplied by unit_price_usd, serialized as a decimal string.
        - `reuse_reason` 'reused_parse' | 'repeat_of_job', nullable: Why the lower reuse rate applies; null on a normal-price line.
    - `usage` JobUsage: How a job's final credits were computed.
      - `lines` UsageLine[], required: One line per part. Their credits sum to the job's credits; waived parts remain listed at 0 credits.
        - `part` 'parse' | 'operation' | 'image_ocr', required: parse: reading the document into text. operation: the tool's own work on what was parsed. image_ocr: reading the images inside an email, HTML or spreadsheet file. A ground job's image_ocr is the email images it read to find targets its text search missed.
        - `quantity` integer, required: How many units of work this part measured, including waived or included work.
        - `unit` 'page' | 'item' | 'unit' | 'image', required: What quantity counts: page is a page or page-equivalent (a spreadsheet's non-empty cells, a minute of audio), item is a ground item, image is an image Parse read inside an email, HTML or spreadsheet file, unit is a job priced by its units directly (agent steps).
        - `credits_per_unit` integer, required: The credit schedule's rate for one unit of this part; zero for OCR included in the ordinary price.
        - `multiplier` integer, required: 2 on the parse and image_ocr parts of a parse_mode "high" job, otherwise 1.
        - `credits` integer, required: quantity x credits_per_unit x multiplier: what this part added to the job's credits. 0 when waived or included in the ordinary price.
        - `waiver` UsageWaiver: Why part of a job, or the whole job, was not charged.
          - `part` 'parse' | 'job', required: parse: the parse part was not charged. job: nothing in the job was charged.
          - `reason` 'reused_parse' | 'repeat_of_job', required: reused_parse: the job read a parse your earlier job already paid for. repeat_of_job: the job repeated your earlier job exactly and was answered with its result.
          - `source_job_id` string, uuid, nullable: Always null: billing history must not disclose another job as its visibility changes.
    - `created_at` string, date-time, required: When the job row was created.
    - `started_at` string, date-time, nullable: When execution began. Null while still queued.
    - `finished_at` string, date-time, nullable: When the job reached a terminal status.
    - `payload_expires_at` string, date-time, nullable: Cleanup deadline after explicit deletion. Null for retained content; Zero retention uses its delivery window.
    - `deleted_at` string, date-time, nullable: When the caller hid this run from listings, via `delete_job`. A soft delete: the job still reads by id and still counts toward `GET /v1/usage` — what it stops doing is appearing in `GET /v1/jobs`.
  - CategorizeJob
    - `job_id` string, uuid, required: Server-minted id. Poll GET /v1/jobs/{job_id} or pass wait_seconds on create.
    - `kind` 'categorize'
    - `status` 'queued' | 'running' | 'succeeded' | 'failed' | 'cancelled', required: ``cancelled`` is a first-class terminal status, not a ``failed`` variant. Conflating them makes error-rate metrics lie and forces every client to string-match an error code.
    - `name` string, nullable: Display name (the console's 'project'). Public Parse, Split, Classify, Extract, and Ground runs supply a stable server default when blank; citation-generated Ground jobs are excluded. mutable via PATCH /v1/jobs/{job_id}.
    - `project_id` string, uuid, nullable: Caller-minted grouping id shared by jobs submitted together. Filterable on GET /v1/jobs.
    - `workspace_id` string, uuid, nullable: The workspace this job reads. Null for a document operation whose source is an upload, a URL, or a prior parse result.
    - `idempotency_key` string, nullable: Key that was replayed to return this job. Absent when the create call had no Idempotency-Key.
    - `force` boolean, nullable: The request's own `force` flag, echoed regardless of outcome. Present for kinds whose request carries one (e.g. kg_build, where a true value cleared the durable store before the job ran) so a caller can tell that apart from a build that failed leaving the prior store untouched — `result` alone cannot, since it is absent on failure either way.
    - `source` union: What this job read, for the document operations that take a single `source` (parse, split, classify, extract, ground); null for every other kind. Together with `workspace_id` this is what lets a caller re-open the document a past run was about — both `workspace_file` and `upload` sources remain until explicit deletion. Read from the stored request, so it rides the same retention path as `query` and `result`: null once `result_state` leaves `available`.
      - UploadSource: Bytes staged through ``POST /v1/uploads``. Retained until explicit deletion, except under Zero retention.
        - `type` 'upload'
        - `upload_id` string, uuid, required: Id returned by POST /v1/uploads. Ordinary bytes remain until their consuming jobs or tenant are deleted.
      - UrlSource: A public or presigned HTTPS location we fetch server-side.
        - `type` 'url'
        - `url` string, required: HTTPS URL the service fetches.
        - `file_name` string, required: Filename to record for the fetched bytes.
      - WorkspaceFileSource: A file already in a workspace. The single seam between the two contexts: a granular operation may read a workspace file and still write nothing back. The file need **not** be ingested — granular operations read source bytes, not representations.
        - `type` 'workspace_file'
        - `workspace_id` string, uuid, required: Workspace that owns the file.
        - `file_id` string, uuid, required: File id from GET /v1/workspaces/{workspace_id}/files.
      - ParseResultSource: A prior Parse job whose parsed artifact can be reused by downstream methods.
        - `type` 'parse_result'
        - `job_id` string, uuid, required: Succeeded parse job whose artifact should be reused.
    - `query` string, nullable: The question this job was asked, for the search kinds that carry one (intelligent_search, qa_file, qa_tables, filtered_search); null for every other kind. Read from the stored request, so it rides the same retention path as `result`: null once `result_state` leaves `available`. Narrow by design — the frozen access context never reaches the wire. File scope is summarised separately as `query_scope`.
    - `query_scope` union: Where this search ran, summarised for history UIs: the whole workspace, one file path, one folder prefix, or a count of files. Null for non-query jobs, filtered-search page continuations, and once `result_state` leaves `available`. Never lists multiple filenames, never the raw request, and never the frozen API-key id.
      - WorkspaceQueryScope: The question ran against the whole workspace.
        - `type` 'workspace'
      - FileQueryScope: The question ran against one workspace-relative file.
        - `type` 'file'
        - `path` string, required
      - PathPrefixQueryScope: The question ran against one folder subtree.
        - `type` 'path_prefix'
        - `prefix` string, required
      - FilesQueryScope: The question ran against several files; only the count is public.
        - `type` 'files'
        - `count` integer, required
    - `effort` string, nullable: How hard this run worked, for the search kinds that carry it. A deep-search job (kind deep_search_v2) created before the effort knob was removed echoes its requested level: 'low', 'medium', or 'high'; newer deep-search jobs, which all run one configuration, read null. An intelligent_search row carries an internal selector instead: 'fast' is fact-search, 'automatic' is automatic-search, and 'deep' (or the legacy 'balanced') is a deep-search or console QA chat job created before the 2026-09 consolidation — those two are not distinguishable by this field. Null for every other kind. Read from the stored request, so it rides the same retention path as `query` and `result`: null once `result_state` leaves `available`.
    - `surface` 'deep_search' | 'qa' | 'workbook', nullable: Which console history this deep-search job belongs to. `deep_search` is the public route and the Deep Search page; `qa` and `workbook` are the console chat skins over the same agent. It does not pick the implementation. Null for fact-search and every non-deep-search kind. Jobs stored before this field existed echo `deep_search`. Read from the stored payload, so it rides the same retention path as `query`.
    - `progress` JobProgress: Absent until the job is ``running``.
      - `units_total` integer, nullable
      - `units_done` integer
      - `items_total` integer, nullable: Files, for batch kinds.
      - `items_done` integer
      - `items_failed` integer: Quarantined after exhausting retries, for batch kinds. Excluded from items_done.
      - `stage` string, nullable: Current pipeline stage, for display only.
      - `knowledge_graph` KnowledgeGraphBuildProgress: Where a ``kg_build`` is, beyond what ``stage`` alone can say. A graph build runs for minutes to hours through a fixed sequence of phases, and ``stage`` names only the current one. Turning that into "how far along" otherwise means hard-coding the build engine's phase order in every client — an order that lives in another package and is free to gain a phase. So the position is reported rather than inferred. The counters are deliberately not a percentage. The phases are wildly unequal (extraction is ~60% of a build, and its per-document cost varies 25x with how dense the documents are), so weighting them is a presentation decision, made against measurements, that belongs to whoever is drawing the bar.
        - `phase_index` integer, required: 0-based position of the current phase in the build's fixed run order; equals phase_count when done.
        - `phase_count` integer, required: How many phases a build passes through before it is done. Currently 9.
        - `countable` boolean, required: Whether items_total/items_done describe this phase. False for the phases that are a single step, which have no unit to count — draw those as indeterminate rather than as a bar stuck at zero.
        - `documents` integer, nullable: Documents registered from the corpus, once the registering phase has run.
        - `mentions` integer, nullable: Mentions extracted across the corpus, once the validating phase has run.
        - `step` string, nullable: The span in flight inside a single-step phase, reported by the running step itself. Null when the phase counts its own units, or before the step has reported one.
        - `step_done` integer, nullable: Units of `step` finished, when it has countable units.
        - `step_total` integer, nullable: Units `step` has to get through, when that is known.
      - `routing` AutomaticSearchRouting
        - `selected` 'fact' | 'agent' | 'deep' | 'filtered', required: The route the router chose, the one the request's `mode` named, or `fact` when nothing was searched: the workspace had no ingested files, or the query asked for nothing that a document contains.
        - `final` 'fact' | 'agent' | 'deep' | 'filtered', required: The pipeline that produced the returned output. It differs from `selected` only in `auto` mode, when the router chose fact search, fact search could not settle the question, and the agent answered instead.
        - `reason` string, required: The router's explanation of its decision, or why no router ran.
        - `router_model` string, required: The model used to choose the search pipeline; `none` when no router ran.
        - `router` union: Which router decided, and what it measured: an LLM (its explanation is ``reason``), NaceDM (its probability and decision point), or `requested` when no router ran: the request's `mode` was `fact` or `agent`, or the workspace had no ingested files. Results routed before this field existed read as the LLM.
          - AutomaticLlmRouter: An LLM chose the route. Its explanation is the routing's ``reason``.
            - `kind` 'llm'
          - AutomaticNaceDmRouter: NaceDM, the decision model, chose the route by scoring how likely the query needs the agent.
            - `kind` 'nacedm'
            - `p_agent` number, nullable, required: NaceDM's probability that the query needs the investigating agent rather than one fact-search pass; 1 - p_agent is the probability that fact search settles it. Null when the request was too long for NaceDM to score, which routes to the agent.
            - `decision_point` number, required: The agent runs when ``p_agent`` is at or above this; fact search runs below it.
          - AutomaticRequestedRouter: No router ran: the request's ``mode`` named the route, or the workspace had no ingested files.
            - `kind` 'requested'
        - `router_wall_ms` integer, nullable: Wall time spent selecting the initial route.
        - `fact_wall_ms` integer, nullable: Wall time spent running fact search, when selected.
        - `delegated_wall_ms` integer, nullable: Wall time spent preparing and running the native deep or filtered job.
    - `result` NdiServiceAppPlatformApiResultsCategorizeResult
      - `result_type` 'categorize'
      - `assignments` CategoryAssignment[], required
        - `source_index` integer, required: Index into the request's sources list.
        - `file_name` string, required
        - `category` string, nullable: Null when nothing cleared the threshold.
        - `confidence` number, required
        - `evidence` Evidence: The citation primitive. Every result that asserts something about a document or the corpus carries it. A synthesised sentence with no evidence behind it is a bug, not a degraded mode. ``path`` is always the uploaded **source** path — that is what every external audience (``/v1`` bodies, the console, the search-agent result) is shown. The two ``exclude=True`` fields below are the host's internal twin of it, for the audience that must not see source paths at all (the agent's tools address ingestion derivatives); they are never serialized, so the ``/v1`` wire shape is exactly what it was before they existed. ``component`` closes the loop from a hit back to the file tools: it is the public component name of the hit — exactly the vocabulary ``file-metadata`` lists and ``qa-file`` / ``read-file`` accept as ``component=`` (the tab name for a spreadsheet, the section/subcategory for a split document). ``None`` means a single-component file or that attribution was unavailable. ``sql_table`` takes the next step for a component that is a table: the name ``run-sql`` queries it under — a spreadsheet tab's table, or the table every row a document prints in its long tables lands in. Search sets it so a hit can go straight to SQL; other producers leave it ``None``. ``locator`` is present only when the producer actually located the snippet. ``None`` is not a degraded mode: an engine that answered from a whole component (the QA agent's quotes, the DuckDB agent's SQL receipts) asserts the file and ``component``, nothing finer. Pinning such a snippet to a region — a page bbox, a sheet cell range — is a separate ``ground`` call on ``path`` with ``snippet`` as the target text (and ``component`` as the ``sheet`` hint when the file is a spreadsheet).
          - `file_id` string, uuid, required
          - `path` string, required
          - `locator` union: Where the snippet sits, when the producer located it. None means the citation is component-level: ground the snippet against path (sheet=component for spreadsheets) to place it.
            - TextRange: A character span in a derivative's text, with the page(s) it falls on. ``pages`` is every page the span overlaps — a chunk that straddles a page marker belongs to both, and a reader handed only the first would miss the part of the span that carries the match. ``page`` is the first of them, kept because most producers and consumers only ever deal in one page.
              - …
            - VisualRegion
              - …
            - SpreadsheetRange
              - …
            - JsonlRecord
              - …
            - JsonPointer: An RFC 6901 pointer into a JSON source; an empty string means the root.
              - …
            - AudioRange
              - …
            - RowRange: A 0-based inclusive row run on a parquet table.
              - …
          - `page` integer, nullable: The 1-based page the producer attributed the snippet to when it produced no locator; None when unknown or when the locator already carries the page.
          - `relevance_score` number, required
          - `snippet` string, required
          - `why` string, required: Which retrieval leg matched, or which graph path led here.
          - `component` string, nullable: Public component of the hit, in the vocabulary file-metadata lists and qa-file/read-file accept as component= (tab name for spreadsheets, section for split documents); None for a single-component file or when attribution is unavailable.
          - `sql_table` string, nullable: The run-sql table name holding the hit's component as rows (a spreadsheet tab's table, or a document's printed long tables); None when the component is not a table or the producer does not say.
          - `line_start` integer, nullable: First line (1-based) of the hit in the text read-file returns for path and component; None when the producer did not place the hit in lines.
          - `line_end` integer, nullable: Last line (1-based, inclusive) of the hit, counted like line_start; None with it.
        - `alternatives` ScoredCategory[]
          - `category` string, required
          - `confidence` number, required
      - `units` integer, required
      - `warnings` string[]: Parse settings this run accepted but did not apply, each naming the setting and why, for example a page selection on a file with no pages. Left out when empty.
    - `result_state` 'available' | 'expired' | 'not_retained' | 'cancelled' | 'storage_unavailable': Why ``Job.result`` is null. ``available`` with a null result means the job has not succeeded yet; ``expired`` means its content was removed; ``not_retained`` means the client is provisioned ``zero_retention`` and already had its one chance to read it; ``cancelled`` means the job, or the root job it ran for, was cancelled. A cancelled job serves no results (it is not charged, including delivered parts) — not its own progressive values, and not a delegated child's result. It is a read state only, derived on the wire; no row stores it. ``storage_unavailable`` withholds a retained result while its workspace binding refuses access; ``Job.result_error`` explains the current refusal. A late poll returns the job with a state, never a 404 — "this expired" and "this never existed" are a scheduling problem and a bug respectively, and a 404 makes them identical.
    - `result_error` Error: The error object, per spec 12 §1.8.
      - `code` 'storage_connection_not_found' | 'storage_connection_in_use' | 'storage_limit_exceeded' | 'storage_conflict' | 'storage_auth_required' | 'storage_unavailable' | 'scan_required' | 'scan_rejected' | 'scan_unavailable' | 'invalid_request' | 'unsupported_file_type' | 'file_too_large' | 'invalid_schema' | 'invalid_sql' | 'invalid_path_prefix' | 'command_not_permitted' | 'batch_too_large' | 'domain_validation_failed' | 'default_label_required' | 'wait_required_for_zero_retention' | 'ledger_decision_invalid' | 'tenant_required' | 'partner_required' | 'workspace_not_found' | 'file_not_found' | 'job_not_found' | 'upload_not_found' | 'node_not_found' | 'schema_not_found' | 'domain_not_found' | 'session_not_found' | 'access_label_not_found' | 'ledger_understanding_not_found' | 'ledger_table_not_found' | 'tenant_not_found' | 'path_conflict' | 'schema_version_conflict' | 'access_label_name_taken' | 'workspace_name_taken' | 'access_label_in_use' | 'upload_expired' | 'file_not_ingested' | 'workspace_deleting' | 'workspace_not_deletable' | 'ingestion_in_progress' | 'job_not_cancellable' | 'confirmation_required' | 'domain_not_published' | 'domain_in_use' | 'reserved_domain_slug' | 'kg_not_built' | 'kg_build_in_progress' | 'ledger_understanding_in_progress' | 'ledger_understanding_version_outdated' | 'session_busy' | 'tenant_limit_reached' | 'range_not_satisfiable' | 'result_expired' | 'access_denied' | 'tenant_disabled' | 'access_backstop_violation' | 'access_mode_not_implemented' | 'rate_limited' | 'concurrency_limit_reached' | 'quota_exceeded' | 'corrupt_file' | 'zero_byte_file' | 'encrypted_file' | 'csv_dialect_ambiguous' | 'csv_parse_failed' | 'parse_failed' | 'ocr_failed' | 'conversion_failed' | 'sheetless_workbook' | 'unplayable_media' | 'unclassified_pages' | 'job_cancelled' | 'job_timeout' | 'upstream_unavailable' | 'internal_error' | 'unauthorized' | 'not_implemented', required: Every error the ``/v1`` surface can return. Clients must treat this as an **open** enum: a new member is an additive change (spec 12 §9), and a client that crashes on an unknown code makes every future error a breaking change.
      - `message` string, required: Human- and model-readable; says what to do differently.
      - `detail` object, nullable: Structured payload, when the code carries one.
      - `retryable` boolean, required: Whether retrying the identical request could succeed.
      - `request_id` string, nullable: Per-request correlation id; matches the ``X-Request-Id`` header. Null only when the error was minted outside any HTTP request, e.g. by a worker.
    - `error` Error: The error object, per spec 12 §1.8.
      - `code` 'storage_connection_not_found' | 'storage_connection_in_use' | 'storage_limit_exceeded' | 'storage_conflict' | 'storage_auth_required' | 'storage_unavailable' | 'scan_required' | 'scan_rejected' | 'scan_unavailable' | 'invalid_request' | 'unsupported_file_type' | 'file_too_large' | 'invalid_schema' | 'invalid_sql' | 'invalid_path_prefix' | 'command_not_permitted' | 'batch_too_large' | 'domain_validation_failed' | 'default_label_required' | 'wait_required_for_zero_retention' | 'ledger_decision_invalid' | 'tenant_required' | 'partner_required' | 'workspace_not_found' | 'file_not_found' | 'job_not_found' | 'upload_not_found' | 'node_not_found' | 'schema_not_found' | 'domain_not_found' | 'session_not_found' | 'access_label_not_found' | 'ledger_understanding_not_found' | 'ledger_table_not_found' | 'tenant_not_found' | 'path_conflict' | 'schema_version_conflict' | 'access_label_name_taken' | 'workspace_name_taken' | 'access_label_in_use' | 'upload_expired' | 'file_not_ingested' | 'workspace_deleting' | 'workspace_not_deletable' | 'ingestion_in_progress' | 'job_not_cancellable' | 'confirmation_required' | 'domain_not_published' | 'domain_in_use' | 'reserved_domain_slug' | 'kg_not_built' | 'kg_build_in_progress' | 'ledger_understanding_in_progress' | 'ledger_understanding_version_outdated' | 'session_busy' | 'tenant_limit_reached' | 'range_not_satisfiable' | 'result_expired' | 'access_denied' | 'tenant_disabled' | 'access_backstop_violation' | 'access_mode_not_implemented' | 'rate_limited' | 'concurrency_limit_reached' | 'quota_exceeded' | 'corrupt_file' | 'zero_byte_file' | 'encrypted_file' | 'csv_dialect_ambiguous' | 'csv_parse_failed' | 'parse_failed' | 'ocr_failed' | 'conversion_failed' | 'sheetless_workbook' | 'unplayable_media' | 'unclassified_pages' | 'job_cancelled' | 'job_timeout' | 'upstream_unavailable' | 'internal_error' | 'unauthorized' | 'not_implemented', required: Every error the ``/v1`` surface can return. Clients must treat this as an **open** enum: a new member is an additive change (spec 12 §9), and a client that crashes on an unknown code makes every future error a breaking change.
      - `message` string, required: Human- and model-readable; says what to do differently.
      - `detail` object, nullable: Structured payload, when the code carries one.
      - `retryable` boolean, required: Whether retrying the identical request could succeed.
      - `request_id` string, nullable: Per-request correlation id; matches the ``X-Request-Id`` header. Null only when the error was minted outside any HTTP request, e.g. by a worker.
    - `units` integer: Measured work in legacy credit units, retained even when a failed or cancelled job is not billed. Kept for compatibility: `credits` is the authoritative billed amount.
    - `credits` integer, nullable: Final credits billed for this job, priced once under `credit_schedule_version`. Null until `usage_final` is true. Never changes afterwards, including after payload expiry or deletion. Failed and cancelled-before-completion jobs finalize with 0 credits, including delivered parts; ones finalized before that policy keep their credits. Always null on a stage job that Automatic Search delegated: its work is billed on the parent job.
    - `usage_final` boolean: True once `credits` is final and will not change. False while the job is queued or running, and until any cleanup of its delegated stages has been accounted for. A failed tool job (read_file, run_sql, hybrid_search, grep, file_metadata, folder_metadata) created with an Idempotency-Key stays false for one hour after it fails, because a retry with the same key re-runs it; after that hour it is final and the retry returns the stored error.
    - `credit_schedule_version` integer, nullable: Version of the credit schedule `credits` were priced under. Null until `usage_final` is true.
    - `usage_finalized_at` string, date-time, nullable: When `credits` became final. Null until `usage_final` is true.
    - `cost` JobCost: An immutable USD price independent of the legacy credit schedule.
      - `currency` 'USD'
      - `pricing_version` string, required: The dollar price-sheet version pinned when this job was accepted.
      - `total_usd` string, required: Sum of the component amounts in USD, serialized as a decimal string.
      - `lines` CostLine[], required: The independently priced Parse and operation components, including paid reuse.
        - `operation` 'parse' | 'split' | 'classify' | 'extract' | 'ground', required: The work this dollar amount pays for.
        - `quantity` integer, required: The number of page-equivalents or Ground targets priced by this line.
        - `unit` 'page' | 'item', required: The quantity's billing unit: page-equivalent or Ground target.
        - `execution` 'fresh' | 'reused', required: The price tier: normal work or verified, billing-eligible tenant reuse; not provider token caching.
        - `unit_price_usd` string, required: USD per unit under the job's pinned price sheet, serialized as a decimal string.
        - `amount_usd` string, required: This line's USD amount: quantity multiplied by unit_price_usd, serialized as a decimal string.
        - `reuse_reason` 'reused_parse' | 'repeat_of_job', nullable: Why the lower reuse rate applies; null on a normal-price line.
    - `usage` JobUsage: How a job's final credits were computed.
      - `lines` UsageLine[], required: One line per part. Their credits sum to the job's credits; waived parts remain listed at 0 credits.
        - `part` 'parse' | 'operation' | 'image_ocr', required: parse: reading the document into text. operation: the tool's own work on what was parsed. image_ocr: reading the images inside an email, HTML or spreadsheet file. A ground job's image_ocr is the email images it read to find targets its text search missed.
        - `quantity` integer, required: How many units of work this part measured, including waived or included work.
        - `unit` 'page' | 'item' | 'unit' | 'image', required: What quantity counts: page is a page or page-equivalent (a spreadsheet's non-empty cells, a minute of audio), item is a ground item, image is an image Parse read inside an email, HTML or spreadsheet file, unit is a job priced by its units directly (agent steps).
        - `credits_per_unit` integer, required: The credit schedule's rate for one unit of this part; zero for OCR included in the ordinary price.
        - `multiplier` integer, required: 2 on the parse and image_ocr parts of a parse_mode "high" job, otherwise 1.
        - `credits` integer, required: quantity x credits_per_unit x multiplier: what this part added to the job's credits. 0 when waived or included in the ordinary price.
        - `waiver` UsageWaiver: Why part of a job, or the whole job, was not charged.
          - `part` 'parse' | 'job', required: parse: the parse part was not charged. job: nothing in the job was charged.
          - `reason` 'reused_parse' | 'repeat_of_job', required: reused_parse: the job read a parse your earlier job already paid for. repeat_of_job: the job repeated your earlier job exactly and was answered with its result.
          - `source_job_id` string, uuid, nullable: Always null: billing history must not disclose another job as its visibility changes.
    - `created_at` string, date-time, required: When the job row was created.
    - `started_at` string, date-time, nullable: When execution began. Null while still queued.
    - `finished_at` string, date-time, nullable: When the job reached a terminal status.
    - `payload_expires_at` string, date-time, nullable: Cleanup deadline after explicit deletion. Null for retained content; Zero retention uses its delivery window.
    - `deleted_at` string, date-time, nullable: When the caller hid this run from listings, via `delete_job`. A soft delete: the job still reads by id and still counts toward `GET /v1/usage` — what it stops doing is appearing in `GET /v1/jobs`.

## Other responses

- `400`: Bad Request
- `401`: Unauthorized
- `402`: Payment Required
- `403`: Forbidden
- `404`: Not Found
- `409`: Conflict
- `410`: Gone
- `413`: Request Entity Too Large
- `422`: Unprocessable Entity
- `424`: Failed Dependency
- `429`: Too Many Requests
- `500`: Internal Server Error
- `501`: Not Implemented
- `503`: Service Unavailable
- `504`: Gateway Timeout

---

[API](https://skmtc.dev/nace/apis/ndi-service.md) - [All operations](https://skmtc.dev/nace/apis/ndi-service/llms.txt) - [OpenAPI document](https://skmtc.dev/nace/apis/ndi-service/revisions/d9d1f8ec2207?raw)
