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 doErrors
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.