Skip to main content
The Documents API owns the document upload and AI processing pipeline. It is separate from the Estimate API because document processing includes object storage, malware scanning, OCR/parsing, AI providers, queues, retries, area runs, and reviewable suggestions. Documents are owned by exactly one of a project or a company (party). Every upload must supply either projectId or partyId — not both. Company documents carry additional metadata (title, docKind, issueDate, reviewDate) and two pinned document kinds (contract_agreement, day_rates) are reserved for company ownership only.

Base prefix

Upload flow

  1. POST /api/documents validates file metadata and creates document rows.
  2. The response returns presigned upload instructions for accepted files.
  3. The browser uploads directly to S3 or LocalStack.
  4. Object-created events trigger malware scanning.
  5. Estimate-scoped uploads continue through parsing, summarisation, classification, and area modules. Project-scoped and company-scoped on-demand uploads stop at stored immediately after a clean scan, before parsing, until the user routes and processes them.
  6. Users review suggestions before any estimate records are created.

Service version endpoint

GET /version

Returns the running service’s configured version and deployed commit. This endpoint is unauthenticated and is intended for the frontend coordinator’s minimum-version gate. Response
  • version — value from SERVICE_VERSION, falling back to package.json only when that variable is absent.
  • commit — the Git commit SHA stamped into the Lambda as SERVICE_COMMIT at deploy time. Returns "unknown" when SERVICE_COMMIT is not set.
The response includes cache-control: no-store so the gate never acts on a cached value. Release coordinators compare the returned value with the manifest’s minimum version before promotion. The commit field lets operators tie the serving Lambda to an exact source commit, including on dev where no semver is assigned. Status codes

OpenAPI contract endpoint

GET /openapi.json

Returns the OpenAPI 3.1 document describing the descriptor-backed business routes. Health, version, and OpenAPI infrastructure routes are intentionally excluded. The document is built once at service boot from the live route descriptors and served as cached bytes — it is byte-stable per deploy and never recomputed per request. Served at /api/documents/openapi.json. Auth-exempt. Response includes cache-control: no-store. Status codes

Document endpoints

Document update endpoint

PATCH /:id

Updates the user-editable fields of a document. At least one field is required. Omitting a field preserves its current value; passing null clears it. Request body
  • relatedFolder — optional. A slash-delimited breadcrumb string up to 2048 characters, or null to clear it. Each segment must contain at least one non-whitespace character (Tender//Issued is rejected). Leading and trailing whitespace per segment is trimmed; the canonical form uses / (space–slash–space) as separator.
  • title — optional. A display name up to 300 characters, or null to clear it. Used on company document lists when set; the UI falls back to fileName when absent.
  • issueDate — optional. Calendar date in YYYY-MM-DD format, or null to clear it.
  • reviewDate — optional. Calendar date in YYYY-MM-DD format, or null to clear it.
Status codes

Delete endpoint

DELETE /:id

Soft-deletes a document. The row is stamped with deletedAt and deletedBy but is not removed from the database. The document’s file, renders, and all pipeline output (extractions, area runs, suggestions) are preserved, so the document remains renderable from history. What changes on delete:
  • Document lists (GET /api/documents?estimateId=, ?projectId=, ?partyId=) no longer return the document.
  • The suggestions summary (GET /suggestions/summary) excludes the document.
  • Any active drive-sync mapping for the document is set to excluded in the same transaction, so the file is never re-imported.
What is preserved:
  • The document row is still accessible via GET /:id. The response includes deletedAt.
  • The upload file, all rendered pages, conversion output, extracted text, area runs, and suggestions remain intact.
Writing to a deleted document. Any write to a deleted document returns 409 with { code: "deleted" }: PATCH /:id, POST /:id/route and unroute, POST /:id/process, POST /:id/retry, POST /:id/areas/:area/run, and the takeoff-pages add, replace and review writes. Reads keep working: GET /:id (with deletedAt), download, text, page images and thumbnails. Idempotency. A repeat DELETE on an already-deleted document returns 204 with no change. Blocking conditions. The delete is blocked while the document is mid-pipeline (scanning, parsing, summarising, or processing_areas). Deleting a routed document with live references in the estimate returns 409 document_in_use. The estimate-side reference check is server-to-server; returns 502 if the upstream estimate service is unreachable. Status codes

Upload scoping

Each file in a POST /api/documents request must supply exactly one of projectId or partyId. Supplying both projectId and partyId, or neither, returns 400 with the message "Provide exactly one of projectId or partyId." Supplying estimateId alongside partyId also returns 400.

Pinned kind constraints

contract_agreement and day_rates are company-only kinds. Uploading either without partyId returns 400. Project documents always use docKind: "other" (the default); attempting to set a pinned kind on a project document is rejected.

Routing and processing endpoints

POST /:id/route

Routes a project document into an estimate, or unroutes it. Optionally starts AI processing in the same database transaction. Request body
  • estimateId — required. UUID of the target estimate, or null to unroute.
  • process — optional boolean. When true, the document is moved to processing in the same transaction as the route write.
  • targetAreas — optional non-empty array. Limits AI processing to the listed areas. Valid values: takeoff, work_items, project_rates, programme_tender, subcontracts. Supplying this field requests processing even when process is omitted.
Behaviour
  • The estimate must belong to the document’s project. A mismatch returns 422. Company documents (owned by partyId) have no project and always return 422 from this endpoint.
  • Re-routing an active document to its current estimate is idempotent. Routing it to a different estimate while it is not settled returns 409.
  • Changing an estimate link or unroute requires a settled state (stored, processed, or failed); re-routing an active document to its existing estimate is the idempotent exception.
  • Processing starts only when the document is stored and scan-clean. A route request can still succeed when processing cannot start; its 200 response reports processing: { started: false, reason: "not_stored" }.
  • A linked drive document routed with processing requested is fetched first: the response reports processing: { started: false, reason: "fetching", jobId }, and the fetch job starts processing once the copy scans clean. A store-only document routes normally and reports reason: "store_only".
  • The estimate-service reference validation is server-to-server. If the upstream service is unreachable, the endpoint returns 502.
Status codes

POST /:id/process

Starts AI processing on a stored, routed document. This is the explicit processing trigger — equivalent to scanning having passed for an uploaded document that was routed with process: false. Request body (optional)
  • targetAreas — optional array. Limits processing to the listed areas. An explicit empty array returns 400. Omitting the field processes all applicable areas.
Behaviour
  • A first processing start requires stored, a clean scan verdict, a non-null estimateId, and no superseding document.
  • The endpoint uses a compare-and-swap write. A retry or race that finds the document on the active processing path (parsing, summarising, processing_areas, or processed) with a clean scan is a benign 200 no-op with reason: "already_processing".
  • A DWG or DXF drawing whose PDF derivative has not yet been produced returns 200 with started: false, reason: "conversion_pending". The document will become processable once conversion completes. The client should poll and retry.
  • A routed linked drive document is fetched first: the endpoint creates or joins a fetch job and returns 202 with { started: false, reason: "fetching", job }. Processing starts once the copy scans clean.
  • Store-only documents return 409 with a store_only: message. A linked file that can be viewed but not processed returns 409 not_processable, and one that opens only in the drive returns 409 drive_only.
  • Unrouted, infected, superseded, failed, rejected, or otherwise genuinely unprocessable documents return 409. This includes DWG or DXF documents whose conversion has failed.
Status codes

Takeoff review endpoint

PUT /:id/takeoff-review

Sets or updates the document-level takeoff review decision. Approving a document makes all its pages available on the takeoff canvas; rejecting excludes the document. Request body
  • status — required. Must be "approved" or "rejected".
Response
Behaviour
  • The document must have status: "processed". A document that is still in the pipeline returns 422 Unprocessable Entity.
  • The decision is changeable. Calling the endpoint again with a different status overwrites the previous decision and refreshes takeoffReviewedAt.
  • The write is atomic: the status = processed precondition and the decision commit in one statement, so a concurrent area rerun cannot produce an inconsistent state.
  • A decision does not modify the per-page takeoff page selection stored by PUT /:id/takeoff-pages.
  • In-flight sync rows are invisible to this endpoint. An ID that names a sync-provisioning row returns 404, the same as a document that does not exist.
Status codes

Page render endpoints

These endpoints serve rendered PDF pages for the takeoff picker and canvas, and hold each page’s drawing scale. Rendering and scale detection run on the render worker. The API never renders pages on the request path: a page that is not ready is queued for the worker. Setting a scale with the same-size fill reads page sizes from the cached page-sizes.json, and parses the PDF once when that file is missing. Two routes take an async query flag (1 or true): GET /:id/pages/:page/image and POST /:id/takeoff-pages/:page/scale-detection. With async=1, a page that is not ready is answered at once with 202 and a retryAfterMs for the client to poll after. Without it, the route behaves for older clients that read any 2xx as the finished answer, as described under each route.

GET /:id/thumbnails

Returns every page’s thumbnail status and, for ready pages, a presigned URL. One access check and one storage listing cover the whole document, so the picker does not need one request per page. Query parameters At most 1,000 pages can be requested in one call. A larger document asks by from and to. Response
  • expiresAt: taken before signing, so it is never later than any URL’s own expiry.
  • pageCount: the document’s page count, or 0 while the parse has not counted the pages yet.
  • thumbnails: one entry per page in the range. status is ready (with url), pending (not rendered yet), or failed (the page’s render failed after its retries).
When a page is pending, the endpoint queues a render of the document unless a job already covers it. The enqueue is debounced, so polling never stacks jobs. Poll again while pageCount is 0 or any page is pending. Status codes

GET /:id/page-thumbnails

Returns presigned URLs for every thumbnail rendered so far, a total page count, and a complete flag.
  • items — presigned URLs for contiguous pages rendered so far (pages 1 through thumbnailPageCount).
  • pageCount — the document’s known total page count, or the rendered count when the total is not yet available.
  • complete — true when every page is rendered. false while rendering is in progress, while a DWG drawing is still being converted, or when the total page count is not yet known. true (with an empty items array) when the document is not a scan-clean PDF and conversion is not expected, so the picker degrades to numbered placeholders rather than polling indefinitely.
Render offload behaviour. When complete is false for a scan-clean PDF, the endpoint enqueues a background render job for the pipeline worker and returns immediately with whatever is done so far. The client should poll until complete is true. Polling is debounced server-side: repeat calls within 30 seconds do not stack duplicate render jobs. DWG/DXF conversion behaviour. While a drawing is still being converted to a PDF derivative, the endpoint returns complete: false with an empty items array. The client should continue polling: once conversion finishes, subsequent calls will include rendered thumbnail URLs and eventually return complete: true. If conversion fails terminally, the endpoint returns complete: true (no thumbnails will ever be available), and the document will report a conversion failure through POST /:id/process.

GET /:id/pages/:page/image

Returns a presigned URL for one full-resolution page image (used by the takeoff canvas), together with the page’s true dimensions. Only available for scan-clean renderable documents. Query parameters When the page is rendered:
  • page — the requested page number.
  • url — presigned download URL for the full-resolution render.
  • expiresAt — ISO 8601 expiry timestamp for the presigned URL.
  • width / height — the render’s actual pixel dimensions, read from the PNG header. Use these to derive the true aspect ratio without guessing a paper size.
  • pageWidthPt / pageHeightPt — the PDF page box in points (1 pt = 1/72 in). Use these together with width / height to convert a pixel distance to millimetres exactly: mm = px / width × pageWidthPt / 72 × 25.4.
  • Any field the server cannot establish is omitted from the response. Absence means “not measured” — the url is still valid and is always returned.
When the page is not rendered yet, it is queued for the render worker once, however often it is asked for:
  • With async=1, the answer is 202 { "page": 1, "status": "rendering", "retryAfterMs": 2000 }. Ask again after retryAfterMs.
  • With async=1, a page whose render failed after its retries answers 200 { "page": 1, "status": "failed" } until retry=1 queues it again.
  • Without async=1, the request waits up to 20 seconds for the worker and then returns the image. If the render is still not ready, it returns 503 with a Retry-After: 2 header and { "message": "This page is still being prepared. Please try again in a moment.", "retryAfterSeconds": 2 }. A failed page is queued again.
Status codes The takeoff canvas prefers width and height for its aspect ratio, then the PDF point box, then the estimator-selected paper size. It uses the point box for physical sheet dimensions; when that pair is absent, measurements fall back to the selected paper size and the UI labels the sheet as assumed.

GET /:id/takeoff-pages

Returns the current saved takeoff page selection for a document. Response
  • pages — the saved selection, sorted ascending by page number. Each entry has a page number and an optional scale string.
  • scaleSource: only on a scaled page: manual (set by the estimator), filled (given by the same-size fill when the estimator set another page), or auto (set by the render worker from the page’s one clear scale). A scaled page with no source is manual.
  • scaleAuto: only with scaleSource: "auto": detectedFrom (text or vision) and the detection’s detectedAt.
  • version — monotonically increasing integer, bumped whenever the set changes. Echo this value in PUT /:id/takeoff-pages to prevent stale overwrites.
Status codes

POST /:id/takeoff-pages/add

Adds one or more pages to the saved takeoff selection. This is an add-only, idempotent merge: adding a page that is already in the selection is a no-op and never overwrites an existing page’s scale (first write wins). The version is bumped only when the set actually changes. Request body
  • pages — required, 1–500 entries. Each entry is a page number (1–10,000) or an object with page and an optional scale string (up to 40 characters).
  • A bare page number is accepted and normalised to { page }.
  • All pages must be within the document’s known page count; requests that exceed it return 422.
Response
Behaviour
  • The merge and version bump run inside a single transaction with a row lock, so concurrent adds and replaces cannot race each other.
  • Adding a page that is already selected is a no-op; the existing entry’s scale is preserved.
  • Replaying an identical add does not bump the version.
  • For a renderable document with at least one selected page, full-resolution renders are enqueued after each call, including replays, so a failed warm-up from a previous call can be retried.
  • In-flight sync rows are invisible to this endpoint. An ID that names a sync-provisioning row returns 404, the same as a document that does not exist.
Status codes

PUT /:id/takeoff-pages

Replaces the entire saved takeoff selection. Requires the caller to echo the version it last read. A stale version is rejected with the current selection so the caller can reapply the user’s changes over the fresh set. Request body
  • pages — required, 0–500 entries. Accepts the same shapes as POST /:id/takeoff-pages/add. An empty array clears the selection.
  • version — required. Must match the version returned by the most recent GET /:id/takeoff-pages or write response. A mismatch returns 409 with the current { pages, version } under a current key.
Response
Behaviour
  • The version check and the write share one transaction under a row lock, so a concurrent add cannot slip between the check and the write.
  • A replace that resolves to the identical set writes nothing and does not bump the version.
  • A page sent back with the scale it already had keeps its scaleSource. A page whose scale changed, or that newly has one, becomes manual.
  • For a renderable document with at least one saved page, full-resolution renders are enqueued after every call, including no-op saves, so a previously failed warm-up is retried. Clearing the selection enqueues nothing.
  • In-flight sync rows are invisible to this endpoint. An ID that names a sync-provisioning row returns 404, the same as a document that does not exist.
Status codes

PUT /:id/takeoff-pages/:page/scale

Sets the drawing scale on one page, then gives the same scale to every other page of the document that has no scale yet and is the same sheet size. The set page is added to the selection if it is not already there. Request body
  • scale: required. The ratio as written, up to 40 characters.
  • fillUnscaledPages: optional, defaults to true. false writes only the named page.
Response
  • pages and version: the saved selection after the write. The version is bumped only when the set changed.
  • filledPages: the other pages that gained the scale. Never includes the set page.
  • skippedPages: unscaled pages left alone: differentSize (a different sheet size), unknownSize (a size that could not be read, or every page when the set page’s own size is unknown), and notToScale (the page’s cached scale detection says it is not to scale).
  • pageSize: the set page’s size in millimetres and ISO sheet name (sheet is null for a non-ISO size), or null when unknown.
Behaviour
  • The set page always takes the new scale and is recorded as manual. Filled pages are recorded as filled.
  • Two pages are the same sheet size when each edge matches within 2.5%, either way up, using the PDF’s page boxes. A page that already has a scale is never changed, whatever its source.
  • The write runs in one transaction under the row lock. There is no version echo: the fill only writes pages that are empty at the locked read.
  • Only the set page’s full-resolution render is warmed. Filled pages render when they are opened.
Status codes

DELETE /:id/takeoff-pages/:page/scale

Clears one page’s scale and its source, whatever set it. This is how an estimator undoes a scale the render worker set automatically. The page stays in the selection, unscaled. Response
The version is bumped only when a scale was cleared. Clearing a page with no scale is a no-op that still returns 200. Status codes

POST /:id/takeoff-pages/:page/scale-detection

Reads the scale and sheet size the page states, and what they mean on this page. The result is a suggestion: nothing is saved. The client saves a confirmed scale through PUT /:id/takeoff-pages/:page/scale, like a scale set by hand. Request body (optional)
  • refresh: optional. true reads past a cached answer (including a cached “nothing found”) and queues a fresh detection. The Find scale on drawing button sends it. Poll without refresh, so polling never restarts the job.
Behaviour
  • A cached answer is returned straight away with 200.
  • Otherwise the page is queued for the render worker. With async=1, the answer is 202 { "page": 3, "status": "pending", "retryAfterMs": 2000 }. Without it, the answer is 200 with status: "unavailable".
  • A failed detection answers status: "unavailable" once and is not cached, so the next request asks again. The estimator can always set the scale by hand.
Response (a detection)
  • status: found (one scale that can be applied as is), choose (more than one scale, or a sheet size mismatch), none (nothing states a scale), or unavailable (detection could not run; candidates is empty).
  • candidates: each scale statement found: the ratio as written (statedScale), the sheet size named with it (statedSheetSize), how that size compares with the page (check: matches, converted, mismatch or not-stated), the ratio to save (appliedScale, null when the estimator must choose), and where the text sits as page fractions (scaleEvidence.box). fromViewTitle is true for a scale read from a view title such as “SECTION A 1:20”.
  • preselected: the index of the candidate to start on (the title-block scale), or null when nothing was found.
  • notToScale: true when the drawing says it is not to scale.
  • source: text or vision, or null when nothing was found. visionChecked says whether a vision read was attempted.
Status codes

Suggestion endpoints

Cloud drive sync endpoints

These endpoints manage drive identities, drive sources and project mappings for cloud-drive document sync. Identity, source and connection mutations require an admin JWT. The onboarding-config, check-access and folder-browse routes are also admin-only. Other reads and sync-now are not admin-gated. While the drive-sync kill switch is closed, every identity, source and connection mutation, sync-now, check-access and folder browse returns 409 drive_sync_disabled. Read-only list and detail routes are unaffected. See Cloud drive sync.

Drive identity endpoints

See Per-organisation drive identities for the lifecycle and outcome codes.

Drive source endpoints

Sync connection endpoints

Drive source states

Synced file states

Synced files (returned in GET /sync-connections/:id) can be: Synced drive files are stored as linked documents with no bytes. These endpoints open and fetch them, manage drive versions, and record which entities use which documents. Every call runs under the caller’s organisation and project membership, except the service-token usage PUT. Wire types live in @stralos-co-nz/domain; see Cloud-drive document sync. While the drive-sync kill switch is closed, drive-version, prefetch and processing recommendations return 409 drive_sync_disabled. /open still serves held copies. The usage registry routes are never gated.

POST /:id/open

Request body (all optional)
  • version: "auto" (default), "keep", or "latest". keep and latest act for the entity the open came from.
  • entityType and entityId: the entity the open came from. Supply both or neither.
Response, discriminated on state: A user can create up to 30 fetch jobs per 10 minutes; joining a live job does not count. Beyond that, the endpoint returns 429 too_many_fetches.

GET /drive-fetches/:jobId

Returns { job, download? }. The job carries status (queued, fetching, storing, scanning, ready, or failed), step (1 to 4 for the progress display), kind, origin, bytesDone and bytesTotal, failureReason, processingOutcome, resultDocumentId (the row the bytes land in), and versionLabel once ready. Failure reasons:

POST /:id/drive-version

Conflicts return 409 with version_changed (includes the drive’s latest version), no_newer_version, entity_frozen, or document_superseded (includes currentDocumentId).

POST /prefetch

Queues fetch jobs for the linked documents an entity uses, for files up to 250 MB, and returns 202 { queued, joined, skipped }. Each call queues at most 25 jobs, and the organisation can queue at most 100 per hour; documents beyond either limit are listed in skipped with reason prefetch_limit.
  • A repeat call for the same entity within 30 minutes returns 202 { queued: 0, joined: 0, skipped: [] }, because the entity is already warm or warming.
  • When the organisation has already used its hourly budget, the call returns 429 prefetch_throttled with retryAfterSeconds. This is the only case that returns 429.

GET /usages/:entityType/:entityId/changes

Returns { entity, sources, counts }. entity is null when nothing is registered. Each source carries the document’s driveLink, the version delta, any newer Stralos version, whether it was removed from the drive, the entity’s acknowledgements, and three flags: promptWorthy, flagItems, and possibleVariation. counts totals changed, removed, promptWorthy, flagged, and possibleVariations.

PUT /usages/:module/:entityType/:entityId

Replaces an entity’s full document usage set. It takes the same body as the DocumentUsagesChanged v1 bus event, for modules without a bus publisher. A document absent from usages is no longer used by the entity.
  • frozen: required. true when the entity is locked, for example a submitted estimate. Changes then produce possible-variation flags rather than prompts.
  • usageVersion: required integer, 0 or more, rising per entity. The set is applied only when this is higher than the version held.
  • label: optional string or null. Surrounding whitespace is trimmed, a blank value is stored as null, and a value longer than 200 characters is cut to 200. It is never a reason to reject the set.
  • projectId: optional UUID or null.
  • usages: required array of up to 5,000 entries. purpose is 2 to 40 characters of lower-case letters, digits and underscores, starting with a letter. count is 1 to 1,000,000.
Returns { "applied": true }, or { "applied": false } when the version was not higher. A caller whose token is not service:<module> for the path’s module receives 403.

POST /projects/:projectId/processing-recommendations

Asks the AI which of the project’s documents are worth processing. The AI sees file names and folder paths only, never file contents. Nothing is routed or processed; the user chooses and routes documents separately. There is no request body. The endpoint considers up to 500 of the project’s linked and stored documents that are not superseded or deleted.
  • folderPath: the document’s relatedFolder, or null.
  • suggestedAreas: de-duplicated area keys: takeoff, work_items, project_rates, programme_tender, or subcontracts.
An empty array means there were no eligible documents or the AI suggested none. Each user can call the endpoint five times per hour; beyond that it returns 429 too_many_recommendations with Retry-After: 3600.

Schedule export endpoint

POST /:id/schedule-export

Takes an imported .xlsx or .xlsm workbook already stored as a project document, locates its schedule columns using an AI completion, writes the authoritative rates and amounts from the estimate, and returns the completed file as a download. Request body
  • estimateId — required. The estimate the workbook belongs to. The document identified by :id must belong to the same estimate.
  • items — required array, 1–2,000 entries. Each item is a priced schedule line.
    • itemNumber — the item’s reference number as it appears in the schedule, or null when there is no item number column.
    • description — the item description.
    • quantity — the item quantity.
    • unit — the unit of measurement.
    • rate — the unit rate to write into the workbook, or null to leave the matched row’s rate cell unchanged.
    • amount — the line amount to write. Required.
  • tags — required array, up to 5,000 entries; an empty array is valid. Each entry becomes a row on the TAGs - departures sheet appended to the workbook.
    • listKey — the clause list this tag belongs to (for example, "departures" or "assumptions").
    • code — the clause reference code.
    • text — the clause text.
    • linkedItemNumbers — schedule item numbers this clause relates to.
Behaviour
  • The document must be a .xlsx or .xlsm file with a clean scan verdict and no syncAttempt flag.
  • The AI surveys up to the first 10 sheets and first 100 rows of each to identify where each schedule table’s header row sits and which columns hold item number, description, rate, and amount. The AI does not calculate, change, or invent any value — it only identifies column positions.
  • A column mapping with confidence below 0.75 is rejected as ambiguous.
  • Rates and amounts are written only into rows that match an item exactly (by normalised item number or normalised description). A match that is duplicated in the workbook returns 422. An item with no matching row returns 422.
  • A TAGs - departures sheet is always replaced or appended with four columns: Type, Reference, Text, and Linked schedule items. An empty tags array produces the header row with no tag entries.
  • The response is the completed Excel file. The Content-Disposition header sets a filename of the form <original stem> - Priced.<ext>.
  • If the completed workbook exceeds 4 MB, the request returns 422.
Status codes

Variation assessment endpoints

These service-to-service endpoints run an AI-assisted contract-variation assessment against selected contract documents. They are called by the Estimate service, not directly from the browser.

Tender scope draft endpoint

This endpoint drafts a project scope with AI from the estimate’s items and bounded excerpts of the project’s processed documents. The browser calls it from the estimate’s Tender download tab with the user’s token. The draft is returned for review and is never saved here; the estimator saves a reviewed scope through PUT /api/global/projects/:id.

POST /projects/:projectId/tender-scope-draft

Request body
  • projectName: required, 1 to 1,000 characters after trimming.
  • items: required, 1 to 10,000 entries, each with description (up to 4,000 characters), section (up to 1,000), quantity (a finite number or null), and unit (up to 100). No other fields are accepted.
The serialised body must be at most 200,000 bytes; a larger one is 400 with “This estimate is too large to draft a scope. Reduce the selected estimate items.” The raw request limit is 250,000 bytes. Document excerpts. The service reads up to 20 of the project’s current documents with extracted text, ordered by file name, leaving out deleted, superseded, infected, and rejected files. It takes up to 12,000 characters from each and 60,000 in total. Response
  • scope: a rich-text document of level 2 heading and paragraph nodes, in the same format as the project scope. The model is asked for at most 8 sections and 2,000 words.
  • documentsUsed: the file names whose text was included.
  • documentsArePartial: always true, because excerpts are bounded. Review the draft against the original documents.
Limits. Each signed-in user can create ten drafts an hour per organisation; the eleventh is 429 with Retry-After: 3600 and “You have used your ten AI scope drafts for this hour. Please try again later.” The route also allows at most five requests a minute. Status codes

Tender check endpoints

These service-to-service endpoints run an AI-assisted tender check of an estimate against selected project documents. The Estimate service is the only caller and forwards the user’s token; the job’s organisation comes from that token. No model work runs on the request path: jobs run on the tender check worker. A review request carries estimateId, projectId, runId, up to 60 unique documentIds, usedDocuments (up to 500 { documentId, purposes } entries for the documents the estimate already uses), a snapshot of at most 200,000 characters as JSON, up to 200 signals, and contractForm. It can also carry up to 20 importFiles: Rates Library import files sent inline as parsed text, at most 300,000 characters in total. Citations from an import file carry kind: "rate_import". The estimate must belong to the named project: a missing estimate returns 404 and an estimate from another project returns 422. The service allows at most 55 review jobs per organisation in a rolling 24 hours and returns 429 beyond that. See Tender check for the job lifecycle, snapshot fields and review steps.

Document response fields

The document list and document detail responses include:
  • projectId — UUID of the owning project, or null for company documents.
  • partyId — UUID of the owning company, or null for project documents.
  • title — display name set by a user, or null. Company document lists show this field; the UI falls back to fileName when absent.
  • docKind — one of prequal, contract_agreement, day_rates, or other. Always other for project documents.
  • issueDate — calendar date (YYYY-MM-DD), or null.
  • reviewDate — calendar date (YYYY-MM-DD), or null.
  • supersededAt — ISO 8601 timestamp at which a pinned document was replaced by a newer upload, or null when the document is current. Only meaningful for contract_agreement and day_rates kinds.
  • relatedFolder — the document’s folder assignment as a canonical breadcrumb string (e.g. "Tender / Drawings / Issued"), or null when no folder is assigned. Use PATCH /:id to set or clear this field.
  • takeoffReviewStatus — "approved", "rejected", or null (no decision yet).
  • takeoffReviewedAt — ISO 8601 timestamp of the last decision, or null.
  • deletedAt — ISO 8601 timestamp at which the document was soft-deleted, or null when the document is live. Only present on GET /:id responses; document lists never return deleted documents.
  • storeOnly: "store_only_type" (emails and MS Project plans), "too_large_to_process" (PNG or JPEG images over 10 MB), or null when the document can be processed. Store-only documents can be downloaded but never processed.
  • driveLink: for a drive-synced document, what the drive holds compared with what Stralos holds; null for any other document. See Drive link object.
The takeoff panel treats pages as drawable only when takeoffReviewStatus is "approved".

Document kinds

Pinned kinds enforce a single current file per company. When a new contract_agreement or day_rates file is uploaded to the same company, the previous file’s supersededAt is stamped in the same transaction that marks the new file stored. An abandoned upload (one that never reaches stored) leaves the previous file current. The includeSuperseded=true query parameter on GET /api/documents?partyId= returns both current and superseded rows for a company. By default only the current row for each pinned kind is returned.

Status model

Documents move through:
  • linked
  • uploading
  • scanning
  • parsing
  • summarising
  • processing_areas
  • stored
  • processed
  • failed
  • infected
  • rejected
linked and stored are resting states. A linked document is a drive file Stralos has not fetched: it has a reserved storage key but no content, so it cannot be downloaded or processed until a fetch job copies and scans it. POST /:id/open on a linked document creates or joins a fetch job and returns 202. Delete is blocked while a document is scanning, parsing, summarising, or processing_areas. Documents in any other status, including linked, uploading, stored, processed, failed, infected, and rejected, can be deleted. Deletion is a soft-delete: the row is stamped with deletedAt and hidden from lists, but the document remains accessible by ID and all stored data is preserved.

Areas

Supported suggestion areas are:
  • takeoff
  • work_items
  • project_rates
  • programme_tender
  • subcontracts

Billing access

When billing enforcement is enabled for the environment, the Documents API checks the organisation’s subscription access before handling a user request. Platform service tokens bypass the check. A 402 body carries code, message, and readOnlyEndsAt when a read-only period has an end date.

Download gate

Downloads fail closed. A document must have a clean database scan verdict, and GuardDuty mode also checks the S3 object tag before returning a presigned download URL.

Provider modes

The service supports provider modes for AWS and local development:
  • AI: the central api-assistant structured-completion gateway (Gemini), or fixture
  • OCR: textract, local, or fixture
  • Scan: guardduty or stub
  • DWG/DXF conversion: aps (Autodesk Platform Services) or fixture (deterministic, no external calls)
  • Drive sync: live or fixture
The state machine, API behaviour, queues, prompts, and schemas should stay aligned across provider modes.