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
POST /api/documentsvalidates file metadata and creates document rows.- The response returns presigned upload instructions for accepted files.
- The browser uploads directly to S3 or LocalStack.
- Object-created events trigger malware scanning.
- Estimate-scoped uploads continue through parsing, summarisation, classification,
and area modules. Project-scoped and company-scoped on-demand uploads stop at
storedimmediately after a clean scan, before parsing, until the user routes and processes them. - 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 fromSERVICE_VERSION, falling back topackage.jsononly when that variable is absent.commit— the Git commit SHA stamped into the Lambda asSERVICE_COMMITat deploy time. Returns"unknown"whenSERVICE_COMMITis not set.
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, ornullto clear it. Each segment must contain at least one non-whitespace character (Tender//Issuedis 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, ornullto clear it. Used on company document lists when set; the UI falls back tofileNamewhen absent.issueDate— optional. Calendar date inYYYY-MM-DDformat, ornullto clear it.reviewDate— optional. Calendar date inYYYY-MM-DDformat, ornullto clear it.
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
excludedin the same transaction, so the file is never re-imported.
- The document row is still accessible via
GET /:id. The response includesdeletedAt. - The upload file, all rendered pages, conversion output, extracted text, area runs, and suggestions remain intact.
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 aPOST /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, ornullto unroute.process— optional boolean. Whentrue, 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 whenprocessis omitted.
- The estimate must belong to the document’s project. A mismatch returns
422. Company documents (owned bypartyId) have no project and always return422from 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, orfailed); 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
200response reportsprocessing: { started: false, reason: "not_stored" }. - A
linkeddrive document routed with processing requested is fetched first: the response reportsprocessing: { started: false, reason: "fetching", jobId }, and the fetch job starts processing once the copy scans clean. A store-only document routes normally and reportsreason: "store_only". - The estimate-service reference validation is server-to-server. If the upstream service is unreachable, the endpoint returns
502.
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 returns400. Omitting the field processes all applicable areas.
- A first processing start requires
stored, a clean scan verdict, a non-nullestimateId, 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, orprocessed) with a clean scan is a benign200no-op withreason: "already_processing". - A DWG or DXF drawing whose PDF derivative has not yet been produced returns
200withstarted: false, reason: "conversion_pending". The document will become processable once conversion completes. The client should poll and retry. - A routed
linkeddrive document is fetched first: the endpoint creates or joins a fetch job and returns202with{ started: false, reason: "fetching", job }. Processing starts once the copy scans clean. - Store-only documents return
409with astore_only:message. A linked file that can be viewed but not processed returns409 not_processable, and one that opens only in the drive returns409 drive_only. - Unrouted, infected, superseded, failed, rejected, or otherwise genuinely unprocessable documents
return
409. This includes DWG or DXF documents whose conversion has failed.
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".
- The document must have
status: "processed". A document that is still in the pipeline returns422 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 = processedprecondition 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.
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 cachedpage-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, or0while the parse has not counted the pages yet.thumbnails: one entry per page in the range.statusisready(withurl),pending(not rendered yet), orfailed(the page’s render failed after its retries).
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 throughthumbnailPageCount).pageCount— the document’s known total page count, or the rendered count when the total is not yet available.complete—truewhen every page is rendered.falsewhile 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 emptyitemsarray) 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.
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 withwidth/heightto 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
urlis still valid and is always returned.
- With
async=1, the answer is202 { "page": 1, "status": "rendering", "retryAfterMs": 2000 }. Ask again afterretryAfterMs. - With
async=1, a page whose render failed after its retries answers200 { "page": 1, "status": "failed" }untilretry=1queues 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 returns503with aRetry-After: 2header and{ "message": "This page is still being prepared. Please try again in a moment.", "retryAfterSeconds": 2 }. A failed page is queued again.
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 apagenumber and an optionalscalestring.scaleSource: only on a scaled page:manual(set by the estimator),filled(given by the same-size fill when the estimator set another page), orauto(set by the render worker from the page’s one clear scale). A scaled page with no source ismanual.scaleAuto: only withscaleSource: "auto":detectedFrom(textorvision) and the detection’sdetectedAt.version— monotonically increasing integer, bumped whenever the set changes. Echo this value inPUT /:id/takeoff-pagesto prevent stale overwrites.
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 withpageand an optionalscalestring (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.
- 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.
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 asPOST /:id/takeoff-pages/add. An empty array clears the selection.version— required. Must match the version returned by the most recentGET /:id/takeoff-pagesor write response. A mismatch returns409with the current{ pages, version }under acurrentkey.
- 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, becomesmanual. - 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.
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 totrue.falsewrites only the named page.
pagesandversion: 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), andnotToScale(the page’s cached scale detection says it is not to scale).pageSize: the set page’s size in millimetres and ISO sheet name (sheetisnullfor a non-ISO size), ornullwhen unknown.
- The set page always takes the new scale and is recorded as
manual. Filled pages are recorded asfilled. - 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.
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
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.truereads past a cached answer (including a cached “nothing found”) and queues a fresh detection. The Find scale on drawing button sends it. Poll withoutrefresh, so polling never restarts the job.
- A cached answer is returned straight away with
200. - Otherwise the page is queued for the render worker. With
async=1, the answer is202 { "page": 3, "status": "pending", "retryAfterMs": 2000 }. Without it, the answer is200withstatus: "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.
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), orunavailable(detection could not run;candidatesis 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,mismatchornot-stated), the ratio to save (appliedScale,nullwhen the estimator must choose), and where the text sits as page fractions (scaleEvidence.box).fromViewTitleistruefor 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), ornullwhen nothing was found.notToScale:truewhen the drawing says it is not to scale.source:textorvision, ornullwhen nothing was found.visionCheckedsays whether a vision read was attempted.
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 andsync-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 inGET /sync-connections/:id) can be:
Link-first drive endpoints
Synced drive files are stored aslinked 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".keepandlatestact for the entity the open came from.entityTypeandentityId: the entity the open came from. Supply both or neither.
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_throttledwithretryAfterSeconds. This is the only case that returns429.
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.truewhen 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 ornull. Surrounding whitespace is trimmed, a blank value is stored asnull, and a value longer than 200 characters is cut to 200. It is never a reason to reject the set.projectId: optional UUID ornull.usages: required array of up to 5,000 entries.purposeis 2 to 40 characters of lower-case letters, digits and underscores, starting with a letter.countis 1 to 1,000,000.
{ "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’srelatedFolder, ornull.suggestedAreas: de-duplicated area keys:takeoff,work_items,project_rates,programme_tender, orsubcontracts.
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:idmust 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, ornullwhen 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, ornullto 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.
- The document must be a
.xlsxor.xlsmfile with a clean scan verdict and nosyncAttemptflag. - 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 returns422. - A TAGs - departures sheet is always replaced or appended with four columns: Type, Reference, Text, and Linked schedule items. An empty
tagsarray produces the header row with no tag entries. - The response is the completed Excel file. The
Content-Dispositionheader sets a filename of the form<original stem> - Priced.<ext>. - If the completed workbook exceeds 4 MB, the request returns
422.
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 throughPUT /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 withdescription(up to 4,000 characters),section(up to 1,000),quantity(a finite number ornull), andunit(up to 100). No other fields are accepted.
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 2headingandparagraphnodes, 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: alwaystrue, because excerpts are bounded. Review the draft against the original documents.
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, ornullfor company documents.partyId— UUID of the owning company, ornullfor project documents.title— display name set by a user, ornull. Company document lists show this field; the UI falls back tofileNamewhen absent.docKind— one ofprequal,contract_agreement,day_rates, orother. Alwaysotherfor project documents.issueDate— calendar date (YYYY-MM-DD), ornull.reviewDate— calendar date (YYYY-MM-DD), ornull.supersededAt— ISO 8601 timestamp at which a pinned document was replaced by a newer upload, ornullwhen the document is current. Only meaningful forcontract_agreementandday_rateskinds.relatedFolder— the document’s folder assignment as a canonical breadcrumb string (e.g."Tender / Drawings / Issued"), ornullwhen no folder is assigned. UsePATCH /:idto set or clear this field.takeoffReviewStatus—"approved","rejected", ornull(no decision yet).takeoffReviewedAt— ISO 8601 timestamp of the last decision, ornull.deletedAt— ISO 8601 timestamp at which the document was soft-deleted, ornullwhen the document is live. Only present onGET /:idresponses; 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), ornullwhen 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;nullfor any other document. See Drive link object.
takeoffReviewStatus is "approved".
Drive link object
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:linkeduploadingscanningparsingsummarisingprocessing_areasstoredprocessedfailedinfectedrejected
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:takeoffwork_itemsproject_ratesprogramme_tendersubcontracts
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-assistantstructured-completion gateway (Gemini), orfixture - OCR:
textract,local, orfixture - Scan:
guarddutyorstub - DWG/DXF conversion:
aps(Autodesk Platform Services) orfixture(deterministic, no external calls) - Drive sync:
liveorfixture

