Files
professional_management/professional-platform-openapi_9.yaml
T
2026-08-27 21:52:21 -04:00

7965 lines
295 KiB
YAML

openapi: 3.1.0
info:
title: Professional Management Platform API
version: 1.0.0-milestone.9
summary: Engineering delivery control with phases, time capture, and approved budgets.
description: |
Executable API contract for Milestones 1 through 4 of the Professional Management Platform.
Tenant-scoped operations require `X-Organization-Id`. Cross-tenant resources are
reported as not found. Resource creation and material commands require an
`Idempotency-Key`. Mutable resources use ETags and require `If-Match`.
Error responses use RFC 9457 Problem Details extended with stable `code`,
`requestId`, and optional field-level `errors`.
contact:
name: Platform API Team
servers:
- url: https://api.example.com/api/v1
description: Production
- url: https://sandbox-api.example.com/api/v1
description: Sandbox
tags:
- name: Authentication
- name: Sessions
- name: Current User
- name: Organizations
- name: Membership Invitations
- name: Memberships
- name: Roles
- name: Permissions
- name: Engineering Clients
- name: Engineering Client Contacts
- name: Engineering Projects
- name: Engineering Project Members
- name: Engineering Tasks
- name: Engineering Sites
- name: Documents
- name: Engineering Project Documents
- name: Engineering Designs
- name: Engineering Design Assignments
- name: Engineering Design Versions
- name: Engineering Design Reviews
- name: Engineering Inspections
- name: Engineering Inspection Documents
- name: Engineering Inspection Findings
- name: Engineering Inspection Follow-ups
- name: Engineering Specifications
- name: Engineering Specification Documents
- name: Engineering Project Phases
- name: Engineering Time Entries
- name: Engineering Project Budgets
paths:
/auth/register:
post:
tags: [Authentication]
operationId: registerUser
summary: Register a user identity
description: |
Creates a global user identity. When public registration is disabled, this
operation returns `REGISTRATION_DISABLED`; invitation acceptance remains
available to authenticated identities created through the configured onboarding flow.
security: []
parameters:
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/IdempotencyKey'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/RegisterRequest'
responses:
'201':
description: User identity created; email verification may still be required.
headers:
Location:
$ref: '#/components/headers/Location'
X-Request-Id:
$ref: '#/components/headers/RequestId'
content:
application/json:
schema:
$ref: '#/components/schemas/UserResponse'
'409':
$ref: '#/components/responses/Conflict'
'422':
$ref: '#/components/responses/ValidationError'
'429':
$ref: '#/components/responses/RateLimited'
/auth/login:
post:
tags: [Authentication]
operationId: login
summary: Authenticate with email and password
security: []
parameters:
- $ref: '#/components/parameters/RequestId'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/LoginRequest'
responses:
'200':
description: Authentication succeeded.
headers:
Cache-Control:
schema:
type: string
const: no-store
X-Request-Id:
$ref: '#/components/headers/RequestId'
content:
application/json:
schema:
$ref: '#/components/schemas/TokenPairResponse'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'422':
$ref: '#/components/responses/ValidationError'
'429':
$ref: '#/components/responses/RateLimited'
/auth/logout:
post:
tags: [Authentication]
operationId: logout
summary: Revoke the current session
parameters:
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/IdempotencyKey'
responses:
'204':
description: Current session revoked.
headers:
X-Request-Id:
$ref: '#/components/headers/RequestId'
'401':
$ref: '#/components/responses/Unauthorized'
'429':
$ref: '#/components/responses/RateLimited'
/auth/token/refresh:
post:
tags: [Authentication]
operationId: refreshAccessToken
summary: Rotate a refresh token and issue a new token pair
description: Reuse of a rotated refresh token revokes its token family and session.
security: []
parameters:
- $ref: '#/components/parameters/RequestId'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/RefreshTokenRequest'
responses:
'200':
description: Token rotated.
headers:
Cache-Control:
schema:
type: string
const: no-store
X-Request-Id:
$ref: '#/components/headers/RequestId'
content:
application/json:
schema:
$ref: '#/components/schemas/TokenPairResponse'
'401':
$ref: '#/components/responses/Unauthorized'
'429':
$ref: '#/components/responses/RateLimited'
/auth/token/revoke:
post:
tags: [Authentication]
operationId: revokeRefreshToken
summary: Revoke one refresh-token family
parameters:
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/IdempotencyKey'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/RefreshTokenRequest'
responses:
'204':
description: Token family revoked or already revoked.
headers:
X-Request-Id:
$ref: '#/components/headers/RequestId'
'401':
$ref: '#/components/responses/Unauthorized'
'429':
$ref: '#/components/responses/RateLimited'
/auth/token/revoke-all:
post:
tags: [Authentication]
operationId: revokeAllSessions
summary: Revoke all sessions for the current user
parameters:
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/IdempotencyKey'
responses:
'204':
description: All sessions revoked, including the current session.
headers:
X-Request-Id:
$ref: '#/components/headers/RequestId'
'401':
$ref: '#/components/responses/Unauthorized'
'429':
$ref: '#/components/responses/RateLimited'
/auth/sessions:
get:
tags: [Sessions]
operationId: listSessions
summary: List sessions for the current user
parameters:
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/Limit'
- $ref: '#/components/parameters/Cursor'
responses:
'200':
description: Sessions returned.
headers:
X-Request-Id:
$ref: '#/components/headers/RequestId'
content:
application/json:
schema:
$ref: '#/components/schemas/SessionCollectionResponse'
'401':
$ref: '#/components/responses/Unauthorized'
'429':
$ref: '#/components/responses/RateLimited'
/auth/sessions/{sessionId}:
delete:
tags: [Sessions]
operationId: revokeSession
summary: Revoke a specific session
parameters:
- $ref: '#/components/parameters/SessionId'
- $ref: '#/components/parameters/RequestId'
responses:
'204':
description: Session revoked or already revoked.
headers:
X-Request-Id:
$ref: '#/components/headers/RequestId'
'401':
$ref: '#/components/responses/Unauthorized'
'404':
$ref: '#/components/responses/NotFound'
'429':
$ref: '#/components/responses/RateLimited'
/me:
get:
tags: [Current User]
operationId: getCurrentUser
summary: Get the current user
parameters:
- $ref: '#/components/parameters/RequestId'
responses:
'200':
description: Current user returned.
headers:
ETag:
$ref: '#/components/headers/ETag'
X-Request-Id:
$ref: '#/components/headers/RequestId'
content:
application/json:
schema:
$ref: '#/components/schemas/UserResponse'
'401':
$ref: '#/components/responses/Unauthorized'
'429':
$ref: '#/components/responses/RateLimited'
patch:
tags: [Current User]
operationId: updateCurrentUser
summary: Update the current user's profile
parameters:
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/IfMatch'
requestBody:
required: true
content:
application/merge-patch+json:
schema:
$ref: '#/components/schemas/UpdateCurrentUserRequest'
responses:
'200':
description: Current user updated.
headers:
ETag:
$ref: '#/components/headers/ETag'
X-Request-Id:
$ref: '#/components/headers/RequestId'
content:
application/json:
schema:
$ref: '#/components/schemas/UserResponse'
'401':
$ref: '#/components/responses/Unauthorized'
'409':
$ref: '#/components/responses/Conflict'
'422':
$ref: '#/components/responses/ValidationError'
'428':
$ref: '#/components/responses/PreconditionRequired'
'429':
$ref: '#/components/responses/RateLimited'
/me/organizations:
get:
tags: [Current User]
operationId: listCurrentUserOrganizations
summary: List organizations accessible to the current user
parameters:
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/Limit'
- $ref: '#/components/parameters/Cursor'
responses:
'200':
description: Accessible organizations returned.
headers:
X-Request-Id:
$ref: '#/components/headers/RequestId'
content:
application/json:
schema:
$ref: '#/components/schemas/OrganizationCollectionResponse'
'401':
$ref: '#/components/responses/Unauthorized'
'429':
$ref: '#/components/responses/RateLimited'
/organizations:
post:
tags: [Organizations]
operationId: createOrganization
summary: Create an organization
x-authorization-policy: authenticated_user_may_create_organization
x-audit-action: organizations.create
description: |
Atomically creates the organization, enables its initial profession modules,
creates an active owner membership, assigns the immutable Owner system role,
and writes audit and outbox records.
parameters:
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/IdempotencyKey'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateOrganizationRequest'
responses:
'201':
description: Organization created.
headers:
Location:
$ref: '#/components/headers/Location'
ETag:
$ref: '#/components/headers/ETag'
X-Request-Id:
$ref: '#/components/headers/RequestId'
content:
application/json:
schema:
$ref: '#/components/schemas/OrganizationResponse'
'401':
$ref: '#/components/responses/Unauthorized'
'409':
$ref: '#/components/responses/Conflict'
'422':
$ref: '#/components/responses/ValidationError'
'429':
$ref: '#/components/responses/RateLimited'
/organizations/{organizationId}:
parameters:
- $ref: '#/components/parameters/OrganizationId'
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
get:
tags: [Organizations]
operationId: getOrganization
summary: Get an organization
x-required-permissions: [organizations.read]
responses:
'200':
description: Organization returned.
headers:
ETag:
$ref: '#/components/headers/ETag'
X-Request-Id:
$ref: '#/components/headers/RequestId'
content:
application/json:
schema:
$ref: '#/components/schemas/OrganizationResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'429':
$ref: '#/components/responses/RateLimited'
patch:
tags: [Organizations]
operationId: updateOrganization
summary: Update organization settings
x-required-permissions: [organizations.update]
x-audit-action: organizations.update
parameters:
- $ref: '#/components/parameters/IfMatch'
requestBody:
required: true
content:
application/merge-patch+json:
schema:
$ref: '#/components/schemas/UpdateOrganizationRequest'
responses:
'200':
description: Organization updated.
headers:
ETag:
$ref: '#/components/headers/ETag'
X-Request-Id:
$ref: '#/components/headers/RequestId'
content:
application/json:
schema:
$ref: '#/components/schemas/OrganizationResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'409':
$ref: '#/components/responses/Conflict'
'422':
$ref: '#/components/responses/ValidationError'
'428':
$ref: '#/components/responses/PreconditionRequired'
'429':
$ref: '#/components/responses/RateLimited'
/membership-invitations:
get:
tags: [Membership Invitations]
operationId: listMembershipInvitations
summary: List membership invitations
x-required-permissions: [members.read]
parameters:
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/Limit'
- $ref: '#/components/parameters/Cursor'
- name: status
in: query
schema:
$ref: '#/components/schemas/InvitationStatus'
responses:
'200':
description: Invitations returned.
headers:
X-Request-Id:
$ref: '#/components/headers/RequestId'
content:
application/json:
schema:
$ref: '#/components/schemas/InvitationCollectionResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'429':
$ref: '#/components/responses/RateLimited'
post:
tags: [Membership Invitations]
operationId: createMembershipInvitation
summary: Invite a person to the current organization
x-required-permissions: [members.invite]
x-audit-action: memberships.invite
parameters:
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/IdempotencyKey'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateInvitationRequest'
responses:
'201':
description: Invitation created.
headers:
Location:
$ref: '#/components/headers/Location'
ETag:
$ref: '#/components/headers/ETag'
X-Request-Id:
$ref: '#/components/headers/RequestId'
content:
application/json:
schema:
$ref: '#/components/schemas/InvitationResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'409':
$ref: '#/components/responses/Conflict'
'422':
$ref: '#/components/responses/ValidationError'
'429':
$ref: '#/components/responses/RateLimited'
/membership-invitations/accept:
post:
tags: [Membership Invitations]
operationId: acceptMembershipInvitation
summary: Accept an invitation for the current user
x-authorization-policy: invitation_email_must_match_current_user
x-audit-action: memberships.accept_invitation
description: |
The invitation token is sent in the request body to avoid path and access-log
disclosure. Acceptance atomically creates the membership, copies valid intended
roles, marks the invitation accepted, and writes audit and outbox records.
parameters:
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/IdempotencyKey'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/AcceptInvitationRequest'
responses:
'201':
description: Invitation accepted and membership created.
headers:
Location:
$ref: '#/components/headers/Location'
X-Request-Id:
$ref: '#/components/headers/RequestId'
content:
application/json:
schema:
$ref: '#/components/schemas/MembershipResponse'
'401':
$ref: '#/components/responses/Unauthorized'
'404':
$ref: '#/components/responses/NotFound'
'409':
$ref: '#/components/responses/Conflict'
'422':
$ref: '#/components/responses/ValidationError'
'429':
$ref: '#/components/responses/RateLimited'
/membership-invitations/{invitationId}:
get:
tags: [Membership Invitations]
operationId: getMembershipInvitation
summary: Get a membership invitation
x-required-permissions: [members.read]
parameters:
- $ref: '#/components/parameters/InvitationId'
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
responses:
'200':
description: Invitation returned.
headers:
ETag:
$ref: '#/components/headers/ETag'
X-Request-Id:
$ref: '#/components/headers/RequestId'
content:
application/json:
schema:
$ref: '#/components/schemas/InvitationResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'429':
$ref: '#/components/responses/RateLimited'
/membership-invitations/{invitationId}/revoke:
post:
tags: [Membership Invitations]
operationId: revokeMembershipInvitation
summary: Revoke a pending invitation
x-required-permissions: [members.invite]
x-audit-action: memberships.revoke_invitation
parameters:
- $ref: '#/components/parameters/InvitationId'
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/IdempotencyKey'
- $ref: '#/components/parameters/IfMatch'
responses:
'200':
description: Invitation revoked or already revoked.
headers:
ETag:
$ref: '#/components/headers/ETag'
X-Request-Id:
$ref: '#/components/headers/RequestId'
content:
application/json:
schema:
$ref: '#/components/schemas/InvitationResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'409':
$ref: '#/components/responses/Conflict'
'428':
$ref: '#/components/responses/PreconditionRequired'
'429':
$ref: '#/components/responses/RateLimited'
/membership-invitations/{invitationId}/resend:
post:
tags: [Membership Invitations]
operationId: resendMembershipInvitation
summary: Rotate the token and resend a pending invitation
x-required-permissions: [members.invite]
x-audit-action: memberships.resend_invitation
parameters:
- $ref: '#/components/parameters/InvitationId'
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/IdempotencyKey'
- $ref: '#/components/parameters/IfMatch'
responses:
'200':
description: Invitation token rotated and delivery queued.
headers:
ETag:
$ref: '#/components/headers/ETag'
X-Request-Id:
$ref: '#/components/headers/RequestId'
content:
application/json:
schema:
$ref: '#/components/schemas/InvitationResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'409':
$ref: '#/components/responses/Conflict'
'428':
$ref: '#/components/responses/PreconditionRequired'
'429':
$ref: '#/components/responses/RateLimited'
/memberships:
get:
tags: [Memberships]
operationId: listMemberships
summary: List memberships in the current organization
x-required-permissions: [members.read]
parameters:
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/Limit'
- $ref: '#/components/parameters/Cursor'
- name: status
in: query
schema:
$ref: '#/components/schemas/MembershipStatus'
- name: userId
in: query
schema:
$ref: '#/components/schemas/Uuid'
responses:
'200':
description: Memberships returned.
headers:
X-Request-Id:
$ref: '#/components/headers/RequestId'
content:
application/json:
schema:
$ref: '#/components/schemas/MembershipCollectionResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'429':
$ref: '#/components/responses/RateLimited'
/memberships/{membershipId}:
get:
tags: [Memberships]
operationId: getMembership
summary: Get a membership
x-required-permissions: [members.read]
parameters:
- $ref: '#/components/parameters/MembershipId'
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
responses:
'200':
description: Membership returned.
headers:
ETag:
$ref: '#/components/headers/ETag'
X-Request-Id:
$ref: '#/components/headers/RequestId'
content:
application/json:
schema:
$ref: '#/components/schemas/MembershipResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'429':
$ref: '#/components/responses/RateLimited'
/memberships/{membershipId}/deactivate:
post:
tags: [Memberships]
operationId: deactivateMembership
summary: Deactivate a membership
description: |
Rejected when the member is the last active organization Owner or manages any
active engineering project, has active project participation, or is assigned open
engineering tasks. Those responsibilities must be reassigned or ended first.
x-required-permissions: [members.update]
x-audit-action: memberships.deactivate
parameters:
- $ref: '#/components/parameters/MembershipId'
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/IdempotencyKey'
- $ref: '#/components/parameters/IfMatch'
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/ReasonRequest'
responses:
'200':
description: Membership deactivated or already inactive.
headers:
ETag:
$ref: '#/components/headers/ETag'
X-Request-Id:
$ref: '#/components/headers/RequestId'
content:
application/json:
schema:
$ref: '#/components/schemas/MembershipResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'409':
$ref: '#/components/responses/Conflict'
'428':
$ref: '#/components/responses/PreconditionRequired'
'429':
$ref: '#/components/responses/RateLimited'
/memberships/{membershipId}/reactivate:
post:
tags: [Memberships]
operationId: reactivateMembership
summary: Reactivate an inactive membership
x-required-permissions: [members.update]
x-audit-action: memberships.reactivate
parameters:
- $ref: '#/components/parameters/MembershipId'
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/IdempotencyKey'
- $ref: '#/components/parameters/IfMatch'
responses:
'200':
description: Membership reactivated or already active.
headers:
ETag:
$ref: '#/components/headers/ETag'
X-Request-Id:
$ref: '#/components/headers/RequestId'
content:
application/json:
schema:
$ref: '#/components/schemas/MembershipResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'409':
$ref: '#/components/responses/Conflict'
'428':
$ref: '#/components/responses/PreconditionRequired'
'429':
$ref: '#/components/responses/RateLimited'
/memberships/{membershipId}/roles:
put:
tags: [Memberships, Roles]
operationId: replaceMembershipRoles
summary: Replace all roles assigned to a membership
x-required-permissions: [roles.manage]
x-audit-action: memberships.replace_roles
description: |
The replacement is atomic. Every supplied role must belong to the current
organization. The operation rejects removal of the last active Owner.
parameters:
- $ref: '#/components/parameters/MembershipId'
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/IfMatch'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/ReplaceMembershipRolesRequest'
responses:
'200':
description: Membership roles replaced.
headers:
ETag:
$ref: '#/components/headers/ETag'
X-Request-Id:
$ref: '#/components/headers/RequestId'
content:
application/json:
schema:
$ref: '#/components/schemas/MembershipResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'409':
$ref: '#/components/responses/Conflict'
'422':
$ref: '#/components/responses/ValidationError'
'428':
$ref: '#/components/responses/PreconditionRequired'
'429':
$ref: '#/components/responses/RateLimited'
/roles:
get:
tags: [Roles]
operationId: listRoles
summary: List roles in the current organization
x-required-permissions: [roles.read]
parameters:
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/Limit'
- $ref: '#/components/parameters/Cursor'
responses:
'200':
description: Roles returned.
headers:
X-Request-Id:
$ref: '#/components/headers/RequestId'
content:
application/json:
schema:
$ref: '#/components/schemas/RoleCollectionResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'429':
$ref: '#/components/responses/RateLimited'
post:
tags: [Roles]
operationId: createRole
summary: Create a custom role
x-required-permissions: [roles.manage]
x-audit-action: roles.create
parameters:
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/IdempotencyKey'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateRoleRequest'
responses:
'201':
description: Role created.
headers:
Location:
$ref: '#/components/headers/Location'
ETag:
$ref: '#/components/headers/ETag'
X-Request-Id:
$ref: '#/components/headers/RequestId'
content:
application/json:
schema:
$ref: '#/components/schemas/RoleResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'409':
$ref: '#/components/responses/Conflict'
'422':
$ref: '#/components/responses/ValidationError'
'429':
$ref: '#/components/responses/RateLimited'
/roles/{roleId}:
parameters:
- $ref: '#/components/parameters/RoleId'
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
get:
tags: [Roles]
operationId: getRole
summary: Get a role
x-required-permissions: [roles.read]
responses:
'200':
description: Role returned.
headers:
ETag:
$ref: '#/components/headers/ETag'
X-Request-Id:
$ref: '#/components/headers/RequestId'
content:
application/json:
schema:
$ref: '#/components/schemas/RoleResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'429':
$ref: '#/components/responses/RateLimited'
patch:
tags: [Roles]
operationId: updateRole
summary: Update a custom role
x-required-permissions: [roles.manage]
x-audit-action: roles.update
description: Immutable system roles cannot be modified.
parameters:
- $ref: '#/components/parameters/IfMatch'
requestBody:
required: true
content:
application/merge-patch+json:
schema:
$ref: '#/components/schemas/UpdateRoleRequest'
responses:
'200':
description: Role updated.
headers:
ETag:
$ref: '#/components/headers/ETag'
X-Request-Id:
$ref: '#/components/headers/RequestId'
content:
application/json:
schema:
$ref: '#/components/schemas/RoleResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'409':
$ref: '#/components/responses/Conflict'
'422':
$ref: '#/components/responses/ValidationError'
'428':
$ref: '#/components/responses/PreconditionRequired'
'429':
$ref: '#/components/responses/RateLimited'
/roles/{roleId}/deactivate:
post:
tags: [Roles]
operationId: deactivateRole
summary: Deactivate a custom role
x-required-permissions: [roles.manage]
x-audit-action: roles.deactivate
description: |
Prevents future assignment of the role without deleting historical assignments.
Immutable system roles cannot be deactivated.
parameters:
- $ref: '#/components/parameters/RoleId'
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/IdempotencyKey'
- $ref: '#/components/parameters/IfMatch'
responses:
'200':
description: Role deactivated or already inactive.
headers:
ETag:
$ref: '#/components/headers/ETag'
X-Request-Id:
$ref: '#/components/headers/RequestId'
content:
application/json:
schema:
$ref: '#/components/schemas/RoleResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'409':
$ref: '#/components/responses/Conflict'
'428':
$ref: '#/components/responses/PreconditionRequired'
'429':
$ref: '#/components/responses/RateLimited'
/roles/{roleId}/reactivate:
post:
tags: [Roles]
operationId: reactivateRole
summary: Reactivate an inactive custom role
x-required-permissions: [roles.manage]
x-audit-action: roles.reactivate
parameters:
- $ref: '#/components/parameters/RoleId'
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/IdempotencyKey'
- $ref: '#/components/parameters/IfMatch'
responses:
'200':
description: Role reactivated or already active.
headers:
ETag:
$ref: '#/components/headers/ETag'
X-Request-Id:
$ref: '#/components/headers/RequestId'
content:
application/json:
schema:
$ref: '#/components/schemas/RoleResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'409':
$ref: '#/components/responses/Conflict'
'428':
$ref: '#/components/responses/PreconditionRequired'
'429':
$ref: '#/components/responses/RateLimited'
/permissions:
get:
tags: [Permissions]
operationId: listPermissions
summary: List registered permissions available to the organization
x-required-permissions: [roles.read]
parameters:
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/Limit'
- $ref: '#/components/parameters/Cursor'
- name: profession
in: query
schema:
$ref: '#/components/schemas/Profession'
responses:
'200':
description: Permissions returned.
headers:
X-Request-Id:
$ref: '#/components/headers/RequestId'
content:
application/json:
schema:
$ref: '#/components/schemas/PermissionCollectionResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'429':
$ref: '#/components/responses/RateLimited'
/engineering/clients:
get:
tags: [Engineering Clients]
operationId: listEngineeringClients
summary: List engineering clients
description: Archived clients are excluded unless `status=archived` is requested explicitly.
x-required-profession: engineering
x-required-permissions: [engineering.clients.read]
parameters:
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/Limit'
- $ref: '#/components/parameters/Cursor'
- name: clientType
in: query
schema:
$ref: '#/components/schemas/EngineeringClientType'
- name: status
in: query
schema:
$ref: '#/components/schemas/EngineeringClientStatus'
- name: q
in: query
description: Case-insensitive search across display name and legal name.
schema:
type: string
minLength: 2
maxLength: 200
- name: sort
in: query
schema:
type: string
enum: [displayName, -displayName, createdAt, -createdAt]
default: displayName
responses:
'200':
description: Engineering clients returned.
headers:
X-Request-Id:
$ref: '#/components/headers/RequestId'
content:
application/json:
schema:
$ref: '#/components/schemas/EngineeringClientCollectionResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'422':
$ref: '#/components/responses/ValidationError'
'429':
$ref: '#/components/responses/RateLimited'
post:
tags: [Engineering Clients]
operationId: createEngineeringClient
summary: Create an engineering client
x-required-profession: engineering
x-required-permissions: [engineering.clients.create]
x-audit-action: engineering.clients.create
parameters:
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/IdempotencyKey'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateEngineeringClientRequest'
responses:
'201':
description: Engineering client created.
headers:
Location:
$ref: '#/components/headers/Location'
ETag:
$ref: '#/components/headers/ETag'
X-Request-Id:
$ref: '#/components/headers/RequestId'
content:
application/json:
schema:
$ref: '#/components/schemas/EngineeringClientResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'409':
$ref: '#/components/responses/Conflict'
'422':
$ref: '#/components/responses/ValidationError'
'429':
$ref: '#/components/responses/RateLimited'
/engineering/clients/{clientId}:
parameters:
- $ref: '#/components/parameters/ClientId'
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
get:
tags: [Engineering Clients]
operationId: getEngineeringClient
summary: Get an engineering client
x-required-profession: engineering
x-required-permissions: [engineering.clients.read]
responses:
'200':
description: Engineering client returned.
headers:
ETag:
$ref: '#/components/headers/ETag'
X-Request-Id:
$ref: '#/components/headers/RequestId'
content:
application/json:
schema:
$ref: '#/components/schemas/EngineeringClientResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'429':
$ref: '#/components/responses/RateLimited'
patch:
tags: [Engineering Clients]
operationId: updateEngineeringClient
summary: Update an active engineering client
description: Status changes are not accepted here; use archive and restore commands.
x-required-profession: engineering
x-required-permissions: [engineering.clients.update]
x-audit-action: engineering.clients.update
parameters:
- $ref: '#/components/parameters/IfMatch'
requestBody:
required: true
content:
application/merge-patch+json:
schema:
$ref: '#/components/schemas/UpdateEngineeringClientRequest'
responses:
'200':
description: Engineering client updated.
headers:
ETag:
$ref: '#/components/headers/ETag'
X-Request-Id:
$ref: '#/components/headers/RequestId'
content:
application/json:
schema:
$ref: '#/components/schemas/EngineeringClientResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'409':
$ref: '#/components/responses/Conflict'
'422':
$ref: '#/components/responses/ValidationError'
'428':
$ref: '#/components/responses/PreconditionRequired'
'429':
$ref: '#/components/responses/RateLimited'
/engineering/clients/{clientId}/archive:
post:
tags: [Engineering Clients]
operationId: archiveEngineeringClient
summary: Archive an engineering client
description: |
Archiving removes the client from default active lists without deleting client,
contact, project, billing, audit, or document history. The command is rejected
while the client has any project in `draft` or `active` status.
x-required-profession: engineering
x-required-permissions: [engineering.clients.archive]
x-audit-action: engineering.clients.archive
parameters:
- $ref: '#/components/parameters/ClientId'
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/IdempotencyKey'
- $ref: '#/components/parameters/IfMatch'
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/ReasonRequest'
responses:
'200':
description: Engineering client archived or already archived.
headers:
ETag:
$ref: '#/components/headers/ETag'
X-Request-Id:
$ref: '#/components/headers/RequestId'
content:
application/json:
schema:
$ref: '#/components/schemas/EngineeringClientResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'409':
$ref: '#/components/responses/Conflict'
'422':
$ref: '#/components/responses/ValidationError'
'428':
$ref: '#/components/responses/PreconditionRequired'
'429':
$ref: '#/components/responses/RateLimited'
/engineering/clients/{clientId}/restore:
post:
tags: [Engineering Clients]
operationId: restoreEngineeringClient
summary: Restore an archived engineering client
description: Restore is rejected when organization policy or retention rules prohibit it.
x-required-profession: engineering
x-required-permissions: [engineering.clients.archive]
x-audit-action: engineering.clients.restore
parameters:
- $ref: '#/components/parameters/ClientId'
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/IdempotencyKey'
- $ref: '#/components/parameters/IfMatch'
responses:
'200':
description: Engineering client restored or already active.
headers:
ETag:
$ref: '#/components/headers/ETag'
X-Request-Id:
$ref: '#/components/headers/RequestId'
content:
application/json:
schema:
$ref: '#/components/schemas/EngineeringClientResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'409':
$ref: '#/components/responses/Conflict'
'422':
$ref: '#/components/responses/ValidationError'
'428':
$ref: '#/components/responses/PreconditionRequired'
'429':
$ref: '#/components/responses/RateLimited'
/engineering/clients/{clientId}/projects:
get:
tags: [Engineering Clients]
operationId: listEngineeringClientProjects
summary: List projects belonging to an engineering client
description: This is a client-scoped projection; full project representations arrive in Milestone 3.
x-required-profession: engineering
x-required-permissions: [engineering.clients.read, engineering.projects.read]
parameters:
- $ref: '#/components/parameters/ClientId'
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/Limit'
- $ref: '#/components/parameters/Cursor'
- name: status
in: query
schema:
$ref: '#/components/schemas/EngineeringProjectStatus'
- name: sort
in: query
schema:
type: string
enum: [projectNumber, -projectNumber, createdAt, -createdAt]
default: -createdAt
responses:
'200':
description: Client projects returned.
headers:
X-Request-Id:
$ref: '#/components/headers/RequestId'
content:
application/json:
schema:
$ref: '#/components/schemas/EngineeringProjectSummaryCollectionResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'422':
$ref: '#/components/responses/ValidationError'
'429':
$ref: '#/components/responses/RateLimited'
/engineering/clients/{clientId}/contacts:
get:
tags: [Engineering Client Contacts]
operationId: listEngineeringClientContacts
summary: List contacts for an engineering client
description: Archived contacts are excluded unless `status=archived` is requested explicitly.
x-required-profession: engineering
x-required-permissions: [engineering.clients.read]
parameters:
- $ref: '#/components/parameters/ClientId'
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/Limit'
- $ref: '#/components/parameters/Cursor'
- name: contactType
in: query
schema:
$ref: '#/components/schemas/EngineeringContactType'
- name: status
in: query
schema:
$ref: '#/components/schemas/EngineeringContactStatus'
- name: isPrimary
in: query
schema:
type: boolean
- name: sort
in: query
schema:
type: string
enum: [name, -name, createdAt, -createdAt]
default: name
responses:
'200':
description: Client contacts returned.
headers:
X-Request-Id:
$ref: '#/components/headers/RequestId'
content:
application/json:
schema:
$ref: '#/components/schemas/EngineeringClientContactCollectionResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'422':
$ref: '#/components/responses/ValidationError'
'429':
$ref: '#/components/responses/RateLimited'
post:
tags: [Engineering Client Contacts]
operationId: createEngineeringClientContact
summary: Create a contact for an engineering client
description: |
When `isPrimary=true`, any current primary contact of the same contact type
is demoted atomically in the same transaction.
x-required-profession: engineering
x-required-permissions: [engineering.clients.update]
x-audit-action: engineering.client_contacts.create
parameters:
- $ref: '#/components/parameters/ClientId'
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/IdempotencyKey'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateEngineeringClientContactRequest'
responses:
'201':
description: Client contact created.
headers:
Location:
$ref: '#/components/headers/Location'
ETag:
$ref: '#/components/headers/ETag'
X-Request-Id:
$ref: '#/components/headers/RequestId'
content:
application/json:
schema:
$ref: '#/components/schemas/EngineeringClientContactResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'409':
$ref: '#/components/responses/Conflict'
'422':
$ref: '#/components/responses/ValidationError'
'429':
$ref: '#/components/responses/RateLimited'
/engineering/clients/{clientId}/contacts/{contactId}:
parameters:
- $ref: '#/components/parameters/ClientId'
- $ref: '#/components/parameters/ContactId'
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
get:
tags: [Engineering Client Contacts]
operationId: getEngineeringClientContact
summary: Get an engineering client contact
x-required-profession: engineering
x-required-permissions: [engineering.clients.read]
responses:
'200':
description: Client contact returned.
headers:
ETag:
$ref: '#/components/headers/ETag'
X-Request-Id:
$ref: '#/components/headers/RequestId'
content:
application/json:
schema:
$ref: '#/components/schemas/EngineeringClientContactResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'429':
$ref: '#/components/responses/RateLimited'
patch:
tags: [Engineering Client Contacts]
operationId: updateEngineeringClientContact
summary: Update an active engineering client contact
description: |
When `isPrimary=true`, any current primary contact of the resulting contact
type is demoted atomically. Status is not patchable.
x-required-profession: engineering
x-required-permissions: [engineering.clients.update]
x-audit-action: engineering.client_contacts.update
parameters:
- $ref: '#/components/parameters/IfMatch'
requestBody:
required: true
content:
application/merge-patch+json:
schema:
$ref: '#/components/schemas/UpdateEngineeringClientContactRequest'
responses:
'200':
description: Client contact updated.
headers:
ETag:
$ref: '#/components/headers/ETag'
X-Request-Id:
$ref: '#/components/headers/RequestId'
content:
application/json:
schema:
$ref: '#/components/schemas/EngineeringClientContactResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'409':
$ref: '#/components/responses/Conflict'
'422':
$ref: '#/components/responses/ValidationError'
'428':
$ref: '#/components/responses/PreconditionRequired'
'429':
$ref: '#/components/responses/RateLimited'
delete:
tags: [Engineering Client Contacts]
operationId: archiveEngineeringClientContact
summary: Archive an engineering client contact
description: |
This operation is a recoverable logical archive, not a physical delete. Historical
references remain intact. Archiving a primary contact clears its primary flag.
Repeating the operation for an archived contact returns 204.
x-required-profession: engineering
x-required-permissions: [engineering.clients.update]
x-audit-action: engineering.client_contacts.archive
parameters:
- $ref: '#/components/parameters/IfMatch'
- $ref: '#/components/parameters/IdempotencyKey'
responses:
'204':
description: Client contact archived or already archived.
headers:
X-Request-Id:
$ref: '#/components/headers/RequestId'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'409':
$ref: '#/components/responses/Conflict'
'428':
$ref: '#/components/responses/PreconditionRequired'
'429':
$ref: '#/components/responses/RateLimited'
/engineering/clients/{clientId}/contacts/{contactId}/restore:
post:
tags: [Engineering Client Contacts]
operationId: restoreEngineeringClientContact
summary: Restore an archived engineering client contact
description: The parent client must be active. Restored contacts are not primary by default.
x-required-profession: engineering
x-required-permissions: [engineering.clients.update]
x-audit-action: engineering.client_contacts.restore
parameters:
- $ref: '#/components/parameters/ClientId'
- $ref: '#/components/parameters/ContactId'
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/IdempotencyKey'
- $ref: '#/components/parameters/IfMatch'
responses:
'200':
description: Client contact restored or already active.
headers:
ETag:
$ref: '#/components/headers/ETag'
X-Request-Id:
$ref: '#/components/headers/RequestId'
content:
application/json:
schema:
$ref: '#/components/schemas/EngineeringClientContactResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'409':
$ref: '#/components/responses/Conflict'
'422':
$ref: '#/components/responses/ValidationError'
'428':
$ref: '#/components/responses/PreconditionRequired'
'429':
$ref: '#/components/responses/RateLimited'
/engineering/projects:
get:
tags: [Engineering Projects]
operationId: listEngineeringProjects
summary: List engineering projects
description: |
Archived projects are excluded unless `status=archived` is requested explicitly.
Permission scope is enforced in the query: `assigned` resolves through active project
membership or the project-manager pointer; `organization` resolves across the tenant.
x-required-profession: engineering
x-required-permissions: [engineering.projects.read]
x-permission-scope-policy: engineering_project_assignment
parameters:
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/Limit'
- $ref: '#/components/parameters/Cursor'
- name: clientId
in: query
schema:
$ref: '#/components/schemas/Uuid'
- name: status
in: query
schema:
$ref: '#/components/schemas/EngineeringProjectStatus'
- name: discipline
in: query
schema:
$ref: '#/components/schemas/EngineeringDiscipline'
- name: projectManagerUserId
in: query
schema:
$ref: '#/components/schemas/Uuid'
- name: q
in: query
description: Case-insensitive search across project number, project name, and client name.
schema:
type: string
minLength: 2
maxLength: 200
- name: sort
in: query
description: Supported deterministic sort. Null date values are always placed last.
schema:
type: string
enum:
- projectNumber
- -projectNumber
- name
- -name
- startDate
- -startDate
- expectedCompletionDate
- -expectedCompletionDate
- createdAt
- -createdAt
default: -createdAt
responses:
'200':
description: Engineering projects returned.
headers:
X-Request-Id:
$ref: '#/components/headers/RequestId'
content:
application/json:
schema:
$ref: '#/components/schemas/EngineeringProjectCollectionResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'422':
$ref: '#/components/responses/ValidationError'
'429':
$ref: '#/components/responses/RateLimited'
post:
tags: [Engineering Projects]
operationId: createEngineeringProject
summary: Create an engineering project in draft status
description: |
`projectNumber` is immutable and unique case-insensitively within the organization.
The referenced client must be active. A supplied project manager must have an active
membership in the same organization. `projectManagerUserId` is the sole project-manager
authority and is not duplicated as a project-member role.
x-required-profession: engineering
x-required-permissions: [engineering.projects.create]
x-audit-action: engineering.projects.create
parameters:
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/IdempotencyKey'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateEngineeringProjectRequest'
responses:
'201':
description: Engineering project created.
headers:
Location:
$ref: '#/components/headers/Location'
ETag:
$ref: '#/components/headers/ETag'
X-Request-Id:
$ref: '#/components/headers/RequestId'
content:
application/json:
schema:
$ref: '#/components/schemas/EngineeringProjectResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'409':
$ref: '#/components/responses/Conflict'
'422':
$ref: '#/components/responses/ValidationError'
'429':
$ref: '#/components/responses/RateLimited'
/engineering/projects/{projectId}:
parameters:
- $ref: '#/components/parameters/ProjectId'
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
get:
tags: [Engineering Projects]
operationId: getEngineeringProject
summary: Get an engineering project
x-required-profession: engineering
x-required-permissions: [engineering.projects.read]
x-permission-scope-policy: engineering_project_assignment
responses:
'200':
description: Engineering project returned.
headers:
ETag:
$ref: '#/components/headers/ETag'
X-Request-Id:
$ref: '#/components/headers/RequestId'
content:
application/json:
schema:
$ref: '#/components/schemas/EngineeringProjectResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'429':
$ref: '#/components/responses/RateLimited'
patch:
tags: [Engineering Projects]
operationId: updateEngineeringProject
summary: Update editable engineering project fields
description: |
`projectNumber`, `status`, completion fields, and archive fields are not patchable.
`clientId` may change only while the project is `draft` and has no dependent records.
Changing `projectManagerUserId` changes assigned-scope access and is audited. It does
not create a duplicate `project_manager` project-member role. Open tasks assigned to
the outgoing manager must first be reassigned unless that user remains an active member.
x-required-profession: engineering
x-required-permissions: [engineering.projects.update]
x-audit-action: engineering.projects.update
parameters:
- $ref: '#/components/parameters/IfMatch'
requestBody:
required: true
content:
application/merge-patch+json:
schema:
$ref: '#/components/schemas/UpdateEngineeringProjectRequest'
responses:
'200':
description: Engineering project updated.
headers:
ETag:
$ref: '#/components/headers/ETag'
X-Request-Id:
$ref: '#/components/headers/RequestId'
content:
application/json:
schema:
$ref: '#/components/schemas/EngineeringProjectResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'409':
$ref: '#/components/responses/Conflict'
'422':
$ref: '#/components/responses/ValidationError'
'428':
$ref: '#/components/responses/PreconditionRequired'
'429':
$ref: '#/components/responses/RateLimited'
/engineering/projects/{projectId}/activate:
post:
tags: [Engineering Projects]
operationId: activateEngineeringProject
summary: Activate a draft engineering project
description: |
Transition: `draft → active`. The client and project manager must both be active.
When `startDate` is absent from both the project and request, the server uses the
current date in the organization's configured time zone.
x-required-profession: engineering
x-required-permissions: [engineering.projects.activate]
x-audit-action: engineering.projects.activate
parameters:
- $ref: '#/components/parameters/ProjectId'
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/IdempotencyKey'
- $ref: '#/components/parameters/IfMatch'
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/ActivateEngineeringProjectRequest'
responses:
'200':
description: Engineering project activated or idempotent result replayed.
headers:
ETag:
$ref: '#/components/headers/ETag'
X-Request-Id:
$ref: '#/components/headers/RequestId'
content:
application/json:
schema:
$ref: '#/components/schemas/EngineeringProjectResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'409':
$ref: '#/components/responses/Conflict'
'422':
$ref: '#/components/responses/ValidationError'
'428':
$ref: '#/components/responses/PreconditionRequired'
'429':
$ref: '#/components/responses/RateLimited'
/engineering/projects/{projectId}/close:
post:
tags: [Engineering Projects]
operationId: closeEngineeringProject
summary: Close an active engineering project
description: |
Transition: `active → closed`. When `completedDate` is omitted, the server uses
the current date in the organization's configured time zone. The completed date
cannot precede the project start date. Every task must already be `completed` or
`cancelled`.
x-required-profession: engineering
x-required-permissions: [engineering.projects.close]
x-audit-action: engineering.projects.close
parameters:
- $ref: '#/components/parameters/ProjectId'
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/IdempotencyKey'
- $ref: '#/components/parameters/IfMatch'
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/CloseEngineeringProjectRequest'
responses:
'200':
description: Engineering project closed or idempotent result replayed.
headers:
ETag:
$ref: '#/components/headers/ETag'
X-Request-Id:
$ref: '#/components/headers/RequestId'
content:
application/json:
schema:
$ref: '#/components/schemas/EngineeringProjectResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'409':
$ref: '#/components/responses/Conflict'
'422':
$ref: '#/components/responses/ValidationError'
'428':
$ref: '#/components/responses/PreconditionRequired'
'429':
$ref: '#/components/responses/RateLimited'
/engineering/projects/{projectId}/archive:
post:
tags: [Engineering Projects]
operationId: archiveEngineeringProject
summary: Archive a draft or closed engineering project
description: |
Transition: `draft|closed → archived`. Active projects must be closed first.
The prior status is retained so restore is deterministic. Related records and
audit history are never physically deleted. Every task must already be `completed`
or `cancelled`.
x-required-profession: engineering
x-required-permissions: [engineering.projects.archive]
x-audit-action: engineering.projects.archive
parameters:
- $ref: '#/components/parameters/ProjectId'
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/IdempotencyKey'
- $ref: '#/components/parameters/IfMatch'
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/ReasonRequest'
responses:
'200':
description: Engineering project archived or idempotent result replayed.
headers:
ETag:
$ref: '#/components/headers/ETag'
X-Request-Id:
$ref: '#/components/headers/RequestId'
content:
application/json:
schema:
$ref: '#/components/schemas/EngineeringProjectResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'409':
$ref: '#/components/responses/Conflict'
'422':
$ref: '#/components/responses/ValidationError'
'428':
$ref: '#/components/responses/PreconditionRequired'
'429':
$ref: '#/components/responses/RateLimited'
/engineering/projects/{projectId}/restore:
post:
tags: [Engineering Projects]
operationId: restoreEngineeringProject
summary: Restore an archived engineering project
description: |
Transition: `archived → archivedFromStatus`, which is either `draft` or `closed`.
Restore never reactivates a project implicitly. The referenced client must be active.
x-required-profession: engineering
x-required-permissions: [engineering.projects.archive]
x-audit-action: engineering.projects.restore
parameters:
- $ref: '#/components/parameters/ProjectId'
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/IdempotencyKey'
- $ref: '#/components/parameters/IfMatch'
responses:
'200':
description: Engineering project restored or idempotent result replayed.
headers:
ETag:
$ref: '#/components/headers/ETag'
X-Request-Id:
$ref: '#/components/headers/RequestId'
content:
application/json:
schema:
$ref: '#/components/schemas/EngineeringProjectResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'409':
$ref: '#/components/responses/Conflict'
'422':
$ref: '#/components/responses/ValidationError'
'428':
$ref: '#/components/responses/PreconditionRequired'
'429':
$ref: '#/components/responses/RateLimited'
/engineering/projects/{projectId}/summary:
get:
tags: [Engineering Projects]
operationId: getEngineeringProjectSummary
summary: Get the engineering project dashboard summary
description: |
Returns a purpose-built read model. Counts are permission-filtered and include
only records visible to the caller. Modules not yet enabled return zero counts,
not omitted fields, preserving the response shape.
x-required-profession: engineering
x-required-permissions: [engineering.projects.read]
x-permission-scope-policy: engineering_project_assignment
parameters:
- $ref: '#/components/parameters/ProjectId'
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
responses:
'200':
description: Project summary returned.
headers:
ETag:
$ref: '#/components/headers/ETag'
X-Request-Id:
$ref: '#/components/headers/RequestId'
content:
application/json:
schema:
$ref: '#/components/schemas/EngineeringProjectDashboardResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'429':
$ref: '#/components/responses/RateLimited'
/engineering/projects/{projectId}/members:
get:
tags: [Engineering Project Members]
operationId: listEngineeringProjectMembers
summary: List temporal project-member records
description: By default, only active participation records are returned.
x-required-profession: engineering
x-required-permissions: [engineering.projects.read]
x-permission-scope-policy: engineering_project_assignment
parameters:
- $ref: '#/components/parameters/ProjectId'
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/Limit'
- $ref: '#/components/parameters/Cursor'
- name: status
in: query
schema:
$ref: '#/components/schemas/EngineeringProjectMemberStatus'
- name: projectRole
in: query
schema:
$ref: '#/components/schemas/EngineeringProjectMemberRole'
- name: userId
in: query
schema:
$ref: '#/components/schemas/Uuid'
- name: sort
in: query
schema:
type: string
enum: [joinedAt, -joinedAt, name, -name]
default: name
responses:
'200':
description: Project-member records returned.
headers:
X-Request-Id:
$ref: '#/components/headers/RequestId'
content:
application/json:
schema:
$ref: '#/components/schemas/EngineeringProjectMemberCollectionResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'422':
$ref: '#/components/responses/ValidationError'
'429':
$ref: '#/components/responses/RateLimited'
post:
tags: [Engineering Project Members]
operationId: addEngineeringProjectMember
summary: Add an active organization member to a project
description: |
The project must be `draft` or `active`. The user must have an active organization
membership. Rejoining after departure creates a new temporal row. Only one active
row may exist for a user in a project. Project-manager assignment is controlled by
`projectManagerUserId`, not by this endpoint.
x-required-profession: engineering
x-required-permissions: [engineering.project_members.manage]
x-audit-action: engineering.project_members.add
parameters:
- $ref: '#/components/parameters/ProjectId'
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/IdempotencyKey'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateEngineeringProjectMemberRequest'
responses:
'201':
description: Project member added.
headers:
Location:
$ref: '#/components/headers/Location'
ETag:
$ref: '#/components/headers/ETag'
X-Request-Id:
$ref: '#/components/headers/RequestId'
content:
application/json:
schema:
$ref: '#/components/schemas/EngineeringProjectMemberResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'409':
$ref: '#/components/responses/Conflict'
'422':
$ref: '#/components/responses/ValidationError'
'429':
$ref: '#/components/responses/RateLimited'
/engineering/projects/{projectId}/members/{memberId}:
parameters:
- $ref: '#/components/parameters/ProjectId'
- $ref: '#/components/parameters/ProjectMemberId'
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
get:
tags: [Engineering Project Members]
operationId: getEngineeringProjectMember
summary: Get a project-member record
x-required-profession: engineering
x-required-permissions: [engineering.projects.read]
x-permission-scope-policy: engineering_project_assignment
responses:
'200':
description: Project-member record returned.
headers:
ETag:
$ref: '#/components/headers/ETag'
X-Request-Id:
$ref: '#/components/headers/RequestId'
content:
application/json:
schema:
$ref: '#/components/schemas/EngineeringProjectMemberResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'429':
$ref: '#/components/responses/RateLimited'
patch:
tags: [Engineering Project Members]
operationId: updateEngineeringProjectMember
summary: Change the participation role of an active project member
description: Only `projectRole` is patchable in v1.
x-required-profession: engineering
x-required-permissions: [engineering.project_members.manage]
x-audit-action: engineering.project_members.update
parameters:
- $ref: '#/components/parameters/IfMatch'
requestBody:
required: true
content:
application/merge-patch+json:
schema:
$ref: '#/components/schemas/UpdateEngineeringProjectMemberRequest'
responses:
'200':
description: Participation role updated.
headers:
ETag:
$ref: '#/components/headers/ETag'
X-Request-Id:
$ref: '#/components/headers/RequestId'
content:
application/json:
schema:
$ref: '#/components/schemas/EngineeringProjectMemberResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'409':
$ref: '#/components/responses/Conflict'
'422':
$ref: '#/components/responses/ValidationError'
'428':
$ref: '#/components/responses/PreconditionRequired'
'429':
$ref: '#/components/responses/RateLimited'
delete:
tags: [Engineering Project Members]
operationId: endEngineeringProjectMembership
summary: End a user's project participation
description: |
Sets `leftAt`; it never deletes history. Repeating the command with the same
idempotency key replays the original 204 response. Open tasks assigned to the
user must be reassigned or unassigned first.
x-required-profession: engineering
x-required-permissions: [engineering.project_members.manage]
x-audit-action: engineering.project_members.remove
parameters:
- $ref: '#/components/parameters/IfMatch'
- $ref: '#/components/parameters/IdempotencyKey'
responses:
'204':
description: Project participation ended or idempotent result replayed.
headers:
X-Request-Id:
$ref: '#/components/headers/RequestId'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'409':
$ref: '#/components/responses/Conflict'
'428':
$ref: '#/components/responses/PreconditionRequired'
'429':
$ref: '#/components/responses/RateLimited'
/engineering/tasks:
get:
tags: [Engineering Tasks]
operationId: listEngineeringTasks
summary: List engineering tasks
description: |
Permission scope is enforced per task. Assigned scope resolves when the caller is
the task assignee, an active member of the parent project, or its project manager.
x-required-profession: engineering
x-required-permissions: [engineering.tasks.manage]
x-permission-scope-policy: engineering_task_or_parent_project_assignment
parameters:
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/Limit'
- $ref: '#/components/parameters/Cursor'
- name: projectId
in: query
schema:
$ref: '#/components/schemas/Uuid'
- name: status
in: query
schema:
$ref: '#/components/schemas/EngineeringTaskStatus'
- name: priority
in: query
schema:
$ref: '#/components/schemas/EngineeringTaskPriority'
- name: assignedToUserId
in: query
schema:
$ref: '#/components/schemas/Uuid'
- name: assignmentStatus
in: query
schema:
type: string
enum: [assigned, unassigned, any]
default: any
- name: dueBefore
in: query
schema:
$ref: '#/components/schemas/Timestamp'
- name: dueAfter
in: query
schema:
$ref: '#/components/schemas/Timestamp'
- name: q
in: query
description: Case-insensitive search across task title and description.
schema:
type: string
minLength: 2
maxLength: 200
- name: sort
in: query
description: Null due dates are always placed last.
schema:
type: string
enum: [createdAt, -createdAt, dueAt, -dueAt, priority, -priority]
default: -createdAt
responses:
'200':
description: Engineering tasks returned.
headers:
X-Request-Id:
$ref: '#/components/headers/RequestId'
content:
application/json:
schema:
$ref: '#/components/schemas/EngineeringTaskCollectionResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'422':
$ref: '#/components/responses/ValidationError'
'429':
$ref: '#/components/responses/RateLimited'
post:
tags: [Engineering Tasks]
operationId: createEngineeringTask
summary: Create a task in todo status
description: |
The project must be `draft` or `active`. A supplied assignee must be the project
manager or an active project member and must retain an active organization membership.
x-required-profession: engineering
x-required-permissions: [engineering.tasks.manage]
x-audit-action: engineering.tasks.create
parameters:
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/IdempotencyKey'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateEngineeringTaskRequest'
responses:
'201':
description: Engineering task created.
headers:
Location:
$ref: '#/components/headers/Location'
ETag:
$ref: '#/components/headers/ETag'
X-Request-Id:
$ref: '#/components/headers/RequestId'
content:
application/json:
schema:
$ref: '#/components/schemas/EngineeringTaskResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'409':
$ref: '#/components/responses/Conflict'
'422':
$ref: '#/components/responses/ValidationError'
'429':
$ref: '#/components/responses/RateLimited'
/engineering/tasks/{taskId}:
parameters:
- $ref: '#/components/parameters/TaskId'
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
get:
tags: [Engineering Tasks]
operationId: getEngineeringTask
summary: Get an engineering task
x-required-profession: engineering
x-required-permissions: [engineering.tasks.manage]
x-permission-scope-policy: engineering_task_or_parent_project_assignment
responses:
'200':
description: Engineering task returned.
headers:
ETag:
$ref: '#/components/headers/ETag'
X-Request-Id:
$ref: '#/components/headers/RequestId'
content:
application/json:
schema:
$ref: '#/components/schemas/EngineeringTaskResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'429':
$ref: '#/components/responses/RateLimited'
patch:
tags: [Engineering Tasks]
operationId: updateEngineeringTask
summary: Update mutable task fields
description: |
`projectId`, status, creator, and terminal metadata are immutable through PATCH.
Assignment changes revalidate active organization and project participation.
Completed and cancelled tasks must be reopened before they can be edited. The parent
project must be `draft` or `active`.
x-required-profession: engineering
x-required-permissions: [engineering.tasks.manage]
x-permission-scope-policy: engineering_task_or_parent_project_assignment
x-audit-action: engineering.tasks.update
parameters:
- $ref: '#/components/parameters/IfMatch'
requestBody:
required: true
content:
application/merge-patch+json:
schema:
$ref: '#/components/schemas/UpdateEngineeringTaskRequest'
responses:
'200':
description: Engineering task updated.
headers:
ETag:
$ref: '#/components/headers/ETag'
X-Request-Id:
$ref: '#/components/headers/RequestId'
content:
application/json:
schema:
$ref: '#/components/schemas/EngineeringTaskResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'409':
$ref: '#/components/responses/Conflict'
'422':
$ref: '#/components/responses/ValidationError'
'428':
$ref: '#/components/responses/PreconditionRequired'
'429':
$ref: '#/components/responses/RateLimited'
/engineering/tasks/{taskId}/start:
post:
tags: [Engineering Tasks]
operationId: startEngineeringTask
summary: Start a todo task
description: 'Transition: `todo → in_progress`; the parent project must be `draft` or `active`.'
x-required-profession: engineering
x-required-permissions: [engineering.tasks.manage]
x-permission-scope-policy: engineering_task_or_parent_project_assignment
x-audit-action: engineering.tasks.start
parameters:
- $ref: '#/components/parameters/TaskId'
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/IdempotencyKey'
- $ref: '#/components/parameters/IfMatch'
responses:
'200':
description: Engineering task started or idempotent result replayed.
headers:
ETag:
$ref: '#/components/headers/ETag'
X-Request-Id:
$ref: '#/components/headers/RequestId'
content:
application/json:
schema:
$ref: '#/components/schemas/EngineeringTaskResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'409':
$ref: '#/components/responses/Conflict'
'428':
$ref: '#/components/responses/PreconditionRequired'
'429':
$ref: '#/components/responses/RateLimited'
/engineering/tasks/{taskId}/complete:
post:
tags: [Engineering Tasks]
operationId: completeEngineeringTask
summary: Complete a todo or in-progress task
description: 'Transition: `todo|in_progress → completed`; the parent project must be `draft` or `active`.'
x-required-profession: engineering
x-required-permissions: [engineering.tasks.manage]
x-permission-scope-policy: engineering_task_or_parent_project_assignment
x-audit-action: engineering.tasks.complete
parameters:
- $ref: '#/components/parameters/TaskId'
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/IdempotencyKey'
- $ref: '#/components/parameters/IfMatch'
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/CompleteEngineeringTaskRequest'
responses:
'200':
description: Engineering task completed or idempotent result replayed.
headers:
ETag:
$ref: '#/components/headers/ETag'
X-Request-Id:
$ref: '#/components/headers/RequestId'
content:
application/json:
schema:
$ref: '#/components/schemas/EngineeringTaskResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'409':
$ref: '#/components/responses/Conflict'
'422':
$ref: '#/components/responses/ValidationError'
'428':
$ref: '#/components/responses/PreconditionRequired'
'429':
$ref: '#/components/responses/RateLimited'
/engineering/tasks/{taskId}/reopen:
post:
tags: [Engineering Tasks]
operationId: reopenEngineeringTask
summary: Reopen a completed or cancelled task
description: |
Transition: `completed|cancelled → todo`. Completion and cancellation metadata
plus any prior start metadata are cleared, while their prior values remain available
through audit history. The parent project must be `draft` or `active`.
x-required-profession: engineering
x-required-permissions: [engineering.tasks.manage]
x-permission-scope-policy: engineering_task_or_parent_project_assignment
x-audit-action: engineering.tasks.reopen
parameters:
- $ref: '#/components/parameters/TaskId'
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/IdempotencyKey'
- $ref: '#/components/parameters/IfMatch'
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/ReasonRequest'
responses:
'200':
description: Engineering task reopened or idempotent result replayed.
headers:
ETag:
$ref: '#/components/headers/ETag'
X-Request-Id:
$ref: '#/components/headers/RequestId'
content:
application/json:
schema:
$ref: '#/components/schemas/EngineeringTaskResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'409':
$ref: '#/components/responses/Conflict'
'422':
$ref: '#/components/responses/ValidationError'
'428':
$ref: '#/components/responses/PreconditionRequired'
'429':
$ref: '#/components/responses/RateLimited'
/engineering/tasks/{taskId}/cancel:
post:
tags: [Engineering Tasks]
operationId: cancelEngineeringTask
summary: Cancel a todo or in-progress task
description: 'Transition: `todo|in_progress → cancelled`; the parent project must be `draft` or `active`.'
x-required-profession: engineering
x-required-permissions: [engineering.tasks.manage]
x-permission-scope-policy: engineering_task_or_parent_project_assignment
x-audit-action: engineering.tasks.cancel
parameters:
- $ref: '#/components/parameters/TaskId'
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/IdempotencyKey'
- $ref: '#/components/parameters/IfMatch'
requestBody:
content:
application/json:
schema:
$ref: '#/components/schemas/CancelEngineeringTaskRequest'
responses:
'200':
description: Engineering task cancelled or idempotent result replayed.
headers:
ETag:
$ref: '#/components/headers/ETag'
X-Request-Id:
$ref: '#/components/headers/RequestId'
content:
application/json:
schema:
$ref: '#/components/schemas/EngineeringTaskResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'409':
$ref: '#/components/responses/Conflict'
'422':
$ref: '#/components/responses/ValidationError'
'428':
$ref: '#/components/responses/PreconditionRequired'
'429':
$ref: '#/components/responses/RateLimited'
/engineering/tasks/batch/assign:
post:
tags: [Engineering Tasks]
operationId: batchAssignEngineeringTasks
summary: Assign multiple tasks
description: |
Every item carries its expected version and is independently tenant-, permission-,
scope-, project-, assignee-, and state-validated. Atomic mode rolls back all items
on any failure. Partial mode commits valid items and returns per-item failures.
Only `todo` and `in_progress` tasks may be assigned, and the assignee must be an
active participant or project manager for every affected project.
Milestone 4 executes at most 100 items synchronously; larger requests are rejected.
Asynchronous execution is introduced with the background-jobs milestone.
x-required-profession: engineering
x-required-permissions: [engineering.tasks.manage]
x-audit-action: engineering.tasks.batch_assign
parameters:
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/IdempotencyKey'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/BatchAssignEngineeringTasksRequest'
responses:
'200':
description: Batch executed synchronously.
headers:
X-Request-Id:
$ref: '#/components/headers/RequestId'
content:
application/json:
schema:
$ref: '#/components/schemas/EngineeringTaskBatchResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'409':
$ref: '#/components/responses/Conflict'
'422':
$ref: '#/components/responses/ValidationError'
'429':
$ref: '#/components/responses/RateLimited'
/engineering/tasks/batch/complete:
post:
tags: [Engineering Tasks]
operationId: batchCompleteEngineeringTasks
summary: Complete multiple tasks
description: |
Every item carries its expected version and is independently authorized and
state-validated. Atomic and partial modes follow the same semantics as batch assign.
Only `todo` and `in_progress` tasks may be completed.
Milestone 4 executes at most 100 items synchronously; larger requests are rejected.
Asynchronous execution is introduced with the background-jobs milestone.
x-required-profession: engineering
x-required-permissions: [engineering.tasks.manage]
x-audit-action: engineering.tasks.batch_complete
parameters:
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/IdempotencyKey'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/BatchCompleteEngineeringTasksRequest'
responses:
'200':
description: Batch executed synchronously.
headers:
X-Request-Id:
$ref: '#/components/headers/RequestId'
content:
application/json:
schema:
$ref: '#/components/schemas/EngineeringTaskBatchResponse'
'400':
$ref: '#/components/responses/BadRequest'
'401':
$ref: '#/components/responses/Unauthorized'
'403':
$ref: '#/components/responses/Forbidden'
'404':
$ref: '#/components/responses/NotFound'
'409':
$ref: '#/components/responses/Conflict'
'422':
$ref: '#/components/responses/ValidationError'
'429':
$ref: '#/components/responses/RateLimited'
/engineering/sites:
get:
tags: [Engineering Sites]
operationId: listEngineeringSites
summary: List engineering sites across the active organization
x-required-profession: engineering
x-required-permissions: [engineering.sites.read]
parameters:
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/Limit'
- $ref: '#/components/parameters/Cursor'
- name: projectId
in: query
schema: {$ref: '#/components/schemas/Uuid'}
- name: search
in: query
schema: {type: string, minLength: 1, maxLength: 200}
responses:
'200':
description: Sites visible to the caller.
headers: {X-Request-Id: {$ref: '#/components/headers/RequestId'}}
content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringSiteCollectionResponse'}}}
'401': {$ref: '#/components/responses/Unauthorized'}
'403': {$ref: '#/components/responses/Forbidden'}
/engineering/projects/{projectId}/sites:
parameters:
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/ProjectId'
get:
tags: [Engineering Sites]
operationId: listEngineeringProjectSites
summary: List sites for one project
x-required-profession: engineering
x-required-permissions: [engineering.sites.read]
parameters:
- $ref: '#/components/parameters/Limit'
- $ref: '#/components/parameters/Cursor'
responses:
'200':
description: Project sites.
content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringSiteCollectionResponse'}}}
'404': {$ref: '#/components/responses/NotFound'}
post:
tags: [Engineering Sites]
operationId: createEngineeringProjectSite
summary: Create a site within a project
x-required-profession: engineering
x-required-permissions: [engineering.sites.manage]
x-audit-action: engineering.site.created
parameters:
- $ref: '#/components/parameters/IdempotencyKey'
requestBody:
required: true
content: {application/json: {schema: {$ref: '#/components/schemas/CreateEngineeringSiteRequest'}}}
responses:
'201':
description: Site created.
headers: {Location: {$ref: '#/components/headers/Location'}, ETag: {$ref: '#/components/headers/ETag'}}
content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringSiteResponse'}}}
'404': {$ref: '#/components/responses/NotFound'}
'422': {$ref: '#/components/responses/ValidationError'}
/engineering/sites/{siteId}:
parameters:
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/SiteId'
get:
tags: [Engineering Sites]
operationId: getEngineeringSite
summary: Retrieve an engineering site
x-required-profession: engineering
x-required-permissions: [engineering.sites.read]
responses:
'200':
description: Site.
headers: {ETag: {$ref: '#/components/headers/ETag'}}
content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringSiteResponse'}}}
'404': {$ref: '#/components/responses/NotFound'}
patch:
tags: [Engineering Sites]
operationId: updateEngineeringSite
summary: Update an engineering site
x-required-profession: engineering
x-required-permissions: [engineering.sites.manage]
x-audit-action: engineering.site.updated
parameters:
- $ref: '#/components/parameters/IfMatch'
- $ref: '#/components/parameters/IdempotencyKey'
requestBody:
required: true
content: {application/merge-patch+json: {schema: {$ref: '#/components/schemas/UpdateEngineeringSiteRequest'}}}
responses:
'200':
description: Site updated.
headers: {ETag: {$ref: '#/components/headers/ETag'}}
content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringSiteResponse'}}}
'404': {$ref: '#/components/responses/NotFound'}
'409': {$ref: '#/components/responses/Conflict'}
'428': {$ref: '#/components/responses/PreconditionRequired'}
'422': {$ref: '#/components/responses/ValidationError'}
/documents:
get:
tags: [Documents]
operationId: listDocuments
summary: List document metadata
description: Quarantined and infected versions are excluded unless the caller has documents.security_review.
x-required-permissions: [documents.read]
parameters:
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/Limit'
- $ref: '#/components/parameters/Cursor'
- name: classification
in: query
schema: {$ref: '#/components/schemas/DocumentClassification'}
- name: categoryId
in: query
schema: {$ref: '#/components/schemas/Uuid'}
- name: search
in: query
schema: {type: string, minLength: 1, maxLength: 200}
responses:
'200':
description: Document metadata.
content: {application/json: {schema: {$ref: '#/components/schemas/DocumentCollectionResponse'}}}
'403': {$ref: '#/components/responses/Forbidden'}
/documents/upload-url:
post:
tags: [Documents]
operationId: createDocumentUploadUrl
summary: Initialize a single-part document upload
description: Creates quarantined document and version metadata, then returns a short-lived signed PUT URL.
x-required-permissions: [documents.upload]
x-audit-action: document.upload_initialized
parameters:
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/IdempotencyKey'
requestBody:
required: true
content: {application/json: {schema: {$ref: '#/components/schemas/InitializeDocumentUploadRequest'}}}
responses:
'201':
description: Upload initialized.
content: {application/json: {schema: {$ref: '#/components/schemas/InitializeDocumentUploadResponse'}}}
'409': {$ref: '#/components/responses/Conflict'}
'422': {$ref: '#/components/responses/ValidationError'}
/documents/{documentId}:
parameters:
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/DocumentId'
get:
tags: [Documents]
operationId: getDocument
summary: Retrieve document metadata
x-required-permissions: [documents.read]
responses:
'200':
description: Document metadata. No storage key or unsigned object URL is exposed.
headers: {ETag: {$ref: '#/components/headers/ETag'}}
content: {application/json: {schema: {$ref: '#/components/schemas/DocumentResponse'}}}
'404': {$ref: '#/components/responses/NotFound'}
patch:
tags: [Documents]
operationId: updateDocumentMetadata
summary: Update mutable document metadata
description: Classification cannot be weakened below the linked domain record's required classification.
x-required-permissions: [documents.manage]
x-audit-action: document.metadata_updated
parameters:
- $ref: '#/components/parameters/IfMatch'
- $ref: '#/components/parameters/IdempotencyKey'
requestBody:
required: true
content: {application/merge-patch+json: {schema: {$ref: '#/components/schemas/UpdateDocumentRequest'}}}
responses:
'200':
description: Document updated.
headers: {ETag: {$ref: '#/components/headers/ETag'}}
content: {application/json: {schema: {$ref: '#/components/schemas/DocumentResponse'}}}
'409': {$ref: '#/components/responses/Conflict'}
'428': {$ref: '#/components/responses/PreconditionRequired'}
'422': {$ref: '#/components/responses/ValidationError'}
/documents/{documentId}/complete-upload:
post:
tags: [Documents]
operationId: completeDocumentUpload
summary: Verify a single-part upload and enqueue malware inspection
description: Completion changes uploadStatus to completed and scanStatus to pending; it never makes the file downloadable.
x-required-permissions: [documents.upload]
x-audit-action: document.upload_completed
parameters:
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/DocumentId'
- $ref: '#/components/parameters/IdempotencyKey'
requestBody:
required: true
content: {application/json: {schema: {$ref: '#/components/schemas/CompleteDocumentUploadRequest'}}}
responses:
'202':
description: Object verified and security scan queued.
content: {application/json: {schema: {$ref: '#/components/schemas/DocumentVersionResponse'}}}
'409': {$ref: '#/components/responses/Conflict'}
'422': {$ref: '#/components/responses/ValidationError'}
/documents/{documentId}/versions:
parameters:
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/DocumentId'
get:
tags: [Documents]
operationId: listDocumentVersions
summary: List immutable document versions
x-required-permissions: [documents.read]
responses:
'200':
description: Version metadata.
content: {application/json: {schema: {$ref: '#/components/schemas/DocumentVersionCollectionResponse'}}}
'404': {$ref: '#/components/responses/NotFound'}
post:
tags: [Documents]
operationId: initializeNewDocumentVersion
summary: Initialize a new single-part version upload
description: The current version pointer changes only after upload verification and a clean scan.
x-required-permissions: [documents.upload]
x-audit-action: document.version_upload_initialized
parameters:
- $ref: '#/components/parameters/IdempotencyKey'
requestBody:
required: true
content: {application/json: {schema: {$ref: '#/components/schemas/InitializeDocumentVersionRequest'}}}
responses:
'201':
description: Version upload initialized.
content: {application/json: {schema: {$ref: '#/components/schemas/InitializeDocumentUploadResponse'}}}
'409': {$ref: '#/components/responses/Conflict'}
'422': {$ref: '#/components/responses/ValidationError'}
/documents/{documentId}/download-url:
post:
tags: [Documents]
operationId: createDocumentDownloadUrl
summary: Create a short-lived download URL for a clean version
description: Infected, pending, failed, or quarantined versions are never downloadable.
x-required-permissions: [documents.download]
x-audit-action: document.download_authorized
parameters:
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/DocumentId'
- $ref: '#/components/parameters/IdempotencyKey'
requestBody:
content: {application/json: {schema: {$ref: '#/components/schemas/CreateDocumentDownloadRequest'}}}
responses:
'200':
description: Short-lived download authorization.
content: {application/json: {schema: {$ref: '#/components/schemas/DocumentDownloadResponse'}}}
'409': {$ref: '#/components/responses/Conflict'}
/documents/multipart-uploads:
post:
tags: [Documents]
operationId: initializeMultipartDocumentUpload
summary: Initialize a multipart document upload
x-required-permissions: [documents.upload]
x-audit-action: document.multipart_initialized
parameters:
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/IdempotencyKey'
requestBody:
required: true
content: {application/json: {schema: {$ref: '#/components/schemas/InitializeDocumentUploadRequest'}}}
responses:
'201':
description: Multipart upload initialized.
content: {application/json: {schema: {$ref: '#/components/schemas/InitializeMultipartUploadResponse'}}}
'422': {$ref: '#/components/responses/ValidationError'}
/documents/{documentId}/multipart-uploads/{uploadId}/parts:
post:
tags: [Documents]
operationId: createMultipartPartUploadUrls
summary: Create signed URLs for selected multipart parts
x-required-permissions: [documents.upload]
parameters:
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/DocumentId'
- $ref: '#/components/parameters/UploadId'
- $ref: '#/components/parameters/IdempotencyKey'
requestBody:
required: true
content: {application/json: {schema: {$ref: '#/components/schemas/MultipartPartUrlsRequest'}}}
responses:
'200':
description: Signed part URLs.
content: {application/json: {schema: {$ref: '#/components/schemas/MultipartPartUrlsResponse'}}}
'409': {$ref: '#/components/responses/Conflict'}
'422': {$ref: '#/components/responses/ValidationError'}
/documents/{documentId}/multipart-uploads/{uploadId}/complete:
post:
tags: [Documents]
operationId: completeMultipartDocumentUpload
summary: Assemble multipart upload and enqueue malware inspection
x-required-permissions: [documents.upload]
x-audit-action: document.multipart_completed
parameters:
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/DocumentId'
- $ref: '#/components/parameters/UploadId'
- $ref: '#/components/parameters/IdempotencyKey'
requestBody:
required: true
content: {application/json: {schema: {$ref: '#/components/schemas/CompleteMultipartUploadRequest'}}}
responses:
'202':
description: Multipart object assembled and security scan queued.
content: {application/json: {schema: {$ref: '#/components/schemas/DocumentVersionResponse'}}}
'409': {$ref: '#/components/responses/Conflict'}
'422': {$ref: '#/components/responses/ValidationError'}
/documents/{documentId}/multipart-uploads/{uploadId}:
delete:
tags: [Documents]
operationId: abortMultipartDocumentUpload
summary: Abort an unfinished multipart upload
x-required-permissions: [documents.upload]
x-audit-action: document.multipart_aborted
parameters:
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/DocumentId'
- $ref: '#/components/parameters/UploadId'
- $ref: '#/components/parameters/IdempotencyKey'
responses:
'204': {description: Upload aborted; staged object parts are scheduled for cleanup.}
'409': {$ref: '#/components/responses/Conflict'}
/engineering/projects/{projectId}/documents:
parameters:
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/ProjectId'
get:
tags: [Engineering Project Documents]
operationId: listEngineeringProjectDocuments
summary: List active project-document links
x-required-profession: engineering
x-required-permissions: [engineering.documents.read]
responses:
'200':
description: Project documents filtered by document authorization and scan state.
content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringProjectDocumentCollectionResponse'}}}
'404': {$ref: '#/components/responses/NotFound'}
post:
tags: [Engineering Project Documents]
operationId: linkEngineeringProjectDocument
summary: Link a clean shared document to a project
description: Pending, failed, or infected versions cannot be linked as the active project document.
x-required-profession: engineering
x-required-permissions: [engineering.documents.manage]
x-audit-action: engineering.project_document.linked
parameters:
- $ref: '#/components/parameters/IdempotencyKey'
requestBody:
required: true
content: {application/json: {schema: {$ref: '#/components/schemas/LinkEngineeringProjectDocumentRequest'}}}
responses:
'201':
description: Document linked.
headers: {Location: {$ref: '#/components/headers/Location'}}
content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringProjectDocumentResponse'}}}
'409': {$ref: '#/components/responses/Conflict'}
'422': {$ref: '#/components/responses/ValidationError'}
/engineering/project-documents/{documentLinkId}:
delete:
tags: [Engineering Project Documents]
operationId: unlinkEngineeringProjectDocument
summary: Temporally unlink a document from a project
description: Sets unlinkedAt; it does not delete the shared document or its versions.
x-required-profession: engineering
x-required-permissions: [engineering.documents.manage]
x-audit-action: engineering.project_document.unlinked
parameters:
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/DocumentLinkId'
- $ref: '#/components/parameters/IdempotencyKey'
responses:
'204': {description: Link ended.}
'404': {$ref: '#/components/responses/NotFound'}
'409': {$ref: '#/components/responses/Conflict'}
/engineering/projects/{projectId}/designs:
parameters:
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/ProjectId'
get:
tags: [Engineering Designs]
operationId: listEngineeringProjectDesigns
summary: List designs for a project
x-required-profession: engineering
x-required-permissions: [engineering.designs.read]
parameters:
- $ref: '#/components/parameters/Limit'
- $ref: '#/components/parameters/Cursor'
- name: status
in: query
schema: {$ref: '#/components/schemas/EngineeringDesignStatus'}
- name: discipline
in: query
schema: {type: string, minLength: 1, maxLength: 100}
responses:
'200':
description: Project designs.
content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringDesignCollectionResponse'}}}
'404': {$ref: '#/components/responses/NotFound'}
post:
tags: [Engineering Designs]
operationId: createEngineeringDesign
summary: Create a draft design
description: Atomically creates version 1 and the required owner/preparer assignments.
x-required-profession: engineering
x-required-permissions: [engineering.designs.manage]
x-audit-action: engineering.design.created
parameters:
- $ref: '#/components/parameters/IdempotencyKey'
requestBody:
required: true
content: {application/json: {schema: {$ref: '#/components/schemas/CreateEngineeringDesignRequest'}}}
responses:
'201':
description: Draft design and initial version created.
headers: {Location: {$ref: '#/components/headers/Location'}, ETag: {$ref: '#/components/headers/ETag'}}
content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringDesignResponse'}}}
'409': {$ref: '#/components/responses/Conflict'}
'422': {$ref: '#/components/responses/ValidationError'}
/engineering/designs/{designId}:
parameters:
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/DesignId'
get:
tags: [Engineering Designs]
operationId: getEngineeringDesign
summary: Retrieve a design
x-required-profession: engineering
x-required-permissions: [engineering.designs.read]
responses:
'200':
description: Design.
headers: {ETag: {$ref: '#/components/headers/ETag'}}
content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringDesignResponse'}}}
'404': {$ref: '#/components/responses/NotFound'}
patch:
tags: [Engineering Designs]
operationId: updateEngineeringDesign
summary: Update editable design metadata
description: Only draft or changes_requested designs are editable; status changes use commands.
x-required-profession: engineering
x-required-permissions: [engineering.designs.manage]
x-audit-action: engineering.design.updated
parameters:
- $ref: '#/components/parameters/IfMatch'
- $ref: '#/components/parameters/IdempotencyKey'
requestBody:
required: true
content: {application/merge-patch+json: {schema: {$ref: '#/components/schemas/UpdateEngineeringDesignRequest'}}}
responses:
'200':
description: Design updated.
headers: {ETag: {$ref: '#/components/headers/ETag'}}
content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringDesignResponse'}}}
'409': {$ref: '#/components/responses/Conflict'}
'428': {$ref: '#/components/responses/PreconditionRequired'}
'422': {$ref: '#/components/responses/ValidationError'}
/engineering/designs/{designId}/submit-review:
post:
tags: [Engineering Designs]
operationId: submitEngineeringDesignForReview
summary: Submit the current version for review
description: Requires a clean primary drawing and at least one active reviewer assignment.
x-required-profession: engineering
x-required-permissions: [engineering.designs.submit]
x-audit-action: engineering.design.submitted_for_review
parameters: &designCommandParameters
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/DesignId'
- $ref: '#/components/parameters/IfMatch'
- $ref: '#/components/parameters/IdempotencyKey'
requestBody:
content: {application/json: {schema: {$ref: '#/components/schemas/OptionalDesignReasonCommand'}}}
responses: &designCommandResponses
'200':
description: Design transitioned.
headers: {ETag: {$ref: '#/components/headers/ETag'}}
content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringDesignResponse'}}}
'403': {$ref: '#/components/responses/Forbidden'}
'404': {$ref: '#/components/responses/NotFound'}
'409': {$ref: '#/components/responses/Conflict'}
'428': {$ref: '#/components/responses/PreconditionRequired'}
'422': {$ref: '#/components/responses/ValidationError'}
/engineering/designs/{designId}/request-changes:
post:
tags: [Engineering Designs]
operationId: requestEngineeringDesignChanges
summary: Return a design to changes requested
x-required-profession: engineering
x-required-permissions: [engineering.designs.review]
x-audit-action: engineering.design.changes_requested
parameters: *designCommandParameters
requestBody:
required: true
content: {application/json: {schema: {$ref: '#/components/schemas/DesignDecisionCommand'}}}
responses: *designCommandResponses
/engineering/designs/{designId}/approve:
post:
tags: [Engineering Designs]
operationId: approveEngineeringDesign
summary: Professionally approve the current design version
description: Revalidates current credential, discipline, scope-of-practice, and approval policy.
x-required-profession: engineering
x-required-permissions: [engineering.designs.approve]
x-audit-action: engineering.design.approved
parameters: *designCommandParameters
requestBody:
required: true
content: {application/json: {schema: {$ref: '#/components/schemas/ApproveDesignCommand'}}}
responses: *designCommandResponses
/engineering/designs/{designId}/reject:
post:
tags: [Engineering Designs]
operationId: rejectEngineeringDesign
summary: Reject the current design version
x-required-profession: engineering
x-required-permissions: [engineering.designs.review]
x-audit-action: engineering.design.rejected
parameters: *designCommandParameters
requestBody:
required: true
content: {application/json: {schema: {$ref: '#/components/schemas/DesignDecisionCommand'}}}
responses: *designCommandResponses
/engineering/designs/{designId}/revise:
post:
tags: [Engineering Designs]
operationId: reviseRejectedEngineeringDesign
summary: Reopen a rejected design as draft with a new version
x-required-profession: engineering
x-required-permissions: [engineering.designs.manage]
x-audit-action: engineering.design.revised
parameters: *designCommandParameters
requestBody:
required: true
content: {application/json: {schema: {$ref: '#/components/schemas/DesignReasonCommand'}}}
responses: *designCommandResponses
/engineering/designs/{designId}/cancel:
post:
tags: [Engineering Designs]
operationId: cancelEngineeringDesign
summary: Cancel a draft design
x-required-profession: engineering
x-required-permissions: [engineering.designs.manage]
x-audit-action: engineering.design.cancelled
parameters: *designCommandParameters
requestBody:
required: true
content: {application/json: {schema: {$ref: '#/components/schemas/DesignReasonCommand'}}}
responses: *designCommandResponses
/engineering/designs/{designId}/withdraw:
post:
tags: [Engineering Designs]
operationId: withdrawEngineeringDesign
summary: Withdraw a design from review
x-required-profession: engineering
x-required-permissions: [engineering.designs.manage]
x-audit-action: engineering.design.withdrawn
parameters: *designCommandParameters
requestBody:
required: true
content: {application/json: {schema: {$ref: '#/components/schemas/DesignReasonCommand'}}}
responses: *designCommandResponses
/engineering/designs/{designId}/supersede:
post:
tags: [Engineering Designs]
operationId: supersedeEngineeringDesign
summary: Supersede an approved design
description: Requires the replacement to be a different approved design in the same project and discipline.
x-required-profession: engineering
x-required-permissions: [engineering.designs.approve]
x-audit-action: engineering.design.superseded
parameters: *designCommandParameters
requestBody:
required: true
content: {application/json: {schema: {$ref: '#/components/schemas/SupersedeDesignCommand'}}}
responses: *designCommandResponses
/engineering/designs/{designId}/assignments:
parameters:
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/DesignId'
get:
tags: [Engineering Design Assignments]
operationId: listEngineeringDesignAssignments
summary: List current and historical design assignments
x-required-profession: engineering
x-required-permissions: [engineering.designs.read]
parameters:
- name: activeOnly
in: query
schema: {type: boolean, default: true}
responses:
'200':
description: Assignments.
content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringDesignAssignmentCollectionResponse'}}}
'404': {$ref: '#/components/responses/NotFound'}
post:
tags: [Engineering Design Assignments]
operationId: assignEngineeringDesignParticipant
summary: Assign a member to a design role
x-required-profession: engineering
x-required-permissions: [engineering.designs.assign]
x-audit-action: engineering.design.assignment_created
parameters:
- $ref: '#/components/parameters/IdempotencyKey'
requestBody:
required: true
content: {application/json: {schema: {$ref: '#/components/schemas/AssignEngineeringDesignRequest'}}}
responses:
'201':
description: Assignment created.
headers: {Location: {$ref: '#/components/headers/Location'}}
content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringDesignAssignmentResponse'}}}
'409': {$ref: '#/components/responses/Conflict'}
'422': {$ref: '#/components/responses/ValidationError'}
/engineering/designs/{designId}/unassign:
post:
tags: [Engineering Design Assignments]
operationId: unassignEngineeringDesignParticipant
summary: End an active design assignment
description: Sets unassignedAt. The final active owner or required reviewer cannot be removed while workflow depends on that role.
x-required-profession: engineering
x-required-permissions: [engineering.designs.assign]
x-audit-action: engineering.design.assignment_ended
parameters:
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/DesignId'
- $ref: '#/components/parameters/IdempotencyKey'
requestBody:
required: true
content: {application/json: {schema: {$ref: '#/components/schemas/UnassignEngineeringDesignRequest'}}}
responses:
'204': {description: Assignment ended.}
'404': {$ref: '#/components/responses/NotFound'}
'409': {$ref: '#/components/responses/Conflict'}
/engineering/designs/{designId}/versions:
parameters:
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/DesignId'
get:
tags: [Engineering Design Versions]
operationId: listEngineeringDesignVersions
summary: List immutable logical design versions
x-required-profession: engineering
x-required-permissions: [engineering.designs.read]
responses:
'200':
description: Design versions.
content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringDesignVersionCollectionResponse'}}}
'404': {$ref: '#/components/responses/NotFound'}
post:
tags: [Engineering Design Versions]
operationId: createEngineeringDesignVersion
summary: Create the next logical design version
description: Allowed only in draft or changes_requested. Version numbers are allocated transactionally.
x-required-profession: engineering
x-required-permissions: [engineering.designs.manage]
x-audit-action: engineering.design.version_created
parameters:
- $ref: '#/components/parameters/IdempotencyKey'
requestBody:
content: {application/json: {schema: {$ref: '#/components/schemas/CreateEngineeringDesignVersionRequest'}}}
responses:
'201':
description: Design version created.
headers: {Location: {$ref: '#/components/headers/Location'}}
content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringDesignVersionResponse'}}}
'409': {$ref: '#/components/responses/Conflict'}
/engineering/design-versions/{designVersionId}/documents:
parameters:
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/DesignVersionId'
get:
tags: [Engineering Design Versions]
operationId: listEngineeringDesignVersionDocuments
summary: List documents linked to a design version
x-required-profession: engineering
x-required-permissions: [engineering.designs.read]
responses:
'200':
description: Version documents.
content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringDesignVersionDocumentCollectionResponse'}}}
'404': {$ref: '#/components/responses/NotFound'}
post:
tags: [Engineering Design Versions]
operationId: linkEngineeringDesignVersionDocument
summary: Link a clean document to an editable design version
description: Only scan-clean documents may be linked; a version may have only one active primary_drawing.
x-required-profession: engineering
x-required-permissions: [engineering.designs.manage]
x-audit-action: engineering.design.version_document_linked
parameters:
- $ref: '#/components/parameters/IdempotencyKey'
requestBody:
required: true
content: {application/json: {schema: {$ref: '#/components/schemas/LinkEngineeringDesignVersionDocumentRequest'}}}
responses:
'201':
description: Document linked.
headers: {Location: {$ref: '#/components/headers/Location'}}
content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringDesignVersionDocumentResponse'}}}
'409': {$ref: '#/components/responses/Conflict'}
'422': {$ref: '#/components/responses/ValidationError'}
/engineering/design-version-documents/{documentLinkId}:
delete:
tags: [Engineering Design Versions]
operationId: unlinkEngineeringDesignVersionDocument
summary: Temporally unlink a document from an editable design version
description: Submitted, approved, rejected, or superseded version evidence cannot be unlinked.
x-required-profession: engineering
x-required-permissions: [engineering.designs.manage]
x-audit-action: engineering.design.version_document_unlinked
parameters:
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/DocumentLinkId'
- $ref: '#/components/parameters/IdempotencyKey'
responses:
'204': {description: Link ended.}
'404': {$ref: '#/components/responses/NotFound'}
'409': {$ref: '#/components/responses/Conflict'}
/engineering/designs/{designId}/reviews:
parameters:
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/DesignId'
get:
tags: [Engineering Design Reviews]
operationId: listEngineeringDesignReviews
summary: List review recommendations
x-required-profession: engineering
x-required-permissions: [engineering.designs.read]
responses:
'200':
description: Reviews.
content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringDesignReviewCollectionResponse'}}}
'404': {$ref: '#/components/responses/NotFound'}
post:
tags: [Engineering Design Reviews]
operationId: recordEngineeringDesignReview
summary: Record a reviewer recommendation for the submitted version
description: A recommendation is immutable and never directly changes design status.
x-required-profession: engineering
x-required-permissions: [engineering.designs.review]
x-audit-action: engineering.design.review_recorded
parameters:
- $ref: '#/components/parameters/IdempotencyKey'
requestBody:
required: true
content: {application/json: {schema: {$ref: '#/components/schemas/CreateEngineeringDesignReviewRequest'}}}
responses:
'201':
description: Review recommendation recorded.
headers: {Location: {$ref: '#/components/headers/Location'}}
content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringDesignReviewResponse'}}}
'409': {$ref: '#/components/responses/Conflict'}
'422': {$ref: '#/components/responses/ValidationError'}
/engineering/projects/{projectId}/inspections:
parameters:
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/ProjectId'
get:
tags: [Engineering Inspections]
operationId: listEngineeringProjectInspections
summary: List inspections for a project
x-required-profession: engineering
x-required-permissions: [engineering.inspections.read]
parameters:
- $ref: '#/components/parameters/Limit'
- $ref: '#/components/parameters/Cursor'
- name: siteId
in: query
schema: {$ref: '#/components/schemas/Uuid'}
- name: status
in: query
schema: {$ref: '#/components/schemas/EngineeringInspectionStatus'}
responses:
'200':
description: Project inspections.
content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringInspectionCollectionResponse'}}}
'404': {$ref: '#/components/responses/NotFound'}
post:
tags: [Engineering Inspections]
operationId: createEngineeringInspection
summary: Create a draft inspection
description: Site and inspector must belong to the same project and active organization context.
x-required-profession: engineering
x-required-permissions: [engineering.inspections.manage]
x-audit-action: engineering.inspection.created
parameters:
- $ref: '#/components/parameters/IdempotencyKey'
requestBody:
required: true
content: {application/json: {schema: {$ref: '#/components/schemas/CreateEngineeringInspectionRequest'}}}
responses:
'201':
description: Draft inspection created.
headers: {Location: {$ref: '#/components/headers/Location'}, ETag: {$ref: '#/components/headers/ETag'}}
content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringInspectionResponse'}}}
'409': {$ref: '#/components/responses/Conflict'}
'422': {$ref: '#/components/responses/ValidationError'}
/engineering/inspections/{inspectionId}:
parameters:
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/InspectionId'
get:
tags: [Engineering Inspections]
operationId: getEngineeringInspection
summary: Retrieve an inspection
x-required-profession: engineering
x-required-permissions: [engineering.inspections.read]
responses:
'200':
description: Inspection.
headers: {ETag: {$ref: '#/components/headers/ETag'}}
content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringInspectionResponse'}}}
'404': {$ref: '#/components/responses/NotFound'}
patch:
tags: [Engineering Inspections]
operationId: updateEngineeringInspection
summary: Update editable inspection metadata
description: Draft and scheduled inspections are editable; lifecycle fields use commands.
x-required-profession: engineering
x-required-permissions: [engineering.inspections.manage]
x-audit-action: engineering.inspection.updated
parameters:
- $ref: '#/components/parameters/IfMatch'
- $ref: '#/components/parameters/IdempotencyKey'
requestBody:
required: true
content: {application/merge-patch+json: {schema: {$ref: '#/components/schemas/UpdateEngineeringInspectionRequest'}}}
responses:
'200':
description: Inspection updated.
headers: {ETag: {$ref: '#/components/headers/ETag'}}
content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringInspectionResponse'}}}
'409': {$ref: '#/components/responses/Conflict'}
'428': {$ref: '#/components/responses/PreconditionRequired'}
'422': {$ref: '#/components/responses/ValidationError'}
/engineering/inspections/{inspectionId}/schedule:
post:
tags: [Engineering Inspections]
operationId: scheduleEngineeringInspection
summary: Schedule a draft inspection
x-required-profession: engineering
x-required-permissions: [engineering.inspections.manage]
x-audit-action: engineering.inspection.scheduled
parameters: &inspectionCommandParameters
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/InspectionId'
- $ref: '#/components/parameters/IfMatch'
- $ref: '#/components/parameters/IdempotencyKey'
requestBody:
required: true
content: {application/json: {schema: {$ref: '#/components/schemas/ScheduleInspectionCommand'}}}
responses: &inspectionCommandResponses
'200':
description: Inspection transitioned.
headers: {ETag: {$ref: '#/components/headers/ETag'}}
content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringInspectionResponse'}}}
'403': {$ref: '#/components/responses/Forbidden'}
'404': {$ref: '#/components/responses/NotFound'}
'409': {$ref: '#/components/responses/Conflict'}
'428': {$ref: '#/components/responses/PreconditionRequired'}
'422': {$ref: '#/components/responses/ValidationError'}
/engineering/inspections/{inspectionId}/start:
post:
tags: [Engineering Inspections]
operationId: startEngineeringInspection
summary: Start a scheduled inspection
x-required-profession: engineering
x-required-permissions: [engineering.inspections.perform]
x-audit-action: engineering.inspection.started
parameters: *inspectionCommandParameters
requestBody:
content: {application/json: {schema: {$ref: '#/components/schemas/StartInspectionCommand'}}}
responses: *inspectionCommandResponses
/engineering/inspections/{inspectionId}/complete:
post:
tags: [Engineering Inspections]
operationId: completeEngineeringInspection
summary: Complete an in-progress inspection
description: Outcome is mandatory. Passed outcomes are rejected while major or critical findings remain open.
x-required-profession: engineering
x-required-permissions: [engineering.inspections.perform]
x-audit-action: engineering.inspection.completed
parameters: *inspectionCommandParameters
requestBody:
required: true
content: {application/json: {schema: {$ref: '#/components/schemas/CompleteInspectionCommand'}}}
responses: *inspectionCommandResponses
/engineering/inspections/{inspectionId}/cancel:
post:
tags: [Engineering Inspections]
operationId: cancelEngineeringInspection
summary: Cancel a draft or scheduled inspection
x-required-profession: engineering
x-required-permissions: [engineering.inspections.manage]
x-audit-action: engineering.inspection.cancelled
parameters: *inspectionCommandParameters
requestBody:
required: true
content: {application/json: {schema: {$ref: '#/components/schemas/InspectionReasonCommand'}}}
responses: *inspectionCommandResponses
/engineering/inspections/{inspectionId}/documents:
parameters:
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/InspectionId'
get:
tags: [Engineering Inspection Documents]
operationId: listEngineeringInspectionDocuments
summary: List active inspection-document links
x-required-profession: engineering
x-required-permissions: [engineering.inspections.read]
responses:
'200':
description: Inspection documents.
content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringInspectionDocumentCollectionResponse'}}}
'404': {$ref: '#/components/responses/NotFound'}
post:
tags: [Engineering Inspection Documents]
operationId: linkEngineeringInspectionDocument
summary: Link a scan-clean document to an inspection
x-required-profession: engineering
x-required-permissions: [engineering.inspections.manage]
x-audit-action: engineering.inspection.document_linked
parameters:
- $ref: '#/components/parameters/IdempotencyKey'
requestBody:
required: true
content: {application/json: {schema: {$ref: '#/components/schemas/LinkEngineeringInspectionDocumentRequest'}}}
responses:
'201':
description: Document linked.
headers: {Location: {$ref: '#/components/headers/Location'}}
content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringInspectionDocumentResponse'}}}
'409': {$ref: '#/components/responses/Conflict'}
'422': {$ref: '#/components/responses/ValidationError'}
/engineering/inspection-documents/{documentLinkId}:
delete:
tags: [Engineering Inspection Documents]
operationId: unlinkEngineeringInspectionDocument
summary: Temporally unlink an inspection document
description: Completed inspection evidence cannot be unlinked through ordinary workflow.
x-required-profession: engineering
x-required-permissions: [engineering.inspections.manage]
x-audit-action: engineering.inspection.document_unlinked
parameters:
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/DocumentLinkId'
- $ref: '#/components/parameters/IdempotencyKey'
responses:
'204': {description: Link ended.}
'404': {$ref: '#/components/responses/NotFound'}
'409': {$ref: '#/components/responses/Conflict'}
/engineering/inspections/{inspectionId}/findings:
parameters:
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/InspectionId'
get:
tags: [Engineering Inspection Findings]
operationId: listEngineeringInspectionFindings
summary: List findings for an inspection
x-required-profession: engineering
x-required-permissions: [engineering.inspections.read]
responses:
'200':
description: Inspection findings.
content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringInspectionFindingCollectionResponse'}}}
'404': {$ref: '#/components/responses/NotFound'}
post:
tags: [Engineering Inspection Findings]
operationId: createEngineeringInspectionFinding
summary: Record a finding during an in-progress inspection
x-required-profession: engineering
x-required-permissions: [engineering.inspections.perform]
x-audit-action: engineering.inspection.finding_created
parameters:
- $ref: '#/components/parameters/IdempotencyKey'
requestBody:
required: true
content: {application/json: {schema: {$ref: '#/components/schemas/CreateEngineeringInspectionFindingRequest'}}}
responses:
'201':
description: Finding recorded.
headers: {Location: {$ref: '#/components/headers/Location'}, ETag: {$ref: '#/components/headers/ETag'}}
content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringInspectionFindingResponse'}}}
'409': {$ref: '#/components/responses/Conflict'}
'422': {$ref: '#/components/responses/ValidationError'}
/engineering/inspection-findings/{findingId}:
parameters:
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/FindingId'
get:
tags: [Engineering Inspection Findings]
operationId: getEngineeringInspectionFinding
summary: Retrieve an inspection finding
x-required-profession: engineering
x-required-permissions: [engineering.inspections.read]
responses:
'200':
description: Finding.
headers: {ETag: {$ref: '#/components/headers/ETag'}}
content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringInspectionFindingResponse'}}}
'404': {$ref: '#/components/responses/NotFound'}
patch:
tags: [Engineering Inspection Findings]
operationId: updateEngineeringInspectionFinding
summary: Update finding description, severity, owner, or target date
description: Resolved and accepted-risk findings are immutable except through explicit reopen policy added later.
x-required-profession: engineering
x-required-permissions: [engineering.inspections.manage_findings]
x-audit-action: engineering.inspection.finding_updated
parameters:
- $ref: '#/components/parameters/IfMatch'
- $ref: '#/components/parameters/IdempotencyKey'
requestBody:
required: true
content: {application/merge-patch+json: {schema: {$ref: '#/components/schemas/UpdateEngineeringInspectionFindingRequest'}}}
responses:
'200':
description: Finding updated.
headers: {ETag: {$ref: '#/components/headers/ETag'}}
content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringInspectionFindingResponse'}}}
'409': {$ref: '#/components/responses/Conflict'}
'428': {$ref: '#/components/responses/PreconditionRequired'}
'422': {$ref: '#/components/responses/ValidationError'}
/engineering/inspection-findings/{findingId}/start-remediation:
post:
tags: [Engineering Inspection Findings]
operationId: startEngineeringFindingRemediation
summary: Start corrective work for an open finding
x-required-profession: engineering
x-required-permissions: [engineering.inspections.manage_findings]
x-audit-action: engineering.inspection.finding_remediation_started
parameters: &findingCommandParameters
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/FindingId'
- $ref: '#/components/parameters/IfMatch'
- $ref: '#/components/parameters/IdempotencyKey'
responses: &findingCommandResponses
'200':
description: Finding transitioned.
headers: {ETag: {$ref: '#/components/headers/ETag'}}
content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringInspectionFindingResponse'}}}
'403': {$ref: '#/components/responses/Forbidden'}
'404': {$ref: '#/components/responses/NotFound'}
'409': {$ref: '#/components/responses/Conflict'}
'428': {$ref: '#/components/responses/PreconditionRequired'}
'422': {$ref: '#/components/responses/ValidationError'}
/engineering/inspection-findings/{findingId}/resolve:
post:
tags: [Engineering Inspection Findings]
operationId: resolveEngineeringInspectionFinding
summary: Independently verify and resolve a remediated finding
description: Verifier must differ from remediation owner unless an explicit privileged override is audited.
x-required-profession: engineering
x-required-permissions: [engineering.inspections.verify_findings]
x-audit-action: engineering.inspection.finding_resolved
parameters: *findingCommandParameters
requestBody:
required: true
content: {application/json: {schema: {$ref: '#/components/schemas/ResolveEngineeringFindingCommand'}}}
responses: *findingCommandResponses
/engineering/inspection-findings/{findingId}/accept-risk:
post:
tags: [Engineering Inspection Findings]
operationId: acceptEngineeringInspectionFindingRisk
summary: Accept the documented risk of an unresolved finding
description: Major and critical acceptance requires privileged authority and a review date.
x-required-profession: engineering
x-required-permissions: [engineering.inspections.accept_risk]
x-audit-action: engineering.inspection.finding_risk_accepted
parameters: *findingCommandParameters
requestBody:
required: true
content: {application/json: {schema: {$ref: '#/components/schemas/AcceptEngineeringFindingRiskCommand'}}}
responses: *findingCommandResponses
/engineering/inspections/{inspectionId}/followups:
parameters:
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/InspectionId'
get:
tags: [Engineering Inspection Follow-ups]
operationId: listEngineeringInspectionFollowups
summary: List follow-up actions
x-required-profession: engineering
x-required-permissions: [engineering.inspections.read]
responses:
'200':
description: Follow-ups.
content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringInspectionFollowupCollectionResponse'}}}
'404': {$ref: '#/components/responses/NotFound'}
post:
tags: [Engineering Inspection Follow-ups]
operationId: createEngineeringInspectionFollowup
summary: Create a corrective-task or follow-up-inspection relationship
x-required-profession: engineering
x-required-permissions: [engineering.inspections.manage_findings]
x-audit-action: engineering.inspection.followup_created
parameters:
- $ref: '#/components/parameters/IdempotencyKey'
requestBody:
required: true
content: {application/json: {schema: {$ref: '#/components/schemas/CreateEngineeringInspectionFollowupRequest'}}}
responses:
'201':
description: Follow-up created.
headers: {Location: {$ref: '#/components/headers/Location'}}
content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringInspectionFollowupResponse'}}}
'409': {$ref: '#/components/responses/Conflict'}
'422': {$ref: '#/components/responses/ValidationError'}
/engineering/inspection-followups/{followupId}/{command}:
post:
tags: [Engineering Inspection Follow-ups]
operationId: commandEngineeringInspectionFollowup
summary: Start, complete, or cancel a follow-up
x-required-profession: engineering
x-required-permissions: [engineering.inspections.manage_findings]
x-audit-action: engineering.inspection.followup_commanded
parameters:
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/FollowupId'
- name: command
in: path
required: true
schema: {type: string, enum: [start, complete, cancel]}
- $ref: '#/components/parameters/IfMatch'
- $ref: '#/components/parameters/IdempotencyKey'
requestBody:
content: {application/json: {schema: {$ref: '#/components/schemas/OptionalInspectionReasonCommand'}}}
responses:
'200':
description: Follow-up transitioned.
content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringInspectionFollowupResponse'}}}
'404': {$ref: '#/components/responses/NotFound'}
'409': {$ref: '#/components/responses/Conflict'}
/engineering/projects/{projectId}/specifications:
parameters:
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/ProjectId'
get:
tags: [Engineering Specifications]
operationId: listEngineeringProjectSpecifications
summary: List specifications for a project
x-required-profession: engineering
x-required-permissions: [engineering.specifications.read]
parameters:
- $ref: '#/components/parameters/Limit'
- $ref: '#/components/parameters/Cursor'
- name: status
in: query
schema: {$ref: '#/components/schemas/EngineeringSpecificationStatus'}
- name: search
in: query
schema: {type: string, minLength: 1, maxLength: 200}
responses:
'200':
description: Project specifications.
content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringSpecificationCollectionResponse'}}}
'404': {$ref: '#/components/responses/NotFound'}
post:
tags: [Engineering Specifications]
operationId: createEngineeringSpecification
summary: Create a draft specification
x-required-profession: engineering
x-required-permissions: [engineering.specifications.manage]
x-audit-action: engineering.specification.created
parameters:
- $ref: '#/components/parameters/IdempotencyKey'
requestBody:
required: true
content: {application/json: {schema: {$ref: '#/components/schemas/CreateEngineeringSpecificationRequest'}}}
responses:
'201':
description: Draft specification created.
headers: {Location: {$ref: '#/components/headers/Location'}, ETag: {$ref: '#/components/headers/ETag'}}
content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringSpecificationResponse'}}}
'409': {$ref: '#/components/responses/Conflict'}
'422': {$ref: '#/components/responses/ValidationError'}
/engineering/specifications/{specificationId}:
parameters:
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/SpecificationId'
get:
tags: [Engineering Specifications]
operationId: getEngineeringSpecification
summary: Retrieve a specification
x-required-profession: engineering
x-required-permissions: [engineering.specifications.read]
responses:
'200':
description: Specification.
headers: {ETag: {$ref: '#/components/headers/ETag'}}
content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringSpecificationResponse'}}}
'404': {$ref: '#/components/responses/NotFound'}
patch:
tags: [Engineering Specifications]
operationId: updateEngineeringSpecification
summary: Update draft specification metadata
description: Active, superseded and archived specifications reject PATCH; lifecycle changes use commands.
x-required-profession: engineering
x-required-permissions: [engineering.specifications.manage]
x-audit-action: engineering.specification.updated
parameters:
- $ref: '#/components/parameters/IfMatch'
- $ref: '#/components/parameters/IdempotencyKey'
requestBody:
required: true
content: {application/merge-patch+json: {schema: {$ref: '#/components/schemas/UpdateEngineeringSpecificationRequest'}}}
responses:
'200':
description: Specification updated.
headers: {ETag: {$ref: '#/components/headers/ETag'}}
content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringSpecificationResponse'}}}
'409': {$ref: '#/components/responses/Conflict'}
'428': {$ref: '#/components/responses/PreconditionRequired'}
'422': {$ref: '#/components/responses/ValidationError'}
/engineering/specifications/{specificationId}/activate:
post:
tags: [Engineering Specifications]
operationId: activateEngineeringSpecification
summary: Activate a draft specification
description: Requires exactly one active, scan-clean primary document.
x-required-profession: engineering
x-required-permissions: [engineering.specifications.activate]
x-audit-action: engineering.specification.activated
parameters: &specificationCommandParameters
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/SpecificationId'
- $ref: '#/components/parameters/IfMatch'
- $ref: '#/components/parameters/IdempotencyKey'
requestBody:
content: {application/json: {schema: {$ref: '#/components/schemas/OptionalSpecificationReasonCommand'}}}
responses: &specificationCommandResponses
'200':
description: Specification transitioned.
headers: {ETag: {$ref: '#/components/headers/ETag'}}
content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringSpecificationResponse'}}}
'403': {$ref: '#/components/responses/Forbidden'}
'404': {$ref: '#/components/responses/NotFound'}
'409': {$ref: '#/components/responses/Conflict'}
'428': {$ref: '#/components/responses/PreconditionRequired'}
'422': {$ref: '#/components/responses/ValidationError'}
/engineering/specifications/{specificationId}/supersede:
post:
tags: [Engineering Specifications]
operationId: supersedeEngineeringSpecification
summary: Supersede an active specification
description: Replacement must be a different active specification in the same project.
x-required-profession: engineering
x-required-permissions: [engineering.specifications.activate]
x-audit-action: engineering.specification.superseded
parameters: *specificationCommandParameters
requestBody:
required: true
content: {application/json: {schema: {$ref: '#/components/schemas/SupersedeSpecificationCommand'}}}
responses: *specificationCommandResponses
/engineering/specifications/{specificationId}/archive:
post:
tags: [Engineering Specifications]
operationId: archiveEngineeringSpecification
summary: Archive a draft or active specification
description: Stores archivedFromStatus; active specifications referenced by open work may be blocked.
x-required-profession: engineering
x-required-permissions: [engineering.specifications.manage]
x-audit-action: engineering.specification.archived
parameters: *specificationCommandParameters
requestBody:
required: true
content: {application/json: {schema: {$ref: '#/components/schemas/SpecificationReasonCommand'}}}
responses: *specificationCommandResponses
/engineering/specifications/{specificationId}/restore:
post:
tags: [Engineering Specifications]
operationId: restoreEngineeringSpecification
summary: Restore an archived specification to its prior status
x-required-profession: engineering
x-required-permissions: [engineering.specifications.manage]
x-audit-action: engineering.specification.restored
parameters: *specificationCommandParameters
requestBody:
content: {application/json: {schema: {$ref: '#/components/schemas/OptionalSpecificationReasonCommand'}}}
responses: *specificationCommandResponses
/engineering/specifications/{specificationId}/documents:
parameters:
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/SpecificationId'
get:
tags: [Engineering Specification Documents]
operationId: listEngineeringSpecificationDocuments
summary: List current and historical specification-document links
x-required-profession: engineering
x-required-permissions: [engineering.specifications.read]
parameters:
- name: activeOnly
in: query
schema: {type: boolean, default: true}
responses:
'200':
description: Specification documents.
content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringSpecificationDocumentCollectionResponse'}}}
'404': {$ref: '#/components/responses/NotFound'}
post:
tags: [Engineering Specification Documents]
operationId: linkEngineeringSpecificationDocument
summary: Link a scan-clean document to a draft specification
description: A draft may have only one active primary link; active or terminal evidence is immutable.
x-required-profession: engineering
x-required-permissions: [engineering.specifications.manage]
x-audit-action: engineering.specification.document_linked
parameters:
- $ref: '#/components/parameters/IdempotencyKey'
requestBody:
required: true
content: {application/json: {schema: {$ref: '#/components/schemas/LinkEngineeringSpecificationDocumentRequest'}}}
responses:
'201':
description: Document linked.
headers: {Location: {$ref: '#/components/headers/Location'}}
content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringSpecificationDocumentResponse'}}}
'409': {$ref: '#/components/responses/Conflict'}
'422': {$ref: '#/components/responses/ValidationError'}
/engineering/specification-documents/{documentLinkId}:
delete:
tags: [Engineering Specification Documents]
operationId: unlinkEngineeringSpecificationDocument
summary: Temporally unlink a document from a draft specification
description: Sets unlinkedAt; it never deletes the shared document or active/terminal evidence.
x-required-profession: engineering
x-required-permissions: [engineering.specifications.manage]
x-audit-action: engineering.specification.document_unlinked
parameters:
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/DocumentLinkId'
- $ref: '#/components/parameters/IdempotencyKey'
responses:
'204': {description: Link ended.}
'404': {$ref: '#/components/responses/NotFound'}
'409': {$ref: '#/components/responses/Conflict'}
/engineering/projects/{projectId}/phases:
parameters:
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/ProjectId'
get:
tags: [Engineering Project Phases]
operationId: listEngineeringProjectPhases
summary: List ordered project phases
x-required-profession: engineering
x-required-permissions: [engineering.projects.read]
responses:
'200': {description: Ordered phases., content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringProjectPhaseCollectionResponse'}}}}
'404': {$ref: '#/components/responses/NotFound'}
post:
tags: [Engineering Project Phases]
operationId: createEngineeringProjectPhase
summary: Add a planned phase
x-required-profession: engineering
x-required-permissions: [engineering.projects.manage]
x-audit-action: engineering.project.phase_created
parameters:
- $ref: '#/components/parameters/IdempotencyKey'
requestBody: {required: true, content: {application/json: {schema: {$ref: '#/components/schemas/CreateEngineeringProjectPhaseRequest'}}}}
responses:
'201':
description: Phase created.
headers: {Location: {$ref: '#/components/headers/Location'}, ETag: {$ref: '#/components/headers/ETag'}}
content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringProjectPhaseResponse'}}}
'409': {$ref: '#/components/responses/Conflict'}
'422': {$ref: '#/components/responses/ValidationError'}
/engineering/projects/{projectId}/phases/{phaseId}:
patch:
tags: [Engineering Project Phases]
operationId: updateEngineeringProjectPhase
summary: Update phase metadata
description: Completion and ordering use commands; planned/active/cancelled may be managed while the phase is otherwise editable.
x-required-profession: engineering
x-required-permissions: [engineering.projects.manage]
x-audit-action: engineering.project.phase_updated
parameters:
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/ProjectId'
- $ref: '#/components/parameters/PhaseId'
- $ref: '#/components/parameters/IfMatch'
- $ref: '#/components/parameters/IdempotencyKey'
requestBody: {required: true, content: {application/merge-patch+json: {schema: {$ref: '#/components/schemas/UpdateEngineeringProjectPhaseRequest'}}}}
responses:
'200':
description: Phase updated.
headers: {ETag: {$ref: '#/components/headers/ETag'}}
content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringProjectPhaseResponse'}}}
'409': {$ref: '#/components/responses/Conflict'}
'428': {$ref: '#/components/responses/PreconditionRequired'}
'422': {$ref: '#/components/responses/ValidationError'}
/engineering/projects/{projectId}/phases/{phaseId}/complete:
post:
tags: [Engineering Project Phases]
operationId: completeEngineeringProjectPhase
summary: Complete an active phase
x-required-profession: engineering
x-required-permissions: [engineering.projects.manage]
x-audit-action: engineering.project.phase_completed
parameters:
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/ProjectId'
- $ref: '#/components/parameters/PhaseId'
- $ref: '#/components/parameters/IfMatch'
- $ref: '#/components/parameters/IdempotencyKey'
responses:
'200':
description: Phase completed.
headers: {ETag: {$ref: '#/components/headers/ETag'}}
content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringProjectPhaseResponse'}}}
'409': {$ref: '#/components/responses/Conflict'}
'428': {$ref: '#/components/responses/PreconditionRequired'}
/engineering/projects/{projectId}/phases/reorder:
post:
tags: [Engineering Project Phases]
operationId: reorderEngineeringProjectPhases
summary: Transactionally reorder every phase in a project
x-required-profession: engineering
x-required-permissions: [engineering.projects.manage]
x-audit-action: engineering.project.phases_reordered
parameters:
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/ProjectId'
- $ref: '#/components/parameters/IdempotencyKey'
requestBody: {required: true, content: {application/json: {schema: {$ref: '#/components/schemas/ReorderEngineeringProjectPhasesRequest'}}}}
responses:
'200': {description: Phases reordered., content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringProjectPhaseCollectionResponse'}}}}
'409': {$ref: '#/components/responses/Conflict'}
'422': {$ref: '#/components/responses/ValidationError'}
/engineering/time-entries:
get:
tags: [Engineering Time Entries]
operationId: listEngineeringTimeEntries
summary: List time entries
x-required-profession: engineering
x-required-permissions: [engineering.time_entries.read]
parameters:
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/Limit'
- $ref: '#/components/parameters/Cursor'
- name: projectId
in: query
schema: {$ref: '#/components/schemas/Uuid'}
- name: userId
in: query
schema: {$ref: '#/components/schemas/Uuid'}
- name: fromDate
in: query
schema: {$ref: '#/components/schemas/Date'}
- name: toDate
in: query
schema: {$ref: '#/components/schemas/Date'}
- name: billable
in: query
schema: {type: boolean}
responses:
'200': {description: Time entries., content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringTimeEntryCollectionResponse'}}}}
post:
tags: [Engineering Time Entries]
operationId: createEngineeringTimeEntry
summary: Create a time entry
description: At most one work-item reference is allowed and it must belong to the same project.
x-required-profession: engineering
x-required-permissions: [engineering.time_entries.create]
x-audit-action: engineering.time_entry.created
parameters:
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/IdempotencyKey'
requestBody: {required: true, content: {application/json: {schema: {$ref: '#/components/schemas/CreateEngineeringTimeEntryRequest'}}}}
responses:
'201':
description: Time entry created with immutable billing snapshot.
headers: {Location: {$ref: '#/components/headers/Location'}, ETag: {$ref: '#/components/headers/ETag'}}
content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringTimeEntryResponse'}}}
'409': {$ref: '#/components/responses/Conflict'}
'422': {$ref: '#/components/responses/ValidationError'}
/engineering/time-entries/{timeEntryId}:
parameters:
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/TimeEntryId'
get:
tags: [Engineering Time Entries]
operationId: getEngineeringTimeEntry
summary: Retrieve a time entry
x-required-profession: engineering
x-required-permissions: [engineering.time_entries.read]
responses:
'200':
description: Time entry.
headers: {ETag: {$ref: '#/components/headers/ETag'}}
content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringTimeEntryResponse'}}}
'404': {$ref: '#/components/responses/NotFound'}
patch:
tags: [Engineering Time Entries]
operationId: updateEngineeringTimeEntry
summary: Update an uninvoiced time entry
description: Once referenced by issued billing, financial snapshot fields and duration are immutable.
x-required-profession: engineering
x-required-permissions: [engineering.time_entries.update]
x-audit-action: engineering.time_entry.updated
parameters:
- $ref: '#/components/parameters/IfMatch'
- $ref: '#/components/parameters/IdempotencyKey'
requestBody: {required: true, content: {application/merge-patch+json: {schema: {$ref: '#/components/schemas/UpdateEngineeringTimeEntryRequest'}}}}
responses:
'200':
description: Time entry updated.
headers: {ETag: {$ref: '#/components/headers/ETag'}}
content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringTimeEntryResponse'}}}
'409': {$ref: '#/components/responses/Conflict'}
'428': {$ref: '#/components/responses/PreconditionRequired'}
'422': {$ref: '#/components/responses/ValidationError'}
/engineering/time-entries/batch/submit:
post:
tags: [Engineering Time Entries]
operationId: batchCreateEngineeringTimeEntries
summary: Create up to 100 time entries synchronously
description: Atomic mode rolls back all entries; partial mode returns per-item results.
x-required-profession: engineering
x-required-permissions: [engineering.time_entries.create]
x-audit-action: engineering.time_entries.batch_created
parameters:
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/IdempotencyKey'
requestBody: {required: true, content: {application/json: {schema: {$ref: '#/components/schemas/BatchCreateEngineeringTimeEntriesRequest'}}}}
responses:
'200': {description: Batch processed., content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringTimeEntryBatchResponse'}}}}
'409': {$ref: '#/components/responses/Conflict'}
'422': {$ref: '#/components/responses/ValidationError'}
/engineering/projects/{projectId}/budgets:
parameters:
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/ProjectId'
get:
tags: [Engineering Project Budgets]
operationId: listEngineeringProjectBudgets
summary: List project budgets
x-required-profession: engineering
x-required-permissions: [engineering.budgets.read]
responses:
'200': {description: Budgets., content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringProjectBudgetCollectionResponse'}}}}
'404': {$ref: '#/components/responses/NotFound'}
post:
tags: [Engineering Project Budgets]
operationId: createEngineeringProjectBudget
summary: Create a draft project budget
x-required-profession: engineering
x-required-permissions: [engineering.budgets.manage]
x-audit-action: engineering.budget.created
parameters:
- $ref: '#/components/parameters/IdempotencyKey'
requestBody: {required: true, content: {application/json: {schema: {$ref: '#/components/schemas/CreateEngineeringProjectBudgetRequest'}}}}
responses:
'201':
description: Draft budget created.
headers: {Location: {$ref: '#/components/headers/Location'}, ETag: {$ref: '#/components/headers/ETag'}}
content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringProjectBudgetResponse'}}}
'409': {$ref: '#/components/responses/Conflict'}
'422': {$ref: '#/components/responses/ValidationError'}
/engineering/budgets/{budgetId}:
parameters:
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/BudgetId'
get:
tags: [Engineering Project Budgets]
operationId: getEngineeringProjectBudget
summary: Retrieve a project budget
x-required-profession: engineering
x-required-permissions: [engineering.budgets.read]
responses:
'200':
description: Budget.
headers: {ETag: {$ref: '#/components/headers/ETag'}}
content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringProjectBudgetResponse'}}}
'404': {$ref: '#/components/responses/NotFound'}
patch:
tags: [Engineering Project Budgets]
operationId: updateEngineeringProjectBudget
summary: Update a draft budget
x-required-profession: engineering
x-required-permissions: [engineering.budgets.manage]
x-audit-action: engineering.budget.updated
parameters:
- $ref: '#/components/parameters/IfMatch'
- $ref: '#/components/parameters/IdempotencyKey'
requestBody: {required: true, content: {application/merge-patch+json: {schema: {$ref: '#/components/schemas/UpdateEngineeringProjectBudgetRequest'}}}}
responses:
'200':
description: Budget updated.
headers: {ETag: {$ref: '#/components/headers/ETag'}}
content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringProjectBudgetResponse'}}}
'409': {$ref: '#/components/responses/Conflict'}
'428': {$ref: '#/components/responses/PreconditionRequired'}
/engineering/budgets/{budgetId}/approve:
post:
tags: [Engineering Project Budgets]
operationId: approveEngineeringProjectBudget
summary: Approve and freeze a draft budget
x-required-profession: engineering
x-required-permissions: [engineering.budgets.approve]
x-audit-action: engineering.budget.approved
parameters:
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/BudgetId'
- $ref: '#/components/parameters/IfMatch'
- $ref: '#/components/parameters/IdempotencyKey'
requestBody: {required: true, content: {application/json: {schema: {$ref: '#/components/schemas/ApproveEngineeringBudgetCommand'}}}}
responses:
'200':
description: Budget approved and frozen.
headers: {ETag: {$ref: '#/components/headers/ETag'}}
content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringProjectBudgetResponse'}}}
'409': {$ref: '#/components/responses/Conflict'}
'428': {$ref: '#/components/responses/PreconditionRequired'}
'422': {$ref: '#/components/responses/ValidationError'}
/engineering/budgets/{budgetId}/items:
parameters:
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/BudgetId'
get:
tags: [Engineering Project Budgets]
operationId: listEngineeringProjectBudgetItems
summary: List budget items
x-required-profession: engineering
x-required-permissions: [engineering.budgets.read]
responses:
'200': {description: Budget items., content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringProjectBudgetItemCollectionResponse'}}}}
'404': {$ref: '#/components/responses/NotFound'}
post:
tags: [Engineering Project Budgets]
operationId: createEngineeringProjectBudgetItem
summary: Add an item to a draft budget
x-required-profession: engineering
x-required-permissions: [engineering.budgets.manage]
x-audit-action: engineering.budget.item_created
parameters:
- $ref: '#/components/parameters/IdempotencyKey'
requestBody: {required: true, content: {application/json: {schema: {$ref: '#/components/schemas/CreateEngineeringProjectBudgetItemRequest'}}}}
responses:
'201':
description: Budget item created.
headers: {Location: {$ref: '#/components/headers/Location'}, ETag: {$ref: '#/components/headers/ETag'}}
content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringProjectBudgetItemResponse'}}}
'409': {$ref: '#/components/responses/Conflict'}
'422': {$ref: '#/components/responses/ValidationError'}
/engineering/budgets/{budgetId}/projection:
get:
tags: [Engineering Project Budgets]
operationId: getEngineeringProjectBudgetProjection
summary: Retrieve derived allocations, commitments, actuals, and variance
description: Projection values are derived from authoritative records and are not independently mutable.
x-required-profession: engineering
x-required-permissions: [engineering.budgets.read]
parameters:
- $ref: '#/components/parameters/OrganizationContext'
- $ref: '#/components/parameters/RequestId'
- $ref: '#/components/parameters/BudgetId'
responses:
'200': {description: Derived budget projection., content: {application/json: {schema: {$ref: '#/components/schemas/EngineeringBudgetProjectionResponse'}}}}
'404': {$ref: '#/components/responses/NotFound'}
components:
securitySchemes:
bearerAuth:
type: http
scheme: bearer
bearerFormat: JWT
parameters:
OrganizationContext:
name: X-Organization-Id
in: header
required: true
description: Active organization context for the tenant-scoped request.
schema:
$ref: '#/components/schemas/Uuid'
RequestId:
name: X-Request-Id
in: header
required: false
description: Client-generated request identifier. The server generates one when omitted.
schema:
$ref: '#/components/schemas/Uuid'
IdempotencyKey:
name: Idempotency-Key
in: header
required: true
description: |
Unique key for replay-safe execution. Reuse with a different normalized request
returns `IDEMPOTENCY_KEY_CONFLICT`.
schema:
type: string
minLength: 16
maxLength: 128
IfMatch:
name: If-Match
in: header
required: true
description: ETag returned by the latest representation of the resource.
schema:
type: string
minLength: 3
maxLength: 128
Limit:
name: limit
in: query
required: false
schema:
type: integer
minimum: 1
maximum: 100
default: 25
Cursor:
name: cursor
in: query
required: false
schema:
type: string
minLength: 1
maxLength: 2048
OrganizationId:
name: organizationId
in: path
required: true
schema:
$ref: '#/components/schemas/Uuid'
SessionId:
name: sessionId
in: path
required: true
schema:
$ref: '#/components/schemas/Uuid'
InvitationId:
name: invitationId
in: path
required: true
schema:
$ref: '#/components/schemas/Uuid'
MembershipId:
name: membershipId
in: path
required: true
schema:
$ref: '#/components/schemas/Uuid'
RoleId:
name: roleId
in: path
required: true
schema:
$ref: '#/components/schemas/Uuid'
ClientId:
name: clientId
in: path
required: true
schema:
$ref: '#/components/schemas/Uuid'
ContactId:
name: contactId
in: path
required: true
schema:
$ref: '#/components/schemas/Uuid'
ProjectId:
name: projectId
in: path
required: true
schema:
$ref: '#/components/schemas/Uuid'
ProjectMemberId:
name: memberId
in: path
required: true
schema:
$ref: '#/components/schemas/Uuid'
TaskId:
name: taskId
in: path
required: true
schema:
$ref: '#/components/schemas/Uuid'
SiteId:
name: siteId
in: path
required: true
schema: {$ref: '#/components/schemas/Uuid'}
DocumentId:
name: documentId
in: path
required: true
schema: {$ref: '#/components/schemas/Uuid'}
UploadId:
name: uploadId
in: path
required: true
schema: {$ref: '#/components/schemas/Uuid'}
DocumentLinkId:
name: documentLinkId
in: path
required: true
schema: {$ref: '#/components/schemas/Uuid'}
DesignId:
name: designId
in: path
required: true
schema: {$ref: '#/components/schemas/Uuid'}
DesignVersionId:
name: designVersionId
in: path
required: true
schema: {$ref: '#/components/schemas/Uuid'}
InspectionId:
name: inspectionId
in: path
required: true
schema: {$ref: '#/components/schemas/Uuid'}
FindingId:
name: findingId
in: path
required: true
schema: {$ref: '#/components/schemas/Uuid'}
FollowupId:
name: followupId
in: path
required: true
schema: {$ref: '#/components/schemas/Uuid'}
SpecificationId:
name: specificationId
in: path
required: true
schema: {$ref: '#/components/schemas/Uuid'}
PhaseId:
name: phaseId
in: path
required: true
schema: {$ref: '#/components/schemas/Uuid'}
TimeEntryId:
name: timeEntryId
in: path
required: true
schema: {$ref: '#/components/schemas/Uuid'}
BudgetId:
name: budgetId
in: path
required: true
schema: {$ref: '#/components/schemas/Uuid'}
headers:
RequestId:
description: Request identifier used for logs, audit, and diagnostics.
schema:
$ref: '#/components/schemas/Uuid'
ETag:
description: Strong validator for optimistic concurrency.
schema:
type: string
examples: ['"6"']
Location:
description: Canonical URI of the created resource.
schema:
type: string
format: uri-reference
RetryAfter:
description: Seconds or HTTP date after which the client may retry.
schema:
oneOf:
- type: integer
minimum: 0
- type: string
responses:
BadRequest:
description: Request is malformed or required organization context is missing.
headers:
X-Request-Id:
$ref: '#/components/headers/RequestId'
content:
application/problem+json:
schema:
$ref: '#/components/schemas/Problem'
examples:
organizationContextRequired:
value:
type: https://api.example.com/problems/organization-context-required
title: Organization context required
status: 400
detail: X-Organization-Id is required for this operation.
code: ORGANIZATION_CONTEXT_REQUIRED
requestId: 0193c0a0-7c1e-7b3a-8c4d-6e5f4a3b2cff
Unauthorized:
description: Authentication is missing, invalid, expired, or revoked.
headers:
X-Request-Id:
$ref: '#/components/headers/RequestId'
content:
application/problem+json:
schema:
$ref: '#/components/schemas/Problem'
examples:
invalidToken:
value:
type: https://api.example.com/problems/auth-token-invalid
title: Authentication failed
status: 401
detail: The access token is invalid.
code: AUTH_TOKEN_INVALID
requestId: 0193c0a0-7c1e-7b3a-8c4d-6e5f4a3b2cff
Forbidden:
description: The authenticated actor is not permitted to perform the operation.
headers:
X-Request-Id:
$ref: '#/components/headers/RequestId'
content:
application/problem+json:
schema:
$ref: '#/components/schemas/Problem'
NotFound:
description: Resource not found, including cross-tenant resource access.
headers:
X-Request-Id:
$ref: '#/components/headers/RequestId'
content:
application/problem+json:
schema:
$ref: '#/components/schemas/Problem'
examples:
notFound:
value:
type: https://api.example.com/problems/resource-not-found
title: Resource not found
status: 404
detail: The requested resource was not found.
code: RESOURCE_NOT_FOUND
requestId: 0193c0a0-7c1e-7b3a-8c4d-6e5f4a3b2cff
Conflict:
description: Conflict with an existing resource, state, idempotency record, or version.
headers:
X-Request-Id:
$ref: '#/components/headers/RequestId'
content:
application/problem+json:
schema:
$ref: '#/components/schemas/Problem'
ValidationError:
description: Request is structurally valid but fails field or business validation.
headers:
X-Request-Id:
$ref: '#/components/headers/RequestId'
content:
application/problem+json:
schema:
$ref: '#/components/schemas/Problem'
examples:
invalidEmail:
value:
type: https://api.example.com/problems/validation-error
title: Request validation failed
status: 422
detail: One or more fields are invalid.
code: VALIDATION_ERROR
requestId: 0193c0a0-7c1e-7b3a-8c4d-6e5f4a3b2cff
errors:
- field: email
code: INVALID_FORMAT
message: Must be a valid email address.
PreconditionRequired:
description: "`If-Match` is required for this mutation."
headers:
X-Request-Id:
$ref: '#/components/headers/RequestId'
content:
application/problem+json:
schema:
$ref: '#/components/schemas/Problem'
RateLimited:
description: Request rate limit exceeded.
headers:
Retry-After:
$ref: '#/components/headers/RetryAfter'
X-Request-Id:
$ref: '#/components/headers/RequestId'
content:
application/problem+json:
schema:
$ref: '#/components/schemas/Problem'
schemas:
Uuid:
type: string
format: uuid
description: UUIDv7 serialized in canonical lowercase form.
examples: [0193c0a0-7c1e-7b3a-8c4d-6e5f4a3b2c1d]
Timestamp:
type: string
format: date-time
examples: ['2026-08-26T12:00:00Z']
Date:
type: string
format: date
examples: ['2026-08-26']
Email:
type: string
format: email
maxLength: 320
CountryCode:
type: string
pattern: '^[A-Z]{2}$'
examples: [MA]
CurrencyCode:
type: string
pattern: '^[A-Z]{3}$'
examples: [MAD]
EngineeringSite:
type: object
additionalProperties: false
required: [id, organizationId, projectId, name, address, version, createdAt, updatedAt]
properties:
id: {$ref: '#/components/schemas/Uuid'}
organizationId: {$ref: '#/components/schemas/Uuid'}
projectId: {$ref: '#/components/schemas/Uuid'}
name: {type: string, minLength: 1, maxLength: 200}
address: {$ref: '#/components/schemas/EngineeringSiteAddress'}
latitude: {type: [number, 'null'], minimum: -90, maximum: 90}
longitude: {type: [number, 'null'], minimum: -180, maximum: 180}
version: {type: integer, minimum: 1}
createdAt: {$ref: '#/components/schemas/Timestamp'}
updatedAt: {$ref: '#/components/schemas/Timestamp'}
EngineeringSiteAddress:
type: object
additionalProperties: false
required: [line1, city, countryCode]
properties:
line1: {type: string, minLength: 1, maxLength: 200}
line2: {type: [string, 'null'], maxLength: 200}
city: {type: string, minLength: 1, maxLength: 120}
region: {type: [string, 'null'], maxLength: 120}
postalCode: {type: [string, 'null'], maxLength: 32}
countryCode: {$ref: '#/components/schemas/CountryCode'}
CreateEngineeringSiteRequest:
type: object
additionalProperties: false
required: [name, address]
properties:
name: {type: string, minLength: 1, maxLength: 200}
address: {$ref: '#/components/schemas/EngineeringSiteAddress'}
latitude: {type: [number, 'null'], minimum: -90, maximum: 90}
longitude: {type: [number, 'null'], minimum: -180, maximum: 180}
UpdateEngineeringSiteRequest:
type: object
additionalProperties: false
minProperties: 1
properties:
name: {type: string, minLength: 1, maxLength: 200}
address: {$ref: '#/components/schemas/EngineeringSiteAddress'}
latitude: {type: [number, 'null'], minimum: -90, maximum: 90}
longitude: {type: [number, 'null'], minimum: -180, maximum: 180}
EngineeringSiteResponse:
type: object
required: [data]
properties: {data: {$ref: '#/components/schemas/EngineeringSite'}}
EngineeringSiteCollectionResponse:
type: object
required: [data, meta]
properties:
data: {type: array, items: {$ref: '#/components/schemas/EngineeringSite'}}
meta: {$ref: '#/components/schemas/CollectionMeta'}
DocumentClassification:
type: string
enum: [public, internal, confidential, restricted, regulated]
DocumentUploadStatus:
type: string
enum: [initialized, uploading, completed, failed, aborted, expired]
MalwareScanStatus:
type: string
enum: [not_started, pending, scanning, clean, infected, failed]
Document:
type: object
additionalProperties: false
required: [id, organizationId, name, classification, currentVersionId, version, createdByUserId, createdAt, updatedAt]
properties:
id: {$ref: '#/components/schemas/Uuid'}
organizationId: {$ref: '#/components/schemas/Uuid'}
name: {type: string, minLength: 1, maxLength: 255}
categoryId: {oneOf: [{$ref: '#/components/schemas/Uuid'}, {type: 'null'}]}
classification: {$ref: '#/components/schemas/DocumentClassification'}
retentionPolicyId: {oneOf: [{$ref: '#/components/schemas/Uuid'}, {type: 'null'}]}
currentVersionId: {oneOf: [{$ref: '#/components/schemas/Uuid'}, {type: 'null'}]}
currentVersion: {oneOf: [{$ref: '#/components/schemas/DocumentVersion'}, {type: 'null'}]}
version: {type: integer, minimum: 1}
createdByUserId: {$ref: '#/components/schemas/Uuid'}
createdAt: {$ref: '#/components/schemas/Timestamp'}
updatedAt: {$ref: '#/components/schemas/Timestamp'}
DocumentVersion:
type: object
additionalProperties: false
required: [id, organizationId, documentId, versionNumber, mimeType, sizeBytes, uploadStatus, scanStatus, uploadedByUserId, createdAt]
properties:
id: {$ref: '#/components/schemas/Uuid'}
organizationId: {$ref: '#/components/schemas/Uuid'}
documentId: {$ref: '#/components/schemas/Uuid'}
versionNumber: {type: integer, minimum: 1}
mimeType: {type: string, minLength: 1, maxLength: 255}
sizeBytes: {type: integer, minimum: 1, maximum: 5368709120}
contentHash: {type: [string, 'null'], pattern: '^sha256:[a-f0-9]{64}$'}
hashAlgorithm: {type: string, const: sha256}
uploadStatus: {$ref: '#/components/schemas/DocumentUploadStatus'}
scanStatus: {$ref: '#/components/schemas/MalwareScanStatus'}
scanCompletedAt: {type: [string, 'null'], format: date-time}
available: {type: boolean, readOnly: true, description: True only when uploadStatus is completed and scanStatus is clean.}
uploadedByUserId: {$ref: '#/components/schemas/Uuid'}
createdAt: {$ref: '#/components/schemas/Timestamp'}
InitializeDocumentUploadRequest:
type: object
additionalProperties: false
required: [name, classification, mimeType, sizeBytes]
properties:
name: {type: string, minLength: 1, maxLength: 255}
categoryId: {oneOf: [{$ref: '#/components/schemas/Uuid'}, {type: 'null'}]}
classification: {$ref: '#/components/schemas/DocumentClassification'}
retentionPolicyId: {oneOf: [{$ref: '#/components/schemas/Uuid'}, {type: 'null'}]}
mimeType: {type: string, minLength: 1, maxLength: 255}
sizeBytes: {type: integer, minimum: 1, maximum: 5368709120}
contentHash: {type: [string, 'null'], pattern: '^sha256:[a-f0-9]{64}$'}
InitializeDocumentVersionRequest:
type: object
additionalProperties: false
required: [mimeType, sizeBytes]
properties:
mimeType: {type: string, minLength: 1, maxLength: 255}
sizeBytes: {type: integer, minimum: 1, maximum: 5368709120}
contentHash: {type: [string, 'null'], pattern: '^sha256:[a-f0-9]{64}$'}
UpdateDocumentRequest:
type: object
additionalProperties: false
minProperties: 1
properties:
name: {type: string, minLength: 1, maxLength: 255}
categoryId: {oneOf: [{$ref: '#/components/schemas/Uuid'}, {type: 'null'}]}
classification: {$ref: '#/components/schemas/DocumentClassification'}
retentionPolicyId: {oneOf: [{$ref: '#/components/schemas/Uuid'}, {type: 'null'}]}
CompleteDocumentUploadRequest:
type: object
additionalProperties: false
required: [documentVersionId, contentHash]
properties:
documentVersionId: {$ref: '#/components/schemas/Uuid'}
contentHash: {type: string, pattern: '^sha256:[a-f0-9]{64}$'}
InitializeDocumentUploadData:
type: object
additionalProperties: false
required: [documentId, documentVersionId, uploadUrl, expiresAt]
properties:
documentId: {$ref: '#/components/schemas/Uuid'}
documentVersionId: {$ref: '#/components/schemas/Uuid'}
uploadUrl: {type: string, format: uri}
requiredHeaders: {type: object, additionalProperties: {type: string}}
expiresAt: {$ref: '#/components/schemas/Timestamp'}
InitializeDocumentUploadResponse:
type: object
required: [data]
properties: {data: {$ref: '#/components/schemas/InitializeDocumentUploadData'}}
InitializeMultipartUploadData:
type: object
additionalProperties: false
required: [documentId, documentVersionId, uploadId, recommendedPartSizeBytes, expiresAt]
properties:
documentId: {$ref: '#/components/schemas/Uuid'}
documentVersionId: {$ref: '#/components/schemas/Uuid'}
uploadId: {$ref: '#/components/schemas/Uuid'}
recommendedPartSizeBytes: {type: integer, minimum: 5242880}
expiresAt: {$ref: '#/components/schemas/Timestamp'}
InitializeMultipartUploadResponse:
type: object
required: [data]
properties: {data: {$ref: '#/components/schemas/InitializeMultipartUploadData'}}
MultipartPartUrlsRequest:
type: object
additionalProperties: false
required: [partNumbers]
properties:
partNumbers:
type: array
minItems: 1
maxItems: 100
uniqueItems: true
items: {type: integer, minimum: 1, maximum: 10000}
MultipartPartUploadUrl:
type: object
required: [partNumber, uploadUrl, expiresAt]
properties:
partNumber: {type: integer, minimum: 1}
uploadUrl: {type: string, format: uri}
expiresAt: {$ref: '#/components/schemas/Timestamp'}
MultipartPartUrlsResponse:
type: object
required: [data]
properties: {data: {type: array, items: {$ref: '#/components/schemas/MultipartPartUploadUrl'}}}
CompletedMultipartPart:
type: object
additionalProperties: false
required: [partNumber, etag]
properties:
partNumber: {type: integer, minimum: 1}
etag: {type: string, minLength: 1, maxLength: 200}
CompleteMultipartUploadRequest:
type: object
additionalProperties: false
required: [parts, contentHash]
properties:
parts:
type: array
minItems: 1
maxItems: 10000
items: {$ref: '#/components/schemas/CompletedMultipartPart'}
contentHash: {type: string, pattern: '^sha256:[a-f0-9]{64}$'}
CreateDocumentDownloadRequest:
type: object
additionalProperties: false
properties:
documentVersionId: {oneOf: [{$ref: '#/components/schemas/Uuid'}, {type: 'null'}], description: Defaults to the current clean version.}
DocumentDownloadData:
type: object
required: [downloadUrl, expiresAt]
properties:
downloadUrl: {type: string, format: uri}
expiresAt: {$ref: '#/components/schemas/Timestamp'}
DocumentDownloadResponse:
type: object
required: [data]
properties: {data: {$ref: '#/components/schemas/DocumentDownloadData'}}
DocumentResponse:
type: object
required: [data]
properties: {data: {$ref: '#/components/schemas/Document'}}
DocumentCollectionResponse:
type: object
required: [data, meta]
properties:
data: {type: array, items: {$ref: '#/components/schemas/Document'}}
meta: {$ref: '#/components/schemas/CollectionMeta'}
DocumentVersionResponse:
type: object
required: [data]
properties: {data: {$ref: '#/components/schemas/DocumentVersion'}}
DocumentVersionCollectionResponse:
type: object
required: [data]
properties: {data: {type: array, items: {$ref: '#/components/schemas/DocumentVersion'}}}
EngineeringProjectDocumentLink:
type: object
additionalProperties: false
required: [id, organizationId, projectId, documentId, category, linkedByUserId, linkedAt]
properties:
id: {$ref: '#/components/schemas/Uuid'}
organizationId: {$ref: '#/components/schemas/Uuid'}
projectId: {$ref: '#/components/schemas/Uuid'}
documentId: {$ref: '#/components/schemas/Uuid'}
category: {type: string, minLength: 1, maxLength: 100}
document: {$ref: '#/components/schemas/Document'}
linkedByUserId: {$ref: '#/components/schemas/Uuid'}
linkedAt: {$ref: '#/components/schemas/Timestamp'}
unlinkedAt: {type: [string, 'null'], format: date-time}
LinkEngineeringProjectDocumentRequest:
type: object
additionalProperties: false
required: [documentId, category]
properties:
documentId: {$ref: '#/components/schemas/Uuid'}
category: {type: string, minLength: 1, maxLength: 100}
EngineeringProjectDocumentResponse:
type: object
required: [data]
properties: {data: {$ref: '#/components/schemas/EngineeringProjectDocumentLink'}}
EngineeringProjectDocumentCollectionResponse:
type: object
required: [data]
properties: {data: {type: array, items: {$ref: '#/components/schemas/EngineeringProjectDocumentLink'}}}
EngineeringDesignStatus:
type: string
enum: [draft, under_review, changes_requested, approved, rejected, cancelled, withdrawn, superseded]
EngineeringDesignAssignmentRole:
type: string
enum: [owner, preparer, reviewer, contributor]
EngineeringDesignDocumentRole:
type: string
enum: [primary_drawing, calculation, supporting_document, specification, attachment]
description: Domain-specific registry independent from specification document roles.
EngineeringDesignReviewStatus:
type: string
enum: [approved, changes_requested, rejected]
EngineeringDesign:
type: object
additionalProperties: false
required: [id, organizationId, projectId, designNumber, title, discipline, status, ownerUserId, preparedByUserId, currentVersionId, version, createdAt, updatedAt]
properties:
id: {$ref: '#/components/schemas/Uuid'}
organizationId: {$ref: '#/components/schemas/Uuid'}
projectId: {$ref: '#/components/schemas/Uuid'}
designNumber: {type: string, minLength: 1, maxLength: 64}
title: {type: string, minLength: 1, maxLength: 300}
description: {type: [string, 'null'], maxLength: 10000}
discipline: {type: string, minLength: 1, maxLength: 100}
status: {$ref: '#/components/schemas/EngineeringDesignStatus'}
ownerUserId: {$ref: '#/components/schemas/Uuid'}
preparedByUserId: {$ref: '#/components/schemas/Uuid'}
currentVersionId: {$ref: '#/components/schemas/Uuid'}
approvedVersionId: {oneOf: [{$ref: '#/components/schemas/Uuid'}, {type: 'null'}]}
approvedByUserId: {oneOf: [{$ref: '#/components/schemas/Uuid'}, {type: 'null'}]}
approvedAt: {type: [string, 'null'], format: date-time}
supersededByDesignId: {oneOf: [{$ref: '#/components/schemas/Uuid'}, {type: 'null'}]}
version: {type: integer, minimum: 1}
createdAt: {$ref: '#/components/schemas/Timestamp'}
updatedAt: {$ref: '#/components/schemas/Timestamp'}
CreateEngineeringDesignRequest:
type: object
additionalProperties: false
required: [designNumber, title, discipline, ownerUserId, preparedByUserId]
properties:
designNumber: {type: string, minLength: 1, maxLength: 64}
title: {type: string, minLength: 1, maxLength: 300}
description: {type: [string, 'null'], maxLength: 10000}
discipline: {type: string, minLength: 1, maxLength: 100}
ownerUserId: {$ref: '#/components/schemas/Uuid'}
preparedByUserId: {$ref: '#/components/schemas/Uuid'}
UpdateEngineeringDesignRequest:
type: object
additionalProperties: false
minProperties: 1
properties:
title: {type: string, minLength: 1, maxLength: 300}
description: {type: [string, 'null'], maxLength: 10000}
discipline: {type: string, minLength: 1, maxLength: 100}
EngineeringDesignResponse:
type: object
required: [data]
properties: {data: {$ref: '#/components/schemas/EngineeringDesign'}}
EngineeringDesignCollectionResponse:
type: object
required: [data, meta]
properties:
data: {type: array, items: {$ref: '#/components/schemas/EngineeringDesign'}}
meta: {$ref: '#/components/schemas/CollectionMeta'}
OptionalDesignReasonCommand:
type: object
additionalProperties: false
properties:
reason: {type: string, minLength: 3, maxLength: 1000}
DesignReasonCommand:
type: object
additionalProperties: false
required: [reason]
properties:
reason: {type: string, minLength: 3, maxLength: 1000}
DesignDecisionCommand:
type: object
additionalProperties: false
required: [designVersionId, reason]
properties:
designVersionId: {$ref: '#/components/schemas/Uuid'}
reason: {type: string, minLength: 3, maxLength: 2000}
ApproveDesignCommand:
type: object
additionalProperties: false
required: [designVersionId, attestation]
properties:
designVersionId: {$ref: '#/components/schemas/Uuid'}
attestation: {type: string, minLength: 10, maxLength: 2000}
SupersedeDesignCommand:
type: object
additionalProperties: false
required: [replacementDesignId, reason]
properties:
replacementDesignId: {$ref: '#/components/schemas/Uuid'}
reason: {type: string, minLength: 3, maxLength: 1000}
EngineeringDesignAssignment:
type: object
additionalProperties: false
required: [id, organizationId, designId, userId, assignmentRole, assignedByUserId, assignedAt]
properties:
id: {$ref: '#/components/schemas/Uuid'}
organizationId: {$ref: '#/components/schemas/Uuid'}
designId: {$ref: '#/components/schemas/Uuid'}
userId: {$ref: '#/components/schemas/Uuid'}
assignmentRole: {$ref: '#/components/schemas/EngineeringDesignAssignmentRole'}
notes: {type: [string, 'null'], maxLength: 2000}
assignedByUserId: {$ref: '#/components/schemas/Uuid'}
assignedAt: {$ref: '#/components/schemas/Timestamp'}
unassignedAt: {type: [string, 'null'], format: date-time}
AssignEngineeringDesignRequest:
type: object
additionalProperties: false
required: [userId, assignmentRole]
properties:
userId: {$ref: '#/components/schemas/Uuid'}
assignmentRole: {$ref: '#/components/schemas/EngineeringDesignAssignmentRole'}
notes: {type: [string, 'null'], maxLength: 2000}
UnassignEngineeringDesignRequest:
type: object
additionalProperties: false
required: [assignmentId]
properties:
assignmentId: {$ref: '#/components/schemas/Uuid'}
reason: {type: [string, 'null'], maxLength: 1000}
EngineeringDesignAssignmentResponse:
type: object
required: [data]
properties: {data: {$ref: '#/components/schemas/EngineeringDesignAssignment'}}
EngineeringDesignAssignmentCollectionResponse:
type: object
required: [data]
properties: {data: {type: array, items: {$ref: '#/components/schemas/EngineeringDesignAssignment'}}}
EngineeringDesignVersion:
type: object
additionalProperties: false
required: [id, organizationId, designId, versionNumber, createdByUserId, createdAt]
properties:
id: {$ref: '#/components/schemas/Uuid'}
organizationId: {$ref: '#/components/schemas/Uuid'}
designId: {$ref: '#/components/schemas/Uuid'}
versionNumber: {type: integer, minimum: 1}
changeSummary: {type: [string, 'null'], maxLength: 2000}
createdByUserId: {$ref: '#/components/schemas/Uuid'}
createdAt: {$ref: '#/components/schemas/Timestamp'}
CreateEngineeringDesignVersionRequest:
type: object
additionalProperties: false
properties:
changeSummary: {type: [string, 'null'], maxLength: 2000}
EngineeringDesignVersionResponse:
type: object
required: [data]
properties: {data: {$ref: '#/components/schemas/EngineeringDesignVersion'}}
EngineeringDesignVersionCollectionResponse:
type: object
required: [data]
properties: {data: {type: array, items: {$ref: '#/components/schemas/EngineeringDesignVersion'}}}
EngineeringDesignVersionDocument:
type: object
additionalProperties: false
required: [id, organizationId, designVersionId, documentId, documentRole, linkedByUserId, linkedAt]
properties:
id: {$ref: '#/components/schemas/Uuid'}
organizationId: {$ref: '#/components/schemas/Uuid'}
designVersionId: {$ref: '#/components/schemas/Uuid'}
documentId: {$ref: '#/components/schemas/Uuid'}
documentRole: {$ref: '#/components/schemas/EngineeringDesignDocumentRole'}
document: {$ref: '#/components/schemas/Document'}
linkedByUserId: {$ref: '#/components/schemas/Uuid'}
linkedAt: {$ref: '#/components/schemas/Timestamp'}
unlinkedAt: {type: [string, 'null'], format: date-time}
LinkEngineeringDesignVersionDocumentRequest:
type: object
additionalProperties: false
required: [documentId, documentRole]
properties:
documentId: {$ref: '#/components/schemas/Uuid'}
documentRole: {$ref: '#/components/schemas/EngineeringDesignDocumentRole'}
EngineeringDesignVersionDocumentResponse:
type: object
required: [data]
properties: {data: {$ref: '#/components/schemas/EngineeringDesignVersionDocument'}}
EngineeringDesignVersionDocumentCollectionResponse:
type: object
required: [data]
properties: {data: {type: array, items: {$ref: '#/components/schemas/EngineeringDesignVersionDocument'}}}
EngineeringDesignReview:
type: object
additionalProperties: false
required: [id, organizationId, designId, designVersionId, reviewerUserId, status, comments, reviewedAt, createdAt]
properties:
id: {$ref: '#/components/schemas/Uuid'}
organizationId: {$ref: '#/components/schemas/Uuid'}
designId: {$ref: '#/components/schemas/Uuid'}
designVersionId: {$ref: '#/components/schemas/Uuid'}
reviewerUserId: {$ref: '#/components/schemas/Uuid'}
status: {$ref: '#/components/schemas/EngineeringDesignReviewStatus'}
comments: {type: string, minLength: 1, maxLength: 10000}
reviewedAt: {$ref: '#/components/schemas/Timestamp'}
createdAt: {$ref: '#/components/schemas/Timestamp'}
CreateEngineeringDesignReviewRequest:
type: object
additionalProperties: false
required: [designVersionId, status, comments]
properties:
designVersionId: {$ref: '#/components/schemas/Uuid'}
status: {$ref: '#/components/schemas/EngineeringDesignReviewStatus'}
comments: {type: string, minLength: 1, maxLength: 10000}
EngineeringDesignReviewResponse:
type: object
required: [data]
properties: {data: {$ref: '#/components/schemas/EngineeringDesignReview'}}
EngineeringDesignReviewCollectionResponse:
type: object
required: [data]
properties: {data: {type: array, items: {$ref: '#/components/schemas/EngineeringDesignReview'}}}
EngineeringInspectionStatus:
type: string
enum: [draft, scheduled, in_progress, completed, cancelled]
EngineeringInspectionOutcome:
type: string
enum: [passed, passed_with_observations, followup_required, failed]
EngineeringInspectionFindingSeverity:
type: string
enum: [observation, minor, major, critical]
EngineeringInspectionFindingStatus:
type: string
enum: [open, in_progress, resolved, accepted_risk]
EngineeringInspection:
type: object
additionalProperties: false
required: [id, organizationId, projectId, siteId, inspectionType, inspectorUserId, status, version, createdAt, updatedAt]
properties:
id: {$ref: '#/components/schemas/Uuid'}
organizationId: {$ref: '#/components/schemas/Uuid'}
projectId: {$ref: '#/components/schemas/Uuid'}
siteId: {$ref: '#/components/schemas/Uuid'}
inspectionType: {type: string, minLength: 1, maxLength: 100, description: Controlled application registry key.}
inspectorUserId: {$ref: '#/components/schemas/Uuid'}
status: {$ref: '#/components/schemas/EngineeringInspectionStatus'}
outcome: {oneOf: [{$ref: '#/components/schemas/EngineeringInspectionOutcome'}, {type: 'null'}]}
scheduledAt: {type: [string, 'null'], format: date-time}
startedAt: {type: [string, 'null'], format: date-time}
performedAt: {type: [string, 'null'], format: date-time}
cancelledAt: {type: [string, 'null'], format: date-time}
summary: {type: [string, 'null'], maxLength: 10000}
version: {type: integer, minimum: 1}
createdAt: {$ref: '#/components/schemas/Timestamp'}
updatedAt: {$ref: '#/components/schemas/Timestamp'}
CreateEngineeringInspectionRequest:
type: object
additionalProperties: false
required: [siteId, inspectionType, inspectorUserId]
properties:
siteId: {$ref: '#/components/schemas/Uuid'}
inspectionType: {type: string, minLength: 1, maxLength: 100}
inspectorUserId: {$ref: '#/components/schemas/Uuid'}
summary: {type: [string, 'null'], maxLength: 10000}
UpdateEngineeringInspectionRequest:
type: object
additionalProperties: false
minProperties: 1
properties:
siteId: {$ref: '#/components/schemas/Uuid'}
inspectionType: {type: string, minLength: 1, maxLength: 100}
inspectorUserId: {$ref: '#/components/schemas/Uuid'}
summary: {type: [string, 'null'], maxLength: 10000}
EngineeringInspectionResponse:
type: object
required: [data]
properties: {data: {$ref: '#/components/schemas/EngineeringInspection'}}
EngineeringInspectionCollectionResponse:
type: object
required: [data, meta]
properties:
data: {type: array, items: {$ref: '#/components/schemas/EngineeringInspection'}}
meta: {$ref: '#/components/schemas/CollectionMeta'}
ScheduleInspectionCommand:
type: object
additionalProperties: false
required: [scheduledAt]
properties:
scheduledAt: {$ref: '#/components/schemas/Timestamp'}
StartInspectionCommand:
type: object
additionalProperties: false
properties:
startedAt: {$ref: '#/components/schemas/Timestamp'}
CompleteInspectionCommand:
type: object
additionalProperties: false
required: [outcome, summary]
properties:
outcome: {$ref: '#/components/schemas/EngineeringInspectionOutcome'}
performedAt: {$ref: '#/components/schemas/Timestamp'}
summary: {type: string, minLength: 1, maxLength: 10000}
createFollowups: {type: boolean, default: false}
InspectionReasonCommand:
type: object
additionalProperties: false
required: [reason]
properties:
reason: {type: string, minLength: 3, maxLength: 1000}
OptionalInspectionReasonCommand:
type: object
additionalProperties: false
properties:
reason: {type: string, minLength: 3, maxLength: 1000}
EngineeringInspectionDocument:
type: object
additionalProperties: false
required: [id, organizationId, inspectionId, documentId, category, linkedByUserId, linkedAt]
properties:
id: {$ref: '#/components/schemas/Uuid'}
organizationId: {$ref: '#/components/schemas/Uuid'}
inspectionId: {$ref: '#/components/schemas/Uuid'}
documentId: {$ref: '#/components/schemas/Uuid'}
category: {type: string, enum: [evidence, photo, report, certificate, supporting_document]}
document: {$ref: '#/components/schemas/Document'}
linkedByUserId: {$ref: '#/components/schemas/Uuid'}
linkedAt: {$ref: '#/components/schemas/Timestamp'}
unlinkedAt: {type: [string, 'null'], format: date-time}
LinkEngineeringInspectionDocumentRequest:
type: object
additionalProperties: false
required: [documentId, category]
properties:
documentId: {$ref: '#/components/schemas/Uuid'}
category: {type: string, enum: [evidence, photo, report, certificate, supporting_document]}
EngineeringInspectionDocumentResponse:
type: object
required: [data]
properties: {data: {$ref: '#/components/schemas/EngineeringInspectionDocument'}}
EngineeringInspectionDocumentCollectionResponse:
type: object
required: [data]
properties: {data: {type: array, items: {$ref: '#/components/schemas/EngineeringInspectionDocument'}}}
EngineeringInspectionFinding:
type: object
additionalProperties: false
required: [id, organizationId, inspectionId, severity, description, status, createdByUserId, version, createdAt, updatedAt]
properties:
id: {$ref: '#/components/schemas/Uuid'}
organizationId: {$ref: '#/components/schemas/Uuid'}
inspectionId: {$ref: '#/components/schemas/Uuid'}
severity: {$ref: '#/components/schemas/EngineeringInspectionFindingSeverity'}
description: {type: string, minLength: 1, maxLength: 10000}
status: {$ref: '#/components/schemas/EngineeringInspectionFindingStatus'}
remediationOwnerUserId: {oneOf: [{$ref: '#/components/schemas/Uuid'}, {type: 'null'}]}
targetResolutionDate: {type: [string, 'null'], format: date}
resolutionSummary: {type: [string, 'null'], maxLength: 10000}
resolvedAt: {type: [string, 'null'], format: date-time}
resolvedByUserId: {oneOf: [{$ref: '#/components/schemas/Uuid'}, {type: 'null'}]}
acceptedRiskReason: {type: [string, 'null'], maxLength: 5000}
riskReviewDate: {type: [string, 'null'], format: date}
createdByUserId: {$ref: '#/components/schemas/Uuid'}
version: {type: integer, minimum: 1}
createdAt: {$ref: '#/components/schemas/Timestamp'}
updatedAt: {$ref: '#/components/schemas/Timestamp'}
CreateEngineeringInspectionFindingRequest:
type: object
additionalProperties: false
required: [severity, description]
properties:
severity: {$ref: '#/components/schemas/EngineeringInspectionFindingSeverity'}
description: {type: string, minLength: 1, maxLength: 10000}
remediationOwnerUserId: {oneOf: [{$ref: '#/components/schemas/Uuid'}, {type: 'null'}]}
targetResolutionDate: {type: [string, 'null'], format: date}
UpdateEngineeringInspectionFindingRequest:
type: object
additionalProperties: false
minProperties: 1
properties:
severity: {$ref: '#/components/schemas/EngineeringInspectionFindingSeverity'}
description: {type: string, minLength: 1, maxLength: 10000}
remediationOwnerUserId: {oneOf: [{$ref: '#/components/schemas/Uuid'}, {type: 'null'}]}
targetResolutionDate: {type: [string, 'null'], format: date}
ResolveEngineeringFindingCommand:
type: object
additionalProperties: false
required: [resolutionSummary]
properties:
resolutionSummary: {type: string, minLength: 3, maxLength: 10000}
evidenceDocumentIds:
type: array
maxItems: 50
uniqueItems: true
items: {$ref: '#/components/schemas/Uuid'}
privilegedSelfVerificationReason: {type: [string, 'null'], minLength: 10, maxLength: 2000}
AcceptEngineeringFindingRiskCommand:
type: object
additionalProperties: false
required: [reason, reviewDate]
properties:
reason: {type: string, minLength: 10, maxLength: 5000}
reviewDate: {$ref: '#/components/schemas/Date'}
approvingUserId: {$ref: '#/components/schemas/Uuid'}
EngineeringInspectionFindingResponse:
type: object
required: [data]
properties: {data: {$ref: '#/components/schemas/EngineeringInspectionFinding'}}
EngineeringInspectionFindingCollectionResponse:
type: object
required: [data]
properties: {data: {type: array, items: {$ref: '#/components/schemas/EngineeringInspectionFinding'}}}
EngineeringInspectionFollowupType:
type: string
enum: [corrective_task, followup_inspection, both]
EngineeringInspectionFollowupStatus:
type: string
enum: [open, in_progress, completed, cancelled]
EngineeringInspectionFollowup:
type: object
additionalProperties: false
required: [id, organizationId, inspectionId, followupType, status, createdByUserId, version, createdAt]
properties:
id: {$ref: '#/components/schemas/Uuid'}
organizationId: {$ref: '#/components/schemas/Uuid'}
inspectionId: {$ref: '#/components/schemas/Uuid'}
followupType: {$ref: '#/components/schemas/EngineeringInspectionFollowupType'}
linkedTaskId: {oneOf: [{$ref: '#/components/schemas/Uuid'}, {type: 'null'}]}
linkedInspectionId: {oneOf: [{$ref: '#/components/schemas/Uuid'}, {type: 'null'}]}
status: {$ref: '#/components/schemas/EngineeringInspectionFollowupStatus'}
createdByUserId: {$ref: '#/components/schemas/Uuid'}
version: {type: integer, minimum: 1}
createdAt: {$ref: '#/components/schemas/Timestamp'}
completedAt: {type: [string, 'null'], format: date-time}
cancelledAt: {type: [string, 'null'], format: date-time}
CreateEngineeringInspectionFollowupRequest:
type: object
additionalProperties: false
required: [followupType]
properties:
followupType: {$ref: '#/components/schemas/EngineeringInspectionFollowupType'}
linkedTaskId: {oneOf: [{$ref: '#/components/schemas/Uuid'}, {type: 'null'}]}
linkedInspectionId: {oneOf: [{$ref: '#/components/schemas/Uuid'}, {type: 'null'}]}
EngineeringInspectionFollowupResponse:
type: object
required: [data]
properties: {data: {$ref: '#/components/schemas/EngineeringInspectionFollowup'}}
EngineeringInspectionFollowupCollectionResponse:
type: object
required: [data]
properties: {data: {type: array, items: {$ref: '#/components/schemas/EngineeringInspectionFollowup'}}}
EngineeringSpecificationStatus:
type: string
enum: [draft, active, superseded, archived]
EngineeringSpecificationDocumentRole:
type: string
enum: [primary, attachment, supporting_document]
description: Specification-specific registry; independent from design-version document roles.
EngineeringSpecification:
type: object
additionalProperties: false
required: [id, organizationId, projectId, specificationNumber, title, status, version, createdAt, updatedAt]
properties:
id: {$ref: '#/components/schemas/Uuid'}
organizationId: {$ref: '#/components/schemas/Uuid'}
projectId: {$ref: '#/components/schemas/Uuid'}
specificationNumber: {type: string, minLength: 1, maxLength: 64}
title: {type: string, minLength: 1, maxLength: 300}
description: {type: [string, 'null'], maxLength: 10000}
status: {$ref: '#/components/schemas/EngineeringSpecificationStatus'}
supersededBySpecificationId: {oneOf: [{$ref: '#/components/schemas/Uuid'}, {type: 'null'}]}
archivedFromStatus:
oneOf:
- type: string
enum: [draft, active]
- type: 'null'
archivedAt: {type: [string, 'null'], format: date-time}
archivedByUserId: {oneOf: [{$ref: '#/components/schemas/Uuid'}, {type: 'null'}]}
version: {type: integer, minimum: 1}
createdAt: {$ref: '#/components/schemas/Timestamp'}
updatedAt: {$ref: '#/components/schemas/Timestamp'}
CreateEngineeringSpecificationRequest:
type: object
additionalProperties: false
required: [specificationNumber, title]
properties:
specificationNumber: {type: string, minLength: 1, maxLength: 64}
title: {type: string, minLength: 1, maxLength: 300}
description: {type: [string, 'null'], maxLength: 10000}
UpdateEngineeringSpecificationRequest:
type: object
additionalProperties: false
minProperties: 1
properties:
title: {type: string, minLength: 1, maxLength: 300}
description: {type: [string, 'null'], maxLength: 10000}
EngineeringSpecificationResponse:
type: object
required: [data]
properties: {data: {$ref: '#/components/schemas/EngineeringSpecification'}}
EngineeringSpecificationCollectionResponse:
type: object
required: [data, meta]
properties:
data: {type: array, items: {$ref: '#/components/schemas/EngineeringSpecification'}}
meta: {$ref: '#/components/schemas/CollectionMeta'}
SpecificationReasonCommand:
type: object
additionalProperties: false
required: [reason]
properties:
reason: {type: string, minLength: 3, maxLength: 1000}
OptionalSpecificationReasonCommand:
type: object
additionalProperties: false
properties:
reason: {type: string, minLength: 3, maxLength: 1000}
SupersedeSpecificationCommand:
type: object
additionalProperties: false
required: [supersededBySpecificationId, reason]
properties:
supersededBySpecificationId: {$ref: '#/components/schemas/Uuid'}
reason: {type: string, minLength: 3, maxLength: 1000}
EngineeringSpecificationDocument:
type: object
additionalProperties: false
required: [id, organizationId, specificationId, documentId, documentRole, linkedByUserId, linkedAt]
properties:
id: {$ref: '#/components/schemas/Uuid'}
organizationId: {$ref: '#/components/schemas/Uuid'}
specificationId: {$ref: '#/components/schemas/Uuid'}
documentId: {$ref: '#/components/schemas/Uuid'}
documentRole: {$ref: '#/components/schemas/EngineeringSpecificationDocumentRole'}
document: {$ref: '#/components/schemas/Document'}
linkedByUserId: {$ref: '#/components/schemas/Uuid'}
linkedAt: {$ref: '#/components/schemas/Timestamp'}
unlinkedAt: {type: [string, 'null'], format: date-time}
LinkEngineeringSpecificationDocumentRequest:
type: object
additionalProperties: false
required: [documentId, documentRole]
properties:
documentId: {$ref: '#/components/schemas/Uuid'}
documentRole: {$ref: '#/components/schemas/EngineeringSpecificationDocumentRole'}
EngineeringSpecificationDocumentResponse:
type: object
required: [data]
properties: {data: {$ref: '#/components/schemas/EngineeringSpecificationDocument'}}
EngineeringSpecificationDocumentCollectionResponse:
type: object
required: [data]
properties: {data: {type: array, items: {$ref: '#/components/schemas/EngineeringSpecificationDocument'}}}
EngineeringProjectPhaseStatus:
type: string
enum: [planned, active, completed, cancelled]
EngineeringProjectPhase:
type: object
additionalProperties: false
required: [id, organizationId, projectId, name, sequence, status, version, createdAt, updatedAt]
properties:
id: {$ref: '#/components/schemas/Uuid'}
organizationId: {$ref: '#/components/schemas/Uuid'}
projectId: {$ref: '#/components/schemas/Uuid'}
name: {type: string, minLength: 1, maxLength: 200}
sequence: {type: integer, minimum: 1}
status: {$ref: '#/components/schemas/EngineeringProjectPhaseStatus'}
startDate: {type: [string, 'null'], format: date}
endDate: {type: [string, 'null'], format: date}
version: {type: integer, minimum: 1}
createdAt: {$ref: '#/components/schemas/Timestamp'}
updatedAt: {$ref: '#/components/schemas/Timestamp'}
CreateEngineeringProjectPhaseRequest:
type: object
additionalProperties: false
required: [name]
properties:
name: {type: string, minLength: 1, maxLength: 200}
startDate: {type: [string, 'null'], format: date}
endDate: {type: [string, 'null'], format: date}
UpdateEngineeringProjectPhaseRequest:
type: object
additionalProperties: false
minProperties: 1
properties:
name: {type: string, minLength: 1, maxLength: 200}
startDate: {type: [string, 'null'], format: date}
endDate: {type: [string, 'null'], format: date}
status: {type: string, enum: [planned, active, cancelled]}
ReorderEngineeringProjectPhasesRequest:
type: object
additionalProperties: false
required: [projectVersion, orderedPhaseIds]
properties:
projectVersion: {type: integer, minimum: 1}
orderedPhaseIds:
type: array
minItems: 1
maxItems: 100
uniqueItems: true
items: {$ref: '#/components/schemas/Uuid'}
EngineeringProjectPhaseResponse:
type: object
required: [data]
properties: {data: {$ref: '#/components/schemas/EngineeringProjectPhase'}}
EngineeringProjectPhaseCollectionResponse:
type: object
required: [data]
properties: {data: {type: array, items: {$ref: '#/components/schemas/EngineeringProjectPhase'}}}
EngineeringTimeWorkItem:
oneOf:
- $ref: '#/components/schemas/EngineeringTimePhaseReference'
- $ref: '#/components/schemas/EngineeringTimeTaskReference'
- $ref: '#/components/schemas/EngineeringTimeDesignReference'
- $ref: '#/components/schemas/EngineeringTimeInspectionReference'
discriminator: {propertyName: type}
description: Maps to one project-aware nullable FK column; no unconstrained polymorphic database reference is used.
EngineeringTimePhaseReference:
type: object
additionalProperties: false
required: [type, id]
properties: {type: {type: string, const: phase}, id: {$ref: '#/components/schemas/Uuid'}}
EngineeringTimeTaskReference:
type: object
additionalProperties: false
required: [type, id]
properties: {type: {type: string, const: task}, id: {$ref: '#/components/schemas/Uuid'}}
EngineeringTimeDesignReference:
type: object
additionalProperties: false
required: [type, id]
properties: {type: {type: string, const: design}, id: {$ref: '#/components/schemas/Uuid'}}
EngineeringTimeInspectionReference:
type: object
additionalProperties: false
required: [type, id]
properties: {type: {type: string, const: inspection}, id: {$ref: '#/components/schemas/Uuid'}}
EngineeringTimeEntryInput:
type: object
additionalProperties: false
required: [projectId, userId, workDate, durationMinutes, description, billable]
properties:
projectId: {$ref: '#/components/schemas/Uuid'}
userId: {$ref: '#/components/schemas/Uuid'}
workDate: {$ref: '#/components/schemas/Date'}
durationMinutes: {type: integer, minimum: 1, maximum: 1440}
description: {type: string, minLength: 1, maxLength: 2000}
billable: {type: boolean}
billingRateMinor: {type: [integer, 'null'], minimum: 1}
currencyCode: {oneOf: [{$ref: '#/components/schemas/CurrencyCode'}, {type: 'null'}]}
workItem: {oneOf: [{$ref: '#/components/schemas/EngineeringTimeWorkItem'}, {type: 'null'}]}
allOf:
- if: {properties: {billable: {const: true}}, required: [billable]}
then: {required: [billingRateMinor, currencyCode], properties: {billingRateMinor: {type: integer, minimum: 1}, currencyCode: {$ref: '#/components/schemas/CurrencyCode'}}}
else: {properties: {billingRateMinor: {type: 'null'}, currencyCode: {type: 'null'}}}
CreateEngineeringTimeEntryRequest:
$ref: '#/components/schemas/EngineeringTimeEntryInput'
UpdateEngineeringTimeEntryRequest:
type: object
additionalProperties: false
minProperties: 1
properties:
workDate: {$ref: '#/components/schemas/Date'}
durationMinutes: {type: integer, minimum: 1, maximum: 1440}
description: {type: string, minLength: 1, maxLength: 2000}
billable: {type: boolean}
billingRateMinor: {type: [integer, 'null'], minimum: 1}
currencyCode: {oneOf: [{$ref: '#/components/schemas/CurrencyCode'}, {type: 'null'}]}
workItem: {oneOf: [{$ref: '#/components/schemas/EngineeringTimeWorkItem'}, {type: 'null'}]}
description: The service validates the same billable/rate/currency invariant after merge.
EngineeringTimeEntry:
allOf:
- $ref: '#/components/schemas/EngineeringTimeEntryInput'
- type: object
required: [id, organizationId, version, invoiced, createdAt, updatedAt]
properties:
id: {$ref: '#/components/schemas/Uuid'}
organizationId: {$ref: '#/components/schemas/Uuid'}
version: {type: integer, minimum: 1}
invoiced: {type: boolean, readOnly: true}
createdAt: {$ref: '#/components/schemas/Timestamp'}
updatedAt: {$ref: '#/components/schemas/Timestamp'}
EngineeringTimeEntryResponse:
type: object
required: [data]
properties: {data: {$ref: '#/components/schemas/EngineeringTimeEntry'}}
EngineeringTimeEntryCollectionResponse:
type: object
required: [data, meta]
properties:
data: {type: array, items: {$ref: '#/components/schemas/EngineeringTimeEntry'}}
meta: {$ref: '#/components/schemas/CollectionMeta'}
BatchCreateEngineeringTimeEntriesRequest:
type: object
additionalProperties: false
required: [mode, entries]
properties:
mode: {$ref: '#/components/schemas/BatchExecutionMode'}
entries: {type: array, minItems: 1, maxItems: 100, items: {$ref: '#/components/schemas/EngineeringTimeEntryInput'}}
EngineeringTimeEntryBatchItemResult:
type: object
required: [index, status]
properties:
index: {type: integer, minimum: 0}
status: {type: string, enum: [created, failed]}
data: {oneOf: [{$ref: '#/components/schemas/EngineeringTimeEntry'}, {type: 'null'}]}
problem: {oneOf: [{$ref: '#/components/schemas/Problem'}, {type: 'null'}]}
EngineeringTimeEntryBatchResponse:
type: object
required: [data]
properties: {data: {type: array, items: {$ref: '#/components/schemas/EngineeringTimeEntryBatchItemResult'}}}
EngineeringBudgetStatus:
type: string
enum: [draft, approved, closed]
EngineeringProjectBudget:
type: object
additionalProperties: false
required: [id, organizationId, projectId, name, currencyCode, status, version, createdAt, updatedAt]
properties:
id: {$ref: '#/components/schemas/Uuid'}
organizationId: {$ref: '#/components/schemas/Uuid'}
projectId: {$ref: '#/components/schemas/Uuid'}
name: {type: string, minLength: 1, maxLength: 200}
currencyCode: {$ref: '#/components/schemas/CurrencyCode'}
status: {$ref: '#/components/schemas/EngineeringBudgetStatus'}
approvedByUserId: {oneOf: [{$ref: '#/components/schemas/Uuid'}, {type: 'null'}]}
approvedAt: {type: [string, 'null'], format: date-time}
version: {type: integer, minimum: 1}
createdAt: {$ref: '#/components/schemas/Timestamp'}
updatedAt: {$ref: '#/components/schemas/Timestamp'}
CreateEngineeringProjectBudgetRequest:
type: object
additionalProperties: false
required: [name, currencyCode]
properties:
name: {type: string, minLength: 1, maxLength: 200}
currencyCode: {$ref: '#/components/schemas/CurrencyCode'}
UpdateEngineeringProjectBudgetRequest:
type: object
additionalProperties: false
minProperties: 1
properties: {name: {type: string, minLength: 1, maxLength: 200}}
ApproveEngineeringBudgetCommand:
type: object
additionalProperties: false
required: [attestation]
properties: {attestation: {type: string, minLength: 10, maxLength: 1000}}
EngineeringProjectBudgetResponse:
type: object
required: [data]
properties: {data: {$ref: '#/components/schemas/EngineeringProjectBudget'}}
EngineeringProjectBudgetCollectionResponse:
type: object
required: [data]
properties: {data: {type: array, items: {$ref: '#/components/schemas/EngineeringProjectBudget'}}}
EngineeringProjectBudgetItem:
type: object
additionalProperties: false
required: [id, organizationId, budgetId, category, description, allocatedAmountMinor, version, createdAt, updatedAt]
properties:
id: {$ref: '#/components/schemas/Uuid'}
organizationId: {$ref: '#/components/schemas/Uuid'}
budgetId: {$ref: '#/components/schemas/Uuid'}
category: {type: string, minLength: 1, maxLength: 100}
description: {type: string, minLength: 1, maxLength: 1000}
allocatedAmountMinor: {type: integer, minimum: 0}
version: {type: integer, minimum: 1}
createdAt: {$ref: '#/components/schemas/Timestamp'}
updatedAt: {$ref: '#/components/schemas/Timestamp'}
CreateEngineeringProjectBudgetItemRequest:
type: object
additionalProperties: false
required: [category, description, allocatedAmountMinor]
properties:
category: {type: string, minLength: 1, maxLength: 100}
description: {type: string, minLength: 1, maxLength: 1000}
allocatedAmountMinor: {type: integer, minimum: 0}
EngineeringProjectBudgetItemResponse:
type: object
required: [data]
properties: {data: {$ref: '#/components/schemas/EngineeringProjectBudgetItem'}}
EngineeringProjectBudgetItemCollectionResponse:
type: object
required: [data]
properties: {data: {type: array, items: {$ref: '#/components/schemas/EngineeringProjectBudgetItem'}}}
EngineeringBudgetProjection:
type: object
additionalProperties: false
required: [budgetId, currencyCode, allocatedAmountMinor, committedAmountMinor, actualAmountMinor, varianceAmountMinor, calculatedAt]
properties:
budgetId: {$ref: '#/components/schemas/Uuid'}
currencyCode: {$ref: '#/components/schemas/CurrencyCode'}
allocatedAmountMinor: {type: integer, minimum: 0}
committedAmountMinor: {type: integer, minimum: 0}
actualAmountMinor: {type: integer, minimum: 0}
varianceAmountMinor: {type: integer}
calculatedAt: {$ref: '#/components/schemas/Timestamp'}
sourceWatermark: {type: string, description: Reconciliation watermark for authoritative source records.}
EngineeringBudgetProjectionResponse:
type: object
required: [data]
properties: {data: {$ref: '#/components/schemas/EngineeringBudgetProjection'}}
Profession:
type: string
enum: [engineering, legal, healthcare]
UserStatus:
type: string
enum: [active, inactive, pending_verification]
OrganizationStatus:
type: string
enum: [active, suspended, pending_deletion]
MembershipStatus:
type: string
enum: [active, inactive, pending]
InvitationStatus:
type: string
enum: [pending, accepted, revoked, expired]
description: Derived from invitation timestamps and expiry.
RoleStatus:
type: string
enum: [active, inactive]
description: Inactive roles retain assignments for history but grant no permissions and cannot be newly assigned.
EngineeringClientType:
type: string
enum: [corporate, government, individual]
EngineeringClientStatus:
type: string
enum: [active, archived]
EngineeringContactType:
type: string
enum: [technical, billing, executive, site, contract, other]
EngineeringContactStatus:
type: string
enum: [active, archived]
EngineeringProjectStatus:
type: string
enum: [draft, active, closed, archived]
EngineeringProjectRestorableStatus:
type: string
enum: [draft, closed]
EngineeringDiscipline:
type: string
enum:
- civil
- structural
- mechanical
- electrical
- geotechnical
- environmental
- transportation
- water_resources
- surveying
- multidisciplinary
- other
EngineeringProjectMemberRole:
type: string
enum: [engineer, designer, reviewer, inspector, viewer, contractor]
description: Project manager is intentionally excluded; `projectManagerUserId` is authoritative.
EngineeringProjectMemberStatus:
type: string
enum: [active, left]
description: Derived from whether `leftAt` is null.
EngineeringTaskStatus:
type: string
enum: [todo, in_progress, completed, cancelled]
EngineeringTaskPriority:
type: string
enum: [low, medium, high, urgent]
BatchExecutionMode:
type: string
enum: [atomic, partial]
Problem:
type: object
additionalProperties: true
required: [type, title, status, code, requestId]
properties:
type:
type: string
format: uri-reference
title:
type: string
status:
type: integer
minimum: 400
maximum: 599
detail:
type: string
instance:
type: string
format: uri-reference
code:
type: string
pattern: '^[A-Z][A-Z0-9_]+$'
description: Stable machine-readable application error code.
requestId:
$ref: '#/components/schemas/Uuid'
errors:
type: array
items:
$ref: '#/components/schemas/FieldError'
FieldError:
type: object
additionalProperties: false
required: [field, code, message]
properties:
field:
type: string
code:
type: string
message:
type: string
PaginationMeta:
type: object
additionalProperties: false
required: [nextCursor, hasMore]
properties:
nextCursor:
type: [string, 'null']
hasMore:
type: boolean
CollectionMeta:
type: object
additionalProperties: false
required: [pagination]
properties:
pagination:
$ref: '#/components/schemas/PaginationMeta'
RegisterRequest:
type: object
additionalProperties: false
required: [email, password, firstName, lastName]
properties:
email:
$ref: '#/components/schemas/Email'
password:
type: string
minLength: 12
maxLength: 128
writeOnly: true
firstName:
type: string
minLength: 1
maxLength: 100
lastName:
type: string
minLength: 1
maxLength: 100
LoginRequest:
type: object
additionalProperties: false
required: [email, password]
properties:
email:
$ref: '#/components/schemas/Email'
password:
type: string
minLength: 1
maxLength: 128
writeOnly: true
RefreshTokenRequest:
type: object
additionalProperties: false
required: [refreshToken]
properties:
refreshToken:
type: string
minLength: 32
maxLength: 4096
writeOnly: true
TokenPair:
type: object
additionalProperties: false
required: [accessToken, refreshToken, tokenType, expiresIn, sessionId]
properties:
accessToken:
type: string
readOnly: true
refreshToken:
type: string
readOnly: true
tokenType:
type: string
const: Bearer
expiresIn:
type: integer
minimum: 1
description: Access-token lifetime in seconds.
sessionId:
$ref: '#/components/schemas/Uuid'
TokenPairResponse:
type: object
additionalProperties: false
required: [data]
properties:
data:
$ref: '#/components/schemas/TokenPair'
User:
type: object
additionalProperties: false
required: [id, email, firstName, lastName, status, version, createdAt, updatedAt]
properties:
id:
$ref: '#/components/schemas/Uuid'
email:
$ref: '#/components/schemas/Email'
firstName:
type: string
lastName:
type: string
phone:
type: [string, 'null']
maxLength: 32
avatarUrl:
type: [string, 'null']
format: uri
status:
$ref: '#/components/schemas/UserStatus'
version:
type: integer
minimum: 1
createdAt:
$ref: '#/components/schemas/Timestamp'
updatedAt:
$ref: '#/components/schemas/Timestamp'
UserResponse:
type: object
additionalProperties: false
required: [data]
properties:
data:
$ref: '#/components/schemas/User'
UpdateCurrentUserRequest:
type: object
additionalProperties: false
minProperties: 1
properties:
firstName:
type: string
minLength: 1
maxLength: 100
lastName:
type: string
minLength: 1
maxLength: 100
phone:
type: [string, 'null']
maxLength: 32
avatarUrl:
type: [string, 'null']
format: uri
Session:
type: object
additionalProperties: false
required: [id, current, createdAt, lastActiveAt, expiresAt]
properties:
id:
$ref: '#/components/schemas/Uuid'
current:
type: boolean
deviceName:
type: [string, 'null']
maxLength: 200
ipAddress:
type: [string, 'null']
description: Redacted or omitted according to privacy policy.
userAgent:
type: [string, 'null']
maxLength: 512
createdAt:
$ref: '#/components/schemas/Timestamp'
lastActiveAt:
$ref: '#/components/schemas/Timestamp'
expiresAt:
$ref: '#/components/schemas/Timestamp'
revokedAt:
oneOf:
- $ref: '#/components/schemas/Timestamp'
- type: 'null'
SessionCollectionResponse:
type: object
additionalProperties: false
required: [data, meta]
properties:
data:
type: array
items:
$ref: '#/components/schemas/Session'
meta:
$ref: '#/components/schemas/CollectionMeta'
Organization:
type: object
additionalProperties: false
required: [id, name, slug, status, countryCode, timezone, currencyCode, professions, version, createdAt, updatedAt]
properties:
id:
$ref: '#/components/schemas/Uuid'
name:
type: string
minLength: 1
maxLength: 200
slug:
type: string
pattern: '^[a-z0-9]+(?:-[a-z0-9]+)*$'
minLength: 3
maxLength: 80
status:
$ref: '#/components/schemas/OrganizationStatus'
countryCode:
$ref: '#/components/schemas/CountryCode'
timezone:
type: string
description: IANA time-zone identifier.
examples: [Africa/Casablanca]
currencyCode:
$ref: '#/components/schemas/CurrencyCode'
professions:
type: array
minItems: 1
uniqueItems: true
items:
$ref: '#/components/schemas/Profession'
version:
type: integer
minimum: 1
createdAt:
$ref: '#/components/schemas/Timestamp'
updatedAt:
$ref: '#/components/schemas/Timestamp'
CreateOrganizationRequest:
type: object
additionalProperties: false
required: [name, slug, countryCode, timezone, currencyCode, professions]
properties:
name:
type: string
minLength: 1
maxLength: 200
slug:
type: string
pattern: '^[a-z0-9]+(?:-[a-z0-9]+)*$'
minLength: 3
maxLength: 80
countryCode:
$ref: '#/components/schemas/CountryCode'
timezone:
type: string
minLength: 1
maxLength: 100
currencyCode:
$ref: '#/components/schemas/CurrencyCode'
professions:
type: array
minItems: 1
uniqueItems: true
items:
$ref: '#/components/schemas/Profession'
UpdateOrganizationRequest:
type: object
additionalProperties: false
minProperties: 1
description: Status and enabled professions change through separately authorized commands.
properties:
name:
type: string
minLength: 1
maxLength: 200
slug:
type: string
pattern: '^[a-z0-9]+(?:-[a-z0-9]+)*$'
minLength: 3
maxLength: 80
countryCode:
$ref: '#/components/schemas/CountryCode'
timezone:
type: string
minLength: 1
maxLength: 100
currencyCode:
$ref: '#/components/schemas/CurrencyCode'
OrganizationResponse:
type: object
additionalProperties: false
required: [data]
properties:
data:
$ref: '#/components/schemas/Organization'
OrganizationCollectionResponse:
type: object
additionalProperties: false
required: [data, meta]
properties:
data:
type: array
items:
$ref: '#/components/schemas/Organization'
meta:
$ref: '#/components/schemas/CollectionMeta'
Invitation:
type: object
additionalProperties: false
required: [id, organizationId, email, roleIds, status, invitedByUserId, expiresAt, version, createdAt]
properties:
id:
$ref: '#/components/schemas/Uuid'
organizationId:
$ref: '#/components/schemas/Uuid'
email:
$ref: '#/components/schemas/Email'
roleIds:
type: array
minItems: 1
uniqueItems: true
items:
$ref: '#/components/schemas/Uuid'
status:
$ref: '#/components/schemas/InvitationStatus'
invitedByUserId:
$ref: '#/components/schemas/Uuid'
expiresAt:
$ref: '#/components/schemas/Timestamp'
acceptedAt:
oneOf:
- $ref: '#/components/schemas/Timestamp'
- type: 'null'
revokedAt:
oneOf:
- $ref: '#/components/schemas/Timestamp'
- type: 'null'
version:
type: integer
minimum: 1
createdAt:
$ref: '#/components/schemas/Timestamp'
CreateInvitationRequest:
type: object
additionalProperties: false
required: [email, roleIds]
properties:
email:
$ref: '#/components/schemas/Email'
roleIds:
type: array
minItems: 1
maxItems: 20
uniqueItems: true
items:
$ref: '#/components/schemas/Uuid'
expiresInDays:
type: integer
minimum: 1
maximum: 30
default: 7
AcceptInvitationRequest:
type: object
additionalProperties: false
required: [token]
properties:
token:
type: string
minLength: 32
maxLength: 4096
writeOnly: true
InvitationResponse:
type: object
additionalProperties: false
required: [data]
properties:
data:
$ref: '#/components/schemas/Invitation'
InvitationCollectionResponse:
type: object
additionalProperties: false
required: [data, meta]
properties:
data:
type: array
items:
$ref: '#/components/schemas/Invitation'
meta:
$ref: '#/components/schemas/CollectionMeta'
Membership:
type: object
additionalProperties: false
required: [id, organizationId, user, status, roles, joinedAt, version, createdAt, updatedAt]
properties:
id:
$ref: '#/components/schemas/Uuid'
organizationId:
$ref: '#/components/schemas/Uuid'
user:
$ref: '#/components/schemas/UserSummary'
status:
$ref: '#/components/schemas/MembershipStatus'
roles:
type: array
items:
$ref: '#/components/schemas/RoleSummary'
joinedAt:
oneOf:
- $ref: '#/components/schemas/Timestamp'
- type: 'null'
version:
type: integer
minimum: 1
createdAt:
$ref: '#/components/schemas/Timestamp'
updatedAt:
$ref: '#/components/schemas/Timestamp'
UserSummary:
type: object
additionalProperties: false
required: [id, email, firstName, lastName]
properties:
id:
$ref: '#/components/schemas/Uuid'
email:
$ref: '#/components/schemas/Email'
firstName:
type: string
lastName:
type: string
MembershipResponse:
type: object
additionalProperties: false
required: [data]
properties:
data:
$ref: '#/components/schemas/Membership'
MembershipCollectionResponse:
type: object
additionalProperties: false
required: [data, meta]
properties:
data:
type: array
items:
$ref: '#/components/schemas/Membership'
meta:
$ref: '#/components/schemas/CollectionMeta'
ReplaceMembershipRolesRequest:
type: object
additionalProperties: false
required: [roleIds]
properties:
roleIds:
type: array
minItems: 1
maxItems: 20
uniqueItems: true
items:
$ref: '#/components/schemas/Uuid'
ReasonRequest:
type: object
additionalProperties: false
properties:
reason:
type: string
maxLength: 500
Role:
type: object
additionalProperties: false
required: [id, organizationId, name, slug, description, status, isSystem, permissions, version, createdAt, updatedAt]
properties:
id:
$ref: '#/components/schemas/Uuid'
organizationId:
$ref: '#/components/schemas/Uuid'
name:
type: string
minLength: 1
maxLength: 100
slug:
type: string
pattern: '^[a-z0-9]+(?:_[a-z0-9]+)*$'
minLength: 2
maxLength: 100
description:
type: [string, 'null']
maxLength: 500
status:
$ref: '#/components/schemas/RoleStatus'
isSystem:
type: boolean
permissions:
type: array
uniqueItems: true
items:
$ref: '#/components/schemas/PermissionGrant'
version:
type: integer
minimum: 1
createdAt:
$ref: '#/components/schemas/Timestamp'
updatedAt:
$ref: '#/components/schemas/Timestamp'
RoleSummary:
type: object
additionalProperties: false
required: [id, name, slug, status, isSystem]
properties:
id:
$ref: '#/components/schemas/Uuid'
name:
type: string
slug:
type: string
status:
$ref: '#/components/schemas/RoleStatus'
isSystem:
type: boolean
CreateRoleRequest:
type: object
additionalProperties: false
required: [name, slug, permissions]
properties:
name:
type: string
minLength: 1
maxLength: 100
slug:
type: string
pattern: '^[a-z0-9]+(?:_[a-z0-9]+)*$'
minLength: 2
maxLength: 100
description:
type: [string, 'null']
maxLength: 500
permissions:
type: array
maxItems: 200
uniqueItems: true
items:
$ref: '#/components/schemas/PermissionGrant'
UpdateRoleRequest:
type: object
additionalProperties: false
minProperties: 1
properties:
name:
type: string
minLength: 1
maxLength: 100
description:
type: [string, 'null']
maxLength: 500
permissions:
type: array
maxItems: 200
uniqueItems: true
items:
$ref: '#/components/schemas/PermissionGrant'
RoleResponse:
type: object
additionalProperties: false
required: [data]
properties:
data:
$ref: '#/components/schemas/Role'
RoleCollectionResponse:
type: object
additionalProperties: false
required: [data, meta]
properties:
data:
type: array
items:
$ref: '#/components/schemas/Role'
meta:
$ref: '#/components/schemas/CollectionMeta'
Permission:
type: object
additionalProperties: false
required: [id, code, name, scopeOptions]
properties:
id:
$ref: '#/components/schemas/Uuid'
code:
type: string
pattern: '^[a-z][a-z0-9_]*(?:\.[a-z][a-z0-9_]*)+$'
examples: [engineering.projects.create]
name:
type: string
description:
type: [string, 'null']
profession:
oneOf:
- $ref: '#/components/schemas/Profession'
- type: 'null'
scopeOptions:
type: array
minItems: 1
uniqueItems: true
items:
type: string
enum: [assigned, organization]
PermissionGrant:
type: object
additionalProperties: false
required: [permissionId, scope]
properties:
permissionId:
$ref: '#/components/schemas/Uuid'
scope:
type: string
enum: [assigned, organization]
description: The selected scope must be allowed by the referenced permission.
PermissionCollectionResponse:
type: object
additionalProperties: false
required: [data, meta]
properties:
data:
type: array
items:
$ref: '#/components/schemas/Permission'
meta:
$ref: '#/components/schemas/CollectionMeta'
EngineeringClient:
type: object
additionalProperties: false
required:
- id
- organizationId
- clientType
- displayName
- legalName
- status
- archivedAt
- archivedByUserId
- version
- createdAt
- updatedAt
properties:
id:
$ref: '#/components/schemas/Uuid'
organizationId:
$ref: '#/components/schemas/Uuid'
clientType:
$ref: '#/components/schemas/EngineeringClientType'
displayName:
type: string
minLength: 1
maxLength: 200
legalName:
type: [string, 'null']
minLength: 1
maxLength: 300
status:
$ref: '#/components/schemas/EngineeringClientStatus'
archivedAt:
oneOf:
- $ref: '#/components/schemas/Timestamp'
- type: 'null'
archivedByUserId:
oneOf:
- $ref: '#/components/schemas/Uuid'
- type: 'null'
version:
type: integer
minimum: 1
createdAt:
$ref: '#/components/schemas/Timestamp'
updatedAt:
$ref: '#/components/schemas/Timestamp'
CreateEngineeringClientRequest:
type: object
additionalProperties: false
required: [clientType, displayName]
properties:
clientType:
$ref: '#/components/schemas/EngineeringClientType'
displayName:
type: string
minLength: 1
maxLength: 200
legalName:
type: [string, 'null']
minLength: 1
maxLength: 300
allOf:
- if:
properties:
clientType:
enum: [corporate, government]
required: [clientType]
then:
required: [legalName]
properties:
legalName:
type: string
minLength: 1
maxLength: 300
description: Corporate and government clients require a non-null legal name.
UpdateEngineeringClientRequest:
type: object
additionalProperties: false
minProperties: 1
properties:
clientType:
$ref: '#/components/schemas/EngineeringClientType'
displayName:
type: string
minLength: 1
maxLength: 200
legalName:
type: [string, 'null']
minLength: 1
maxLength: 300
description: The resulting corporate or government client must have a non-null legal name.
EngineeringClientResponse:
type: object
additionalProperties: false
required: [data]
properties:
data:
$ref: '#/components/schemas/EngineeringClient'
EngineeringClientCollectionResponse:
type: object
additionalProperties: false
required: [data, meta]
properties:
data:
type: array
items:
$ref: '#/components/schemas/EngineeringClient'
meta:
$ref: '#/components/schemas/CollectionMeta'
EngineeringClientContact:
type: object
additionalProperties: false
required:
- id
- organizationId
- clientId
- name
- title
- department
- email
- phone
- contactType
- isPrimary
- status
- archivedAt
- archivedByUserId
- version
- createdAt
- updatedAt
properties:
id:
$ref: '#/components/schemas/Uuid'
organizationId:
$ref: '#/components/schemas/Uuid'
clientId:
$ref: '#/components/schemas/Uuid'
name:
type: string
minLength: 1
maxLength: 200
title:
type: [string, 'null']
maxLength: 150
department:
type: [string, 'null']
maxLength: 150
email:
oneOf:
- $ref: '#/components/schemas/Email'
- type: 'null'
phone:
type: [string, 'null']
maxLength: 32
contactType:
$ref: '#/components/schemas/EngineeringContactType'
isPrimary:
type: boolean
status:
$ref: '#/components/schemas/EngineeringContactStatus'
archivedAt:
oneOf:
- $ref: '#/components/schemas/Timestamp'
- type: 'null'
archivedByUserId:
oneOf:
- $ref: '#/components/schemas/Uuid'
- type: 'null'
version:
type: integer
minimum: 1
createdAt:
$ref: '#/components/schemas/Timestamp'
updatedAt:
$ref: '#/components/schemas/Timestamp'
allOf:
- if:
properties:
status:
const: archived
required: [status]
then:
properties:
isPrimary:
const: false
CreateEngineeringClientContactRequest:
type: object
additionalProperties: false
required: [name, contactType]
properties:
name:
type: string
minLength: 1
maxLength: 200
title:
type: [string, 'null']
maxLength: 150
department:
type: [string, 'null']
maxLength: 150
email:
$ref: '#/components/schemas/Email'
phone:
type: string
minLength: 3
maxLength: 32
contactType:
$ref: '#/components/schemas/EngineeringContactType'
isPrimary:
type: boolean
default: false
anyOf:
- required: [email]
- required: [phone]
description: At least one of email or phone is required.
UpdateEngineeringClientContactRequest:
type: object
additionalProperties: false
minProperties: 1
properties:
name:
type: string
minLength: 1
maxLength: 200
title:
type: [string, 'null']
maxLength: 150
department:
type: [string, 'null']
maxLength: 150
email:
oneOf:
- $ref: '#/components/schemas/Email'
- type: 'null'
phone:
type: [string, 'null']
minLength: 3
maxLength: 32
contactType:
$ref: '#/components/schemas/EngineeringContactType'
isPrimary:
type: boolean
description: The resulting contact must retain at least one of email or phone.
EngineeringClientContactResponse:
type: object
additionalProperties: false
required: [data]
properties:
data:
$ref: '#/components/schemas/EngineeringClientContact'
EngineeringClientContactCollectionResponse:
type: object
additionalProperties: false
required: [data, meta]
properties:
data:
type: array
items:
$ref: '#/components/schemas/EngineeringClientContact'
meta:
$ref: '#/components/schemas/CollectionMeta'
EngineeringProject:
type: object
additionalProperties: false
required:
- id
- organizationId
- clientId
- projectNumber
- name
- description
- discipline
- status
- projectManagerUserId
- startDate
- expectedCompletionDate
- completedDate
- archivedAt
- archivedByUserId
- archivedFromStatus
- version
- createdAt
- updatedAt
properties:
id:
$ref: '#/components/schemas/Uuid'
organizationId:
$ref: '#/components/schemas/Uuid'
clientId:
$ref: '#/components/schemas/Uuid'
projectNumber:
type: string
pattern: '^[A-Za-z0-9][A-Za-z0-9._/-]*$'
minLength: 1
maxLength: 100
description: Immutable, organization-unique human project reference.
name:
type: string
minLength: 1
maxLength: 200
description:
type: [string, 'null']
maxLength: 5000
discipline:
$ref: '#/components/schemas/EngineeringDiscipline'
status:
$ref: '#/components/schemas/EngineeringProjectStatus'
projectManagerUserId:
oneOf:
- $ref: '#/components/schemas/Uuid'
- type: 'null'
startDate:
oneOf:
- $ref: '#/components/schemas/Date'
- type: 'null'
expectedCompletionDate:
oneOf:
- $ref: '#/components/schemas/Date'
- type: 'null'
completedDate:
oneOf:
- $ref: '#/components/schemas/Date'
- type: 'null'
archivedAt:
oneOf:
- $ref: '#/components/schemas/Timestamp'
- type: 'null'
archivedByUserId:
oneOf:
- $ref: '#/components/schemas/Uuid'
- type: 'null'
archivedFromStatus:
oneOf:
- $ref: '#/components/schemas/EngineeringProjectRestorableStatus'
- type: 'null'
version:
type: integer
minimum: 1
createdAt:
$ref: '#/components/schemas/Timestamp'
updatedAt:
$ref: '#/components/schemas/Timestamp'
allOf:
- if:
properties:
status:
enum: [active, closed]
required: [status]
then:
properties:
projectManagerUserId:
$ref: '#/components/schemas/Uuid'
startDate:
$ref: '#/components/schemas/Date'
- if:
properties:
status:
const: closed
required: [status]
then:
properties:
completedDate:
$ref: '#/components/schemas/Date'
- if:
properties:
status:
enum: [draft, active]
required: [status]
then:
properties:
completedDate:
type: 'null'
- if:
properties:
status:
const: archived
required: [status]
then:
properties:
archivedAt:
$ref: '#/components/schemas/Timestamp'
archivedByUserId:
$ref: '#/components/schemas/Uuid'
archivedFromStatus:
$ref: '#/components/schemas/EngineeringProjectRestorableStatus'
else:
properties:
archivedAt:
type: 'null'
archivedByUserId:
type: 'null'
archivedFromStatus:
type: 'null'
- if:
properties:
status:
const: archived
archivedFromStatus:
const: closed
required: [status, archivedFromStatus]
then:
properties:
projectManagerUserId:
$ref: '#/components/schemas/Uuid'
startDate:
$ref: '#/components/schemas/Date'
completedDate:
$ref: '#/components/schemas/Date'
- if:
properties:
status:
const: archived
archivedFromStatus:
const: draft
required: [status, archivedFromStatus]
then:
properties:
completedDate:
type: 'null'
description: Expected and completed dates may not precede the start date.
CreateEngineeringProjectRequest:
type: object
additionalProperties: false
required: [clientId, projectNumber, name, discipline]
properties:
clientId:
$ref: '#/components/schemas/Uuid'
projectNumber:
type: string
pattern: '^[A-Za-z0-9][A-Za-z0-9._/-]*$'
minLength: 1
maxLength: 100
name:
type: string
minLength: 1
maxLength: 200
description:
type: [string, 'null']
maxLength: 5000
discipline:
$ref: '#/components/schemas/EngineeringDiscipline'
projectManagerUserId:
oneOf:
- $ref: '#/components/schemas/Uuid'
- type: 'null'
startDate:
oneOf:
- $ref: '#/components/schemas/Date'
- type: 'null'
expectedCompletionDate:
oneOf:
- $ref: '#/components/schemas/Date'
- type: 'null'
description: Expected completion date may not precede start date.
UpdateEngineeringProjectRequest:
type: object
additionalProperties: false
minProperties: 1
properties:
clientId:
$ref: '#/components/schemas/Uuid'
name:
type: string
minLength: 1
maxLength: 200
description:
type: [string, 'null']
maxLength: 5000
discipline:
$ref: '#/components/schemas/EngineeringDiscipline'
projectManagerUserId:
oneOf:
- $ref: '#/components/schemas/Uuid'
- type: 'null'
startDate:
oneOf:
- $ref: '#/components/schemas/Date'
- type: 'null'
expectedCompletionDate:
oneOf:
- $ref: '#/components/schemas/Date'
- type: 'null'
description: The resulting dates and manager assignment must satisfy the project's current state rules.
ActivateEngineeringProjectRequest:
type: object
additionalProperties: false
properties:
startDate:
$ref: '#/components/schemas/Date'
CloseEngineeringProjectRequest:
type: object
additionalProperties: false
properties:
completedDate:
$ref: '#/components/schemas/Date'
reason:
type: string
maxLength: 500
EngineeringProjectResponse:
type: object
additionalProperties: false
required: [data]
properties:
data:
$ref: '#/components/schemas/EngineeringProject'
EngineeringProjectCollectionResponse:
type: object
additionalProperties: false
required: [data, meta]
properties:
data:
type: array
items:
$ref: '#/components/schemas/EngineeringProject'
meta:
$ref: '#/components/schemas/CollectionMeta'
EngineeringProjectSummary:
type: object
additionalProperties: false
required:
- id
- clientId
- projectNumber
- name
- discipline
- status
- projectManagerUserId
- startDate
- expectedCompletionDate
- completedDate
- version
- createdAt
- updatedAt
properties:
id:
$ref: '#/components/schemas/Uuid'
clientId:
$ref: '#/components/schemas/Uuid'
projectNumber:
type: string
minLength: 1
maxLength: 100
name:
type: string
minLength: 1
maxLength: 200
discipline:
$ref: '#/components/schemas/EngineeringDiscipline'
status:
$ref: '#/components/schemas/EngineeringProjectStatus'
projectManagerUserId:
oneOf:
- $ref: '#/components/schemas/Uuid'
- type: 'null'
startDate:
oneOf:
- $ref: '#/components/schemas/Date'
- type: 'null'
expectedCompletionDate:
oneOf:
- $ref: '#/components/schemas/Date'
- type: 'null'
completedDate:
oneOf:
- $ref: '#/components/schemas/Date'
- type: 'null'
version:
type: integer
minimum: 1
createdAt:
$ref: '#/components/schemas/Timestamp'
updatedAt:
$ref: '#/components/schemas/Timestamp'
EngineeringProjectSummaryCollectionResponse:
type: object
additionalProperties: false
required: [data, meta]
properties:
data:
type: array
items:
$ref: '#/components/schemas/EngineeringProjectSummary'
meta:
$ref: '#/components/schemas/CollectionMeta'
EngineeringClientSummary:
type: object
additionalProperties: false
required: [id, clientType, displayName, legalName, status]
properties:
id:
$ref: '#/components/schemas/Uuid'
clientType:
$ref: '#/components/schemas/EngineeringClientType'
displayName:
type: string
legalName:
type: [string, 'null']
status:
$ref: '#/components/schemas/EngineeringClientStatus'
EngineeringProjectActivitySummary:
type: object
additionalProperties: false
required:
- projectMemberCount
- phaseCount
- siteCount
- openTaskCount
- designCount
- designsUnderReviewCount
- inspectionCount
- upcomingInspectionCount
- documentCount
- lastActivityAt
properties:
projectMemberCount:
type: integer
minimum: 0
description: Active participation rows; the separate project-manager pointer is not double-counted.
phaseCount:
type: integer
minimum: 0
siteCount:
type: integer
minimum: 0
openTaskCount:
type: integer
minimum: 0
description: Tasks in todo or in-progress status.
designCount:
type: integer
minimum: 0
designsUnderReviewCount:
type: integer
minimum: 0
inspectionCount:
type: integer
minimum: 0
upcomingInspectionCount:
type: integer
minimum: 0
documentCount:
type: integer
minimum: 0
lastActivityAt:
oneOf:
- $ref: '#/components/schemas/Timestamp'
- type: 'null'
EngineeringProjectDashboard:
type: object
additionalProperties: false
required: [project, client, projectManager, activity]
properties:
project:
$ref: '#/components/schemas/EngineeringProject'
client:
$ref: '#/components/schemas/EngineeringClientSummary'
projectManager:
oneOf:
- $ref: '#/components/schemas/UserSummary'
- type: 'null'
activity:
$ref: '#/components/schemas/EngineeringProjectActivitySummary'
EngineeringProjectDashboardResponse:
type: object
additionalProperties: false
required: [data]
properties:
data:
$ref: '#/components/schemas/EngineeringProjectDashboard'
EngineeringProjectMember:
type: object
additionalProperties: false
required:
- id
- organizationId
- projectId
- user
- projectRole
- status
- joinedAt
- leftAt
- version
- createdAt
- updatedAt
properties:
id:
$ref: '#/components/schemas/Uuid'
organizationId:
$ref: '#/components/schemas/Uuid'
projectId:
$ref: '#/components/schemas/Uuid'
user:
$ref: '#/components/schemas/UserSummary'
projectRole:
$ref: '#/components/schemas/EngineeringProjectMemberRole'
status:
$ref: '#/components/schemas/EngineeringProjectMemberStatus'
joinedAt:
$ref: '#/components/schemas/Timestamp'
leftAt:
oneOf:
- $ref: '#/components/schemas/Timestamp'
- type: 'null'
version:
type: integer
minimum: 1
createdAt:
$ref: '#/components/schemas/Timestamp'
updatedAt:
$ref: '#/components/schemas/Timestamp'
allOf:
- if:
properties:
status:
const: active
required: [status]
then:
properties:
leftAt:
type: 'null'
- if:
properties:
status:
const: left
required: [status]
then:
properties:
leftAt:
$ref: '#/components/schemas/Timestamp'
CreateEngineeringProjectMemberRequest:
type: object
additionalProperties: false
required: [userId, projectRole]
properties:
userId:
$ref: '#/components/schemas/Uuid'
projectRole:
$ref: '#/components/schemas/EngineeringProjectMemberRole'
UpdateEngineeringProjectMemberRequest:
type: object
additionalProperties: false
required: [projectRole]
properties:
projectRole:
$ref: '#/components/schemas/EngineeringProjectMemberRole'
EngineeringProjectMemberResponse:
type: object
additionalProperties: false
required: [data]
properties:
data:
$ref: '#/components/schemas/EngineeringProjectMember'
EngineeringProjectMemberCollectionResponse:
type: object
additionalProperties: false
required: [data, meta]
properties:
data:
type: array
items:
$ref: '#/components/schemas/EngineeringProjectMember'
meta:
$ref: '#/components/schemas/CollectionMeta'
EngineeringTask:
type: object
additionalProperties: false
required:
- id
- organizationId
- projectId
- title
- description
- status
- priority
- createdByUserId
- assignedToUserId
- dueAt
- startedAt
- startedByUserId
- completedAt
- completedByUserId
- cancelledAt
- cancelledByUserId
- cancellationReason
- version
- createdAt
- updatedAt
properties:
id:
$ref: '#/components/schemas/Uuid'
organizationId:
$ref: '#/components/schemas/Uuid'
projectId:
$ref: '#/components/schemas/Uuid'
title:
type: string
minLength: 1
maxLength: 300
description:
type: [string, 'null']
maxLength: 10000
status:
$ref: '#/components/schemas/EngineeringTaskStatus'
priority:
$ref: '#/components/schemas/EngineeringTaskPriority'
createdByUserId:
$ref: '#/components/schemas/Uuid'
assignedToUserId:
oneOf:
- $ref: '#/components/schemas/Uuid'
- type: 'null'
dueAt:
oneOf:
- $ref: '#/components/schemas/Timestamp'
- type: 'null'
startedAt:
oneOf:
- $ref: '#/components/schemas/Timestamp'
- type: 'null'
startedByUserId:
oneOf:
- $ref: '#/components/schemas/Uuid'
- type: 'null'
completedAt:
oneOf:
- $ref: '#/components/schemas/Timestamp'
- type: 'null'
completedByUserId:
oneOf:
- $ref: '#/components/schemas/Uuid'
- type: 'null'
cancelledAt:
oneOf:
- $ref: '#/components/schemas/Timestamp'
- type: 'null'
cancelledByUserId:
oneOf:
- $ref: '#/components/schemas/Uuid'
- type: 'null'
cancellationReason:
type: [string, 'null']
maxLength: 500
version:
type: integer
minimum: 1
createdAt:
$ref: '#/components/schemas/Timestamp'
updatedAt:
$ref: '#/components/schemas/Timestamp'
allOf:
- if:
properties:
status:
const: todo
required: [status]
then:
properties:
startedAt: {type: 'null'}
startedByUserId: {type: 'null'}
completedAt: {type: 'null'}
completedByUserId: {type: 'null'}
cancelledAt: {type: 'null'}
cancelledByUserId: {type: 'null'}
cancellationReason: {type: 'null'}
- if:
properties:
status:
const: in_progress
required: [status]
then:
properties:
startedAt:
$ref: '#/components/schemas/Timestamp'
startedByUserId:
$ref: '#/components/schemas/Uuid'
completedAt: {type: 'null'}
completedByUserId: {type: 'null'}
cancelledAt: {type: 'null'}
cancelledByUserId: {type: 'null'}
cancellationReason: {type: 'null'}
- if:
properties:
status:
const: completed
required: [status]
then:
properties:
completedAt:
$ref: '#/components/schemas/Timestamp'
completedByUserId:
$ref: '#/components/schemas/Uuid'
cancelledAt: {type: 'null'}
cancelledByUserId: {type: 'null'}
cancellationReason: {type: 'null'}
- if:
properties:
status:
const: cancelled
required: [status]
then:
properties:
completedAt: {type: 'null'}
completedByUserId: {type: 'null'}
cancelledAt:
$ref: '#/components/schemas/Timestamp'
cancelledByUserId:
$ref: '#/components/schemas/Uuid'
description: Terminal and start metadata are controlled exclusively by task commands.
CreateEngineeringTaskRequest:
type: object
additionalProperties: false
required: [projectId, title]
properties:
projectId:
$ref: '#/components/schemas/Uuid'
title:
type: string
minLength: 1
maxLength: 300
description:
type: [string, 'null']
maxLength: 10000
priority:
allOf:
- $ref: '#/components/schemas/EngineeringTaskPriority'
default: medium
assignedToUserId:
oneOf:
- $ref: '#/components/schemas/Uuid'
- type: 'null'
dueAt:
oneOf:
- $ref: '#/components/schemas/Timestamp'
- type: 'null'
UpdateEngineeringTaskRequest:
type: object
additionalProperties: false
minProperties: 1
properties:
title:
type: string
minLength: 1
maxLength: 300
description:
type: [string, 'null']
maxLength: 10000
priority:
$ref: '#/components/schemas/EngineeringTaskPriority'
assignedToUserId:
oneOf:
- $ref: '#/components/schemas/Uuid'
- type: 'null'
dueAt:
oneOf:
- $ref: '#/components/schemas/Timestamp'
- type: 'null'
CompleteEngineeringTaskRequest:
type: object
additionalProperties: false
properties:
completedAt:
$ref: '#/components/schemas/Timestamp'
description: A supplied completion time cannot be in the future or precede task creation.
CancelEngineeringTaskRequest:
type: object
additionalProperties: false
properties:
reason:
type: string
maxLength: 500
EngineeringTaskResponse:
type: object
additionalProperties: false
required: [data]
properties:
data:
$ref: '#/components/schemas/EngineeringTask'
EngineeringTaskCollectionResponse:
type: object
additionalProperties: false
required: [data, meta]
properties:
data:
type: array
items:
$ref: '#/components/schemas/EngineeringTask'
meta:
$ref: '#/components/schemas/CollectionMeta'
EngineeringTaskBatchItem:
type: object
additionalProperties: false
required: [id, version]
properties:
id:
$ref: '#/components/schemas/Uuid'
version:
type: integer
minimum: 1
BatchAssignEngineeringTasksRequest:
type: object
additionalProperties: false
required: [tasks, assigneeUserId, mode]
properties:
tasks:
type: array
minItems: 1
maxItems: 100
uniqueItems: true
items:
$ref: '#/components/schemas/EngineeringTaskBatchItem'
assigneeUserId:
$ref: '#/components/schemas/Uuid'
mode:
$ref: '#/components/schemas/BatchExecutionMode'
description: Duplicate task IDs are rejected even when their supplied versions differ.
BatchCompleteEngineeringTasksRequest:
type: object
additionalProperties: false
required: [tasks, mode]
properties:
tasks:
type: array
minItems: 1
maxItems: 100
uniqueItems: true
items:
$ref: '#/components/schemas/EngineeringTaskBatchItem'
completedAt:
$ref: '#/components/schemas/Timestamp'
mode:
$ref: '#/components/schemas/BatchExecutionMode'
description: Duplicate task IDs are rejected; completedAt follows the single-task completion rules.
EngineeringTaskBatchSuccess:
type: object
additionalProperties: false
required: [id, version, status, assignedToUserId]
properties:
id:
$ref: '#/components/schemas/Uuid'
version:
type: integer
minimum: 1
status:
$ref: '#/components/schemas/EngineeringTaskStatus'
assignedToUserId:
oneOf:
- $ref: '#/components/schemas/Uuid'
- type: 'null'
EngineeringTaskBatchFailure:
type: object
additionalProperties: false
required: [id, code, message, currentVersion]
properties:
id:
$ref: '#/components/schemas/Uuid'
code:
type: string
pattern: '^[A-Z][A-Z0-9_]+$'
message:
type: string
maxLength: 500
currentVersion:
type: [integer, 'null']
minimum: 1
EngineeringTaskBatchResult:
type: object
additionalProperties: false
required: [mode, succeeded, failed]
properties:
mode:
$ref: '#/components/schemas/BatchExecutionMode'
succeeded:
type: array
items:
$ref: '#/components/schemas/EngineeringTaskBatchSuccess'
failed:
type: array
items:
$ref: '#/components/schemas/EngineeringTaskBatchFailure'
EngineeringTaskBatchResponse:
type: object
additionalProperties: false
required: [data]
properties:
data:
$ref: '#/components/schemas/EngineeringTaskBatchResult'
security:
- bearerAuth: []