API
The construction routes, the command envelope they take, and the refusals worth designing a client around.
The contract is services/construction/openapi.yaml, generated from the Go types. Everything below describes it; it does not restate it.
Reads use the platform {header, payload} envelope with page pagination under
payload.meta. Writes take {"data": {...}, "why": {...}} and require an
Idempotency-Key: A caller-supplied header, stable across retries, that lets command.Execute claim the operation once and replay the same response on duplicate requests. header. Dates in request and response bodies are
YYYY-MM-DD strings; money is integer minor units.
Routes
A development project:
| Route | What it does |
|---|---|
POST /v1/development-projects | Create a project of one kind (master_plan, ground_up, conversion, capital_improvement) with its seven stages. It starts in initiation |
GET /v1/development-projects | The portfolio, filtered by status, kind, stage, phase, owner_fid, region, health, freshness, and search |
GET /v1/development-projects/{id}, PUT /v1/development-projects/{id} | One project with its health and progress; change its details |
POST /v1/development-projects/{id}/status | Put a project on hold, back to active, or cancel it |
GET /v1/development-projects/{projectId}/overview | Everything the overview tab draws, in one call |
GET /v1/development-projects/{projectId}/plan | Everything the timeline draws: stages, tasks, and milestones |
GET /v1/development-projects/{projectId}/activity | Recent activity on the project |
POST /v1/development-projects/{projectId}/advance | Move to the next stage, if its gate allows it. Leaving award takes the award |
POST /v1/development-projects/{projectId}/complete | Complete a project that is in closeout |
The plan:
| Route | What it does |
|---|---|
GET /v1/development-projects/{projectId}/stages | The seven stages with their dates and status |
PUT /v1/development-projects/{projectId}/stages/{stage}/plan | Set a stage's planned and forecast dates. Actual dates come from advancing |
POST, GET /v1/development-projects/{projectId}/packages | Add a package, fixing how it is measured; list them with their progress |
POST /v1/development-projects/{projectId}/packages/{packageId}/reports | File a reported percentage with a date, for a package measured by report |
POST, GET /v1/development-projects/{projectId}/milestones | Add a milestone, optionally driven by another project; list them |
POST /v1/milestones/{id}/complete, PUT /v1/milestones/{id}/forecast | Give a milestone an actual date; move its forecast |
POST, GET /v1/development-projects/{projectId}/updates | Post the weekly update; list them |
POST, GET /v1/development-projects/{projectId}/relations | Relate a project to another; list its relations |
GET /v1/development-projects/{projectId}/baselines | Every baseline, newest first |
A relation's relation_type is one of parent_of (such as a master plan over one of its
buildings), depends_on, blocks, updates, shared_milestone, or related_to.
Tasks. A task belongs to one project first and can count toward others:
| Route | What it does |
|---|---|
POST /v1/tasks, GET /v1/tasks | Create a task with its primary project and any others; list them, filtered by project_id, owner_fid, status, vendor_id, and search |
GET /v1/tasks/{id}, PUT /v1/tasks/{id} | One task with its links, prerequisites, dependents, notes, and required documents; change its details |
POST /v1/tasks/{id}/progress | Move it, or set its percentage, forecast finish, or blocked reason |
POST /v1/tasks/{id}/links, DELETE /v1/tasks/{id}/links/{projectId} | Count it toward another project, with a share; stop |
POST /v1/tasks/{id}/dependencies | Make it wait for another task |
PUT, DELETE /v1/tasks/{id}/documents/{documentId} | Require a document for it, optionally naming the revision it works from; stop |
POST /v1/tasks/{id}/notes | Add a note |
A link's relationship_type is primary for the task's own project, and affects (the
default), shared, or deliverable_for for the others.
Document control:
| Route | What it does |
|---|---|
POST /v1/documents, GET /v1/documents | Register a document; the register, filtered by doc_type, discipline, owner_fid, project_id, published, and search |
GET /v1/documents/{id}, PUT /v1/documents/{id} | One document with its revisions, trail, links, and impacts; change its details |
POST /v1/documents/{id}/revisions | Start an internal revision, lettered from the current published one, optionally straight into review |
POST /v1/document-revisions/{id}/actions | Take the next step: prepared, reviewed, submitted_for_approval, approved, or rejected |
POST /v1/document-revisions/{id}/publish | Publish an approved internal revision as the next numbered one, recording its impact and optionally drafting a variation |
POST /v1/documents/{id}/links, DELETE /v1/documents/{id}/links/{linkId} | Link it to a project, a stage of one, a task, a milestone, or a firm; remove a link |
Variations and the budget:
| Route | What it does |
|---|---|
POST /v1/variations, GET /v1/variations, GET /v1/variations/{id} | Draft a variation; list them by project_id and status; one variation beside the current baseline |
PUT /v1/variations/{id}/price | Set its cost change and days |
POST /v1/variations/{id}/submit | Send it for approval, naming the reviewers |
POST /v1/variations/{id}/incorporate | Incorporate an approved variation, writing a new baseline |
GET /v1/development-projects/{projectId}/budget | The budget lines with committed and actual totals |
PUT /v1/development-projects/{projectId}/budget/lines | Add or change a budget line for a category |
POST, GET /v1/development-projects/{projectId}/budget/costs | Record money committed or spent; list the cost events |
GET /v1/reports/portfolio | The weekly portfolio report, filtered by status, kind, stage, owner_fid, and region |
A variation's result is recorded by its approval: when the last required reviewer answers, the variation becomes approved or rejected in the same transaction.
The three registers. Each record belongs to one project. A list route takes an optional
project_id; without it the list spans every project:
| Route | What it does |
|---|---|
POST /v1/decisions, GET /v1/decisions | Open a question, list them |
GET /v1/decisions/{id} | One decision, with its alternatives and participants |
POST /v1/decisions/{id}/outcome | Record what was decided and what it cost |
POST /v1/approvals, GET /v1/approvals | Ask for a review of a stage gate, a decision, a budget, or a variation; list them |
GET /v1/approvals/{id} | One approval, with its reviewers |
POST /v1/approvals/{id}/response | Answer as yourself |
POST /v1/risks, GET /v1/risks | Raise a risk, list them |
GET /v1/risks/{id} | One risk |
POST /v1/risks/{id}/status | Open, mitigate, close, or mark it as having happened |
Vendors and files. The firms themselves, their contacts and their insurance are in the vendors service; construction keeps what a firm does on a project:
| Route | What it does |
|---|---|
POST /v1/vendors/{id}/assignments, /assignments/{assignment_id}/end | Put a firm on a project with its role and Flow contact, take it off |
GET /v1/vendors/{id}/assignments | Every project a firm has been on, open ones first |
POST, GET /v1/vendors/{id}/fees, /fees/{fee_id}/invoices | Agree a design fee for a project, record an invoice against it, and list both |
GET /v1/development-projects/{projectId}/vendors | The firms on a project |
GET /v1/development-projects/{projectId}/design-fees | Agreed, invoiced and remaining per firm |
POST /v1/attachments/upload-url | Step one: a link to upload a file to a record |
POST /v1/attachments | Step two: record the uploaded file against the record |
GET /v1/attachments, GET /v1/attachments/{id}/url | List a record's files, get a download link |
An upload is two calls with a blob upload between them. The first returns a short-lived
link to blob storage; the client puts the file there with the header
x-ms-blob-type: BlockBlob, then calls the second with the blob name it was given. Files
can go on a task (record_type task) or on a document revision (document_revision).
A revision takes files only while it is a draft or in internal review.
The vendor portal has its own routes under /v1/vendor, described on
Vendor portal.
Only the single-record routes return nested collections. A decision's alternatives and an approval's reviewers are absent from the list responses, so a list of two hundred decisions is not a list of two hundred decisions and their options.
Who answers an approval
POST /v1/approvals/{id}/response takes a response and an optional note. It does not
take a reviewer, because the reviewer is the authenticated caller. Answering on somebody
else's behalf means signing in as them, which is the point. Reviewers are named by
email, the same lowercased email Cloudflare Access gives the caller, and the caller must
also be an editor.
A caller who is not named on the approval gets 403, not 404: the approval exists and they
may read it, they just may not answer it. A caller who has already answered gets 409
already_answered.
Refusals worth designing a client around
These are the conflicts a client has to have an answer for, rather than generic validation failures.
| Status | Code | What happened |
|---|---|---|
| 409 | no_approved_gate | The stage being left has a gate and no approved stage_gate approval for it |
| 409 | stage_moved | from_stage no longer matches; the screen is stale |
| 409 | last_stage | The project is in closeout; complete it instead |
| 409 | not_in_closeout | A project is completed only from closeout |
| 409 | project_closed | The project is completed or cancelled and takes no further work |
| 409 | unfinished_prerequisite | A task cannot be done while a task it depends on is unfinished |
| 409 | documents_not_current | A task cannot be done while a document it requires has no published revision |
| 409 | dependency_cycle, dependency_exists | The dependency would make a cycle, or already exists |
| 409 | primary_link | A task's primary link cannot be removed or added twice |
| 409 | invalid_transition | The revision is not in a state that takes that step |
| 409 | revision_not_approved | Only an approved internal revision can be published |
| 409 | revision_out_of_date | Another revision was published after this one was started; start a new one from the current revision |
| 409 | revision_locked | Files go on a revision only while it is a draft or in internal review |
| 409 | no_baseline | A variation needs the award baseline, so the project has to have passed award |
| 409 | variation_not_open, variation_not_pending, variation_not_approved | The variation is not in the state the command needs |
| 409 | already_decided | The decision or approval has already been settled |
| 409 | already_answered | This reviewer has already answered |
| 409 | already_complete | The milestone already has an actual date |
| 409 | same_status | The risk already holds that status |
| 409 | code_taken | Another project, or another document, already has that code |
| 409 | vendor_not_on_project, vendor_inactive | A task or award named a firm that is not on the project or not active |
| 503 | attachments_unavailable | CONSTRUCTION_DOCUMENTS_URL is not set |
| 502 | documents_unavailable | The documents service did not answer |
| 502 | vendors_unavailable | The vendors service did not answer, or CONSTRUCTION_VENDORS_URL is not set |
Several 422s exist for inputs that are shaped correctly but mean nothing: leaving award without the award, a reported figure filed against a package measured by tasks, a package that belongs to another project, a chosen alternative that belongs to a different decision, an approval with no required reviewer.
Actions
Every internal route names one of twenty-eight actions:
CreateProject, ReadProject, PlanProject, AdvanceStage, RecordProgress,
OpenDecision, RecordDecision, ReadDecisions, RequestApproval,
RespondToApproval, ReadApprovals, RaiseRisk, ManageRisk, ReadRisks,
ManageVendors, ReadVendors, ManageTasks, ReadTasks, ManageDocuments,
PublishDocument, ReadDocuments, ManageVariations, ReadVariations, ManageBudget,
ReadBudget, ReadReports, AttachDocument, ReadAttachments.
The eleven whose names start with Read are open to anyone Cloudflare Access admits. The
other seventeen need the caller to be in CONSTRUCTION_EDITORS, and a caller who is not
gets 403. Each route's OpenAPI description says which of the two it is. A request with no
valid Access token, or locally no dev principal, gets 401.