Flow
ServicesConstruction Service

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:

RouteWhat it does
POST /v1/development-projectsCreate a project of one kind (master_plan, ground_up, conversion, capital_improvement) with its seven stages. It starts in initiation
GET /v1/development-projectsThe 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}/statusPut a project on hold, back to active, or cancel it
GET /v1/development-projects/{projectId}/overviewEverything the overview tab draws, in one call
GET /v1/development-projects/{projectId}/planEverything the timeline draws: stages, tasks, and milestones
GET /v1/development-projects/{projectId}/activityRecent activity on the project
POST /v1/development-projects/{projectId}/advanceMove to the next stage, if its gate allows it. Leaving award takes the award
POST /v1/development-projects/{projectId}/completeComplete a project that is in closeout

The plan:

RouteWhat it does
GET /v1/development-projects/{projectId}/stagesThe seven stages with their dates and status
PUT /v1/development-projects/{projectId}/stages/{stage}/planSet a stage's planned and forecast dates. Actual dates come from advancing
POST, GET /v1/development-projects/{projectId}/packagesAdd a package, fixing how it is measured; list them with their progress
POST /v1/development-projects/{projectId}/packages/{packageId}/reportsFile a reported percentage with a date, for a package measured by report
POST, GET /v1/development-projects/{projectId}/milestonesAdd a milestone, optionally driven by another project; list them
POST /v1/milestones/{id}/complete, PUT /v1/milestones/{id}/forecastGive a milestone an actual date; move its forecast
POST, GET /v1/development-projects/{projectId}/updatesPost the weekly update; list them
POST, GET /v1/development-projects/{projectId}/relationsRelate a project to another; list its relations
GET /v1/development-projects/{projectId}/baselinesEvery 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:

RouteWhat it does
POST /v1/tasks, GET /v1/tasksCreate 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}/progressMove 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}/dependenciesMake 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}/notesAdd 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:

RouteWhat it does
POST /v1/documents, GET /v1/documentsRegister 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}/revisionsStart an internal revision, lettered from the current published one, optionally straight into review
POST /v1/document-revisions/{id}/actionsTake the next step: prepared, reviewed, submitted_for_approval, approved, or rejected
POST /v1/document-revisions/{id}/publishPublish 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:

RouteWhat 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}/priceSet its cost change and days
POST /v1/variations/{id}/submitSend it for approval, naming the reviewers
POST /v1/variations/{id}/incorporateIncorporate an approved variation, writing a new baseline
GET /v1/development-projects/{projectId}/budgetThe budget lines with committed and actual totals
PUT /v1/development-projects/{projectId}/budget/linesAdd or change a budget line for a category
POST, GET /v1/development-projects/{projectId}/budget/costsRecord money committed or spent; list the cost events
GET /v1/reports/portfolioThe 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:

RouteWhat it does
POST /v1/decisions, GET /v1/decisionsOpen a question, list them
GET /v1/decisions/{id}One decision, with its alternatives and participants
POST /v1/decisions/{id}/outcomeRecord what was decided and what it cost
POST /v1/approvals, GET /v1/approvalsAsk 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}/responseAnswer as yourself
POST /v1/risks, GET /v1/risksRaise a risk, list them
GET /v1/risks/{id}One risk
POST /v1/risks/{id}/statusOpen, 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:

RouteWhat it does
POST /v1/vendors/{id}/assignments, /assignments/{assignment_id}/endPut a firm on a project with its role and Flow contact, take it off
GET /v1/vendors/{id}/assignmentsEvery project a firm has been on, open ones first
POST, GET /v1/vendors/{id}/fees, /fees/{fee_id}/invoicesAgree a design fee for a project, record an invoice against it, and list both
GET /v1/development-projects/{projectId}/vendorsThe firms on a project
GET /v1/development-projects/{projectId}/design-feesAgreed, invoiced and remaining per firm
POST /v1/attachments/upload-urlStep one: a link to upload a file to a record
POST /v1/attachmentsStep two: record the uploaded file against the record
GET /v1/attachments, GET /v1/attachments/{id}/urlList 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.

StatusCodeWhat happened
409no_approved_gateThe stage being left has a gate and no approved stage_gate approval for it
409stage_movedfrom_stage no longer matches; the screen is stale
409last_stageThe project is in closeout; complete it instead
409not_in_closeoutA project is completed only from closeout
409project_closedThe project is completed or cancelled and takes no further work
409unfinished_prerequisiteA task cannot be done while a task it depends on is unfinished
409documents_not_currentA task cannot be done while a document it requires has no published revision
409dependency_cycle, dependency_existsThe dependency would make a cycle, or already exists
409primary_linkA task's primary link cannot be removed or added twice
409invalid_transitionThe revision is not in a state that takes that step
409revision_not_approvedOnly an approved internal revision can be published
409revision_out_of_dateAnother revision was published after this one was started; start a new one from the current revision
409revision_lockedFiles go on a revision only while it is a draft or in internal review
409no_baselineA variation needs the award baseline, so the project has to have passed award
409variation_not_open, variation_not_pending, variation_not_approvedThe variation is not in the state the command needs
409already_decidedThe decision or approval has already been settled
409already_answeredThis reviewer has already answered
409already_completeThe milestone already has an actual date
409same_statusThe risk already holds that status
409code_takenAnother project, or another document, already has that code
409vendor_not_on_project, vendor_inactiveA task or award named a firm that is not on the project or not active
503attachments_unavailableCONSTRUCTION_DOCUMENTS_URL is not set
502documents_unavailableThe documents service did not answer
502vendors_unavailableThe 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.

On this page