openapi: "3.1.0" info: title: Bounty Board API version: "1.0" description: | Consulting work-management platform API (spec §6). All endpoints are session-authenticated (httpOnly cookie `bb_session`); mutations also require the CSRF double-submit header `X-CSRF-Token` mirroring the `bb_csrf` cookie. Errors use the envelope `{"error":{"code":"…","message":"…"}}`. Pagination: `?limit=&cursor=`. servers: - url: /api/v1 tags: - {name: auth} - {name: admin} - {name: consultant} - {name: developer} - {name: tasks} - {name: messaging} - {name: shared} - {name: internal} paths: /auth/register: post: tags: [auth] summary: Self-register a developer account requestBody: {$ref: "#/components/requestBodies/Register"} responses: "201": {description: Account created and session started} "409": {description: Email already registered} /auth/login: post: tags: [auth] summary: Local login requestBody: {$ref: "#/components/requestBodies/Login"} responses: "200": {description: "Session cookies set; body: user + mustChangePassword"} "401": {description: Invalid credentials} "429": {description: Rate limited (10 attempts / 15 min / IP+email)} /auth/logout: post: {tags: [auth], summary: Revoke the current session, responses: {"204": {description: Logged out}}} /auth/logout-all: post: {tags: [auth], summary: Revoke every session of the current user, responses: {"200": {description: Count of revoked sessions}}} /auth/change-password: post: tags: [auth] summary: Change the local password (revokes other sessions) responses: {"204": {description: Changed}, "401": {description: Wrong current password}} /auth/forgot: post: tags: [auth] summary: Request a password-reset email (SMTP required) responses: {"200": {description: Uniform response}, "503": {description: SMTP not configured}} /auth/reset: post: tags: [auth] summary: Set a new password with a one-shot token responses: {"204": {description: Password set}, "400": {description: Invalid or expired token}} /auth/me: get: {tags: [auth], summary: Current user, responses: {"200": {description: User profile + flags}}} /auth/oidc/login: get: {tags: [auth], summary: Start the OIDC code flow with PKCE (302 to the IdP), responses: {"302": {description: Redirect}}} /auth/oidc/callback: get: {tags: [auth], summary: OIDC redirect URI (links by verified email or creates a developer), responses: {"302": {description: Redirect home or to /login?error=…}}} /admin/users: get: tags: [admin] summary: List/search users parameters: - {name: q, in: query, schema: {type: string}} - {name: role, in: query, schema: {type: string, enum: [admin, consultant, developer]}} responses: {"200": {description: Users + nextCursor}} /admin/users/{id}: patch: tags: [admin] summary: "Update roles / disabled / name / force password reset" responses: {"200": {description: Updated user}, "400": {description: Self-lockout guard}, "409": {description: Version conflict}} delete: {tags: [admin], summary: Delete a user and their sessions, responses: {"204": {description: Deleted}}} /admin/customers: get: {tags: [admin], summary: List customers (includeArchived=true optional), responses: {"200": {description: Customers (credentials never returned)}}} post: tags: [admin] summary: Create a customer with per-system credentials (encrypted at rest) requestBody: {$ref: "#/components/requestBodies/Customer"} responses: {"201": {description: Created}, "409": {description: Name taken}} /admin/customers/{id}: get: {tags: [admin], summary: Get one customer, responses: {"200": {description: Customer}}} patch: {tags: [admin], summary: Partial update (credentials rotate when supplied), responses: {"200": {description: Updated}, "409": {description: Version conflict}}} delete: {tags: [admin], summary: Delete (blocked while tasks exist), responses: {"204": {description: Deleted}, "409": {description: Has tasks — archive instead}}} /admin/customers/test-connection: post: {tags: [admin], summary: Test unsaved credentials (wizard), responses: {"200": {description: "{ok, latencyMs, error?}"}}} /admin/customers/{id}/test-connection: post: {tags: [admin], summary: Test the stored connection, responses: {"200": {description: "{ok, latencyMs, error?}"}}} /admin/customers/{id}/sync-now: post: {tags: [admin], summary: Trigger an immediate sync poll, responses: {"202": {description: Queued}}} /admin/settings: get: {tags: [admin], summary: Runtime settings, responses: {"200": {description: Settings}}} patch: {tags: [admin], summary: Update runtime settings (atomizer URL override, branding, …), responses: {"200": {description: Updated settings}}} /admin/audit-log: get: tags: [admin] summary: Audit log of privileged mutations parameters: - {name: actorId, in: query, schema: {type: string}} - {name: entityId, in: query, schema: {type: string}} - {name: cursor, in: query, schema: {type: string}} responses: {"200": {description: Entries + nextCursor}} /admin/service-status: get: {tags: [admin], summary: "Health/latency/breaker for atomizer + work performer, sync workers, job queue (§11.17)", responses: {"200": {description: Status payload}}} /consultant/board: get: tags: [consultant] summary: Atomization board (imported…claim_requested tasks of own customers) parameters: - {name: customerId, in: query, schema: {type: string}} - {name: status, in: query, schema: {type: string}} responses: {"200": {description: Customers + tasks}} /consultant/reviews: get: {tags: [consultant], summary: Tasks in review for own customers, responses: {"200": {description: Tasks}}} /consultant/pool: get: {tags: [consultant], summary: Global developer pool with membership flags, responses: {"200": {description: Developers}}} post: {tags: [consultant], summary: Add a developer to my pool, responses: {"201": {description: Added}, "409": {description: Already in pool}}} /consultant/pool/{developerId}: delete: {tags: [consultant], summary: Remove a developer from my pool, responses: {"204": {description: Removed}}} /consultant/metrics: get: tags: [consultant] summary: "Aggregations per developer/customer + lead time + board depth (§6.2); format=csv exports the ledger" parameters: - {name: customerId, in: query, schema: {type: string}} - {name: developerId, in: query, schema: {type: string}} - {name: from, in: query, schema: {type: string, format: date}} - {name: to, in: query, schema: {type: string, format: date}} - {name: format, in: query, schema: {type: string, enum: [csv]}} responses: {"200": {description: Metrics JSON or CSV}} /board: get: tags: [developer] summary: Bounty board (published tasks of consultants who pooled me) parameters: - {name: customerId, in: query, schema: {type: string}} - {name: q, in: query, schema: {type: string}, description: Mongo text search} - {name: minBounty, in: query, schema: {type: number}} - {name: sort, in: query, schema: {type: string, enum: ["", bounty]}} responses: {"200": {description: Tasks + customers}} /my-tasks: get: {tags: [developer], summary: My kanban (assigned…approved), responses: {"200": {description: Tasks}}} /developer/metrics: get: tags: [developer] summary: "Own §6.2 metrics; format=csv exports the ledger" parameters: - {name: from, in: query, schema: {type: string, format: date}} - {name: to, in: query, schema: {type: string, format: date}} - {name: format, in: query, schema: {type: string, enum: [csv]}} responses: {"200": {description: Metrics JSON or CSV}} /leaderboard: get: {tags: [shared], summary: Top developers by bounty (opt-out honored), responses: {"200": {description: Leaderboard}}} /tasks/{id}: get: {tags: [tasks], summary: Task detail incl. timeline (role-scoped), responses: {"200": {description: Task}, "403": {description: No access}}} patch: {tags: [consultant], summary: "Edit title/description/AC/coefficient/budget (version-checked, bounty recomputed; cascadeBudget optional)", responses: {"200": {description: Updated task}, "409": {description: Conflict or immutable}}} /tasks/{id}/subdivide: post: {tags: [consultant], summary: "Queue AI subdivision (§5.1); re-run needs confirmReplace", responses: {"202": {description: "{jobId}"}, "409": {description: Bad status / confirm required}}} /tasks/{id}/extend: post: {tags: [consultant], summary: Queue AI extension (note required), responses: {"202": {description: "{jobId}"}}} /tasks/{id}/publish: post: {tags: [consultant], summary: Publish an atomized task to the board, responses: {"200": {description: Task}, "409": {description: Illegal transition}}} /tasks/publish: post: {tags: [consultant], summary: "Bulk publish {ids:[…]}", responses: {"200": {description: "{published, failed}"}}} /tasks/archive: post: {tags: [consultant], summary: "Bulk archive {ids:[…]}", responses: {"200": {description: "{archived, failed}"}}} /tasks/{id}/archive: post: {tags: [tasks], summary: Archive (consultant/admin), responses: {"204": {description: Archived}}} /tasks/{id}/claim: post: {tags: [developer], summary: Request assignment (optional pitch note), responses: {"204": {description: Requested}, "409": {description: Already requested / not open}}} /tasks/{id}/claim/withdraw: post: {tags: [developer], summary: Withdraw my claim (back to published when none remain), responses: {"204": {description: Withdrawn}}} /tasks/{id}/approve-claim: post: {tags: [consultant], summary: "Approve one developer {developerId}; others are declined + notified", responses: {"204": {description: Assigned}}} /tasks/{id}/decline-claim: post: {tags: [consultant], summary: Decline one claim, responses: {"204": {description: Declined}}} /tasks/{id}/assign-ai: post: {tags: [consultant], summary: "Create a Work Performer job (§5.2) {context:{repositoryUrl,branch,instructions}}", responses: {"202": {description: "{jobId}"}, "502": {description: Performer rejected the job}}} /tasks/{id}/unassign: post: {tags: [consultant], summary: Return an assigned/in-progress task to the board, responses: {"204": {description: Unassigned}}} /tasks/{id}/start: post: {tags: [developer], summary: Start work (assigned or changes_requested), responses: {"204": {description: In progress}}} /tasks/{id}/submit-review: post: {tags: [developer], summary: Submit for review, responses: {"204": {description: In review}}} /tasks/{id}/abandon: post: {tags: [developer], summary: Abandon back to the board, responses: {"204": {description: Published}}} /tasks/{id}/comments: post: {tags: [tasks], summary: Add a sanitized comment (@mentions notify), responses: {"201": {description: Comment}}} /tasks/{id}/time: post: {tags: [developer], summary: "Log time {minutes, note}", responses: {"201": {description: Entry}}} /tasks/{id}/review: post: {tags: [consultant], summary: "Review {decision: approve|request_changes, note, checklist[]}; approve freezes the bounty into bountyAwards", responses: {"204": {description: Reviewed}}} /conversations: get: {tags: [messaging], summary: My conversations with unread counts, responses: {"200": {description: Conversations}}} post: {tags: [messaging], summary: "Create dm (deduplicated) / group / project conversation", responses: {"200": {description: Existing dm}, "201": {description: Created}}} /conversations/{id}/messages: get: tags: [messaging] summary: Message history (newest page; cursor pages backwards) parameters: [{name: cursor, in: query, schema: {type: string}}] responses: {"200": {description: Messages chronological}} post: {tags: [messaging], summary: "Send {body (sanitized rich text), attachments:[fileIds]}", responses: {"201": {description: Message}}} /conversations/{id}/read: post: {tags: [messaging], summary: Mark all messages read, responses: {"200": {description: "{marked}"}}} /files: post: {tags: [shared], summary: "Upload (multipart field `file`; scope=chat default, task for consultants)", responses: {"201": {description: "{fileId, name, mimeType, size, isImage}"}, "413": {description: Exceeds MAX_UPLOAD_MB}}} /profile: get: {tags: [shared], summary: Own profile, responses: {"200": {description: User}}} patch: {tags: [shared], summary: "Partial profile update (name, bio, contact, extra, settings.theme/leaderboardOptOut); optional version → 409 on conflict", responses: {"200": {description: Updated user}}} /profile/avatar: post: {tags: [shared], summary: Upload avatar (must sniff as an image), responses: {"200": {description: "{fileId}"}}} /users: get: {tags: [shared], summary: User directory search (name/email) for messaging, responses: {"200": {description: Users}}} /users/{id}/card: get: {tags: [shared], summary: Public profile card (hover cards), responses: {"200": {description: Card}}} /notifications: get: {tags: [shared], summary: My notifications + unread count, responses: {"200": {description: Notifications}}} /notifications/read: post: {tags: [shared], summary: "Mark read {ids:[]} (empty = all)", responses: {"200": {description: "{marked}"}}} /service-health: get: {tags: [shared], summary: Atomizer + work-performer health/breaker for UI gating, responses: {"200": {description: Health}}} /internal/work-results: post: tags: [internal] summary: "Work Performer callback (§5.2): HMAC `X-Signature` over the raw body with WORK_PERFORMER_TOKEN; idempotent by jobId; artifacts ingested into GridFS" responses: "200": {description: Accepted or duplicate_ignored} "401": {description: Bad signature} components: requestBodies: Register: content: application/json: schema: type: object required: [email, name, password] properties: email: {type: string, format: email} name: {type: string} password: {type: string, minLength: 8} Login: content: application/json: schema: type: object required: [email, password] properties: email: {type: string} password: {type: string} Customer: content: application/json: schema: type: object required: [name, type] properties: name: {type: string} type: {type: string, enum: [jira, azure_devops, youtrack, wekan, demo]} baseUrl: {type: string} projectKey: {type: string} pollIntervalSec: {type: integer, minimum: 10} defaultBudget: {type: number} consultantIds: {type: array, items: {type: string}} credentials: type: object description: "jira: {email, apiToken} · azure_devops: {organization, project, pat} · youtrack: {permanentToken} · wekan: {username, password} (projectKey = board id) · demo: {}" schemas: Error: type: object properties: error: type: object properties: code: {type: string} message: {type: string}