CONSTRUX
Product

The API is the product

The console talks to exactly the same public API as any other consumer. It holds no privileged path into the platform and is subject to the same roles, scopes and attribute rules.

Shape

Command-and-query over project resources. GET /v1/routes lists every endpoint — 1220 of them, 51 reachable without a credential — and that list is the documentation rather than a copy of it.

POST  /v1/projects/:projectId/lessons
      Capture a lesson against the project that produced it
POST  /v1/projects/:projectId/bid-responses
      Plan a numbered response pack from a compliance matrix: one section per deliverable needing prose
POST  /v1/projects/:projectId/bid-responses/:packId/sections
      Write the next section that has none. Called until nothing remains, so no single call carries a whole submission; a pass that dies leaves its section unwritten and the next call writes exactly that one
POST  /v1/projects/:projectId/bid-responses/:packId/issue
      Issue the pack. Refused unless every deliverable has a response, every stated deadline is dated and the assurance review clears it — a submission missing a mandatory response is rejected, not marked down
POST  /v1/projects/:projectId/bid-responses/:packId/challenge
      Attack the pack as the employer’s evaluator would, and score what a scorer could actually award
POST  /v1/projects/:projectId/assurance/:reviewId/findings/:findingId/disposition
      Record what was decided about a finding. A high finding blocks the submission until this exists; a critical one cannot be disposed of at all
POST  /v1/projects/:projectId/quality/plans
      Create an inspection and test plan for a work package
POST  /v1/projects/:projectId/quality/plans/:planId/approve
      Approve an inspection and test plan — the other side agreeing the criteria, which its author cannot do

Errors

RFC 7807 application/problem+json, with per-field detail where a schema rejected the body. Every response carries x-correlation-id and x-trace-id, and a command accepts an Idempotency-Key so a retried request returns the original result rather than performing the state change twice.

{
  "type": "https://construxvg.com/problems/access-denied",
  "title": "ACCESS_DENIED",
  "status": 403,
  "detail": "No role of PM holds \"C\" on PROJECT_SETUP",
  "traceId": "…",
  "correlationId": "…"
}

Authentication

Bearer access tokens with a short lifetime and rotating refresh tokens, behind an MFA challenge. A public route may begin an authentication and never complete one — an invariant with a test behind it, because a demonstration endpoint once returned a working session to anonymous callers.

A person may enrol a passkey or an authenticator app. Sign-ins are bound to the devices they come from, and a request whose shape is unusual for that person is challenged again rather than refused outright. Repeated failures lock the identity, and the count is held centrally so a lockout cannot be escaped by reaching a different replica. Tokens are signed with a key derived per purpose and carrying an identifier, so one can be rotated with the previous still verifying and nothing signed for one purpose is accepted for another.

Integrating without a person

A machine gets its own credential rather than borrowing somebody's: API keys with their own scopes, separate sandbox and live keys, and webhooks for what you would otherwise poll for. There is a change feed with idempotency keys on it, and a batch contract the field applications are built against — the same one, not a private endpoint, because the console holds no privileged path and neither do they.

What arrives, and what it is checked for

Every uploaded file is streamed to a signature scanner before anything reads it, and a file that could not be scanned is recorded as unscanned rather than assumed clean. Evidence is encrypted at rest where a master key is configured, and the readiness probe treats a missing one as a critical finding rather than a default — a stolen volume should not be a readable archive of somebody's site photographs and signed instructions.

Communication events

187 notification events across 15 categories. Two channels carry them today — email and in-console — and three more are catalogued with no carrier configured: a notice routed to SMS, push or WhatsApp is recorded as dispatched and not transmitted rather than quietly dropped, and the delivery record says so. 31 of the events are mandatory and override a recipient's preferences, because a person is entitled to be told their account was locked whatever they have muted.

Files in, records out

A file is stored behind the hash the record already names, then read: its type from its bytes, a PDF's text and tables, an IFC's structure and geometry fingerprints. What was read is on the ingestion record and can be put to use in one call — a bill's table onto a measurement schedule, a specification's text into the clause register — each item sourced to the document, the page and the table it came from. A scan is reported as needing a model that can see; nothing is invented for it.

Read replicas

A process started in follower mode holds the whole record from Postgres and answers every read with a token the primary issued. It refuses every command with 503 LEDGER_FOLLOWER so a client knows to send commands to the primary rather than retry; sign-in is a command, since it records the device it came from.

What the API will not do

It will not tell you which project ids exist. An id that does not exist and one belonging to another tenancy both answer 200 with an empty list on the generic entity read — a 404 for unknown and a 403 for forbidden would together be an enumeration oracle. That behaviour is tested, so nobody "fixes" it.

Read the route table

Every endpoint, its method, its description and whether it needs a credential.