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

6374 lines
221 KiB
YAML

openapi: 3.1.0
info:
title: Professional Management Platform API
version: 1.0.0-milestone.6
summary: Platform access, engineering collaboration, governed documents, and design control.
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
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'}
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'}
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'}}}
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: []