openapi: 3.0.3 info: title: Dealspace API description: | M&A deal management platform API. Investment Banks, Sellers, and Buyers collaborate on a structured request-and-answer workflow system. ## Authentication All endpoints except `/health` and `/api/auth/*` require a valid Bearer token. Tokens are JWT (HS256) with 1-hour access token lifetime and 7-day refresh token lifetime. ## RBAC Model Access is scoped to projects and workstreams. Roles determine permissions: - **ib_admin** — Full control over project and all workstreams - **ib_member** — Manage requests and vet answers in assigned workstreams - **seller_admin** — Manage seller team, see all seller-directed requests - **seller_member** — Answer requests in assigned workstreams - **buyer_admin** — Manage buyer team, access data room - **buyer_member** — Submit requests, view published answers - **observer** — Read-only access to assigned workstreams ## Error Responses All errors follow a consistent format: ```json {"error": "Human-readable message", "code": "ERROR_CODE", "details": {}} ``` ## Rate Limiting Endpoints are rate-limited per user, IP, and project. Headers `X-RateLimit-Limit`, `X-RateLimit-Remaining`, and `Retry-After` are included in responses. version: 1.0.0 contact: name: Dealspace Support email: support@muskepo.com license: name: Proprietary servers: - url: https://muskepo.com/api description: Production - url: http://localhost:8080/api description: Development tags: - name: Auth description: Authentication and MFA - name: Projects description: Top-level deal containers - name: Workstreams description: RBAC-anchored workflow containers (Finance, Legal, IT, etc.) - name: Request Lists description: Named collections of requests within a workstream - name: Requests description: Individual request items - name: Answers description: Responses to requests - name: Matches description: AI-suggested answer-to-request matching - name: Objects description: File upload/download with watermarking - name: Tasks description: Personal task inbox - name: Access description: RBAC grants - name: Invites description: Project invitations - name: Audit description: Security audit log - name: Health description: Service health check paths: # ============================================================================ # AUTH ENDPOINTS # ============================================================================ /auth/login: post: tags: [Auth] summary: Authenticate with email and password description: | Returns access and refresh tokens. If MFA is required but not yet verified, the token will have `mfa: false` and can only be used on `/auth/mfa/*` endpoints. **Rate limit:** 5 requests/minute per IP operationId: login security: [] requestBody: required: true content: application/json: schema: type: object required: [email, password] properties: email: type: string format: email example: user@example.com password: type: string format: password minLength: 8 fingerprint: type: object description: Device fingerprint for session binding properties: userAgent: type: string acceptLanguage: type: string timezone: type: string responses: '200': description: Authentication successful content: application/json: schema: $ref: '#/components/schemas/AuthTokens' '401': description: Invalid credentials content: application/json: schema: $ref: '#/components/schemas/Error' '429': description: Rate limit exceeded headers: Retry-After: schema: type: integer description: Seconds until rate limit resets content: application/json: schema: $ref: '#/components/schemas/Error' /auth/refresh: post: tags: [Auth] summary: Rotate refresh token description: | Exchange a valid refresh token for new access and refresh tokens. The old refresh token is invalidated (rotation). Fingerprint must match the original session. **Rate limit:** 60 requests/minute per user operationId: refreshToken security: [] requestBody: required: true content: application/json: schema: type: object required: [refresh_token] properties: refresh_token: type: string fingerprint: type: object properties: userAgent: type: string acceptLanguage: type: string timezone: type: string responses: '200': description: Tokens rotated successfully content: application/json: schema: $ref: '#/components/schemas/AuthTokens' '401': description: Invalid or expired refresh token content: application/json: schema: $ref: '#/components/schemas/Error' /auth/logout: post: tags: [Auth] summary: Revoke current session description: | Invalidates the current access token and all associated refresh tokens. **Rate limit:** 60 requests/minute per user operationId: logout security: - bearerAuth: [] responses: '204': description: Session revoked successfully '401': description: Invalid or expired token content: application/json: schema: $ref: '#/components/schemas/Error' /auth/mfa/setup: post: tags: [Auth] summary: Initiate TOTP MFA setup description: | Generates a new TOTP secret and returns a QR code URI for authenticator apps. Also returns recovery codes (store securely, shown only once). **RBAC:** Any authenticated user **Rate limit:** 5 requests/minute per user operationId: mfaSetup security: - bearerAuth: [] responses: '200': description: MFA setup initiated content: application/json: schema: type: object properties: secret: type: string description: Base32-encoded TOTP secret (for manual entry) qr_uri: type: string format: uri description: otpauth:// URI for QR code generation recovery_codes: type: array items: type: string description: 10 single-use recovery codes '400': description: MFA already enabled content: application/json: schema: $ref: '#/components/schemas/Error' /auth/mfa/verify: post: tags: [Auth] summary: Verify TOTP code description: | Completes MFA setup (if pending) or verifies MFA during login. On successful verification, returns a new token with `mfa: true`. **Rate limit:** 5 requests/minute per user (prevents brute force) operationId: mfaVerify security: - bearerAuth: [] requestBody: required: true content: application/json: schema: type: object required: [code] properties: code: type: string pattern: '^\d{6}$' description: 6-digit TOTP code from authenticator app responses: '200': description: MFA verified content: application/json: schema: $ref: '#/components/schemas/AuthTokens' '401': description: Invalid TOTP code content: application/json: schema: $ref: '#/components/schemas/Error' /invites/accept: post: tags: [Auth] summary: Accept invitation and create account description: | Accepts an invite token. If the user does not exist, creates a new account. If the user exists (by email), grants access to the invited project/workstream. **Rate limit:** 10 requests/minute per IP operationId: acceptInvite security: [] requestBody: required: true content: application/json: schema: type: object required: [token] properties: token: type: string description: Invite token from email name: type: string description: User's full name (required if creating new account) password: type: string format: password minLength: 8 description: Password (required if creating new account) responses: '200': description: Invitation accepted content: application/json: schema: type: object properties: user_id: type: string format: uuid project_id: type: string format: uuid role: type: string tokens: $ref: '#/components/schemas/AuthTokens' '400': description: Invalid, expired, or already-used invite content: application/json: schema: $ref: '#/components/schemas/Error' # ============================================================================ # PROJECT ENDPOINTS # ============================================================================ /projects: get: tags: [Projects] summary: List all projects for the authenticated user description: | Returns all projects the user has access to, based on their role grants. **RBAC:** Any authenticated user **Rate limit:** 300 requests/minute per user operationId: listProjects security: - bearerAuth: [] parameters: - name: stage in: query schema: type: string enum: [pre_dataroom, dataroom, closed] description: Filter by deal stage - name: limit in: query schema: type: integer default: 50 maximum: 100 - name: offset in: query schema: type: integer default: 0 responses: '200': description: List of projects content: application/json: schema: type: object properties: projects: type: array items: $ref: '#/components/schemas/Project' total: type: integer limit: type: integer offset: type: integer '401': $ref: '#/components/responses/Unauthorized' post: tags: [Projects] summary: Create a new project description: | Creates a new project. The creator automatically becomes `ib_admin`. **RBAC:** Any authenticated user (typically IB initiates deals) **Rate limit:** 10 requests/minute per user operationId: createProject security: - bearerAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/ProjectCreate' responses: '201': description: Project created content: application/json: schema: $ref: '#/components/schemas/Project' '400': description: Invalid input content: application/json: schema: $ref: '#/components/schemas/Error' '401': $ref: '#/components/responses/Unauthorized' /projects/{id}: parameters: - $ref: '#/components/parameters/ProjectId' get: tags: [Projects] summary: Get project details description: | Returns project metadata and summary statistics. **RBAC:** Any role with access to the project **Rate limit:** 300 requests/minute per user operationId: getProject security: - bearerAuth: [] responses: '200': description: Project details content: application/json: schema: $ref: '#/components/schemas/Project' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' patch: tags: [Projects] summary: Update project description: | Updates project metadata (name, stage, theme, etc.). **RBAC:** `ib_admin` only **Rate limit:** 60 requests/minute per user operationId: updateProject security: - bearerAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/ProjectUpdate' responses: '200': description: Project updated headers: ETag: schema: type: string description: Updated version tag for concurrency control content: application/json: schema: $ref: '#/components/schemas/Project' '400': description: Invalid input content: application/json: schema: $ref: '#/components/schemas/Error' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '409': $ref: '#/components/responses/Conflict' '412': $ref: '#/components/responses/PreconditionFailed' delete: tags: [Projects] summary: Soft delete project description: | Marks the project as deleted. Data is retained for compliance but project becomes inaccessible to all users. **RBAC:** `ib_admin` only **Rate limit:** 10 requests/minute per user operationId: deleteProject security: - bearerAuth: [] responses: '204': description: Project deleted '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' # ============================================================================ # WORKSTREAM ENDPOINTS # ============================================================================ /projects/{id}/workstreams: parameters: - $ref: '#/components/parameters/ProjectId' get: tags: [Workstreams] summary: List workstreams in project description: | Returns all workstreams the user has access to within the project. **RBAC:** Any role with project access (filtered by workstream-level access) **Rate limit:** 300 requests/minute per user operationId: listWorkstreams security: - bearerAuth: [] responses: '200': description: List of workstreams content: application/json: schema: type: object properties: workstreams: type: array items: $ref: '#/components/schemas/Workstream' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' post: tags: [Workstreams] summary: Create workstream description: | Creates a new workstream within the project. Workstreams are RBAC anchors. **RBAC:** `ib_admin` only **Rate limit:** 60 requests/minute per user operationId: createWorkstream security: - bearerAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/WorkstreamCreate' responses: '201': description: Workstream created content: application/json: schema: $ref: '#/components/schemas/Workstream' '400': description: Invalid input content: application/json: schema: $ref: '#/components/schemas/Error' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' /projects/{id}/workstreams/{wsId}: parameters: - $ref: '#/components/parameters/ProjectId' - $ref: '#/components/parameters/WorkstreamId' patch: tags: [Workstreams] summary: Update workstream description: | Updates workstream metadata (name, description). **RBAC:** `ib_admin` or `ib_member` with access to this workstream **Rate limit:** 60 requests/minute per user operationId: updateWorkstream security: - bearerAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/WorkstreamUpdate' responses: '200': description: Workstream updated content: application/json: schema: $ref: '#/components/schemas/Workstream' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' # ============================================================================ # REQUEST LIST ENDPOINTS # ============================================================================ /projects/{id}/workstreams/{wsId}/lists: parameters: - $ref: '#/components/parameters/ProjectId' - $ref: '#/components/parameters/WorkstreamId' get: tags: [Request Lists] summary: List request lists in workstream description: | Returns all request lists within the workstream. **RBAC:** Any role with workstream access **Rate limit:** 300 requests/minute per user operationId: listRequestLists security: - bearerAuth: [] responses: '200': description: List of request lists content: application/json: schema: type: object properties: lists: type: array items: $ref: '#/components/schemas/RequestList' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' post: tags: [Request Lists] summary: Create request list description: | Creates a new named collection of requests within the workstream. **RBAC:** `ib_admin`, `ib_member` **Rate limit:** 60 requests/minute per user operationId: createRequestList security: - bearerAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/RequestListCreate' responses: '201': description: Request list created content: application/json: schema: $ref: '#/components/schemas/RequestList' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' # ============================================================================ # REQUEST ENDPOINTS # ============================================================================ /projects/{id}/workstreams/{wsId}/requests: parameters: - $ref: '#/components/parameters/ProjectId' - $ref: '#/components/parameters/WorkstreamId' get: tags: [Requests] summary: List requests in workstream description: | Returns requests in the workstream. Supports filtering by assignee, status, and stage. **RBAC:** Any role with workstream access. Pre-dataroom entries invisible to buyers. **Rate limit:** 300 requests/minute per user operationId: listRequests security: - bearerAuth: [] parameters: - name: assignee in: query schema: type: string description: Filter by assignee. Use "me" for current user. - name: status in: query schema: type: string enum: [open, assigned, answered, vetted, published, closed] description: Filter by status - name: stage in: query schema: type: string enum: [pre_dataroom, dataroom, closed] description: Filter by deal stage - name: list_id in: query schema: type: string format: uuid description: Filter by request list - name: priority in: query schema: type: string enum: [high, normal, low] - name: overdue in: query schema: type: boolean description: Only return requests past due date - name: limit in: query schema: type: integer default: 50 maximum: 100 - name: offset in: query schema: type: integer default: 0 responses: '200': description: List of requests content: application/json: schema: type: object properties: requests: type: array items: $ref: '#/components/schemas/Request' total: type: integer limit: type: integer offset: type: integer '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' post: tags: [Requests] summary: Create request description: | Creates a new request in the workstream. IB/Seller create for seller action, Buyers create via data room (routed to IB). **RBAC:** `ib_admin`, `ib_member`, `buyer_admin`, `buyer_member` **Rate limit:** 60 requests/minute per user operationId: createRequest security: - bearerAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/RequestCreate' responses: '201': description: Request created content: application/json: schema: $ref: '#/components/schemas/Request' '400': description: Invalid input content: application/json: schema: $ref: '#/components/schemas/Error' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' /projects/{id}/workstreams/{wsId}/requests/{reqId}: parameters: - $ref: '#/components/parameters/ProjectId' - $ref: '#/components/parameters/WorkstreamId' - $ref: '#/components/parameters/RequestId' get: tags: [Requests] summary: Get request details description: | Returns full request details including routing chain (visible to IB only). **RBAC:** Any role with workstream access **Rate limit:** 300 requests/minute per user operationId: getRequest security: - bearerAuth: [] responses: '200': description: Request details headers: ETag: schema: type: string description: Version tag for concurrency control content: application/json: schema: $ref: '#/components/schemas/Request' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' patch: tags: [Requests] summary: Update request description: | Updates request metadata (title, body, priority, due_date, assigned_to, status). Use `If-Match` header with ETag for optimistic concurrency control. **RBAC:** `ib_admin`, `ib_member`, `seller_admin`, `seller_member` (own assignments) **Rate limit:** 60 requests/minute per user operationId: updateRequest security: - bearerAuth: [] parameters: - name: If-Match in: header schema: type: string description: ETag from previous GET for concurrency control requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/RequestUpdate' responses: '200': description: Request updated headers: ETag: schema: type: string content: application/json: schema: $ref: '#/components/schemas/Request' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '409': $ref: '#/components/responses/Conflict' '412': $ref: '#/components/responses/PreconditionFailed' /projects/{id}/workstreams/{wsId}/requests/{reqId}/forward: parameters: - $ref: '#/components/parameters/ProjectId' - $ref: '#/components/parameters/WorkstreamId' - $ref: '#/components/parameters/RequestId' post: tags: [Requests] summary: Forward request in routing chain description: | Forwards the request to another user. Creates a routing chain hop. The `return_to_id` is set automatically so completion returns to the forwarder. **RBAC:** Current assignee or `ib_admin` **Rate limit:** 60 requests/minute per user operationId: forwardRequest security: - bearerAuth: [] requestBody: required: true content: application/json: schema: type: object required: [to_user_id] properties: to_user_id: type: string format: uuid description: User to forward the request to message: type: string description: Optional message for the recipient responses: '200': description: Request forwarded content: application/json: schema: $ref: '#/components/schemas/Request' '400': description: Invalid recipient content: application/json: schema: $ref: '#/components/schemas/Error' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' /projects/{id}/workstreams/{wsId}/requests/{reqId}/complete: parameters: - $ref: '#/components/parameters/ProjectId' - $ref: '#/components/parameters/WorkstreamId' - $ref: '#/components/parameters/RequestId' post: tags: [Requests] summary: Mark request as complete description: | Marks the request as completed by the current assignee. If `return_to_id` is set, the request is automatically routed back to that user's inbox. Otherwise, it moves to the final status. **RBAC:** Current assignee only **Rate limit:** 60 requests/minute per user operationId: completeRequest security: - bearerAuth: [] requestBody: content: application/json: schema: type: object properties: message: type: string description: Optional completion note responses: '200': description: Request completed and routed content: application/json: schema: $ref: '#/components/schemas/Request' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' # ============================================================================ # ANSWER ENDPOINTS # ============================================================================ /projects/{id}/workstreams/{wsId}/answers: parameters: - $ref: '#/components/parameters/ProjectId' - $ref: '#/components/parameters/WorkstreamId' post: tags: [Answers] summary: Create an answer description: | Creates a new answer in the workstream. Answers can be linked to one or more requests. Initially created in `draft` status. **RBAC:** `seller_admin`, `seller_member`, `ib_admin`, `ib_member` **Rate limit:** 60 requests/minute per user operationId: createAnswer security: - bearerAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/AnswerCreate' responses: '201': description: Answer created content: application/json: schema: $ref: '#/components/schemas/Answer' '400': description: Invalid input content: application/json: schema: $ref: '#/components/schemas/Error' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' /projects/{id}/workstreams/{wsId}/answers/{ansId}: parameters: - $ref: '#/components/parameters/ProjectId' - $ref: '#/components/parameters/WorkstreamId' - $ref: '#/components/parameters/AnswerId' get: tags: [Answers] summary: Get answer details description: | Returns full answer details including linked requests and files. **RBAC:** Depends on answer status. Draft/submitted visible to IB+seller. Published visible to all with workstream access. **Rate limit:** 300 requests/minute per user operationId: getAnswer security: - bearerAuth: [] responses: '200': description: Answer details headers: ETag: schema: type: string content: application/json: schema: $ref: '#/components/schemas/Answer' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' patch: tags: [Answers] summary: Update answer description: | Updates answer content. Only allowed in `draft` or `rejected` status. **RBAC:** Answer creator, `seller_admin`, or `ib_admin` **Rate limit:** 60 requests/minute per user operationId: updateAnswer security: - bearerAuth: [] parameters: - name: If-Match in: header schema: type: string requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/AnswerUpdate' responses: '200': description: Answer updated headers: ETag: schema: type: string content: application/json: schema: $ref: '#/components/schemas/Answer' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '409': $ref: '#/components/responses/Conflict' /projects/{id}/workstreams/{wsId}/answers/{ansId}/submit: parameters: - $ref: '#/components/parameters/ProjectId' - $ref: '#/components/parameters/WorkstreamId' - $ref: '#/components/parameters/AnswerId' post: tags: [Answers] summary: Submit answer for vetting description: | Submits the answer for IB review. Transitions status from `draft` to `submitted`. **RBAC:** Answer creator, `seller_admin` **Rate limit:** 60 requests/minute per user operationId: submitAnswer security: - bearerAuth: [] responses: '200': description: Answer submitted for vetting content: application/json: schema: $ref: '#/components/schemas/Answer' '400': description: Answer not in draft status content: application/json: schema: $ref: '#/components/schemas/Error' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' /projects/{id}/workstreams/{wsId}/answers/{ansId}/approve: parameters: - $ref: '#/components/parameters/ProjectId' - $ref: '#/components/parameters/WorkstreamId' - $ref: '#/components/parameters/AnswerId' post: tags: [Answers] summary: Approve answer description: | IB approves the answer. Transitions status from `submitted` to `approved`. Answer is now ready for publishing to data room. **RBAC:** `ib_admin`, `ib_member` with workstream access **Rate limit:** 60 requests/minute per user operationId: approveAnswer security: - bearerAuth: [] responses: '200': description: Answer approved content: application/json: schema: $ref: '#/components/schemas/Answer' '400': description: Answer not in submitted status content: application/json: schema: $ref: '#/components/schemas/Error' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' /projects/{id}/workstreams/{wsId}/answers/{ansId}/reject: parameters: - $ref: '#/components/parameters/ProjectId' - $ref: '#/components/parameters/WorkstreamId' - $ref: '#/components/parameters/AnswerId' post: tags: [Answers] summary: Reject answer description: | IB rejects the answer with a reason. Transitions status from `submitted` to `rejected`. Seller can then edit and resubmit. **RBAC:** `ib_admin`, `ib_member` with workstream access **Rate limit:** 60 requests/minute per user operationId: rejectAnswer security: - bearerAuth: [] requestBody: required: true content: application/json: schema: type: object required: [reason] properties: reason: type: string minLength: 1 description: Explanation for why the answer was rejected responses: '200': description: Answer rejected content: application/json: schema: $ref: '#/components/schemas/Answer' '400': description: Answer not in submitted status or missing reason content: application/json: schema: $ref: '#/components/schemas/Error' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' /projects/{id}/workstreams/{wsId}/answers/{ansId}/publish: parameters: - $ref: '#/components/parameters/ProjectId' - $ref: '#/components/parameters/WorkstreamId' - $ref: '#/components/parameters/AnswerId' post: tags: [Answers] summary: Publish answer to data room description: | Publishes an approved answer to the data room. All linked requests are updated and broadcast notifications sent to all requesters who asked equivalent questions. **RBAC:** `ib_admin`, `ib_member` with workstream access **Rate limit:** 60 requests/minute per user operationId: publishAnswer security: - bearerAuth: [] requestBody: content: application/json: schema: type: object properties: broadcast_to: type: string enum: [linked_requesters, all_workstream, all_dataroom] default: linked_requesters description: | Broadcast scope: - `linked_requesters`: Only users who submitted linked requests - `all_workstream`: All users with workstream access - `all_dataroom`: All users with any data room access responses: '200': description: Answer published content: application/json: schema: $ref: '#/components/schemas/Answer' '400': description: Answer not in approved status content: application/json: schema: $ref: '#/components/schemas/Error' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' # ============================================================================ # AI MATCHING ENDPOINTS # ============================================================================ /projects/{id}/workstreams/{wsId}/requests/{reqId}/matches: parameters: - $ref: '#/components/parameters/ProjectId' - $ref: '#/components/parameters/WorkstreamId' - $ref: '#/components/parameters/RequestId' get: tags: [Matches] summary: Get AI-suggested answer matches description: | Returns published answers that the AI has identified as potentially satisfying this request (cosine similarity >= 0.72). Human confirmation is required before linking. **RBAC:** `ib_admin`, `ib_member` **Rate limit:** 20 requests/minute per user (embedding cost control) operationId: getMatches security: - bearerAuth: [] parameters: - name: min_score in: query schema: type: number minimum: 0 maximum: 1 default: 0.72 description: Minimum similarity score threshold - name: limit in: query schema: type: integer default: 10 maximum: 25 responses: '200': description: List of suggested matches content: application/json: schema: type: object properties: matches: type: array items: $ref: '#/components/schemas/AnswerMatch' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' /projects/{id}/workstreams/{wsId}/requests/{reqId}/matches/{answerId}/confirm: parameters: - $ref: '#/components/parameters/ProjectId' - $ref: '#/components/parameters/WorkstreamId' - $ref: '#/components/parameters/RequestId' - $ref: '#/components/parameters/AnswerIdPath' post: tags: [Matches] summary: Confirm AI match description: | Human confirms that the suggested answer satisfies this request. Creates a confirmed link in `answer_links` and updates request status. Triggers broadcast to the requester. **RBAC:** `ib_admin`, `ib_member` **Rate limit:** 60 requests/minute per user operationId: confirmMatch security: - bearerAuth: [] responses: '200': description: Match confirmed content: application/json: schema: type: object properties: request: $ref: '#/components/schemas/Request' answer: $ref: '#/components/schemas/Answer' link_id: type: string format: uuid '400': description: Answer not published or already linked content: application/json: schema: $ref: '#/components/schemas/Error' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' /projects/{id}/workstreams/{wsId}/requests/{reqId}/matches/{answerId}/reject: parameters: - $ref: '#/components/parameters/ProjectId' - $ref: '#/components/parameters/WorkstreamId' - $ref: '#/components/parameters/RequestId' - $ref: '#/components/parameters/AnswerIdPath' post: tags: [Matches] summary: Reject AI match description: | Human rejects the AI suggestion. The answer will not be suggested again for this request (stored in `answer_links` with rejected flag). **RBAC:** `ib_admin`, `ib_member` **Rate limit:** 60 requests/minute per user operationId: rejectMatch security: - bearerAuth: [] responses: '200': description: Match rejected content: application/json: schema: type: object properties: message: type: string example: Match rejected and will not be suggested again '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' # ============================================================================ # FILE/OBJECT ENDPOINTS # ============================================================================ /projects/{id}/objects: parameters: - $ref: '#/components/parameters/ProjectId' post: tags: [Objects] summary: Upload file description: | Uploads a file to the project's object store. Files are encrypted at rest. Returns an objectId that can be referenced in answers. **RBAC:** `ib_admin`, `ib_member`, `seller_admin`, `seller_member` **Rate limit:** 10 uploads/minute per user **Max size:** 100MB per file operationId: uploadObject security: - bearerAuth: [] requestBody: required: true content: multipart/form-data: schema: type: object required: [file] properties: file: type: string format: binary filename: type: string description: Override filename (defaults to uploaded name) description: type: string description: File description for search responses: '201': description: File uploaded content: application/json: schema: type: object properties: object_id: type: string pattern: '^[a-f0-9]{16}$' description: Content-addressable object ID filename: type: string size: type: integer description: File size in bytes mime_type: type: string uploaded_at: type: string format: date-time '400': description: Invalid file or missing data content: application/json: schema: $ref: '#/components/schemas/Error' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '413': description: File too large content: application/json: schema: $ref: '#/components/schemas/Error' /projects/{id}/objects/{objectId}: parameters: - $ref: '#/components/parameters/ProjectId' - $ref: '#/components/parameters/ObjectId' get: tags: [Objects] summary: Download file with watermark description: | Downloads a file with dynamic watermark applied. Watermarks include: - User name and organization - Timestamp - CONFIDENTIAL marking PDF, Word, Excel, and images have visible watermarks. Other files are encrypted downloads only. **RBAC:** Any role with access to the answer containing this file **Rate limit:** 50 downloads/minute per user (exfiltration protection) operationId: downloadObject security: - bearerAuth: [] parameters: - name: preview in: query schema: type: boolean default: false description: Return preview (lower quality) instead of full file responses: '200': description: File content with watermark headers: Content-Disposition: schema: type: string example: 'attachment; filename="document.pdf"' Content-Type: schema: type: string X-Watermark-Applied: schema: type: string enum: [visible, encrypted, none] content: '*/*': schema: type: string format: binary '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' delete: tags: [Objects] summary: Delete file description: | Deletes a file from the object store. File must not be referenced by any published answer. **RBAC:** File uploader, `ib_admin`, `seller_admin` **Rate limit:** 60 requests/minute per user operationId: deleteObject security: - bearerAuth: [] responses: '204': description: File deleted '400': description: File is referenced by published answer content: application/json: schema: $ref: '#/components/schemas/Error' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' # ============================================================================ # TASK ENDPOINTS # ============================================================================ /tasks: get: tags: [Tasks] summary: List personal tasks description: | Returns all tasks (requests) assigned to the current user across all projects. This is the user's personal inbox. **RBAC:** Any authenticated user **Rate limit:** 300 requests/minute per user operationId: listTasks security: - bearerAuth: [] parameters: - name: status in: query schema: type: string enum: [open, assigned, answered, vetted, published, closed] description: Filter by status - name: overdue in: query schema: type: boolean description: Only return tasks past due date - name: project_id in: query schema: type: string format: uuid description: Filter by project - name: priority in: query schema: type: string enum: [high, normal, low] - name: limit in: query schema: type: integer default: 50 maximum: 100 - name: offset in: query schema: type: integer default: 0 responses: '200': description: List of tasks content: application/json: schema: type: object properties: tasks: type: array items: $ref: '#/components/schemas/Task' total: type: integer limit: type: integer offset: type: integer '401': $ref: '#/components/responses/Unauthorized' # ============================================================================ # ACCESS ENDPOINTS # ============================================================================ /projects/{id}/access: parameters: - $ref: '#/components/parameters/ProjectId' get: tags: [Access] summary: List access grants description: | Returns all access grants for the project. **RBAC:** `ib_admin`, `seller_admin` (own org), `buyer_admin` (own org) **Rate limit:** 300 requests/minute per user operationId: listAccess security: - bearerAuth: [] parameters: - name: role in: query schema: type: string enum: [ib_admin, ib_member, seller_admin, seller_member, buyer_admin, buyer_member, observer] description: Filter by role - name: workstream_id in: query schema: type: string format: uuid description: Filter by workstream responses: '200': description: List of access grants content: application/json: schema: type: object properties: grants: type: array items: $ref: '#/components/schemas/AccessGrant' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' post: tags: [Access] summary: Grant access description: | Grants a role to a user for the project and/or specific workstream. Users can only grant roles at or below their level. IB can grant any role. Seller/Buyer can only grant within their org type. **RBAC:** `ib_admin`, `seller_admin`, `buyer_admin` (with hierarchy constraints) **Rate limit:** 60 requests/minute per user operationId: grantAccess security: - bearerAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/AccessGrantCreate' responses: '201': description: Access granted content: application/json: schema: $ref: '#/components/schemas/AccessGrant' '400': description: Invalid input or role hierarchy violation content: application/json: schema: $ref: '#/components/schemas/Error' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' /projects/{id}/access/{grantId}: parameters: - $ref: '#/components/parameters/ProjectId' - $ref: '#/components/parameters/GrantId' delete: tags: [Access] summary: Revoke access description: | Revokes an access grant. All active sessions for the affected user are immediately invalidated for this project. **RBAC:** `ib_admin`, or admin of same org type who granted the access **Rate limit:** 60 requests/minute per user operationId: revokeAccess security: - bearerAuth: [] parameters: - name: reason in: query schema: type: string description: Audit trail reason for revocation responses: '204': description: Access revoked '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' # ============================================================================ # INVITE ENDPOINTS # ============================================================================ /projects/{id}/invites: parameters: - $ref: '#/components/parameters/ProjectId' get: tags: [Invites] summary: List pending invites description: | Returns all pending invitations for the project. **RBAC:** `ib_admin`, `seller_admin`, `buyer_admin` **Rate limit:** 300 requests/minute per user operationId: listInvites security: - bearerAuth: [] parameters: - name: status in: query schema: type: string enum: [pending, accepted, expired, revoked] responses: '200': description: List of invites content: application/json: schema: type: object properties: invites: type: array items: $ref: '#/components/schemas/Invite' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' post: tags: [Invites] summary: Create invite description: | Creates an invitation for a user to join the project with a specific role. Invites are scoped to a project and optionally to specific workstreams. Invite token expires in 72 hours. **RBAC:** `ib_admin`, `seller_admin`, `buyer_admin` (with role hierarchy constraints) **Rate limit:** 30 requests/minute per user operationId: createInvite security: - bearerAuth: [] requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/InviteCreate' responses: '201': description: Invite created and sent content: application/json: schema: $ref: '#/components/schemas/Invite' '400': description: Invalid input or role hierarchy violation content: application/json: schema: $ref: '#/components/schemas/Error' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' /projects/{id}/invites/{inviteId}: parameters: - $ref: '#/components/parameters/ProjectId' - $ref: '#/components/parameters/InviteId' delete: tags: [Invites] summary: Revoke invite description: | Revokes a pending invitation. If already accepted, use access revocation instead. **RBAC:** Invite creator or `ib_admin` **Rate limit:** 60 requests/minute per user operationId: revokeInvite security: - bearerAuth: [] responses: '204': description: Invite revoked '400': description: Invite already accepted content: application/json: schema: $ref: '#/components/schemas/Error' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' # ============================================================================ # AUDIT ENDPOINT # ============================================================================ /projects/{id}/audit: parameters: - $ref: '#/components/parameters/ProjectId' get: tags: [Audit] summary: Get audit log description: | Returns paginated audit log entries for the project. Includes all security-relevant events: access changes, file downloads, status transitions, logins. Audit entries are hash-chained for tamper evidence. **RBAC:** `ib_admin` only **Rate limit:** 30 requests/minute per user operationId: getAuditLog security: - bearerAuth: [] parameters: - name: action in: query schema: type: string description: Filter by action type (e.g., "file.downloaded", "access.granted") - name: actor_id in: query schema: type: string format: uuid description: Filter by actor - name: target_id in: query schema: type: string format: uuid description: Filter by target entry/user/file - name: from in: query schema: type: string format: date-time description: Start of time range - name: to in: query schema: type: string format: date-time description: End of time range - name: limit in: query schema: type: integer default: 100 maximum: 500 - name: offset in: query schema: type: integer default: 0 responses: '200': description: Audit log entries content: application/json: schema: type: object properties: entries: type: array items: $ref: '#/components/schemas/AuditEntry' total: type: integer limit: type: integer offset: type: integer chain_verified: type: boolean description: Whether hash chain integrity was verified '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' # ============================================================================ # HEALTH ENDPOINT # ============================================================================ /health: get: tags: [Health] summary: Service health check description: | Returns service health status. No authentication required. Suitable for load balancer and monitoring probes. operationId: healthCheck security: [] responses: '200': description: Service healthy content: application/json: schema: type: object properties: status: type: string enum: [healthy, degraded] version: type: string example: "1.0.0" uptime: type: integer description: Uptime in seconds checks: type: object properties: database: type: string enum: [ok, error] object_store: type: string enum: [ok, error] redis: type: string enum: [ok, error] '503': description: Service unhealthy content: application/json: schema: type: object properties: status: type: string enum: [unhealthy] errors: type: array items: type: string # ============================================================================== # COMPONENTS # ============================================================================== components: securitySchemes: bearerAuth: type: http scheme: bearer bearerFormat: JWT description: | JWT token from `/auth/login` or `/auth/refresh`. Token contains: sub (user_id), org, roles[], mfa, sid, fp, exp. parameters: ProjectId: name: id in: path required: true schema: type: string format: uuid description: Project ID WorkstreamId: name: wsId in: path required: true schema: type: string format: uuid description: Workstream ID RequestId: name: reqId in: path required: true schema: type: string format: uuid description: Request ID AnswerId: name: ansId in: path required: true schema: type: string format: uuid description: Answer ID AnswerIdPath: name: answerId in: path required: true schema: type: string format: uuid description: Answer ID for matching ObjectId: name: objectId in: path required: true schema: type: string pattern: '^[a-f0-9]{16}$' description: Content-addressable object ID GrantId: name: grantId in: path required: true schema: type: string format: uuid description: Access grant ID InviteId: name: inviteId in: path required: true schema: type: string format: uuid description: Invite ID responses: Unauthorized: description: Authentication required or token invalid content: application/json: schema: $ref: '#/components/schemas/Error' example: error: "Invalid or expired authentication token" code: "UNAUTHORIZED" details: {} Forbidden: description: Insufficient permissions for this operation content: application/json: schema: $ref: '#/components/schemas/Error' example: error: "You do not have permission to perform this action" code: "FORBIDDEN" details: required_role: "ib_admin" NotFound: description: Resource not found content: application/json: schema: $ref: '#/components/schemas/Error' example: error: "Resource not found" code: "NOT_FOUND" details: {} Conflict: description: Concurrent modification conflict content: application/json: schema: $ref: '#/components/schemas/Error' example: error: "Resource was modified by another user" code: "CONFLICT" details: current_version: 5 PreconditionFailed: description: ETag mismatch - resource changed since last fetch content: application/json: schema: $ref: '#/components/schemas/Error' example: error: "Resource has been modified. Fetch the latest version and retry." code: "PRECONDITION_FAILED" details: current_etag: '"abc123"' schemas: Error: type: object required: [error, code] properties: error: type: string description: Human-readable error message code: type: string description: Machine-readable error code enum: - BAD_REQUEST - UNAUTHORIZED - FORBIDDEN - NOT_FOUND - CONFLICT - PRECONDITION_FAILED - RATE_LIMIT_EXCEEDED - INTERNAL_ERROR - INVALID_INVITE - INVITE_EXPIRED - INVITE_ALREADY_USED - EMAIL_MISMATCH - MFA_REQUIRED - INVALID_TOTP - CONCURRENT_MODIFICATION details: type: object additionalProperties: true description: Additional error context AuthTokens: type: object properties: access_token: type: string description: JWT access token (1 hour lifetime) refresh_token: type: string description: Refresh token (7 day lifetime, rotates on use) token_type: type: string enum: [Bearer] expires_in: type: integer description: Access token lifetime in seconds mfa_required: type: boolean description: Whether MFA verification is pending user: $ref: '#/components/schemas/User' User: type: object properties: id: type: string format: uuid email: type: string format: email name: type: string organization_id: type: string format: uuid organization_name: type: string mfa_enabled: type: boolean created_at: type: string format: date-time Project: type: object properties: id: type: string format: uuid name: type: string example: "Project Falcon" description: type: string stage: type: string enum: [pre_dataroom, dataroom, closed] theme_id: type: string format: uuid nullable: true created_by: type: string format: uuid created_at: type: string format: date-time updated_at: type: string format: date-time stats: type: object properties: workstreams: type: integer requests_total: type: integer requests_open: type: integer requests_completed: type: integer answers_published: type: integer my_role: type: string enum: [ib_admin, ib_member, seller_admin, seller_member, buyer_admin, buyer_member, observer] description: Current user's role in this project ProjectCreate: type: object required: [name] properties: name: type: string minLength: 1 maxLength: 255 description: type: string maxLength: 2000 theme_id: type: string format: uuid ProjectUpdate: type: object properties: name: type: string minLength: 1 maxLength: 255 description: type: string maxLength: 2000 stage: type: string enum: [pre_dataroom, dataroom, closed] theme_id: type: string format: uuid nullable: true Workstream: type: object properties: id: type: string format: uuid project_id: type: string format: uuid name: type: string example: "Finance" slug: type: string example: "finance" description: type: string created_at: type: string format: date-time updated_at: type: string format: date-time stats: type: object properties: requests_total: type: integer requests_open: type: integer answers_published: type: integer WorkstreamCreate: type: object required: [name] properties: name: type: string minLength: 1 maxLength: 100 slug: type: string pattern: '^[a-z0-9-]+$' maxLength: 50 description: URL-safe identifier (auto-generated from name if not provided) description: type: string maxLength: 1000 WorkstreamUpdate: type: object properties: name: type: string minLength: 1 maxLength: 100 description: type: string maxLength: 1000 RequestList: type: object properties: id: type: string format: uuid workstream_id: type: string format: uuid name: type: string example: "Initial Due Diligence" description: type: string created_by: type: string format: uuid created_at: type: string format: date-time request_count: type: integer RequestListCreate: type: object required: [name] properties: name: type: string minLength: 1 maxLength: 255 description: type: string maxLength: 1000 Request: type: object properties: id: type: string format: uuid workstream_id: type: string format: uuid list_id: type: string format: uuid nullable: true ref: type: string example: "FIN-042" description: Human-readable reference number title: type: string example: "Provide audited financials FY2024" body: type: string priority: type: string enum: [high, normal, low] due_date: type: string format: date nullable: true status: type: string enum: [open, assigned, answered, vetted, published, closed] stage: type: string enum: [pre_dataroom, dataroom, closed] assignee_id: type: string format: uuid nullable: true description: Current assignee (who has it RIGHT NOW) assignee: $ref: '#/components/schemas/User' return_to_id: type: string format: uuid nullable: true description: Who it returns to when completed origin_id: type: string format: uuid nullable: true description: Ultimate requester who started the chain created_by: type: string format: uuid created_at: type: string format: date-time updated_at: type: string format: date-time routing_chain: type: array description: Full routing history (visible to IB only) items: type: object properties: actor_id: type: string format: uuid action: type: string enum: [created, forwarded, completed] timestamp: type: string format: date-time message: type: string linked_answers: type: array items: type: object properties: answer_id: type: string format: uuid confirmed: type: boolean linked_at: type: string format: date-time RequestCreate: type: object required: [title] properties: title: type: string minLength: 1 maxLength: 500 body: type: string maxLength: 10000 list_id: type: string format: uuid description: Optional request list to add to priority: type: string enum: [high, normal, low] default: normal due_date: type: string format: date assigned_to: type: array items: type: string format: uuid description: Initial assignees RequestUpdate: type: object properties: title: type: string minLength: 1 maxLength: 500 body: type: string maxLength: 10000 priority: type: string enum: [high, normal, low] due_date: type: string format: date nullable: true assigned_to: type: array items: type: string format: uuid status: type: string enum: [open, assigned, answered, vetted, published, closed] Answer: type: object properties: id: type: string format: uuid workstream_id: type: string format: uuid title: type: string body: type: string status: type: string enum: [draft, submitted, approved, rejected, published] rejection_reason: type: string nullable: true files: type: array items: type: object properties: object_id: type: string pattern: '^[a-f0-9]{16}$' filename: type: string size: type: integer mime_type: type: string linked_requests: type: array items: type: object properties: request_id: type: string format: uuid ref: type: string title: type: string confirmed: type: boolean broadcast_to: type: string enum: [linked_requesters, all_workstream, all_dataroom] nullable: true created_by: type: string format: uuid created_at: type: string format: date-time updated_at: type: string format: date-time submitted_at: type: string format: date-time nullable: true approved_at: type: string format: date-time nullable: true published_at: type: string format: date-time nullable: true AnswerCreate: type: object required: [title] properties: title: type: string minLength: 1 maxLength: 500 body: type: string maxLength: 50000 file_ids: type: array items: type: string pattern: '^[a-f0-9]{16}$' description: Object IDs of uploaded files request_ids: type: array items: type: string format: uuid description: Requests this answer is linked to AnswerUpdate: type: object properties: title: type: string minLength: 1 maxLength: 500 body: type: string maxLength: 50000 file_ids: type: array items: type: string pattern: '^[a-f0-9]{16}$' request_ids: type: array items: type: string format: uuid AnswerMatch: type: object properties: answer_id: type: string format: uuid answer_title: type: string answer_status: type: string enum: [published] similarity_score: type: number format: float minimum: 0 maximum: 1 description: Cosine similarity score (AI-computed) matched_text: type: string description: Snippet of answer that matched the request linked_requests: type: integer description: Number of other requests already linked to this answer Task: type: object description: A request in the user's personal inbox properties: id: type: string format: uuid project_id: type: string format: uuid project_name: type: string workstream_id: type: string format: uuid workstream_name: type: string ref: type: string title: type: string priority: type: string enum: [high, normal, low] due_date: type: string format: date nullable: true status: type: string enum: [open, assigned, answered, vetted, published, closed] is_overdue: type: boolean return_to: $ref: '#/components/schemas/User' forwarded_by: $ref: '#/components/schemas/User' assigned_at: type: string format: date-time created_at: type: string format: date-time AccessGrant: type: object properties: id: type: string format: uuid project_id: type: string format: uuid workstream_id: type: string format: uuid nullable: true description: If null, access applies to all workstreams user_id: type: string format: uuid user: $ref: '#/components/schemas/User' role: type: string enum: [ib_admin, ib_member, seller_admin, seller_member, buyer_admin, buyer_member, observer] ops: type: string description: "Operation flags: r=read, w=write, d=delete, m=manage" example: "rw" granted_by: type: string format: uuid granted_at: type: string format: date-time AccessGrantCreate: type: object required: [user_id, role] properties: user_id: type: string format: uuid role: type: string enum: [ib_admin, ib_member, seller_admin, seller_member, buyer_admin, buyer_member, observer] workstream_id: type: string format: uuid nullable: true description: Scope to specific workstream (null = all workstreams) ops: type: string default: "rw" description: "Operation flags: r=read, w=write, d=delete, m=manage" Invite: type: object properties: id: type: string format: uuid project_id: type: string format: uuid workstream_id: type: string format: uuid nullable: true email: type: string format: email role: type: string enum: [ib_admin, ib_member, seller_admin, seller_member, buyer_admin, buyer_member, observer] status: type: string enum: [pending, accepted, expired, revoked] invited_by: type: string format: uuid invited_by_name: type: string expires_at: type: string format: date-time accepted_at: type: string format: date-time nullable: true created_at: type: string format: date-time InviteCreate: type: object required: [email, role] properties: email: type: string format: email role: type: string enum: [ib_admin, ib_member, seller_admin, seller_member, buyer_admin, buyer_member, observer] workstream_id: type: string format: uuid nullable: true description: Scope to specific workstream message: type: string maxLength: 1000 description: Personal message to include in invite email AuditEntry: type: object properties: id: type: string format: uuid project_id: type: string format: uuid actor_id: type: string format: uuid actor_email: type: string format: email action: type: string description: | Event type. Examples: - auth.login, auth.logout, auth.mfa_enabled - access.granted, access.revoked - entry.created, entry.updated, entry.published - file.uploaded, file.downloaded, file.deleted - session.created, session.revoked target_type: type: string enum: [entry, user, access, session, file] nullable: true target_id: type: string format: uuid nullable: true details: type: object additionalProperties: true description: Action-specific details ip_address: type: string user_agent: type: string timestamp: type: string format: date-time hash: type: string description: SHA-256 hash for tamper detection (chain integrity)