diff --git a/CLAUDE.md b/CLAUDE.md index 7b32ed1f8..3ae10d480 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -9,6 +9,7 @@ bun run setup # One-time setup (deps, Docker, migrations, seed) bun run dev # Dev server at localhost:3000 (login: demo@example.com / password) bun run build && bun run db:generate && bun run db:migrate bun run test && bun run test:e2e && bun run lint && bun run typecheck +bun apps/web/scripts/backfill-ticket-contacts.ts [--dry-run] # One-shot: link existing portal users to contacts and backfill tickets.requesterContactId ``` ## Rules diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6280dc0dd..e3111fe1c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -87,6 +87,22 @@ const post = await db.query.posts.findFirst({ - Single workspace, `DATABASE_URL` singleton +### Authorization + +Quackback has two independent authorization systems serving different product domains: + +| System | Import | Domain | +| ---------- | ---------------------------- | --------------------------------------------------------------- | +| **Policy** | `@/lib/server/policy` | Feedback portal (boards, posts, comments, chat) | +| **Authz** | `@/lib/server/domains/authz` | Ticketing & workspace admin (tickets, teams, inboxes, SLA, CRM) | + +**Which one should I use?** + +- Adding a board/post/comment/chat feature → use `policy` +- Adding a ticket/inbox/team/contact/SLA feature → use `authz` + +The two systems are independent and will be unified in a future iteration. + ## Development Guidelines ### Code Style diff --git a/apps/web/e2e/tests/admin/tickets.spec.ts b/apps/web/e2e/tests/admin/tickets.spec.ts new file mode 100644 index 000000000..06b7794d6 --- /dev/null +++ b/apps/web/e2e/tests/admin/tickets.spec.ts @@ -0,0 +1,164 @@ +import { test, expect } from '@playwright/test' + +const uniqueId = Date.now() + +test.describe('Admin Tickets', () => { + test.describe.configure({ mode: 'serial' }) + + let ticketSubject: string + + test('can navigate to tickets section', async ({ page }) => { + await page.goto('/admin') + await page.waitForLoadState('networkidle') + + // Sidebar should have a Tickets link + const ticketsLink = page.getByRole('link', { name: /tickets/i }) + await expect(ticketsLink).toBeVisible({ timeout: 10000 }) + + await ticketsLink.click() + await page.waitForURL('**/admin/tickets**') + await expect(page).toHaveURL(/\/admin\/tickets/) + }) + + test('can create a new ticket', async ({ page }) => { + await page.goto('/admin/tickets/new') + await page.waitForLoadState('networkidle') + + ticketSubject = `E2E Ticket ${uniqueId}` + + // Fill the subject field + const subjectInput = page.locator('#subject') + await expect(subjectInput).toBeVisible({ timeout: 10000 }) + await subjectInput.fill(ticketSubject) + + // Fill the description (TipTap rich text editor) + const editor = page.locator('.tiptap').first() + await editor.click() + await page.keyboard.type('This is an automated E2E test ticket description.') + + // Submit the form + await page.getByRole('button', { name: /create/i }).click() + + // Should navigate to the ticket detail page + await page.waitForURL('**/admin/tickets/ticket_**', { timeout: 15000 }) + await expect(page).toHaveURL(/\/admin\/tickets\/ticket_/) + }) + + test('can view ticket detail', async ({ page }) => { + // Navigate to tickets list + await page.goto('/admin/tickets') + await page.waitForLoadState('networkidle') + + // Find and click our ticket in the queue + const ticketRow = page.getByText(ticketSubject) + await expect(ticketRow).toBeVisible({ timeout: 10000 }) + await ticketRow.click() + + // Should navigate to detail page + await page.waitForURL('**/admin/tickets/ticket_**') + + // Verify subject is visible in the properties panel + await expect(page.getByText(ticketSubject)).toBeVisible() + + // Verify the properties panel renders key sections + await expect(page.getByText('Status')).toBeVisible() + await expect(page.getByText('Priority')).toBeVisible() + }) + + test('can add a public reply', async ({ page }) => { + // Navigate to tickets list then open our ticket + await page.goto('/admin/tickets') + await page.waitForLoadState('networkidle') + await page.getByText(ticketSubject).click() + await page.waitForURL('**/admin/tickets/ticket_**') + + // Find the thread composer (placeholder "Reply to customer…") + const composer = page.locator('.tiptap').last() + await expect(composer).toBeVisible({ timeout: 10000 }) + await composer.click() + await page.keyboard.type('This is a public reply from E2E test.') + + // Click Post button + await page.getByRole('button', { name: /post/i }).click() + + // The reply should appear in the thread timeline + await expect(page.getByText('This is a public reply from E2E test.')).toBeVisible({ + timeout: 10000, + }) + }) + + test('can change ticket status', async ({ page }) => { + await page.goto('/admin/tickets') + await page.waitForLoadState('networkidle') + await page.getByText(ticketSubject).click() + await page.waitForURL('**/admin/tickets/ticket_**') + + // Find Status section and its picker button + const statusSection = page.locator('text=Status').locator('..') + const statusButton = statusSection.locator('button').first() + await expect(statusButton).toBeVisible({ timeout: 10000 }) + await statusButton.click() + + // Select "Solved" from the dropdown + const solvedOption = page.getByRole('option', { name: /solved/i }).or(page.getByText('Solved')) + await solvedOption.first().click() + + // Wait for the mutation to complete + await page.waitForLoadState('networkidle') + + // Verify the status changed (status badge should show "Solved") + await expect(page.getByText('Solved').first()).toBeVisible({ timeout: 10000 }) + }) + + test('can change ticket priority', async ({ page }) => { + await page.goto('/admin/tickets') + await page.waitForLoadState('networkidle') + await page.getByText(ticketSubject).click() + await page.waitForURL('**/admin/tickets/ticket_**') + + // Find Priority section and its picker + const prioritySection = page.locator('text=Priority').locator('..') + const priorityButton = prioritySection.locator('button').first() + await expect(priorityButton).toBeVisible({ timeout: 10000 }) + await priorityButton.click() + + // Select "High" from the dropdown + const highOption = page.getByRole('option', { name: /high/i }).or(page.getByText('high')) + await highOption.first().click() + + await page.waitForLoadState('networkidle') + + // Verify priority changed + await expect(page.getByText(/high/i).first()).toBeVisible({ timeout: 10000 }) + }) + + test('can edit ticket subject', async ({ page }) => { + await page.goto('/admin/tickets') + await page.waitForLoadState('networkidle') + await page.getByText(ticketSubject).click() + await page.waitForURL('**/admin/tickets/ticket_**') + + // Find the Subject section in properties panel and click it to start editing + const subjectSection = page.locator('text=Subject').locator('..') + await subjectSection.click() + + // The input should appear + const subjectInput = subjectSection.locator('input') + await expect(subjectInput).toBeVisible({ timeout: 5000 }) + + // Clear and type new subject + const newSubject = `Updated E2E Ticket ${uniqueId}` + await subjectInput.clear() + await subjectInput.fill(newSubject) + await subjectInput.press('Enter') + + // Wait for save + await page.waitForLoadState('networkidle') + + // Verify subject updated (it should appear in the header) + await expect(page.getByText(newSubject).first()).toBeVisible({ timeout: 10000 }) + + // Update for subsequent tests + ticketSubject = newSubject + }) +}) diff --git a/apps/web/openapi.json b/apps/web/openapi.json new file mode 100644 index 000000000..d5a4cc282 --- /dev/null +++ b/apps/web/openapi.json @@ -0,0 +1,34211 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Quackback API", + "version": "1.0.0", + "description": "Quackback Public REST API for managing feedback, posts, boards, and more.\n\n## Authentication\n\nAll API endpoints require authentication using an API key. Include your API key in the Authorization header:\n\n```\nAuthorization: Bearer qb_your_api_key_here\n```\n\nAPI keys can be created in the Quackback admin dashboard under Settings > API Keys.\n\n## Rate Limiting\n\nAPI requests are not currently rate limited, but this may change in the future.\n\n## Pagination\n\nList endpoints support cursor-based pagination:\n- Use the `limit` parameter to control page size (1-100, default 20)\n- Use the `cursor` parameter with the value from `meta.pagination.cursor` to fetch the next page\n- When `meta.pagination.hasMore` is false, there are no more items\n\n## TypeIDs\n\nAll resource IDs use TypeID format: `{type}_{base32_uuid}`\nExample: `post_01h455vb4pex5vsknk084sn02q`", + "contact": { + "name": "Quackback Support", + "url": "https://github.com/quackback/quackback" + }, + "license": { + "name": "AGPL-3.0", + "url": "https://www.gnu.org/licenses/agpl-3.0.html" + } + }, + "servers": [ + { + "url": "/api/v1", + "description": "API v1" + } + ], + "security": [ + { + "bearerAuth": [] + } + ], + "tags": [ + { + "name": "Posts", + "description": "Manage feedback posts" + }, + { + "name": "Boards", + "description": "Manage feedback boards" + }, + { + "name": "Comments", + "description": "Manage post comments" + }, + { + "name": "Votes", + "description": "Manage post votes" + }, + { + "name": "Tags", + "description": "Manage tags" + }, + { + "name": "Statuses", + "description": "Manage post statuses" + }, + { + "name": "Users", + "description": "Manage portal users and session user helper payloads" + }, + { + "name": "Members", + "description": "Manage workspace members" + }, + { + "name": "Principals", + "description": "Manage human team principals" + }, + { + "name": "Roadmaps", + "description": "Manage roadmaps" + }, + { + "name": "Changelog", + "description": "Manage changelog entries" + }, + { + "name": "Tickets", + "description": "Customer-support tickets, threads, participants, shares, bulk ops" + }, + { + "name": "Support Config", + "description": "Inboxes, channels, memberships" + }, + { + "name": "Routing", + "description": "Ticket routing rules and ordering" + }, + { + "name": "SLA", + "description": "Business hours, SLA policies, escalation rules, internal cron tick" + }, + { + "name": "Organizations", + "description": "Customer organizations (B2B context)" + }, + { + "name": "Contacts", + "description": "Customer contacts + portal-user links" + }, + { + "name": "Admin", + "description": "Trusted administrative usage endpoints" + }, + { + "name": "Audit", + "description": "Workspace audit log" + }, + { + "name": "API Keys", + "description": "Scoped API keys (legacy compat + per-key scopes)" + }, + { + "name": "Webhooks", + "description": "Webhook management, delivery audit log, and test tools" + }, + { + "name": "Conversations", + "description": "Manage support conversations" + }, + { + "name": "Mentions", + "description": "Session-authenticated mention helper endpoints" + }, + { + "name": "Internal", + "description": "Session-authenticated internal API endpoints" + }, + { + "name": "Moderation", + "description": "Review pending posts and comments" + }, + { + "name": "Settings", + "description": "Workspace feature and help-center settings" + }, + { + "name": "Suggestions", + "description": "Feedback and merge suggestions" + }, + { + "name": "Apps", + "description": "App integration endpoints for embedded surfaces" + }, + { + "name": "Widget Profiles", + "description": "Widget applications and environment profiles" + } + ], + "paths": { + "/posts": { + "get": { + "tags": ["Posts"], + "summary": "List posts", + "description": "Returns a paginated list of posts with optional filtering", + "parameters": [ + { + "name": "boardId", + "in": "query", + "schema": { + "type": "string" + }, + "description": "Filter by board ID" + }, + { + "name": "status", + "in": "query", + "schema": { + "type": "string" + }, + "description": "Filter by status slug" + }, + { + "name": "tagIds", + "in": "query", + "schema": { + "type": "string" + }, + "description": "Filter by tag IDs (comma-separated)" + }, + { + "name": "search", + "in": "query", + "schema": { + "type": "string" + }, + "description": "Search in title and content" + }, + { + "name": "sort", + "in": "query", + "schema": { + "type": "string", + "enum": ["newest", "oldest", "votes"] + }, + "description": "Sort order" + }, + { + "name": "cursor", + "in": "query", + "schema": { + "type": "string" + }, + "description": "Pagination cursor from previous response" + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "default": 20, + "maximum": 100 + }, + "description": "Items per page (max 100)" + } + ], + "responses": { + "200": { + "description": "List of posts", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "title": { + "type": "string", + "example": "Add dark mode support" + }, + "content": { + "type": "string", + "example": "It would be great to have a dark mode option..." + }, + "voteCount": { + "type": "number", + "example": 42 + }, + "commentCount": { + "type": "number", + "example": 5 + }, + "boardId": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "board_01h455vb4pex5vsknk084sn02q" + }, + "boardSlug": { + "type": "string", + "description": "Slug of the parent board", + "example": "feature-requests" + }, + "boardName": { + "type": "string", + "description": "Name of the parent board", + "example": "Feature Requests" + }, + "statusId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ], + "example": "status_01h455vb4pex5vsknk084sn02q" + }, + "authorName": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "example": "John Doe" + }, + "ownerId": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Assigned team member ID" + }, + "tags": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "name": { + "type": "string" + }, + "color": { + "type": "string", + "pattern": "^#[0-9a-fA-F]{6}$", + "description": "Hex color code", + "example": "#3b82f6" + } + }, + "required": ["id", "name", "color"], + "additionalProperties": false + }, + "description": "Tags assigned to this post" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": [ + "id", + "title", + "content", + "voteCount", + "commentCount", + "boardId", + "boardSlug", + "boardName", + "statusId", + "authorName", + "ownerId", + "tags", + "createdAt", + "updatedAt" + ], + "additionalProperties": false + } + }, + "meta": { + "type": "object", + "properties": { + "pagination": { + "type": "object", + "properties": { + "cursor": { + "description": "Cursor for next page, null if no more pages", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "hasMore": { + "type": "boolean", + "description": "Whether there are more items" + }, + "total": { + "description": "Total count (when available)", + "type": "number" + } + }, + "required": ["hasMore"], + "additionalProperties": false + } + }, + "required": ["pagination"], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Paginated posts list" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + } + } + }, + "post": { + "tags": ["Posts"], + "summary": "Create a post", + "description": "Create a new feedback post. The post is created on behalf of the authenticated API key holder.", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "title": { + "type": "string", + "minLength": 1, + "maxLength": 200, + "description": "Post title", + "example": "Add dark mode support" + }, + "content": { + "type": "string", + "maxLength": 10000, + "description": "Post content (optional)", + "example": "It would be great to have..." + }, + "boardId": { + "type": "string", + "description": "Board ID", + "example": "board_01h455vb4pex5vsknk084sn02q" + }, + "statusId": { + "description": "Initial status ID", + "type": "string", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "tagIds": { + "description": "Tag IDs to assign", + "type": "array", + "items": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + } + } + }, + "required": ["title", "content", "boardId"], + "description": "Create post request body" + } + } + } + }, + "responses": { + "201": { + "description": "Post created", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "title": { + "type": "string" + }, + "content": { + "type": "string" + }, + "voteCount": { + "type": "number" + }, + "boardId": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "statusId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "authorName": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": [ + "id", + "title", + "content", + "voteCount", + "boardId", + "statusId", + "authorName", + "createdAt", + "updatedAt" + ], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Created post" + } + } + } + }, + "400": { + "description": "Validation error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "VALIDATION_ERROR" + }, + "message": { + "type": "string" + }, + "details": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Request validation failed" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + } + } + } + }, + "/posts/{postId}": { + "get": { + "tags": ["Posts"], + "summary": "Get a post", + "description": "Get a single post by ID with full details", + "parameters": [ + { + "name": "postId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Post ID" + } + ], + "responses": { + "200": { + "description": "Post details", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "title": { + "type": "string", + "example": "Add dark mode support" + }, + "content": { + "type": "string", + "example": "It would be great to have a dark mode option..." + }, + "contentJson": { + "anyOf": [ + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + { + "type": "null" + } + ], + "description": "Rich text content as TipTap JSON" + }, + "voteCount": { + "type": "number", + "example": 42 + }, + "commentCount": { + "type": "number", + "example": 5 + }, + "boardId": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "board_01h455vb4pex5vsknk084sn02q" + }, + "boardSlug": { + "type": "string", + "description": "Slug of the parent board", + "example": "feature-requests" + }, + "boardName": { + "type": "string", + "description": "Name of the parent board", + "example": "Feature Requests" + }, + "statusId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ], + "example": "status_01h455vb4pex5vsknk084sn02q" + }, + "authorName": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "example": "John Doe" + }, + "authorEmail": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "example": "user@example.com" + }, + "ownerId": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Assigned team member ID" + }, + "tags": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "name": { + "type": "string" + }, + "color": { + "type": "string", + "pattern": "^#[0-9a-fA-F]{6}$", + "description": "Hex color code", + "example": "#3b82f6" + } + }, + "required": ["id", "name", "color"], + "additionalProperties": false + }, + "description": "Tags assigned to this post" + }, + "roadmapIds": { + "type": "array", + "items": { + "type": "string" + }, + "description": "IDs of roadmaps this post belongs to" + }, + "pinnedComment": { + "anyOf": [ + { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "content": { + "type": "string" + }, + "authorName": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": ["id", "content", "authorName", "createdAt"], + "additionalProperties": false + }, + { + "type": "null" + } + ], + "description": "Pinned comment used as official response" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + }, + "deletedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ], + "description": "When the post was deleted, null if active", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": [ + "id", + "title", + "content", + "contentJson", + "voteCount", + "commentCount", + "boardId", + "boardSlug", + "boardName", + "statusId", + "authorName", + "authorEmail", + "ownerId", + "tags", + "roadmapIds", + "pinnedComment", + "createdAt", + "updatedAt", + "deletedAt" + ], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Post details" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + }, + "404": { + "description": "Post not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "NOT_FOUND" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Resource not found" + } + } + } + } + } + }, + "patch": { + "tags": ["Posts"], + "summary": "Update a post", + "description": "Update an existing post", + "parameters": [ + { + "name": "postId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Post ID" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "title": { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + "content": { + "type": "string", + "maxLength": 10000 + }, + "statusId": { + "description": "Status ID (set to null to clear)", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "tagIds": { + "type": "array", + "items": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + } + }, + "ownerId": { + "description": "Assigned team member ID (set to null to unassign)", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "description": "Update post request body" + } + } + } + }, + "responses": { + "200": { + "description": "Post updated", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "title": { + "type": "string" + }, + "content": { + "type": "string" + }, + "contentJson": { + "anyOf": [ + { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + { + "type": "null" + } + ] + }, + "voteCount": { + "type": "number" + }, + "boardId": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "statusId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "authorName": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "ownerId": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": [ + "id", + "title", + "content", + "contentJson", + "voteCount", + "boardId", + "statusId", + "authorName", + "ownerId", + "createdAt", + "updatedAt" + ], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Updated post" + } + } + } + }, + "400": { + "description": "Validation error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "VALIDATION_ERROR" + }, + "message": { + "type": "string" + }, + "details": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Request validation failed" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + }, + "404": { + "description": "Post not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "NOT_FOUND" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Resource not found" + } + } + } + } + } + }, + "delete": { + "tags": ["Posts"], + "summary": "Delete a post", + "description": "Delete a post by ID", + "parameters": [ + { + "name": "postId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Post ID" + } + ], + "responses": { + "204": { + "description": "Post deleted" + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + }, + "404": { + "description": "Post not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "NOT_FOUND" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Resource not found" + } + } + } + } + } + } + }, + "/posts/{postId}/activity": { + "get": { + "tags": ["Posts"], + "summary": "List post activity", + "parameters": [ + { + "name": "postId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "responses": { + "200": { + "description": "Activity rows", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "postId": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "principalId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "actorName": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "type": { + "type": "string" + }, + "metadata": {}, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": [ + "id", + "postId", + "principalId", + "actorName", + "type", + "metadata", + "createdAt" + ], + "additionalProperties": false + } + } + }, + "required": ["data"], + "additionalProperties": false + } + } + } + } + } + } + }, + "/posts/{postId}/merge": { + "post": { + "tags": ["Posts"], + "summary": "Merge a duplicate post into a canonical post", + "parameters": [ + { + "name": "postId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "canonicalPostId": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + } + }, + "required": ["canonicalPostId"] + } + } + } + }, + "responses": { + "200": { + "description": "Merge result", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "canonicalPost": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "voteCount": { + "type": "number" + } + }, + "required": ["id", "voteCount"], + "additionalProperties": false + }, + "duplicatePost": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + } + }, + "required": ["id"], + "additionalProperties": false + } + }, + "required": ["canonicalPost", "duplicatePost"], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Merge result" + } + } + } + }, + "400": { + "description": "Validation error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "VALIDATION_ERROR" + }, + "message": { + "type": "string" + }, + "details": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Request validation failed" + } + } + } + } + } + } + }, + "/posts/{postId}/vote": { + "post": { + "tags": ["Votes"], + "summary": "Toggle vote on a post", + "description": "Vote or unvote on a post (toggle)", + "parameters": [ + { + "name": "postId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Post ID" + } + ], + "responses": { + "200": { + "description": "Vote toggled", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "voted": { + "type": "boolean", + "description": "Whether the post is now voted" + }, + "voteCount": { + "type": "number", + "description": "Current vote count" + } + }, + "required": ["voted", "voteCount"], + "additionalProperties": false, + "description": "Vote result" + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Vote result" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + }, + "404": { + "description": "Post not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "NOT_FOUND" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Resource not found" + } + } + } + } + } + } + }, + "/posts/{postId}/vote/proxy": { + "post": { + "tags": ["Votes"], + "summary": "Add a proxy vote", + "description": "Add a vote on behalf of another user (insert-only, never toggles). Requires team role.", + "parameters": [ + { + "name": "postId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Post ID" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "voterPrincipalId": { + "type": "string", + "description": "Principal ID of the voter", + "example": "post_01h455vb4pex5vsknk084sn02q" + } + }, + "required": ["voterPrincipalId"], + "description": "Proxy vote request body" + } + } + } + }, + "responses": { + "200": { + "description": "Proxy vote added", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "voted": { + "type": "boolean", + "description": "Whether the post is now voted" + }, + "voteCount": { + "type": "number", + "description": "Current vote count" + } + }, + "required": ["voted", "voteCount"], + "additionalProperties": false, + "description": "Vote result" + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Vote result" + } + } + } + }, + "400": { + "description": "Validation error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "VALIDATION_ERROR" + }, + "message": { + "type": "string" + }, + "details": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Request validation failed" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + }, + "404": { + "description": "Post not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "NOT_FOUND" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Resource not found" + } + } + } + } + } + }, + "delete": { + "tags": ["Votes"], + "summary": "Remove a vote", + "description": "Remove any vote (proxy, integration, or direct) for a user. Requires team role.", + "parameters": [ + { + "name": "postId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Post ID" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "voterPrincipalId": { + "type": "string", + "description": "Principal ID of the voter", + "example": "post_01h455vb4pex5vsknk084sn02q" + } + }, + "required": ["voterPrincipalId"], + "description": "Proxy vote request body" + } + } + } + }, + "responses": { + "204": { + "description": "Vote removed" + }, + "400": { + "description": "Validation error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "VALIDATION_ERROR" + }, + "message": { + "type": "string" + }, + "details": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Request validation failed" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + }, + "404": { + "description": "Post not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "NOT_FOUND" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Resource not found" + } + } + } + } + } + } + }, + "/boards": { + "get": { + "tags": ["Boards"], + "summary": "List boards", + "description": "Returns all boards in the workspace", + "responses": { + "200": { + "description": "List of boards", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "board_01h455vb4pex5vsknk084sn02q" + }, + "name": { + "type": "string", + "example": "Feature Requests" + }, + "slug": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "description": "URL-friendly identifier", + "example": "feature-requests" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "example": "Submit and vote on feature ideas" + }, + "audience": { + "oneOf": [ + { + "type": "object", + "properties": { + "kind": { + "type": "string", + "const": "public" + } + }, + "required": ["kind"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "kind": { + "type": "string", + "const": "authenticated" + } + }, + "required": ["kind"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "kind": { + "type": "string", + "const": "team" + } + }, + "required": ["kind"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "kind": { + "type": "string", + "const": "segments" + }, + "segmentIds": { + "maxItems": 50, + "type": "array", + "items": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + } + } + }, + "required": ["kind", "segmentIds"], + "additionalProperties": false + } + ], + "description": "Who can view this board (legacy shape — derived from the internal access matrix). public = everyone; authenticated = signed-in portal users; team = admins & members only; segments = members of the listed segments.", + "example": { + "kind": "public" + }, + "type": "object" + }, + "postCount": { + "type": "number", + "description": "Number of posts in this board", + "example": 42 + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": [ + "id", + "name", + "slug", + "description", + "audience", + "postCount", + "createdAt", + "updatedAt" + ], + "additionalProperties": false + } + }, + "meta": { + "type": "object", + "properties": { + "pagination": { + "type": "object", + "properties": { + "cursor": { + "description": "Cursor for next page, null if no more pages", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "hasMore": { + "type": "boolean", + "description": "Whether there are more items" + }, + "total": { + "description": "Total count (when available)", + "type": "number" + } + }, + "required": ["hasMore"], + "additionalProperties": false + } + }, + "required": ["pagination"], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "List of boards" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + } + } + }, + "post": { + "tags": ["Boards"], + "summary": "Create a board", + "description": "Create a new feedback board", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "description": "Board name", + "example": "Feature Requests" + }, + "slug": { + "description": "URL-friendly slug (auto-generated from name if omitted)", + "example": "feature-requests", + "type": "string", + "minLength": 1, + "maxLength": 100, + "pattern": "^[a-z0-9-]+$" + }, + "description": { + "description": "Board description", + "type": "string", + "maxLength": 500 + } + }, + "required": ["name"], + "description": "Create board request body" + } + } + } + }, + "responses": { + "201": { + "description": "Board created", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "board_01h455vb4pex5vsknk084sn02q" + }, + "name": { + "type": "string", + "example": "Feature Requests" + }, + "slug": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "description": "URL-friendly identifier", + "example": "feature-requests" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "example": "Submit and vote on feature ideas" + }, + "audience": { + "oneOf": [ + { + "type": "object", + "properties": { + "kind": { + "type": "string", + "const": "public" + } + }, + "required": ["kind"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "kind": { + "type": "string", + "const": "authenticated" + } + }, + "required": ["kind"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "kind": { + "type": "string", + "const": "team" + } + }, + "required": ["kind"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "kind": { + "type": "string", + "const": "segments" + }, + "segmentIds": { + "maxItems": 50, + "type": "array", + "items": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + } + } + }, + "required": ["kind", "segmentIds"], + "additionalProperties": false + } + ], + "description": "Who can view this board (legacy shape — derived from the internal access matrix). public = everyone; authenticated = signed-in portal users; team = admins & members only; segments = members of the listed segments.", + "example": { + "kind": "public" + }, + "type": "object" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": [ + "id", + "name", + "slug", + "description", + "audience", + "createdAt", + "updatedAt" + ], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Created board" + } + } + } + }, + "400": { + "description": "Validation error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "VALIDATION_ERROR" + }, + "message": { + "type": "string" + }, + "details": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Request validation failed" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + } + } + } + }, + "/boards/{boardId}": { + "get": { + "tags": ["Boards"], + "summary": "Get a board", + "description": "Get a single board by ID", + "parameters": [ + { + "name": "boardId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Board ID" + } + ], + "responses": { + "200": { + "description": "Board details", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "board_01h455vb4pex5vsknk084sn02q" + }, + "name": { + "type": "string", + "example": "Feature Requests" + }, + "slug": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "description": "URL-friendly identifier", + "example": "feature-requests" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "example": "Submit and vote on feature ideas" + }, + "audience": { + "oneOf": [ + { + "type": "object", + "properties": { + "kind": { + "type": "string", + "const": "public" + } + }, + "required": ["kind"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "kind": { + "type": "string", + "const": "authenticated" + } + }, + "required": ["kind"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "kind": { + "type": "string", + "const": "team" + } + }, + "required": ["kind"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "kind": { + "type": "string", + "const": "segments" + }, + "segmentIds": { + "maxItems": 50, + "type": "array", + "items": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + } + } + }, + "required": ["kind", "segmentIds"], + "additionalProperties": false + } + ], + "description": "Who can view this board (legacy shape — derived from the internal access matrix). public = everyone; authenticated = signed-in portal users; team = admins & members only; segments = members of the listed segments.", + "example": { + "kind": "public" + }, + "type": "object" + }, + "settings": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {}, + "description": "Board-specific settings" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": [ + "id", + "name", + "slug", + "description", + "audience", + "settings", + "createdAt", + "updatedAt" + ], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Board details" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + }, + "404": { + "description": "Board not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "NOT_FOUND" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Resource not found" + } + } + } + } + } + }, + "patch": { + "tags": ["Boards"], + "summary": "Update a board", + "description": "Update an existing board", + "parameters": [ + { + "name": "boardId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Board ID" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "slug": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "description": { + "anyOf": [ + { + "type": "string", + "maxLength": 500 + }, + { + "type": "null" + } + ] + } + }, + "description": "Update board request body" + } + } + } + }, + "responses": { + "200": { + "description": "Board updated", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "board_01h455vb4pex5vsknk084sn02q" + }, + "name": { + "type": "string", + "example": "Feature Requests" + }, + "slug": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "description": "URL-friendly identifier", + "example": "feature-requests" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "example": "Submit and vote on feature ideas" + }, + "audience": { + "oneOf": [ + { + "type": "object", + "properties": { + "kind": { + "type": "string", + "const": "public" + } + }, + "required": ["kind"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "kind": { + "type": "string", + "const": "authenticated" + } + }, + "required": ["kind"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "kind": { + "type": "string", + "const": "team" + } + }, + "required": ["kind"], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "kind": { + "type": "string", + "const": "segments" + }, + "segmentIds": { + "maxItems": 50, + "type": "array", + "items": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + } + } + }, + "required": ["kind", "segmentIds"], + "additionalProperties": false + } + ], + "description": "Who can view this board (legacy shape — derived from the internal access matrix). public = everyone; authenticated = signed-in portal users; team = admins & members only; segments = members of the listed segments.", + "example": { + "kind": "public" + }, + "type": "object" + }, + "settings": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {}, + "description": "Board-specific settings" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": [ + "id", + "name", + "slug", + "description", + "audience", + "settings", + "createdAt", + "updatedAt" + ], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Updated board" + } + } + } + }, + "400": { + "description": "Validation error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "VALIDATION_ERROR" + }, + "message": { + "type": "string" + }, + "details": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Request validation failed" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + }, + "404": { + "description": "Board not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "NOT_FOUND" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Resource not found" + } + } + } + } + } + }, + "delete": { + "tags": ["Boards"], + "summary": "Delete a board", + "description": "Delete a board by ID", + "parameters": [ + { + "name": "boardId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Board ID" + } + ], + "responses": { + "204": { + "description": "Board deleted" + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + }, + "404": { + "description": "Board not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "NOT_FOUND" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Resource not found" + } + } + } + } + } + } + }, + "/posts/{postId}/comments": { + "get": { + "tags": ["Comments"], + "summary": "List comments on a post", + "description": "Returns all comments on a post as a threaded tree with nested replies", + "parameters": [ + { + "name": "postId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Post ID" + } + ], + "responses": { + "200": { + "description": "Threaded list of comments", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/__schema0" + } + }, + "meta": { + "type": "object", + "properties": { + "pagination": { + "type": "object", + "properties": { + "cursor": { + "description": "Cursor for next page, null if no more pages", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "hasMore": { + "type": "boolean", + "description": "Whether there are more items" + }, + "total": { + "description": "Total count (when available)", + "type": "number" + } + }, + "required": ["hasMore"], + "additionalProperties": false + } + }, + "required": ["pagination"], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "List of comments" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + }, + "404": { + "description": "Post not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "NOT_FOUND" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Resource not found" + } + } + } + } + } + }, + "post": { + "tags": ["Comments"], + "summary": "Add a comment to a post", + "description": "Create a new comment on a post. The comment is attributed to the authenticated API key holder.", + "parameters": [ + { + "name": "postId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Post ID" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "content": { + "type": "string", + "minLength": 1, + "maxLength": 5000, + "description": "Comment content" + }, + "parentId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ], + "description": "Parent comment ID for replies" + }, + "isPrivate": { + "description": "Mark comment as private (team-only). Defaults to false.", + "type": "boolean" + } + }, + "required": ["content"], + "description": "Create comment request body" + } + } + } + }, + "responses": { + "201": { + "description": "Comment created", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "comment_01h455vb4pex5vsknk084sn02q" + }, + "postId": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "parentId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "content": { + "type": "string" + }, + "authorName": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "principalId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "isTeamMember": { + "type": "boolean" + }, + "isPrivate": { + "type": "boolean" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": [ + "id", + "postId", + "parentId", + "content", + "authorName", + "principalId", + "isTeamMember", + "isPrivate", + "createdAt" + ], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Created comment" + } + } + } + }, + "400": { + "description": "Validation error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "VALIDATION_ERROR" + }, + "message": { + "type": "string" + }, + "details": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Request validation failed" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + }, + "404": { + "description": "Post not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "NOT_FOUND" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Resource not found" + } + } + } + } + } + } + }, + "/comments/{commentId}": { + "get": { + "tags": ["Comments"], + "summary": "Get a comment", + "description": "Get a single comment by ID", + "parameters": [ + { + "name": "commentId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Comment ID" + } + ], + "responses": { + "200": { + "description": "Comment details", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "comment_01h455vb4pex5vsknk084sn02q" + }, + "postId": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "parentId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ], + "description": "Parent comment ID for replies" + }, + "content": { + "type": "string", + "example": "Great idea! This would be very useful." + }, + "authorName": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "example": "Jane Doe" + }, + "authorEmail": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "example": "user@example.com" + }, + "principalId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ], + "description": "Principal ID of the comment author" + }, + "isTeamMember": { + "type": "boolean", + "description": "Whether the author is a team member", + "example": false + }, + "isPrivate": { + "type": "boolean", + "description": "Whether the comment is only visible to team members", + "example": false + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + }, + "deletedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ], + "description": "When the comment was deleted, null if active", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": [ + "id", + "postId", + "parentId", + "content", + "authorName", + "authorEmail", + "principalId", + "isTeamMember", + "isPrivate", + "createdAt", + "deletedAt" + ], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Comment details" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + }, + "404": { + "description": "Comment not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "NOT_FOUND" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Resource not found" + } + } + } + } + } + }, + "patch": { + "tags": ["Comments"], + "summary": "Update a comment", + "description": "Update an existing comment", + "parameters": [ + { + "name": "commentId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Comment ID" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "content": { + "type": "string", + "minLength": 1, + "maxLength": 5000, + "description": "Updated content" + } + }, + "required": ["content"], + "description": "Update comment request body" + } + } + } + }, + "responses": { + "200": { + "description": "Comment updated", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "comment_01h455vb4pex5vsknk084sn02q" + }, + "postId": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "parentId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "content": { + "type": "string" + }, + "authorName": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "principalId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "isTeamMember": { + "type": "boolean" + }, + "isPrivate": { + "type": "boolean" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": [ + "id", + "postId", + "parentId", + "content", + "authorName", + "principalId", + "isTeamMember", + "isPrivate", + "createdAt" + ], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Updated comment" + } + } + } + }, + "400": { + "description": "Validation error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "VALIDATION_ERROR" + }, + "message": { + "type": "string" + }, + "details": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Request validation failed" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + }, + "404": { + "description": "Comment not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "NOT_FOUND" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Resource not found" + } + } + } + } + } + }, + "delete": { + "tags": ["Comments"], + "summary": "Delete a comment", + "description": "Delete a comment by ID", + "parameters": [ + { + "name": "commentId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Comment ID" + } + ], + "responses": { + "204": { + "description": "Comment deleted" + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + }, + "404": { + "description": "Comment not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "NOT_FOUND" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Resource not found" + } + } + } + } + } + } + }, + "/comments/{commentId}/reactions": { + "post": { + "tags": ["Comments"], + "summary": "Add an emoji reaction to a comment", + "parameters": [ + { + "name": "commentId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Comment ID" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "emoji": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "example": "👍" + } + }, + "required": ["emoji"], + "description": "Comment reaction request body" + } + } + } + }, + "responses": { + "200": { + "description": "Reaction state after the add operation", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "commentId": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "emoji": { + "type": "string", + "example": "👍" + }, + "added": { + "type": "boolean", + "description": "Whether the reaction is now present after the mutation. DELETE returns false when removed." + }, + "reactions": { + "type": "array", + "items": { + "type": "object", + "properties": { + "emoji": { + "type": "string", + "example": "👍" + }, + "count": { + "type": "number", + "example": 3 + }, + "hasReacted": { + "type": "boolean", + "description": "Whether the authenticated user has reacted with this emoji" + } + }, + "required": ["emoji", "count", "hasReacted"], + "additionalProperties": false + } + } + }, + "required": ["commentId", "emoji", "added", "reactions"], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Comment reaction" + } + } + } + }, + "400": { + "description": "Validation error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "VALIDATION_ERROR" + }, + "message": { + "type": "string" + }, + "details": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Request validation failed" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + }, + "404": { + "description": "Comment not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "NOT_FOUND" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Resource not found" + } + } + } + } + } + }, + "delete": { + "tags": ["Comments"], + "summary": "Remove an emoji reaction from a comment", + "parameters": [ + { + "name": "commentId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Comment ID" + }, + { + "name": "emoji", + "in": "query", + "required": false, + "schema": { + "type": "string" + }, + "description": "Emoji to remove. May also be provided in the JSON body." + } + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "emoji": { + "type": "string", + "minLength": 1, + "maxLength": 64, + "example": "👍" + } + }, + "required": ["emoji"], + "description": "Comment reaction request body" + } + } + } + }, + "responses": { + "200": { + "description": "Reaction state after the remove operation", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "commentId": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "emoji": { + "type": "string", + "example": "👍" + }, + "added": { + "type": "boolean", + "description": "Whether the reaction is now present after the mutation. DELETE returns false when removed." + }, + "reactions": { + "type": "array", + "items": { + "type": "object", + "properties": { + "emoji": { + "type": "string", + "example": "👍" + }, + "count": { + "type": "number", + "example": 3 + }, + "hasReacted": { + "type": "boolean", + "description": "Whether the authenticated user has reacted with this emoji" + } + }, + "required": ["emoji", "count", "hasReacted"], + "additionalProperties": false + } + } + }, + "required": ["commentId", "emoji", "added", "reactions"], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Comment reaction" + } + } + } + }, + "400": { + "description": "Validation error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "VALIDATION_ERROR" + }, + "message": { + "type": "string" + }, + "details": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Request validation failed" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + }, + "404": { + "description": "Comment not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "NOT_FOUND" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Resource not found" + } + } + } + } + } + } + }, + "/tags": { + "get": { + "tags": ["Tags"], + "summary": "List tags", + "description": "Returns all tags in the workspace", + "responses": { + "200": { + "description": "List of tags", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "tag_01h455vb4pex5vsknk084sn02q" + }, + "name": { + "type": "string", + "example": "Bug" + }, + "color": { + "type": "string", + "pattern": "^#[0-9a-fA-F]{6}$", + "description": "Hex color code", + "example": "#ef4444" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": ["id", "name", "color", "createdAt"], + "additionalProperties": false + } + }, + "meta": { + "type": "object", + "properties": { + "pagination": { + "type": "object", + "properties": { + "cursor": { + "description": "Cursor for next page, null if no more pages", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "hasMore": { + "type": "boolean", + "description": "Whether there are more items" + }, + "total": { + "description": "Total count (when available)", + "type": "number" + } + }, + "required": ["hasMore"], + "additionalProperties": false + } + }, + "required": ["pagination"], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "List of tags" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + } + } + }, + "post": { + "tags": ["Tags"], + "summary": "Create a tag", + "description": "Create a new tag", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 50, + "description": "Tag name", + "example": "Bug" + }, + "color": { + "description": "Tag color", + "default": "#6b7280", + "type": "string", + "pattern": "^#[0-9a-fA-F]{6}$", + "example": "#3b82f6" + } + }, + "required": ["name"], + "description": "Create tag request body" + } + } + } + }, + "responses": { + "201": { + "description": "Tag created", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "tag_01h455vb4pex5vsknk084sn02q" + }, + "name": { + "type": "string", + "example": "Bug" + }, + "color": { + "type": "string", + "pattern": "^#[0-9a-fA-F]{6}$", + "description": "Hex color code", + "example": "#ef4444" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": ["id", "name", "color", "createdAt"], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Created tag" + } + } + } + }, + "400": { + "description": "Validation error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "VALIDATION_ERROR" + }, + "message": { + "type": "string" + }, + "details": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Request validation failed" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + } + } + } + }, + "/tags/{tagId}": { + "get": { + "tags": ["Tags"], + "summary": "Get a tag", + "description": "Get a single tag by ID", + "parameters": [ + { + "name": "tagId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Tag ID" + } + ], + "responses": { + "200": { + "description": "Tag details", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "tag_01h455vb4pex5vsknk084sn02q" + }, + "name": { + "type": "string", + "example": "Bug" + }, + "color": { + "type": "string", + "pattern": "^#[0-9a-fA-F]{6}$", + "description": "Hex color code", + "example": "#ef4444" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": ["id", "name", "color", "createdAt"], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Tag details" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + }, + "404": { + "description": "Tag not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "NOT_FOUND" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Resource not found" + } + } + } + } + } + }, + "patch": { + "tags": ["Tags"], + "summary": "Update a tag", + "description": "Update an existing tag", + "parameters": [ + { + "name": "tagId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Tag ID" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 50 + }, + "color": { + "type": "string", + "pattern": "^#[0-9a-fA-F]{6}$", + "description": "Hex color code", + "example": "#3b82f6" + } + }, + "description": "Update tag request body" + } + } + } + }, + "responses": { + "200": { + "description": "Tag updated", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "tag_01h455vb4pex5vsknk084sn02q" + }, + "name": { + "type": "string", + "example": "Bug" + }, + "color": { + "type": "string", + "pattern": "^#[0-9a-fA-F]{6}$", + "description": "Hex color code", + "example": "#ef4444" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": ["id", "name", "color", "createdAt"], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Updated tag" + } + } + } + }, + "400": { + "description": "Validation error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "VALIDATION_ERROR" + }, + "message": { + "type": "string" + }, + "details": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Request validation failed" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + }, + "404": { + "description": "Tag not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "NOT_FOUND" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Resource not found" + } + } + } + } + } + }, + "delete": { + "tags": ["Tags"], + "summary": "Delete a tag", + "description": "Delete a tag by ID", + "parameters": [ + { + "name": "tagId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Tag ID" + } + ], + "responses": { + "204": { + "description": "Tag deleted" + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + }, + "404": { + "description": "Tag not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "NOT_FOUND" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Resource not found" + } + } + } + } + } + } + }, + "/statuses": { + "get": { + "tags": ["Statuses"], + "summary": "List statuses", + "description": "Returns all statuses in the workspace, ordered by category and position", + "responses": { + "200": { + "description": "List of statuses", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "status_01h455vb4pex5vsknk084sn02q" + }, + "name": { + "type": "string", + "example": "In Progress" + }, + "slug": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "description": "URL-friendly identifier", + "example": "in_progress" + }, + "color": { + "type": "string", + "pattern": "^#[0-9a-fA-F]{6}$", + "description": "Hex color code", + "example": "#f97316" + }, + "category": { + "type": "string", + "enum": ["active", "complete", "closed"], + "description": "Status category" + }, + "position": { + "type": "number", + "description": "Display order within category" + }, + "showOnRoadmap": { + "type": "boolean", + "description": "Whether to show on public roadmap" + }, + "isDefault": { + "type": "boolean", + "description": "Whether this is the default status for new posts" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": [ + "id", + "name", + "slug", + "color", + "category", + "position", + "showOnRoadmap", + "isDefault", + "createdAt" + ], + "additionalProperties": false + } + }, + "meta": { + "type": "object", + "properties": { + "pagination": { + "type": "object", + "properties": { + "cursor": { + "description": "Cursor for next page, null if no more pages", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "hasMore": { + "type": "boolean", + "description": "Whether there are more items" + }, + "total": { + "description": "Total count (when available)", + "type": "number" + } + }, + "required": ["hasMore"], + "additionalProperties": false + } + }, + "required": ["pagination"], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "List of statuses" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + } + } + }, + "post": { + "tags": ["Statuses"], + "summary": "Create a status", + "description": "Create a new post status", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 50, + "description": "Status name", + "example": "In Progress" + }, + "slug": { + "type": "string", + "minLength": 1, + "maxLength": 50, + "pattern": "^[a-z0-9_]+$", + "description": "URL-friendly slug", + "example": "in_progress" + }, + "color": { + "type": "string", + "pattern": "^#[0-9a-fA-F]{6}$", + "description": "Status color", + "example": "#f97316" + }, + "category": { + "type": "string", + "enum": ["active", "complete", "closed"], + "description": "Status category" + }, + "position": { + "description": "Display order within category", + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "showOnRoadmap": { + "description": "Show on public roadmap", + "default": false, + "type": "boolean" + }, + "isDefault": { + "description": "Set as default for new posts", + "default": false, + "type": "boolean" + } + }, + "required": ["name", "slug", "color", "category"], + "description": "Create status request body" + } + } + } + }, + "responses": { + "201": { + "description": "Status created", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "status_01h455vb4pex5vsknk084sn02q" + }, + "name": { + "type": "string", + "example": "In Progress" + }, + "slug": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "description": "URL-friendly identifier", + "example": "in_progress" + }, + "color": { + "type": "string", + "pattern": "^#[0-9a-fA-F]{6}$", + "description": "Hex color code", + "example": "#f97316" + }, + "category": { + "type": "string", + "enum": ["active", "complete", "closed"], + "description": "Status category" + }, + "position": { + "type": "number", + "description": "Display order within category" + }, + "showOnRoadmap": { + "type": "boolean", + "description": "Whether to show on public roadmap" + }, + "isDefault": { + "type": "boolean", + "description": "Whether this is the default status for new posts" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": [ + "id", + "name", + "slug", + "color", + "category", + "position", + "showOnRoadmap", + "isDefault", + "createdAt" + ], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Created status" + } + } + } + }, + "400": { + "description": "Validation error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "VALIDATION_ERROR" + }, + "message": { + "type": "string" + }, + "details": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Request validation failed" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + } + } + } + }, + "/statuses/{statusId}": { + "get": { + "tags": ["Statuses"], + "summary": "Get a status", + "description": "Get a single status by ID", + "parameters": [ + { + "name": "statusId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Status ID" + } + ], + "responses": { + "200": { + "description": "Status details", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "status_01h455vb4pex5vsknk084sn02q" + }, + "name": { + "type": "string", + "example": "In Progress" + }, + "slug": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "description": "URL-friendly identifier", + "example": "in_progress" + }, + "color": { + "type": "string", + "pattern": "^#[0-9a-fA-F]{6}$", + "description": "Hex color code", + "example": "#f97316" + }, + "category": { + "type": "string", + "enum": ["active", "complete", "closed"], + "description": "Status category" + }, + "position": { + "type": "number", + "description": "Display order within category" + }, + "showOnRoadmap": { + "type": "boolean", + "description": "Whether to show on public roadmap" + }, + "isDefault": { + "type": "boolean", + "description": "Whether this is the default status for new posts" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": [ + "id", + "name", + "slug", + "color", + "category", + "position", + "showOnRoadmap", + "isDefault", + "createdAt" + ], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Status details" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + }, + "404": { + "description": "Status not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "NOT_FOUND" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Resource not found" + } + } + } + } + } + }, + "patch": { + "tags": ["Statuses"], + "summary": "Update a status", + "description": "Update an existing status. Note: slug, category, and position cannot be changed via this endpoint.", + "parameters": [ + { + "name": "statusId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Status ID" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 50 + }, + "color": { + "type": "string", + "pattern": "^#[0-9a-fA-F]{6}$", + "description": "Hex color code", + "example": "#3b82f6" + }, + "showOnRoadmap": { + "type": "boolean" + }, + "isDefault": { + "type": "boolean" + } + }, + "description": "Update status request body" + } + } + } + }, + "responses": { + "200": { + "description": "Status updated", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "status_01h455vb4pex5vsknk084sn02q" + }, + "name": { + "type": "string", + "example": "In Progress" + }, + "slug": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "description": "URL-friendly identifier", + "example": "in_progress" + }, + "color": { + "type": "string", + "pattern": "^#[0-9a-fA-F]{6}$", + "description": "Hex color code", + "example": "#f97316" + }, + "category": { + "type": "string", + "enum": ["active", "complete", "closed"], + "description": "Status category" + }, + "position": { + "type": "number", + "description": "Display order within category" + }, + "showOnRoadmap": { + "type": "boolean", + "description": "Whether to show on public roadmap" + }, + "isDefault": { + "type": "boolean", + "description": "Whether this is the default status for new posts" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": [ + "id", + "name", + "slug", + "color", + "category", + "position", + "showOnRoadmap", + "isDefault", + "createdAt" + ], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Updated status" + } + } + } + }, + "400": { + "description": "Validation error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "VALIDATION_ERROR" + }, + "message": { + "type": "string" + }, + "details": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Request validation failed" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + }, + "404": { + "description": "Status not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "NOT_FOUND" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Resource not found" + } + } + } + } + } + }, + "delete": { + "tags": ["Statuses"], + "summary": "Delete a status", + "description": "Delete a status by ID. Cannot delete the default status or a status with assigned posts.", + "parameters": [ + { + "name": "statusId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Status ID" + } + ], + "responses": { + "204": { + "description": "Status deleted" + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + }, + "403": { + "description": "Cannot delete (default status or has posts)", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Forbidden error" + } + } + } + }, + "404": { + "description": "Status not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "NOT_FOUND" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Resource not found" + } + } + } + } + } + } + }, + "/members": { + "get": { + "tags": ["Members"], + "summary": "List team members", + "description": "Returns all team members (admin and member roles) in the workspace", + "responses": { + "200": { + "description": "List of team members", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "email": { + "type": "string" + }, + "image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": ["id", "name", "email", "image"], + "additionalProperties": false + } + }, + "meta": { + "type": "object", + "properties": { + "pagination": { + "type": "object", + "properties": { + "cursor": { + "description": "Cursor for next page, null if no more pages", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "hasMore": { + "type": "boolean", + "description": "Whether there are more items" + }, + "total": { + "description": "Total count (when available)", + "type": "number" + } + }, + "required": ["hasMore"], + "additionalProperties": false + } + }, + "required": ["pagination"], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "List of team members" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + } + } + } + }, + "/members/{principalId}": { + "get": { + "tags": ["Members"], + "summary": "Get a team member", + "description": "Get a single team member by ID", + "parameters": [ + { + "name": "principalId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Principal ID" + } + ], + "responses": { + "200": { + "description": "Team member details", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Member ID", + "example": "member_01h455vb4pex5vsknk084sn02q" + }, + "userId": { + "type": "string", + "description": "User ID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "role": { + "type": "string", + "enum": ["admin", "member"], + "description": "Member role" + }, + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "example": "John Doe" + }, + "email": { + "type": "string", + "example": "john@example.com" + }, + "image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Profile image URL" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": ["id", "userId", "role", "name", "email", "image", "createdAt"], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Team member details" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + }, + "404": { + "description": "Team member not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "NOT_FOUND" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Resource not found" + } + } + } + } + } + }, + "patch": { + "tags": ["Members"], + "summary": "Update a team member", + "description": "Update a team member's role. Cannot modify your own role.", + "parameters": [ + { + "name": "principalId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Principal ID" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "role": { + "type": "string", + "enum": ["admin", "member"], + "description": "New role for the member" + } + }, + "required": ["role"], + "description": "Update member role request body" + } + } + } + }, + "responses": { + "200": { + "description": "Team member updated", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Member ID", + "example": "member_01h455vb4pex5vsknk084sn02q" + }, + "userId": { + "type": "string", + "description": "User ID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "role": { + "type": "string", + "enum": ["admin", "member"], + "description": "Member role" + }, + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "example": "John Doe" + }, + "email": { + "type": "string", + "example": "john@example.com" + }, + "image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Profile image URL" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": ["id", "userId", "role", "name", "email", "image", "createdAt"], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Updated team member" + } + } + } + }, + "400": { + "description": "Validation error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "VALIDATION_ERROR" + }, + "message": { + "type": "string" + }, + "details": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Request validation failed" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + }, + "403": { + "description": "Cannot modify own role or last admin", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Forbidden error" + } + } + } + }, + "404": { + "description": "Team member not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "NOT_FOUND" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Resource not found" + } + } + } + } + } + }, + "delete": { + "tags": ["Members"], + "summary": "Remove a team member", + "description": "Remove a team member from the workspace (converts them to a portal user). Cannot remove yourself or the last admin.", + "parameters": [ + { + "name": "principalId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Principal ID" + } + ], + "responses": { + "204": { + "description": "Team member removed" + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + }, + "403": { + "description": "Cannot remove self or last admin", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Forbidden error" + } + } + } + }, + "404": { + "description": "Team member not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "NOT_FOUND" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Resource not found" + } + } + } + } + } + } + }, + "/users": { + "get": { + "tags": ["Users"], + "summary": "List portal users", + "description": "Returns a paginated list of portal users (public feedback submitters)", + "parameters": [ + { + "name": "search", + "in": "query", + "schema": { + "type": "string" + }, + "description": "Search by name or email" + }, + { + "name": "verified", + "in": "query", + "schema": { + "type": "string", + "enum": ["true", "false"] + }, + "description": "Filter by email verification status" + }, + { + "name": "dateFrom", + "in": "query", + "schema": { + "type": "string", + "format": "date-time" + }, + "description": "Filter by join date (from)" + }, + { + "name": "dateTo", + "in": "query", + "schema": { + "type": "string", + "format": "date-time" + }, + "description": "Filter by join date (to)" + }, + { + "name": "segmentIds", + "in": "query", + "schema": { + "type": "string" + }, + "description": "Comma-separated segment IDs to filter by (OR logic)" + }, + { + "name": "sort", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "newest", + "oldest", + "most_active", + "most_posts", + "most_comments", + "most_votes", + "name" + ] + }, + "description": "Sort order" + }, + { + "name": "page", + "in": "query", + "schema": { + "type": "integer", + "default": 1 + }, + "description": "Page number" + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "default": 20, + "maximum": 100 + }, + "description": "Items per page" + } + ], + "responses": { + "200": { + "description": "List of portal users", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "type": "object", + "properties": { + "principalId": { + "type": "string", + "description": "Principal ID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "userId": { + "type": "string", + "description": "User ID" + }, + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "example": "Jane Doe" + }, + "email": { + "type": "string", + "example": "jane@example.com" + }, + "image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Profile image URL" + }, + "emailVerified": { + "type": "boolean", + "description": "Whether email is verified" + }, + "attributes": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {}, + "description": "User attributes (must be configured in Settings > User Attributes)" + }, + "joinedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "When the user joined", + "example": "2024-01-15T10:30:00.000Z" + }, + "postCount": { + "type": "number", + "description": "Number of posts created" + }, + "commentCount": { + "type": "number", + "description": "Number of comments made" + }, + "voteCount": { + "type": "number", + "description": "Number of votes cast" + } + }, + "required": [ + "principalId", + "userId", + "name", + "email", + "image", + "emailVerified", + "attributes", + "joinedAt", + "postCount", + "commentCount", + "voteCount" + ], + "additionalProperties": false + } + }, + "total": { + "type": "number" + }, + "hasMore": { + "type": "boolean" + }, + "page": { + "type": "number" + }, + "limit": { + "type": "number" + } + }, + "required": ["items", "total", "hasMore", "page", "limit"], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Paginated portal users response" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + } + } + } + }, + "/users/identify": { + "post": { + "tags": ["Users"], + "summary": "Identify (create or update) a user", + "description": "Create a new portal user or update an existing one by email. User attributes must be configured in Settings > User Attributes before they can be set.", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "email", + "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$", + "description": "User email (used for lookup/creation)" + }, + "name": { + "description": "Display name", + "type": "string" + }, + "image": { + "description": "Profile image URL", + "type": "string", + "format": "uri" + }, + "emailVerified": { + "description": "Email verification status", + "type": "boolean" + }, + "externalId": { + "description": "Your system's user ID for cross-referencing", + "type": "string" + }, + "attributes": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {}, + "description": "User attributes (must be configured in Settings > User Attributes)" + } + }, + "required": ["email"] + } + } + } + }, + "responses": { + "200": { + "description": "Existing user updated", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "principalId": { + "type": "string", + "description": "Principal ID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "userId": { + "type": "string", + "description": "User ID" + }, + "name": { + "type": "string", + "example": "Jane Doe" + }, + "email": { + "type": "string", + "example": "jane@example.com" + }, + "image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Profile image URL" + }, + "emailVerified": { + "type": "boolean", + "description": "Whether email is verified" + }, + "externalId": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Customer-provided external user ID" + }, + "attributes": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {}, + "description": "User attributes (must be configured in Settings > User Attributes)" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "Account creation date", + "example": "2024-01-15T10:30:00.000Z" + }, + "created": { + "type": "boolean", + "description": "true if new user was created, false if existing was updated" + } + }, + "required": [ + "principalId", + "userId", + "name", + "email", + "image", + "emailVerified", + "externalId", + "attributes", + "createdAt", + "created" + ], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Updated user" + } + } + } + }, + "201": { + "description": "New user created", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "principalId": { + "type": "string", + "description": "Principal ID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "userId": { + "type": "string", + "description": "User ID" + }, + "name": { + "type": "string", + "example": "Jane Doe" + }, + "email": { + "type": "string", + "example": "jane@example.com" + }, + "image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Profile image URL" + }, + "emailVerified": { + "type": "boolean", + "description": "Whether email is verified" + }, + "externalId": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Customer-provided external user ID" + }, + "attributes": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {}, + "description": "User attributes (must be configured in Settings > User Attributes)" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "Account creation date", + "example": "2024-01-15T10:30:00.000Z" + }, + "created": { + "type": "boolean", + "description": "true if new user was created, false if existing was updated" + } + }, + "required": [ + "principalId", + "userId", + "name", + "email", + "image", + "emailVerified", + "externalId", + "attributes", + "createdAt", + "created" + ], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Created user" + } + } + } + }, + "400": { + "description": "Validation error (invalid attributes)", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "VALIDATION_ERROR" + }, + "message": { + "type": "string" + }, + "details": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Request validation failed" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + } + } + } + }, + "/users/{principalId}": { + "get": { + "tags": ["Users"], + "summary": "Get a portal user", + "description": "Get detailed information about a portal user, including their activity", + "parameters": [ + { + "name": "principalId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Principal ID" + } + ], + "responses": { + "200": { + "description": "Portal user details", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "principalId": { + "type": "string", + "description": "Principal ID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "userId": { + "type": "string", + "description": "User ID" + }, + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "example": "Jane Doe" + }, + "email": { + "type": "string", + "example": "jane@example.com" + }, + "image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Profile image URL" + }, + "emailVerified": { + "type": "boolean", + "description": "Whether email is verified" + }, + "attributes": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {}, + "description": "User attributes (must be configured in Settings > User Attributes)" + }, + "joinedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "When the user joined", + "example": "2024-01-15T10:30:00.000Z" + }, + "postCount": { + "type": "number", + "description": "Number of posts created" + }, + "commentCount": { + "type": "number", + "description": "Number of comments made" + }, + "voteCount": { + "type": "number", + "description": "Number of votes cast" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "Account creation date", + "example": "2024-01-15T10:30:00.000Z" + }, + "engagedPosts": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "title": { + "type": "string" + }, + "content": { + "type": "string" + }, + "statusId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "statusName": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "statusColor": { + "type": "string" + }, + "voteCount": { + "type": "number" + }, + "commentCount": { + "type": "number" + }, + "boardSlug": { + "type": "string" + }, + "boardName": { + "type": "string" + }, + "authorName": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + }, + "engagementTypes": { + "type": "array", + "items": { + "type": "string", + "enum": ["authored", "commented", "voted"] + } + }, + "engagedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": [ + "id", + "title", + "content", + "statusId", + "statusName", + "statusColor", + "voteCount", + "commentCount", + "boardSlug", + "boardName", + "authorName", + "createdAt", + "engagementTypes", + "engagedAt" + ], + "additionalProperties": false + }, + "description": "Posts the user has engaged with" + } + }, + "required": [ + "principalId", + "userId", + "name", + "email", + "image", + "emailVerified", + "attributes", + "joinedAt", + "postCount", + "commentCount", + "voteCount", + "createdAt", + "engagedPosts" + ], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Portal user details" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + }, + "404": { + "description": "Portal user not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "NOT_FOUND" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Resource not found" + } + } + } + } + } + }, + "patch": { + "tags": ["Users"], + "summary": "Update a portal user", + "description": "Update a portal user's profile and attributes. User attributes must be configured in Settings > User Attributes before they can be set.", + "parameters": [ + { + "name": "principalId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Principal ID" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "description": "Display name", + "type": "string" + }, + "image": { + "description": "Profile image URL", + "anyOf": [ + { + "type": "string", + "format": "uri" + }, + { + "type": "null" + } + ] + }, + "emailVerified": { + "description": "Email verification status", + "type": "boolean" + }, + "externalId": { + "description": "Your system's user ID (null to unset)", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "attributes": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {}, + "description": "User attributes (must be configured in Settings > User Attributes)" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Updated portal user", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "principalId": { + "type": "string", + "description": "Principal ID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "userId": { + "type": "string", + "description": "User ID" + }, + "name": { + "type": "string", + "example": "Jane Doe" + }, + "email": { + "type": "string", + "example": "jane@example.com" + }, + "image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Profile image URL" + }, + "emailVerified": { + "type": "boolean", + "description": "Whether email is verified" + }, + "externalId": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Customer-provided external user ID" + }, + "attributes": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {}, + "description": "User attributes (must be configured in Settings > User Attributes)" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "Account creation date", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": [ + "principalId", + "userId", + "name", + "email", + "image", + "emailVerified", + "externalId", + "attributes", + "createdAt" + ], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Updated user" + } + } + } + }, + "400": { + "description": "Validation error (invalid attributes)", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "VALIDATION_ERROR" + }, + "message": { + "type": "string" + }, + "details": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Request validation failed" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + }, + "404": { + "description": "Portal user not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "NOT_FOUND" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Resource not found" + } + } + } + } + } + }, + "delete": { + "tags": ["Users"], + "summary": "Remove a portal user", + "description": "Remove a portal user from the workspace", + "parameters": [ + { + "name": "principalId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Principal ID" + } + ], + "responses": { + "204": { + "description": "Portal user removed" + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + }, + "404": { + "description": "Portal user not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "NOT_FOUND" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Resource not found" + } + } + } + } + } + } + }, + "/roadmaps": { + "get": { + "tags": ["Roadmaps"], + "summary": "List roadmaps", + "description": "Returns all roadmaps in the workspace", + "responses": { + "200": { + "description": "List of roadmaps", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "roadmap_01h455vb4pex5vsknk084sn02q" + }, + "name": { + "type": "string", + "example": "Product Roadmap" + }, + "slug": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "description": "URL-friendly identifier", + "example": "product-roadmap" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "example": "Our product development roadmap" + }, + "isPublic": { + "type": "boolean", + "description": "Whether the roadmap is publicly visible" + }, + "position": { + "type": "number", + "description": "Display order" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": [ + "id", + "name", + "slug", + "description", + "isPublic", + "position", + "createdAt" + ], + "additionalProperties": false + } + }, + "meta": { + "type": "object", + "properties": { + "pagination": { + "type": "object", + "properties": { + "cursor": { + "description": "Cursor for next page, null if no more pages", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "hasMore": { + "type": "boolean", + "description": "Whether there are more items" + }, + "total": { + "description": "Total count (when available)", + "type": "number" + } + }, + "required": ["hasMore"], + "additionalProperties": false + } + }, + "required": ["pagination"], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "List of roadmaps" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + } + } + }, + "post": { + "tags": ["Roadmaps"], + "summary": "Create a roadmap", + "description": "Create a new roadmap", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "description": "Roadmap name", + "example": "Product Roadmap" + }, + "slug": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "pattern": "^[a-z0-9-]+$", + "description": "URL-friendly slug", + "example": "product-roadmap" + }, + "description": { + "description": "Roadmap description", + "type": "string", + "maxLength": 500 + }, + "isPublic": { + "description": "Make roadmap public", + "default": true, + "type": "boolean" + } + }, + "required": ["name", "slug"], + "description": "Create roadmap request body" + } + } + } + }, + "responses": { + "201": { + "description": "Roadmap created", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "roadmap_01h455vb4pex5vsknk084sn02q" + }, + "name": { + "type": "string", + "example": "Product Roadmap" + }, + "slug": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "description": "URL-friendly identifier", + "example": "product-roadmap" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "example": "Our product development roadmap" + }, + "isPublic": { + "type": "boolean", + "description": "Whether the roadmap is publicly visible" + }, + "position": { + "type": "number", + "description": "Display order" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": [ + "id", + "name", + "slug", + "description", + "isPublic", + "position", + "createdAt" + ], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Created roadmap" + } + } + } + }, + "400": { + "description": "Validation error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "VALIDATION_ERROR" + }, + "message": { + "type": "string" + }, + "details": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Request validation failed" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + } + } + } + }, + "/roadmaps/{roadmapId}": { + "get": { + "tags": ["Roadmaps"], + "summary": "Get a roadmap", + "description": "Get a single roadmap by ID", + "parameters": [ + { + "name": "roadmapId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Roadmap ID" + } + ], + "responses": { + "200": { + "description": "Roadmap details", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "roadmap_01h455vb4pex5vsknk084sn02q" + }, + "name": { + "type": "string", + "example": "Product Roadmap" + }, + "slug": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "description": "URL-friendly identifier", + "example": "product-roadmap" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "example": "Our product development roadmap" + }, + "isPublic": { + "type": "boolean", + "description": "Whether the roadmap is publicly visible" + }, + "position": { + "type": "number", + "description": "Display order" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": [ + "id", + "name", + "slug", + "description", + "isPublic", + "position", + "createdAt" + ], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Roadmap details" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + }, + "404": { + "description": "Roadmap not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "NOT_FOUND" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Resource not found" + } + } + } + } + } + }, + "patch": { + "tags": ["Roadmaps"], + "summary": "Update a roadmap", + "description": "Update an existing roadmap", + "parameters": [ + { + "name": "roadmapId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Roadmap ID" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "description": { + "anyOf": [ + { + "type": "string", + "maxLength": 500 + }, + { + "type": "null" + } + ] + }, + "isPublic": { + "type": "boolean" + } + }, + "description": "Update roadmap request body" + } + } + } + }, + "responses": { + "200": { + "description": "Roadmap updated", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "roadmap_01h455vb4pex5vsknk084sn02q" + }, + "name": { + "type": "string", + "example": "Product Roadmap" + }, + "slug": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "description": "URL-friendly identifier", + "example": "product-roadmap" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "example": "Our product development roadmap" + }, + "isPublic": { + "type": "boolean", + "description": "Whether the roadmap is publicly visible" + }, + "position": { + "type": "number", + "description": "Display order" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": [ + "id", + "name", + "slug", + "description", + "isPublic", + "position", + "createdAt" + ], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Updated roadmap" + } + } + } + }, + "400": { + "description": "Validation error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "VALIDATION_ERROR" + }, + "message": { + "type": "string" + }, + "details": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Request validation failed" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + }, + "404": { + "description": "Roadmap not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "NOT_FOUND" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Resource not found" + } + } + } + } + } + }, + "delete": { + "tags": ["Roadmaps"], + "summary": "Delete a roadmap", + "description": "Delete a roadmap by ID", + "parameters": [ + { + "name": "roadmapId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Roadmap ID" + } + ], + "responses": { + "204": { + "description": "Roadmap deleted" + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + }, + "404": { + "description": "Roadmap not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "NOT_FOUND" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Resource not found" + } + } + } + } + } + } + }, + "/roadmaps/{roadmapId}/posts": { + "get": { + "tags": ["Roadmaps"], + "summary": "List posts in a roadmap", + "description": "Returns posts assigned to a roadmap", + "parameters": [ + { + "name": "roadmapId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Roadmap ID" + }, + { + "name": "statusId", + "in": "query", + "schema": { + "type": "string" + }, + "description": "Filter by status ID" + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "default": 20, + "maximum": 100 + }, + "description": "Items per page" + }, + { + "name": "offset", + "in": "query", + "schema": { + "type": "integer", + "default": 0 + }, + "description": "Offset for pagination" + } + ], + "responses": { + "200": { + "description": "List of roadmap posts", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "title": { + "type": "string" + }, + "voteCount": { + "type": "number" + }, + "statusId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "board": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "name": { + "type": "string" + }, + "slug": { + "type": "string" + } + }, + "required": ["id", "name", "slug"], + "additionalProperties": false + }, + "position": { + "type": "number", + "description": "Position within the roadmap" + } + }, + "required": [ + "id", + "title", + "voteCount", + "statusId", + "board", + "position" + ], + "additionalProperties": false + } + }, + "total": { + "type": "number" + }, + "hasMore": { + "type": "boolean" + } + }, + "required": ["items", "total", "hasMore"], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Paginated roadmap posts response" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + }, + "404": { + "description": "Roadmap not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "NOT_FOUND" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Resource not found" + } + } + } + } + } + }, + "post": { + "tags": ["Roadmaps"], + "summary": "Add a post to a roadmap", + "description": "Add an existing post to a roadmap", + "parameters": [ + { + "name": "roadmapId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Roadmap ID" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "postId": { + "type": "string", + "description": "Post ID to add", + "example": "post_01h455vb4pex5vsknk084sn02q" + } + }, + "required": ["postId"], + "description": "Add post to roadmap request body" + } + } + } + }, + "responses": { + "201": { + "description": "Post added to roadmap", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "roadmapId": { + "type": "string" + }, + "postId": { + "type": "string" + } + }, + "required": ["message", "roadmapId", "postId"], + "additionalProperties": false, + "description": "Post added confirmation" + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Post added confirmation" + } + } + } + }, + "400": { + "description": "Validation error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "VALIDATION_ERROR" + }, + "message": { + "type": "string" + }, + "details": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Request validation failed" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + }, + "404": { + "description": "Roadmap or post not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "NOT_FOUND" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Resource not found" + } + } + } + }, + "409": { + "description": "Post already in roadmap", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "CONFLICT" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Conflict error" + } + } + } + } + } + } + }, + "/roadmaps/{roadmapId}/posts/{postId}": { + "delete": { + "tags": ["Roadmaps"], + "summary": "Remove a post from a roadmap", + "description": "Remove a post from a roadmap", + "parameters": [ + { + "name": "roadmapId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Roadmap ID" + }, + { + "name": "postId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Post ID" + } + ], + "responses": { + "204": { + "description": "Post removed from roadmap" + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + }, + "404": { + "description": "Roadmap or post not found in roadmap", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "NOT_FOUND" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Resource not found" + } + } + } + } + } + } + }, + "/changelog": { + "get": { + "tags": ["Changelog"], + "summary": "List changelog entries", + "description": "Returns changelog entries with optional filtering by published status", + "parameters": [ + { + "name": "published", + "in": "query", + "schema": { + "type": "string", + "enum": ["true", "false"] + }, + "description": "Filter by published status" + }, + { + "name": "cursor", + "in": "query", + "schema": { + "type": "string" + }, + "description": "Pagination cursor for next page" + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "default": 20, + "maximum": 100 + }, + "description": "Items per page" + } + ], + "responses": { + "200": { + "description": "List of changelog entries", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "changelog_01h455vb4pex5vsknk084sn02q" + }, + "title": { + "type": "string", + "example": "New Dark Mode Feature" + }, + "content": { + "type": "string", + "example": "We've added a dark mode option..." + }, + "category": { + "anyOf": [ + { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "name": { + "type": "string" + }, + "slug": { + "type": "string" + }, + "color": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": ["id", "name", "slug", "color"], + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "product": { + "anyOf": [ + { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "name": { + "type": "string" + }, + "slug": { + "type": "string" + } + }, + "required": ["id", "name", "slug"], + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "publishedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ], + "description": "When the entry was published (null if draft)", + "example": "2024-01-15T10:30:00.000Z" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": [ + "id", + "title", + "content", + "category", + "product", + "publishedAt", + "createdAt", + "updatedAt" + ], + "additionalProperties": false + } + }, + "pagination": { + "type": "object", + "properties": { + "cursor": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Cursor for fetching next page (null if no more pages)" + }, + "hasMore": { + "type": "boolean", + "description": "Whether there are more items to fetch" + } + }, + "required": ["cursor", "hasMore"], + "additionalProperties": false, + "description": "Cursor-based pagination metadata" + } + }, + "required": ["data", "pagination"], + "additionalProperties": false, + "description": "Paginated changelog entries" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + } + } + }, + "post": { + "tags": ["Changelog"], + "summary": "Create a changelog entry", + "description": "Create a new changelog entry", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "title": { + "type": "string", + "minLength": 1, + "maxLength": 200, + "description": "Entry title", + "example": "New Dark Mode Feature" + }, + "content": { + "type": "string", + "minLength": 1, + "description": "Entry content (supports markdown)" + }, + "publishedAt": { + "description": "Publish date (omit to save as draft)", + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + "categoryName": { + "anyOf": [ + { + "type": "string", + "maxLength": 200 + }, + { + "type": "null" + } + ] + }, + "productName": { + "anyOf": [ + { + "type": "string", + "maxLength": 200 + }, + { + "type": "null" + } + ] + } + }, + "required": ["title", "content"], + "description": "Create changelog entry request body" + } + } + } + }, + "responses": { + "201": { + "description": "Changelog entry created", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "changelog_01h455vb4pex5vsknk084sn02q" + }, + "title": { + "type": "string", + "example": "New Dark Mode Feature" + }, + "content": { + "type": "string", + "example": "We've added a dark mode option..." + }, + "category": { + "anyOf": [ + { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "name": { + "type": "string" + }, + "slug": { + "type": "string" + }, + "color": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": ["id", "name", "slug", "color"], + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "product": { + "anyOf": [ + { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "name": { + "type": "string" + }, + "slug": { + "type": "string" + } + }, + "required": ["id", "name", "slug"], + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "publishedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ], + "description": "When the entry was published (null if draft)", + "example": "2024-01-15T10:30:00.000Z" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": [ + "id", + "title", + "content", + "category", + "product", + "publishedAt", + "createdAt", + "updatedAt" + ], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Created changelog entry" + } + } + } + }, + "400": { + "description": "Validation error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "VALIDATION_ERROR" + }, + "message": { + "type": "string" + }, + "details": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Request validation failed" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + } + } + } + }, + "/changelog/{entryId}": { + "get": { + "tags": ["Changelog"], + "summary": "Get a changelog entry", + "description": "Get a single changelog entry by ID", + "parameters": [ + { + "name": "entryId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Changelog entry ID" + } + ], + "responses": { + "200": { + "description": "Changelog entry details", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "changelog_01h455vb4pex5vsknk084sn02q" + }, + "title": { + "type": "string", + "example": "New Dark Mode Feature" + }, + "content": { + "type": "string", + "example": "We've added a dark mode option..." + }, + "category": { + "anyOf": [ + { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "name": { + "type": "string" + }, + "slug": { + "type": "string" + }, + "color": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": ["id", "name", "slug", "color"], + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "product": { + "anyOf": [ + { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "name": { + "type": "string" + }, + "slug": { + "type": "string" + } + }, + "required": ["id", "name", "slug"], + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "publishedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ], + "description": "When the entry was published (null if draft)", + "example": "2024-01-15T10:30:00.000Z" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": [ + "id", + "title", + "content", + "category", + "product", + "publishedAt", + "createdAt", + "updatedAt" + ], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Changelog entry details" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + }, + "404": { + "description": "Changelog entry not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "NOT_FOUND" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Resource not found" + } + } + } + } + } + }, + "patch": { + "tags": ["Changelog"], + "summary": "Update a changelog entry", + "description": "Update an existing changelog entry", + "parameters": [ + { + "name": "entryId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Changelog entry ID" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "title": { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + "content": { + "type": "string", + "minLength": 1 + }, + "publishedAt": { + "description": "Set to null to unpublish", + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ] + }, + "categoryName": { + "anyOf": [ + { + "type": "string", + "maxLength": 200 + }, + { + "type": "null" + } + ] + }, + "productName": { + "anyOf": [ + { + "type": "string", + "maxLength": 200 + }, + { + "type": "null" + } + ] + } + }, + "description": "Update changelog entry request body" + } + } + } + }, + "responses": { + "200": { + "description": "Changelog entry updated", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "changelog_01h455vb4pex5vsknk084sn02q" + }, + "title": { + "type": "string", + "example": "New Dark Mode Feature" + }, + "content": { + "type": "string", + "example": "We've added a dark mode option..." + }, + "category": { + "anyOf": [ + { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "name": { + "type": "string" + }, + "slug": { + "type": "string" + }, + "color": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": ["id", "name", "slug", "color"], + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "product": { + "anyOf": [ + { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "name": { + "type": "string" + }, + "slug": { + "type": "string" + } + }, + "required": ["id", "name", "slug"], + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "publishedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ], + "description": "When the entry was published (null if draft)", + "example": "2024-01-15T10:30:00.000Z" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": [ + "id", + "title", + "content", + "category", + "product", + "publishedAt", + "createdAt", + "updatedAt" + ], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Updated changelog entry" + } + } + } + }, + "400": { + "description": "Validation error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "VALIDATION_ERROR" + }, + "message": { + "type": "string" + }, + "details": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Request validation failed" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + }, + "404": { + "description": "Changelog entry not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "NOT_FOUND" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Resource not found" + } + } + } + } + } + }, + "delete": { + "tags": ["Changelog"], + "summary": "Delete a changelog entry", + "description": "Delete a changelog entry by ID", + "parameters": [ + { + "name": "entryId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Changelog entry ID" + } + ], + "responses": { + "204": { + "description": "Changelog entry deleted" + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + }, + "404": { + "description": "Changelog entry not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "NOT_FOUND" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Resource not found" + } + } + } + } + } + } + }, + "/suggestions": { + "get": { + "tags": ["Suggestions"], + "summary": "List AI-generated feedback suggestions", + "parameters": [ + { + "name": "status", + "in": "query", + "schema": { + "def": { + "type": "optional", + "innerType": { + "def": { + "type": "enum", + "entries": { + "pending": "pending", + "dismissed": "dismissed" + } + }, + "type": "enum", + "enum": { + "pending": "pending", + "dismissed": "dismissed" + }, + "options": ["pending", "dismissed"] + } + }, + "type": "optional" + } + }, + { + "name": "type", + "in": "query", + "schema": { + "def": { + "type": "optional", + "innerType": { + "def": { + "type": "enum", + "entries": { + "create_post": "create_post", + "vote_on_post": "vote_on_post", + "duplicate_post": "duplicate_post" + } + }, + "type": "enum", + "enum": { + "create_post": "create_post", + "vote_on_post": "vote_on_post", + "duplicate_post": "duplicate_post" + }, + "options": ["create_post", "vote_on_post", "duplicate_post"] + } + }, + "type": "optional" + } + }, + { + "name": "sort", + "in": "query", + "schema": { + "def": { + "type": "optional", + "innerType": { + "def": { + "type": "enum", + "entries": { + "newest": "newest", + "relevance": "relevance" + } + }, + "type": "enum", + "enum": { + "newest": "newest", + "relevance": "relevance" + }, + "options": ["newest", "relevance"] + } + }, + "type": "optional" + } + }, + { + "name": "limit", + "in": "query", + "schema": { + "def": { + "type": "optional", + "innerType": { + "def": { + "type": "number", + "coerce": true, + "checks": [{}, {}] + }, + "type": "number", + "minValue": 1, + "maxValue": 100, + "isInt": false, + "isFinite": true, + "format": null + } + }, + "type": "optional" + } + }, + { + "name": "cursor", + "in": "query", + "schema": { + "def": { + "type": "optional", + "innerType": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + }, + "type": "optional" + } + } + ], + "responses": { + "200": { + "description": "Suggestions", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "feedback_suggestion_01h455vb4pex5vsknk084sn02q" + }, + "suggestionType": { + "type": "string", + "enum": ["create_post", "vote_on_post", "duplicate_post"] + }, + "status": { + "type": "string", + "enum": ["pending", "dismissed"] + }, + "suggestedTitle": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "suggestedBody": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "reasoning": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "similarityScore": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "rawItem": { + "anyOf": [ + { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "sourceType": { + "type": "string" + }, + "externalUrl": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "author": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": ["id", "sourceType", "externalUrl", "author"], + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "targetPost": {}, + "sourcePost": {}, + "board": {}, + "signal": {}, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": [ + "id", + "suggestionType", + "status", + "suggestedTitle", + "suggestedBody", + "reasoning", + "similarityScore", + "rawItem", + "targetPost", + "sourcePost", + "board", + "signal", + "createdAt", + "updatedAt" + ], + "additionalProperties": false + } + }, + "meta": { + "type": "object", + "properties": { + "pagination": { + "type": "object", + "properties": { + "cursor": { + "description": "Cursor for next page, null if no more pages", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "hasMore": { + "type": "boolean", + "description": "Whether there are more items" + }, + "total": { + "description": "Total count (when available)", + "type": "number" + } + }, + "required": ["hasMore"], + "additionalProperties": false + } + }, + "required": ["pagination"], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Suggestions" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + } + } + } + }, + "/suggestions/{suggestionId}/accept": { + "post": { + "tags": ["Suggestions"], + "summary": "Accept a suggestion", + "description": "Accepts a feedback or merge suggestion. For `vote_on_post` with no edits this proxies a vote; otherwise a post is created. The suggestion ID is a `feedback_suggestion_*` or `merge_sug_*` TypeID.", + "parameters": [ + { + "name": "suggestionId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "edits": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "body": { + "type": "string" + }, + "boardId": { + "type": "string" + }, + "statusId": { + "type": "string" + } + } + }, + "swapDirection": { + "type": "boolean" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Suggestion accepted", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "accepted": { + "type": "boolean", + "const": true + }, + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "resultPostId": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + } + }, + "required": ["accepted", "id"], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Accept result" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + } + } + } + }, + "/suggestions/{suggestionId}/dismiss": { + "post": { + "tags": ["Suggestions"], + "summary": "Dismiss a suggestion", + "parameters": [ + { + "name": "suggestionId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "responses": { + "200": { + "description": "Suggestion dismissed", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "dismissed": { + "type": "boolean", + "const": true + }, + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + } + }, + "required": ["dismissed", "id"], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Dismiss result" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + } + } + } + }, + "/suggestions/{suggestionId}/restore": { + "post": { + "tags": ["Suggestions"], + "summary": "Restore a dismissed suggestion back to pending", + "parameters": [ + { + "name": "suggestionId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "responses": { + "200": { + "description": "Suggestion restored", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "restored": { + "type": "boolean", + "const": true + }, + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + } + }, + "required": ["restored", "id"], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Restore result" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + } + } + } + }, + "/apps/boards": { + "get": { + "tags": ["Apps"], + "summary": "List boards visible to the API key", + "description": "Returns the boards the team-scoped API key can see (id, name, slug), for use as a board picker in an embedded app.", + "responses": { + "200": { + "description": "Boards", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "boards": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "board_01h455vb4pex5vsknk084sn02q" + }, + "name": { + "type": "string" + }, + "slug": { + "type": "string" + } + }, + "required": ["id", "name", "slug"], + "additionalProperties": false + } + } + }, + "required": ["boards"], + "additionalProperties": false + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + } + } + } + }, + "/apps/posts": { + "post": { + "tags": ["Apps"], + "summary": "Create a post", + "description": "Creates a post on a board. Optionally links the created post to an external ticket and/or attaches a requester whose vote is added.", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "boardId": { + "type": "string", + "minLength": 1, + "description": "Board TypeID" + }, + "title": { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + "content": { + "default": "", + "type": "string", + "maxLength": 10000 + }, + "link": { + "description": "Optionally link the new post to an external ticket", + "type": "object", + "properties": { + "integrationType": { + "type": "string", + "minLength": 1 + }, + "externalId": { + "type": "string", + "minLength": 1 + }, + "externalUrl": { + "type": "string" + } + }, + "required": ["integrationType", "externalId"] + }, + "requester": { + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "email", + "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$" + }, + "name": { + "type": "string" + } + }, + "required": ["email"], + "description": "Optional requester whose vote/identity is attached" + } + }, + "required": ["boardId", "title"] + } + } + } + }, + "responses": { + "201": { + "description": "Post created", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "title": { + "type": "string" + }, + "content": { + "type": "string" + }, + "voteCount": { + "type": "number" + }, + "boardId": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "statusId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": [ + "id", + "title", + "content", + "voteCount", + "boardId", + "statusId", + "createdAt" + ], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Post" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + } + } + } + }, + "/apps/search": { + "get": { + "tags": ["Apps"], + "summary": "Search posts", + "description": "Full-text search across visible posts, sorted by top votes.", + "parameters": [ + { + "name": "q", + "in": "query", + "schema": { + "def": { + "type": "optional", + "innerType": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + }, + "type": "optional" + }, + "description": "Search query; empty query returns an empty list" + }, + { + "name": "limit", + "in": "query", + "schema": { + "def": { + "type": "optional", + "innerType": { + "def": { + "type": "number", + "coerce": true, + "checks": [{}, {}] + }, + "type": "number", + "minValue": 1, + "maxValue": 20, + "isInt": false, + "isFinite": true, + "format": null + } + }, + "type": "optional" + }, + "description": "Max results (default 10, capped at 20)" + } + ], + "responses": { + "200": { + "description": "Matching posts", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "posts": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "title": { + "type": "string" + }, + "voteCount": { + "type": "number" + }, + "statusName": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "statusColor": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "board": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": ["name"], + "additionalProperties": false + } + }, + "required": [ + "id", + "title", + "voteCount", + "statusName", + "statusColor", + "board" + ], + "additionalProperties": false + } + } + }, + "required": ["posts"], + "additionalProperties": false + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + } + } + } + }, + "/apps/suggest": { + "get": { + "tags": ["Apps"], + "summary": "Suggest similar posts", + "description": "Returns posts semantically similar to the supplied text via vector embeddings, falling back to text search when AI is not configured.", + "parameters": [ + { + "name": "text", + "in": "query", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + }, + "description": "Text to find similar posts for" + }, + { + "name": "limit", + "in": "query", + "schema": { + "def": { + "type": "optional", + "innerType": { + "def": { + "type": "number", + "coerce": true, + "checks": [{}, {}] + }, + "type": "number", + "minValue": 1, + "maxValue": 20, + "isInt": false, + "isFinite": true, + "format": null + } + }, + "type": "optional" + }, + "description": "Max results (default 5, capped at 20)" + } + ], + "responses": { + "200": { + "description": "Suggested posts", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "posts": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "title": { + "type": "string" + }, + "voteCount": { + "type": "number" + }, + "similarity": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "description": "Cosine similarity (0-1) when AI embeddings are configured; null on text-search fallback" + }, + "board": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": ["name"], + "additionalProperties": false + } + }, + "required": ["id", "title", "voteCount", "similarity", "board"], + "additionalProperties": false + } + } + }, + "required": ["posts"], + "additionalProperties": false + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + } + } + } + }, + "/apps/link": { + "post": { + "tags": ["Apps"], + "summary": "Link a post to an external ticket", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "postId": { + "type": "string", + "minLength": 1, + "description": "Post TypeID" + }, + "integrationType": { + "type": "string", + "minLength": 1 + }, + "externalId": { + "type": "string", + "minLength": 1 + }, + "externalUrl": { + "type": "string" + }, + "requester": { + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "email", + "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$" + }, + "name": { + "type": "string" + } + }, + "required": ["email"], + "description": "Optional requester whose vote/identity is attached" + } + }, + "required": ["postId", "integrationType", "externalId"] + } + } + } + }, + "responses": { + "201": { + "description": "Linked" + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + } + } + } + }, + "/apps/unlink": { + "post": { + "tags": ["Apps"], + "summary": "Unlink a post from an external ticket", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "postId": { + "type": "string", + "minLength": 1, + "description": "Post TypeID" + }, + "integrationType": { + "type": "string", + "minLength": 1 + }, + "externalId": { + "type": "string", + "minLength": 1 + } + }, + "required": ["postId", "integrationType", "externalId"] + } + } + } + }, + "responses": { + "200": { + "description": "Unlinked", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + } + }, + "required": ["success"], + "additionalProperties": false + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + } + } + } + }, + "/apps/linked": { + "get": { + "tags": ["Apps"], + "summary": "List posts linked to an external ticket", + "parameters": [ + { + "name": "integrationType", + "in": "query", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + }, + { + "name": "externalId", + "in": "query", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "responses": { + "200": { + "description": "Linked posts", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "posts": { + "type": "array", + "items": {} + } + }, + "required": ["posts"], + "additionalProperties": false + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + } + } + } + }, + "/tickets": { + "get": { + "tags": ["Tickets"], + "summary": "List tickets in scope", + "description": "Returns tickets visible to the caller, filtered by scope.", + "parameters": [ + { + "name": "scope", + "in": "query", + "schema": { + "def": { + "type": "default", + "innerType": { + "def": { + "type": "enum", + "entries": { + "all": "all", + "my_assigned": "my_assigned", + "my_team": "my_team", + "shared_with_me": "shared_with_me", + "unassigned": "unassigned", + "my_inbox": "my_inbox", + "inbox": "inbox" + } + }, + "type": "enum", + "enum": { + "all": "all", + "my_assigned": "my_assigned", + "my_team": "my_team", + "shared_with_me": "shared_with_me", + "unassigned": "unassigned", + "my_inbox": "my_inbox", + "inbox": "inbox" + }, + "options": [ + "all", + "my_assigned", + "my_team", + "shared_with_me", + "unassigned", + "my_inbox", + "inbox" + ] + }, + "defaultValue": "my_team" + }, + "type": "default" + } + }, + { + "name": "statusCategory", + "in": "query", + "schema": { + "def": { + "type": "optional", + "innerType": { + "def": { + "type": "enum", + "entries": { + "open": "open", + "pending": "pending", + "on_hold": "on_hold", + "solved": "solved", + "closed": "closed" + } + }, + "type": "enum", + "enum": { + "open": "open", + "pending": "pending", + "on_hold": "on_hold", + "solved": "solved", + "closed": "closed" + }, + "options": ["open", "pending", "on_hold", "solved", "closed"] + } + }, + "type": "optional" + } + }, + { + "name": "inboxId", + "in": "query", + "schema": { + "def": { + "type": "optional", + "innerType": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + }, + "type": "optional" + } + }, + { + "name": "search", + "in": "query", + "schema": { + "def": { + "type": "optional", + "innerType": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + }, + "type": "optional" + } + }, + { + "name": "cursor", + "in": "query", + "schema": { + "def": { + "type": "optional", + "innerType": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + }, + "type": "optional" + } + }, + { + "name": "limit", + "in": "query", + "schema": { + "def": { + "type": "optional", + "innerType": { + "def": { + "type": "number", + "coerce": true, + "checks": [{}, {}] + }, + "type": "number", + "minValue": 1, + "maxValue": 200, + "isInt": false, + "isFinite": true, + "format": null + } + }, + "type": "optional" + } + } + ], + "responses": { + "200": { + "description": "Ticket queue", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "ticket_01h455vb4pex5vsknk084sn02q" + }, + "subject": { + "type": "string" + }, + "descriptionText": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "priority": { + "type": "string", + "enum": ["low", "normal", "high", "urgent"] + }, + "channel": { + "type": "string", + "enum": ["portal", "email", "api", "widget"] + }, + "visibilityScope": { + "type": "string", + "enum": ["team", "org", "shared", "private"] + }, + "statusId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "primaryTeamId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "assigneePrincipalId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "assigneeTeamId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "requesterPrincipalId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "requesterContactId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "organizationId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "inboxId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "slaPolicyId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "firstResponseAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ], + "description": "ISO 8601 timestamp or null", + "example": "2024-01-15T10:30:00.000Z" + }, + "resolvedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ], + "description": "ISO 8601 timestamp or null", + "example": "2024-01-15T10:30:00.000Z" + }, + "reopenedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ], + "description": "ISO 8601 timestamp or null", + "example": "2024-01-15T10:30:00.000Z" + }, + "closedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ], + "description": "ISO 8601 timestamp or null", + "example": "2024-01-15T10:30:00.000Z" + }, + "lastActivityAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ], + "description": "ISO 8601 timestamp or null", + "example": "2024-01-15T10:30:00.000Z" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": [ + "id", + "subject", + "descriptionText", + "priority", + "channel", + "visibilityScope", + "statusId", + "primaryTeamId", + "assigneePrincipalId", + "assigneeTeamId", + "requesterPrincipalId", + "requesterContactId", + "organizationId", + "inboxId", + "slaPolicyId", + "firstResponseAt", + "resolvedAt", + "reopenedAt", + "closedAt", + "lastActivityAt", + "createdAt", + "updatedAt" + ], + "additionalProperties": false, + "description": "Ticket header record" + } + }, + "meta": { + "type": "object", + "properties": { + "pagination": { + "type": "object", + "properties": { + "cursor": { + "description": "Cursor for next page, null if no more pages", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "hasMore": { + "type": "boolean", + "description": "Whether there are more items" + }, + "total": { + "description": "Total count (when available)", + "type": "number" + } + }, + "required": ["hasMore"], + "additionalProperties": false + } + }, + "required": ["pagination"], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Tickets" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + } + } + }, + "post": { + "tags": ["Tickets"], + "summary": "Create a ticket", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "subject": { + "type": "string", + "minLength": 1, + "maxLength": 500 + }, + "descriptionJson": { + "anyOf": [ + {}, + { + "type": "null" + } + ] + }, + "descriptionText": { + "anyOf": [ + { + "type": "string", + "maxLength": 100000 + }, + { + "type": "null" + } + ] + }, + "priority": { + "type": "string", + "enum": ["low", "normal", "high", "urgent"] + }, + "channel": { + "type": "string", + "enum": ["portal", "email", "api", "widget"] + }, + "visibilityScope": { + "type": "string", + "enum": ["team", "org", "shared", "private"] + }, + "statusId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "primaryTeamId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "assigneePrincipalId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "assigneeTeamId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "requesterPrincipalId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "requesterContactId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "organizationId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "inboxId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + } + }, + "required": ["subject"], + "description": "Create ticket request body" + } + } + } + }, + "responses": { + "201": { + "description": "Ticket created", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "ticket_01h455vb4pex5vsknk084sn02q" + }, + "subject": { + "type": "string" + }, + "descriptionText": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "priority": { + "type": "string", + "enum": ["low", "normal", "high", "urgent"] + }, + "channel": { + "type": "string", + "enum": ["portal", "email", "api", "widget"] + }, + "visibilityScope": { + "type": "string", + "enum": ["team", "org", "shared", "private"] + }, + "statusId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "primaryTeamId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "assigneePrincipalId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "assigneeTeamId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "requesterPrincipalId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "requesterContactId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "organizationId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "inboxId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "slaPolicyId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "firstResponseAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ], + "description": "ISO 8601 timestamp or null", + "example": "2024-01-15T10:30:00.000Z" + }, + "resolvedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ], + "description": "ISO 8601 timestamp or null", + "example": "2024-01-15T10:30:00.000Z" + }, + "reopenedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ], + "description": "ISO 8601 timestamp or null", + "example": "2024-01-15T10:30:00.000Z" + }, + "closedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ], + "description": "ISO 8601 timestamp or null", + "example": "2024-01-15T10:30:00.000Z" + }, + "lastActivityAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ], + "description": "ISO 8601 timestamp or null", + "example": "2024-01-15T10:30:00.000Z" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": [ + "id", + "subject", + "descriptionText", + "priority", + "channel", + "visibilityScope", + "statusId", + "primaryTeamId", + "assigneePrincipalId", + "assigneeTeamId", + "requesterPrincipalId", + "requesterContactId", + "organizationId", + "inboxId", + "slaPolicyId", + "firstResponseAt", + "resolvedAt", + "reopenedAt", + "closedAt", + "lastActivityAt", + "createdAt", + "updatedAt" + ], + "additionalProperties": false, + "description": "Ticket header record" + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Ticket" + } + } + } + }, + "400": { + "description": "Validation error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "VALIDATION_ERROR" + }, + "message": { + "type": "string" + }, + "details": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Request validation failed" + } + } + } + } + } + } + }, + "/tickets/{ticketId}": { + "get": { + "tags": ["Tickets"], + "summary": "Get a ticket", + "parameters": [ + { + "name": "ticketId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "responses": { + "200": { + "description": "Ticket", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "ticket_01h455vb4pex5vsknk084sn02q" + }, + "subject": { + "type": "string" + }, + "descriptionText": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "priority": { + "type": "string", + "enum": ["low", "normal", "high", "urgent"] + }, + "channel": { + "type": "string", + "enum": ["portal", "email", "api", "widget"] + }, + "visibilityScope": { + "type": "string", + "enum": ["team", "org", "shared", "private"] + }, + "statusId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "primaryTeamId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "assigneePrincipalId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "assigneeTeamId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "requesterPrincipalId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "requesterContactId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "organizationId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "inboxId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "slaPolicyId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "firstResponseAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ], + "description": "ISO 8601 timestamp or null", + "example": "2024-01-15T10:30:00.000Z" + }, + "resolvedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ], + "description": "ISO 8601 timestamp or null", + "example": "2024-01-15T10:30:00.000Z" + }, + "reopenedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ], + "description": "ISO 8601 timestamp or null", + "example": "2024-01-15T10:30:00.000Z" + }, + "closedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ], + "description": "ISO 8601 timestamp or null", + "example": "2024-01-15T10:30:00.000Z" + }, + "lastActivityAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ], + "description": "ISO 8601 timestamp or null", + "example": "2024-01-15T10:30:00.000Z" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": [ + "id", + "subject", + "descriptionText", + "priority", + "channel", + "visibilityScope", + "statusId", + "primaryTeamId", + "assigneePrincipalId", + "assigneeTeamId", + "requesterPrincipalId", + "requesterContactId", + "organizationId", + "inboxId", + "slaPolicyId", + "firstResponseAt", + "resolvedAt", + "reopenedAt", + "closedAt", + "lastActivityAt", + "createdAt", + "updatedAt" + ], + "additionalProperties": false, + "description": "Ticket header record" + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Ticket" + } + } + } + }, + "404": { + "description": "Not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "NOT_FOUND" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Resource not found" + } + } + } + } + } + }, + "patch": { + "tags": ["Tickets"], + "summary": "Update ticket header (optimistic concurrency)", + "parameters": [ + { + "name": "ticketId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "expectedUpdatedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + }, + "subject": { + "type": "string", + "minLength": 1, + "maxLength": 500 + }, + "priority": { + "type": "string", + "enum": ["low", "normal", "high", "urgent"] + }, + "visibilityScope": { + "type": "string", + "enum": ["team", "org", "shared", "private"] + }, + "primaryTeamId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "organizationId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "requesterContactId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + } + }, + "required": ["expectedUpdatedAt"], + "description": "Patch ticket request body (optimistic concurrency)" + } + } + } + }, + "responses": { + "200": { + "description": "Updated ticket", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "ticket_01h455vb4pex5vsknk084sn02q" + }, + "subject": { + "type": "string" + }, + "descriptionText": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "priority": { + "type": "string", + "enum": ["low", "normal", "high", "urgent"] + }, + "channel": { + "type": "string", + "enum": ["portal", "email", "api", "widget"] + }, + "visibilityScope": { + "type": "string", + "enum": ["team", "org", "shared", "private"] + }, + "statusId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "primaryTeamId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "assigneePrincipalId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "assigneeTeamId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "requesterPrincipalId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "requesterContactId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "organizationId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "inboxId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "slaPolicyId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "firstResponseAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ], + "description": "ISO 8601 timestamp or null", + "example": "2024-01-15T10:30:00.000Z" + }, + "resolvedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ], + "description": "ISO 8601 timestamp or null", + "example": "2024-01-15T10:30:00.000Z" + }, + "reopenedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ], + "description": "ISO 8601 timestamp or null", + "example": "2024-01-15T10:30:00.000Z" + }, + "closedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ], + "description": "ISO 8601 timestamp or null", + "example": "2024-01-15T10:30:00.000Z" + }, + "lastActivityAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ], + "description": "ISO 8601 timestamp or null", + "example": "2024-01-15T10:30:00.000Z" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": [ + "id", + "subject", + "descriptionText", + "priority", + "channel", + "visibilityScope", + "statusId", + "primaryTeamId", + "assigneePrincipalId", + "assigneeTeamId", + "requesterPrincipalId", + "requesterContactId", + "organizationId", + "inboxId", + "slaPolicyId", + "firstResponseAt", + "resolvedAt", + "reopenedAt", + "closedAt", + "lastActivityAt", + "createdAt", + "updatedAt" + ], + "additionalProperties": false, + "description": "Ticket header record" + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Ticket" + } + } + } + }, + "409": { + "description": "Conflict (stale expectedUpdatedAt)" + } + } + }, + "delete": { + "tags": ["Tickets"], + "summary": "Soft-delete a ticket", + "parameters": [ + { + "name": "ticketId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "responses": { + "204": { + "description": "Deleted" + } + } + } + }, + "/tickets/{ticketId}/threads": { + "get": { + "tags": ["Tickets"], + "summary": "List threads on a ticket (audience-filtered)", + "parameters": [ + { + "name": "ticketId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "responses": { + "200": { + "description": "Threads", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "ticket_thread_01h455vb4pex5vsknk084sn02q" + }, + "ticketId": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "principalId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "audience": { + "type": "string", + "enum": ["public", "internal", "shared_team"] + }, + "sharedWithTeamId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "bodyText": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "bodyJson": { + "anyOf": [ + {}, + { + "type": "null" + } + ] + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + }, + "editedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ], + "description": "ISO 8601 timestamp or null", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": [ + "id", + "ticketId", + "principalId", + "audience", + "sharedWithTeamId", + "bodyText", + "bodyJson", + "createdAt", + "editedAt" + ], + "additionalProperties": false, + "description": "Ticket thread (message)" + } + }, + "meta": { + "type": "object", + "properties": { + "pagination": { + "type": "object", + "properties": { + "cursor": { + "description": "Cursor for next page, null if no more pages", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "hasMore": { + "type": "boolean", + "description": "Whether there are more items" + }, + "total": { + "description": "Total count (when available)", + "type": "number" + } + }, + "required": ["hasMore"], + "additionalProperties": false + } + }, + "required": ["pagination"], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Threads" + } + } + } + } + } + }, + "post": { + "tags": ["Tickets"], + "summary": "Add a thread (public reply / internal note / shared note)", + "parameters": [ + { + "name": "ticketId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "audience": { + "type": "string", + "enum": ["public", "internal", "shared_team"] + }, + "bodyJson": { + "anyOf": [ + {}, + { + "type": "null" + } + ] + }, + "bodyText": { + "anyOf": [ + { + "type": "string", + "maxLength": 100000 + }, + { + "type": "null" + } + ] + }, + "sharedWithTeamId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + } + }, + "required": ["audience"], + "description": "Add thread to ticket request body" + } + } + } + }, + "responses": { + "201": { + "description": "Thread created", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "ticket_thread_01h455vb4pex5vsknk084sn02q" + }, + "ticketId": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "principalId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "audience": { + "type": "string", + "enum": ["public", "internal", "shared_team"] + }, + "sharedWithTeamId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "bodyText": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "bodyJson": { + "anyOf": [ + {}, + { + "type": "null" + } + ] + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + }, + "editedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ], + "description": "ISO 8601 timestamp or null", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": [ + "id", + "ticketId", + "principalId", + "audience", + "sharedWithTeamId", + "bodyText", + "bodyJson", + "createdAt", + "editedAt" + ], + "additionalProperties": false, + "description": "Ticket thread (message)" + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Thread" + } + } + } + } + } + } + }, + "/tickets/{ticketId}/participants": { + "get": { + "tags": ["Tickets"], + "summary": "List participants", + "parameters": [ + { + "name": "ticketId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "responses": { + "200": { + "description": "Participants", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "ticketId": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "principalId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "contactId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "role": { + "type": "string", + "enum": ["watcher", "collaborator", "cc"] + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": [ + "id", + "ticketId", + "principalId", + "contactId", + "role", + "createdAt" + ], + "additionalProperties": false, + "description": "Ticket participant" + } + }, + "meta": { + "type": "object", + "properties": { + "pagination": { + "type": "object", + "properties": { + "cursor": { + "description": "Cursor for next page, null if no more pages", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "hasMore": { + "type": "boolean", + "description": "Whether there are more items" + }, + "total": { + "description": "Total count (when available)", + "type": "number" + } + }, + "required": ["hasMore"], + "additionalProperties": false + } + }, + "required": ["pagination"], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Participants" + } + } + } + } + } + }, + "post": { + "tags": ["Tickets"], + "summary": "Add a participant", + "parameters": [ + { + "name": "ticketId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "role": { + "type": "string", + "enum": ["watcher", "collaborator", "cc"] + }, + "principalId": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "contactId": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + } + }, + "required": ["role"] + } + } + } + }, + "responses": { + "201": { + "description": "Participant added" + } + } + } + }, + "/tickets/{ticketId}/shares": { + "get": { + "tags": ["Tickets"], + "summary": "List share grants", + "parameters": [ + { + "name": "ticketId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "responses": { + "200": { + "description": "Shares", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "ticketId": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "teamId": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "accessLevel": { + "type": "string", + "enum": ["read", "comment", "full"] + }, + "grantedByPrincipalId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "grantedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + }, + "revokedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ], + "description": "ISO 8601 timestamp or null", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": [ + "id", + "ticketId", + "teamId", + "accessLevel", + "grantedByPrincipalId", + "grantedAt", + "revokedAt" + ], + "additionalProperties": false, + "description": "Cross-team share grant" + } + }, + "meta": { + "type": "object", + "properties": { + "pagination": { + "type": "object", + "properties": { + "cursor": { + "description": "Cursor for next page, null if no more pages", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "hasMore": { + "type": "boolean", + "description": "Whether there are more items" + }, + "total": { + "description": "Total count (when available)", + "type": "number" + } + }, + "required": ["hasMore"], + "additionalProperties": false + } + }, + "required": ["pagination"], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Shares" + } + } + } + } + } + }, + "post": { + "tags": ["Tickets"], + "summary": "Share ticket with another team", + "parameters": [ + { + "name": "ticketId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "teamId": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "accessLevel": { + "default": "read", + "type": "string", + "enum": ["read", "comment", "full"] + } + }, + "required": ["teamId"] + } + } + } + }, + "responses": { + "201": { + "description": "Share created" + } + } + } + }, + "/tickets/{ticketId}/take": { + "post": { + "tags": ["Tickets"], + "summary": "Take (self-assign) a ticket", + "parameters": [ + { + "name": "ticketId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "expectedUpdatedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": ["expectedUpdatedAt"] + } + } + } + }, + "responses": { + "200": { + "description": "Ticket taken" + } + } + } + }, + "/tickets/{ticketId}/return": { + "post": { + "tags": ["Tickets"], + "summary": "Return (un-self-assign) a ticket", + "parameters": [ + { + "name": "ticketId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "responses": { + "200": { + "description": "Ticket returned" + } + } + } + }, + "/tickets/{ticketId}/sla": { + "get": { + "tags": ["Tickets"], + "summary": "Get active SLA clocks for a ticket", + "parameters": [ + { + "name": "ticketId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "responses": { + "200": { + "description": "Clocks", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "ticketId": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "kind": { + "type": "string", + "enum": ["first_response", "next_response", "resolution"] + }, + "state": { + "type": "string", + "enum": ["running", "paused", "met", "breached", "cancelled"] + }, + "startedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + }, + "dueAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + }, + "pausedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ], + "description": "ISO 8601 timestamp or null", + "example": "2024-01-15T10:30:00.000Z" + }, + "breachedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ], + "description": "ISO 8601 timestamp or null", + "example": "2024-01-15T10:30:00.000Z" + }, + "metAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ], + "description": "ISO 8601 timestamp or null", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": [ + "id", + "ticketId", + "kind", + "state", + "startedAt", + "dueAt", + "pausedAt", + "breachedAt", + "metAt" + ], + "additionalProperties": false, + "description": "Per-ticket SLA clock" + } + }, + "meta": { + "type": "object", + "properties": { + "pagination": { + "type": "object", + "properties": { + "cursor": { + "description": "Cursor for next page, null if no more pages", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "hasMore": { + "type": "boolean", + "description": "Whether there are more items" + }, + "total": { + "description": "Total count (when available)", + "type": "number" + } + }, + "required": ["hasMore"], + "additionalProperties": false + } + }, + "required": ["pagination"], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Clocks" + } + } + } + } + } + } + }, + "/tickets/bulk/assign": { + "post": { + "tags": ["Tickets"], + "summary": "Bulk-assign tickets (best-effort)", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "ticketIds": { + "minItems": 1, + "maxItems": 500, + "type": "array", + "items": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + } + }, + "assigneePrincipalId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + } + }, + "required": ["ticketIds", "assigneePrincipalId"] + } + } + } + }, + "responses": { + "200": { + "description": "Per-ticket result", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "succeeded": { + "type": "array", + "items": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + } + }, + "failed": { + "type": "array", + "items": { + "type": "object", + "properties": { + "ticketId": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "reason": { + "type": "string" + } + }, + "required": ["ticketId", "reason"], + "additionalProperties": false + } + } + }, + "required": ["succeeded", "failed"], + "additionalProperties": false, + "description": "Best-effort bulk operation result" + } + } + } + } + } + } + }, + "/tickets/bulk/transition": { + "post": { + "tags": ["Tickets"], + "summary": "Bulk-transition ticket statuses (best-effort)", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "ticketIds": { + "minItems": 1, + "maxItems": 500, + "type": "array", + "items": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + } + }, + "statusId": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + } + }, + "required": ["ticketIds", "statusId"] + } + } + } + }, + "responses": { + "200": { + "description": "Per-ticket result" + } + } + } + }, + "/tickets/bulk/change-inbox": { + "post": { + "tags": ["Tickets"], + "summary": "Bulk-move tickets to a different inbox (best-effort)", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "ticketIds": { + "minItems": 1, + "maxItems": 500, + "type": "array", + "items": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + } + }, + "inboxId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + } + }, + "required": ["ticketIds", "inboxId"] + } + } + } + }, + "responses": { + "200": { + "description": "Per-ticket result" + } + } + } + }, + "/tickets/{ticketId}/restore": { + "post": { + "tags": ["Tickets"], + "summary": "Restore a soft-deleted ticket (admin)", + "description": "Pairs with `DELETE /tickets/{ticketId}`. Admin-only. Returns 409 if the ticket is not deleted.", + "parameters": [ + { + "name": "ticketId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "responses": { + "200": { + "description": "Ticket restored", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "ticket_01h455vb4pex5vsknk084sn02q" + }, + "subject": { + "type": "string" + }, + "descriptionText": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "priority": { + "type": "string", + "enum": ["low", "normal", "high", "urgent"] + }, + "channel": { + "type": "string", + "enum": ["portal", "email", "api", "widget"] + }, + "visibilityScope": { + "type": "string", + "enum": ["team", "org", "shared", "private"] + }, + "statusId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "primaryTeamId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "assigneePrincipalId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "assigneeTeamId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "requesterPrincipalId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "requesterContactId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "organizationId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "inboxId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "slaPolicyId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "firstResponseAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ], + "description": "ISO 8601 timestamp or null", + "example": "2024-01-15T10:30:00.000Z" + }, + "resolvedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ], + "description": "ISO 8601 timestamp or null", + "example": "2024-01-15T10:30:00.000Z" + }, + "reopenedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ], + "description": "ISO 8601 timestamp or null", + "example": "2024-01-15T10:30:00.000Z" + }, + "closedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ], + "description": "ISO 8601 timestamp or null", + "example": "2024-01-15T10:30:00.000Z" + }, + "lastActivityAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ], + "description": "ISO 8601 timestamp or null", + "example": "2024-01-15T10:30:00.000Z" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": [ + "id", + "subject", + "descriptionText", + "priority", + "channel", + "visibilityScope", + "statusId", + "primaryTeamId", + "assigneePrincipalId", + "assigneeTeamId", + "requesterPrincipalId", + "requesterContactId", + "organizationId", + "inboxId", + "slaPolicyId", + "firstResponseAt", + "resolvedAt", + "reopenedAt", + "closedAt", + "lastActivityAt", + "createdAt", + "updatedAt" + ], + "additionalProperties": false, + "description": "Ticket header record" + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Ticket" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + }, + "404": { + "description": "Ticket not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "NOT_FOUND" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Resource not found" + } + } + } + }, + "409": { + "description": "Ticket is not deleted" + } + } + } + }, + "/tickets/{ticketId}/activity": { + "get": { + "tags": ["Tickets"], + "summary": "List ticket activity (timeline)", + "description": "Reverse-chronological feed of all activity events on the ticket. Use `before` (ISO timestamp) for pagination.", + "parameters": [ + { + "name": "ticketId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + }, + { + "name": "before", + "in": "query", + "required": false, + "schema": { + "type": "string", + "format": "date-time" + }, + "description": "Return rows strictly older than this timestamp" + }, + { + "name": "limit", + "in": "query", + "required": false, + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 200 + }, + "description": "Default 50, max 200" + } + ], + "responses": { + "200": { + "description": "Activity timeline page", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "activity": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "ticketId": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "principalId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "type": { + "type": "string", + "description": "Event type, e.g. ticket.created, ticket.status_changed, thread.added" + }, + "metadata": {}, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + }, + "actorName": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "actorAvatarUrl": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "id", + "ticketId", + "principalId", + "type", + "metadata", + "createdAt", + "actorName", + "actorAvatarUrl" + ], + "additionalProperties": false, + "description": "Single ticket-activity event" + } + }, + "nextCursor": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "ISO timestamp cursor for the next page; null when none" + } + }, + "required": ["activity", "nextCursor"], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Ticket activity timeline response" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + }, + "404": { + "description": "Ticket not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "NOT_FOUND" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Resource not found" + } + } + } + } + } + } + }, + "/tickets/{ticketId}/threads/{threadId}": { + "get": { + "tags": ["Tickets"], + "summary": "Get a single thread", + "parameters": [ + { + "name": "ticketId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + }, + { + "name": "threadId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "responses": { + "200": { + "description": "Thread detail", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "ticket_thread_01h455vb4pex5vsknk084sn02q" + }, + "ticketId": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "principalId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "audience": { + "type": "string", + "enum": ["public", "internal", "shared_team"] + }, + "sharedWithTeamId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "bodyText": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "bodyJson": { + "anyOf": [ + {}, + { + "type": "null" + } + ] + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + }, + "editedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ], + "description": "ISO 8601 timestamp or null", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": [ + "id", + "ticketId", + "principalId", + "audience", + "sharedWithTeamId", + "bodyText", + "bodyJson", + "createdAt", + "editedAt" + ], + "additionalProperties": false, + "description": "Ticket thread (message)" + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Thread" + } + } + } + }, + "404": { + "description": "Ticket or thread not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "NOT_FOUND" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Resource not found" + } + } + } + } + } + }, + "patch": { + "tags": ["Tickets"], + "summary": "Edit a thread (author only)", + "description": "Only the original author may edit. Stamps `editedAt` and `editedByPrincipalId`.", + "parameters": [ + { + "name": "ticketId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + }, + { + "name": "threadId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "bodyJson": { + "anyOf": [ + {}, + { + "type": "null" + } + ] + }, + "bodyText": { + "anyOf": [ + { + "type": "string", + "maxLength": 100000 + }, + { + "type": "null" + } + ] + } + }, + "description": "Edit thread body — author only. Provide bodyJson or bodyText." + } + } + } + }, + "responses": { + "200": { + "description": "Thread updated", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "ticket_thread_01h455vb4pex5vsknk084sn02q" + }, + "ticketId": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "principalId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "audience": { + "type": "string", + "enum": ["public", "internal", "shared_team"] + }, + "sharedWithTeamId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "bodyText": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "bodyJson": { + "anyOf": [ + {}, + { + "type": "null" + } + ] + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + }, + "editedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ], + "description": "ISO 8601 timestamp or null", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": [ + "id", + "ticketId", + "principalId", + "audience", + "sharedWithTeamId", + "bodyText", + "bodyJson", + "createdAt", + "editedAt" + ], + "additionalProperties": false, + "description": "Ticket thread (message)" + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Thread" + } + } + } + }, + "400": { + "description": "Validation error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "VALIDATION_ERROR" + }, + "message": { + "type": "string" + }, + "details": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Request validation failed" + } + } + } + }, + "403": { + "description": "Not the author" + }, + "404": { + "description": "Ticket or thread not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "NOT_FOUND" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Resource not found" + } + } + } + } + } + }, + "delete": { + "tags": ["Tickets"], + "summary": "Soft-delete a thread", + "description": "Author or any caller with `ticket.edit_fields` may soft-delete. The row is marked `deletedAt`.", + "parameters": [ + { + "name": "ticketId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + }, + { + "name": "threadId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "responses": { + "204": { + "description": "Deleted" + }, + "403": { + "description": "Not the author and lacks ticket.edit_fields" + }, + "404": { + "description": "Ticket or thread not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "NOT_FOUND" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Resource not found" + } + } + } + } + } + } + }, + "/tickets/{ticketId}/threads/{threadId}/attachments": { + "get": { + "tags": ["Tickets"], + "summary": "List attachments on a thread", + "parameters": [ + { + "name": "ticketId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + }, + { + "name": "threadId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "responses": { + "200": { + "description": "Attachments", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "ticket_att_01h455vb4pex5vsknk084sn02q" + }, + "threadId": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "uploadedByPrincipalId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "filename": { + "type": "string" + }, + "mimeType": { + "type": "string" + }, + "sizeBytes": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + }, + "storageKey": { + "type": "string" + }, + "publicUrl": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": [ + "id", + "threadId", + "uploadedByPrincipalId", + "filename", + "mimeType", + "sizeBytes", + "storageKey", + "publicUrl", + "createdAt" + ], + "additionalProperties": false, + "description": "Ticket attachment metadata" + } + }, + "meta": { + "type": "object", + "properties": { + "pagination": { + "type": "object", + "properties": { + "cursor": { + "description": "Cursor for next page, null if no more pages", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "hasMore": { + "type": "boolean", + "description": "Whether there are more items" + }, + "total": { + "description": "Total count (when available)", + "type": "number" + } + }, + "required": ["hasMore"], + "additionalProperties": false + } + }, + "required": ["pagination"], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Attachments" + } + } + } + }, + "404": { + "description": "Ticket or thread not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "NOT_FOUND" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Resource not found" + } + } + } + } + } + }, + "post": { + "tags": ["Tickets"], + "summary": "Upload an attachment to a thread (multipart)", + "description": "multipart/form-data with a `file` field. Image MIME types only (jpeg/png/gif/webp/avif), 5 MB cap.", + "parameters": [ + { + "name": "ticketId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + }, + { + "name": "threadId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "file": { + "type": "string", + "format": "binary" + } + }, + "required": ["file"] + } + } + } + }, + "responses": { + "201": { + "description": "Attachment created", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "ticket_att_01h455vb4pex5vsknk084sn02q" + }, + "threadId": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "uploadedByPrincipalId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "filename": { + "type": "string" + }, + "mimeType": { + "type": "string" + }, + "sizeBytes": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + }, + "storageKey": { + "type": "string" + }, + "publicUrl": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": [ + "id", + "threadId", + "uploadedByPrincipalId", + "filename", + "mimeType", + "sizeBytes", + "storageKey", + "publicUrl", + "createdAt" + ], + "additionalProperties": false, + "description": "Ticket attachment metadata" + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Attachment" + } + } + } + }, + "400": { + "description": "Invalid upload", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "VALIDATION_ERROR" + }, + "message": { + "type": "string" + }, + "details": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Request validation failed" + } + } + } + }, + "404": { + "description": "Ticket or thread not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "NOT_FOUND" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Resource not found" + } + } + } + } + } + } + }, + "/tickets/{ticketId}/threads/{threadId}/attachments/{attachmentId}": { + "get": { + "tags": ["Tickets"], + "summary": "Get a single attachment", + "parameters": [ + { + "name": "ticketId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + }, + { + "name": "threadId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + }, + { + "name": "attachmentId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "responses": { + "200": { + "description": "Attachment detail", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "ticket_att_01h455vb4pex5vsknk084sn02q" + }, + "threadId": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "uploadedByPrincipalId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "filename": { + "type": "string" + }, + "mimeType": { + "type": "string" + }, + "sizeBytes": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + }, + "storageKey": { + "type": "string" + }, + "publicUrl": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": [ + "id", + "threadId", + "uploadedByPrincipalId", + "filename", + "mimeType", + "sizeBytes", + "storageKey", + "publicUrl", + "createdAt" + ], + "additionalProperties": false, + "description": "Ticket attachment metadata" + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Attachment" + } + } + } + }, + "404": { + "description": "Ticket / thread / attachment not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "NOT_FOUND" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Resource not found" + } + } + } + } + } + }, + "delete": { + "tags": ["Tickets"], + "summary": "Delete an attachment", + "description": "Removes the metadata row. Allowed for the original uploader or any caller with `ticket.edit_fields`.", + "parameters": [ + { + "name": "ticketId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + }, + { + "name": "threadId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + }, + { + "name": "attachmentId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "responses": { + "204": { + "description": "Deleted" + }, + "403": { + "description": "Not the uploader and lacks ticket.edit_fields" + }, + "404": { + "description": "Ticket / thread / attachment not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "NOT_FOUND" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Resource not found" + } + } + } + } + } + } + }, + "/tickets/{ticketId}/assign": { + "post": { + "tags": ["Tickets"], + "summary": "Assign a ticket", + "description": "Assign to an agent (assigneePrincipalId) and/or a team (assigneeTeamId). Requires `ticket.assign_any` or `ticket.assign_self` if assigning to self.", + "parameters": [ + { + "name": "ticketId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "expectedUpdatedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + "assigneePrincipalId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "assigneeTeamId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + } + }, + "required": ["expectedUpdatedAt"] + } + } + } + }, + "responses": { + "200": { + "description": "Ticket assigned", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "ticket_01h455vb4pex5vsknk084sn02q" + }, + "subject": { + "type": "string" + }, + "descriptionText": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "priority": { + "type": "string", + "enum": ["low", "normal", "high", "urgent"] + }, + "channel": { + "type": "string", + "enum": ["portal", "email", "api", "widget"] + }, + "visibilityScope": { + "type": "string", + "enum": ["team", "org", "shared", "private"] + }, + "statusId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "primaryTeamId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "assigneePrincipalId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "assigneeTeamId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "requesterPrincipalId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "requesterContactId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "organizationId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "inboxId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "slaPolicyId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "firstResponseAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ], + "description": "ISO 8601 timestamp or null", + "example": "2024-01-15T10:30:00.000Z" + }, + "resolvedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ], + "description": "ISO 8601 timestamp or null", + "example": "2024-01-15T10:30:00.000Z" + }, + "reopenedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ], + "description": "ISO 8601 timestamp or null", + "example": "2024-01-15T10:30:00.000Z" + }, + "closedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ], + "description": "ISO 8601 timestamp or null", + "example": "2024-01-15T10:30:00.000Z" + }, + "lastActivityAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ], + "description": "ISO 8601 timestamp or null", + "example": "2024-01-15T10:30:00.000Z" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": [ + "id", + "subject", + "descriptionText", + "priority", + "channel", + "visibilityScope", + "statusId", + "primaryTeamId", + "assigneePrincipalId", + "assigneeTeamId", + "requesterPrincipalId", + "requesterContactId", + "organizationId", + "inboxId", + "slaPolicyId", + "firstResponseAt", + "resolvedAt", + "reopenedAt", + "closedAt", + "lastActivityAt", + "createdAt", + "updatedAt" + ], + "additionalProperties": false, + "description": "Ticket header record" + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Ticket" + } + } + } + }, + "400": { + "description": "Validation error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "VALIDATION_ERROR" + }, + "message": { + "type": "string" + }, + "details": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Request validation failed" + } + } + } + }, + "403": { + "description": "Insufficient permissions" + }, + "404": { + "description": "Ticket not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "NOT_FOUND" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Resource not found" + } + } + } + } + } + } + }, + "/tickets/{ticketId}/transition": { + "post": { + "tags": ["Tickets"], + "summary": "Transition ticket status", + "description": "Move the ticket to a new status. Sets lifecycle timestamps based on the destination category. Requires `ticket.edit_fields`.", + "parameters": [ + { + "name": "ticketId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "expectedUpdatedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + "statusId": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + } + }, + "required": ["expectedUpdatedAt", "statusId"] + } + } + } + }, + "responses": { + "200": { + "description": "Ticket transitioned", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "ticket_01h455vb4pex5vsknk084sn02q" + }, + "subject": { + "type": "string" + }, + "descriptionText": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "priority": { + "type": "string", + "enum": ["low", "normal", "high", "urgent"] + }, + "channel": { + "type": "string", + "enum": ["portal", "email", "api", "widget"] + }, + "visibilityScope": { + "type": "string", + "enum": ["team", "org", "shared", "private"] + }, + "statusId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "primaryTeamId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "assigneePrincipalId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "assigneeTeamId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "requesterPrincipalId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "requesterContactId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "organizationId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "inboxId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "slaPolicyId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "firstResponseAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ], + "description": "ISO 8601 timestamp or null", + "example": "2024-01-15T10:30:00.000Z" + }, + "resolvedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ], + "description": "ISO 8601 timestamp or null", + "example": "2024-01-15T10:30:00.000Z" + }, + "reopenedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ], + "description": "ISO 8601 timestamp or null", + "example": "2024-01-15T10:30:00.000Z" + }, + "closedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ], + "description": "ISO 8601 timestamp or null", + "example": "2024-01-15T10:30:00.000Z" + }, + "lastActivityAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ], + "description": "ISO 8601 timestamp or null", + "example": "2024-01-15T10:30:00.000Z" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": [ + "id", + "subject", + "descriptionText", + "priority", + "channel", + "visibilityScope", + "statusId", + "primaryTeamId", + "assigneePrincipalId", + "assigneeTeamId", + "requesterPrincipalId", + "requesterContactId", + "organizationId", + "inboxId", + "slaPolicyId", + "firstResponseAt", + "resolvedAt", + "reopenedAt", + "closedAt", + "lastActivityAt", + "createdAt", + "updatedAt" + ], + "additionalProperties": false, + "description": "Ticket header record" + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Ticket" + } + } + } + }, + "400": { + "description": "Validation error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "VALIDATION_ERROR" + }, + "message": { + "type": "string" + }, + "details": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Request validation failed" + } + } + } + }, + "403": { + "description": "ticket.edit_fields required" + }, + "404": { + "description": "Ticket not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "NOT_FOUND" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Resource not found" + } + } + } + } + } + } + }, + "/tickets/{ticketId}/shares/{shareId}": { + "delete": { + "tags": ["Tickets"], + "summary": "Revoke a share", + "description": "Revoke a cross-team share grant. Requires `ticket.share_cross_team`.", + "parameters": [ + { + "name": "ticketId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + }, + { + "name": "shareId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "responses": { + "204": { + "description": "Share revoked" + }, + "403": { + "description": "ticket.share_cross_team required" + }, + "404": { + "description": "Ticket or share not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "NOT_FOUND" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Resource not found" + } + } + } + } + } + } + }, + "/tickets/{ticketId}/participants/{participantId}": { + "delete": { + "tags": ["Tickets"], + "summary": "Remove a participant", + "description": "Remove a watcher/collaborator/CC from the ticket. Requires `ticket.manage_participants`.", + "parameters": [ + { + "name": "ticketId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + }, + { + "name": "participantId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "responses": { + "204": { + "description": "Participant removed" + }, + "403": { + "description": "ticket.manage_participants required" + }, + "404": { + "description": "Ticket or participant not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "NOT_FOUND" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Resource not found" + } + } + } + } + } + } + }, + "/ticket-statuses": { + "get": { + "tags": ["Ticket Statuses"], + "summary": "List ticket statuses", + "description": "Returns the workspace ticket-status catalogue, ordered by position.", + "parameters": [ + { + "name": "includeDeleted", + "in": "query", + "required": false, + "schema": { + "type": "boolean" + }, + "description": "Include archived statuses" + } + ], + "responses": { + "200": { + "description": "List of ticket statuses", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "ticket_status_01h455vb4pex5vsknk084sn02q" + }, + "name": { + "type": "string" + }, + "slug": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "description": "URL-friendly identifier", + "example": "feature-requests" + }, + "color": { + "anyOf": [ + { + "type": "string", + "pattern": "^#[0-9a-fA-F]{6}$", + "description": "Hex color code", + "example": "#3b82f6" + }, + { + "type": "null" + } + ] + }, + "category": { + "type": "string", + "enum": ["open", "pending", "on_hold", "solved", "closed"] + }, + "position": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + }, + "isDefault": { + "type": "boolean" + }, + "isSystem": { + "type": "boolean", + "description": "System statuses cannot be archived" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + }, + "deletedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ], + "description": "ISO 8601 timestamp or null", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": [ + "id", + "name", + "slug", + "color", + "category", + "position", + "isDefault", + "isSystem", + "createdAt", + "deletedAt" + ], + "additionalProperties": false, + "description": "Ticket workflow status (workflow state)" + } + }, + "meta": { + "type": "object", + "properties": { + "pagination": { + "type": "object", + "properties": { + "cursor": { + "description": "Cursor for next page, null if no more pages", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "hasMore": { + "type": "boolean", + "description": "Whether there are more items" + }, + "total": { + "description": "Total count (when available)", + "type": "number" + } + }, + "required": ["hasMore"], + "additionalProperties": false + } + }, + "required": ["pagination"], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Statuses" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + } + } + }, + "post": { + "tags": ["Ticket Statuses"], + "summary": "Create a ticket status (admin)", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 50 + }, + "slug": { + "type": "string", + "minLength": 1, + "maxLength": 50, + "pattern": "^[a-z0-9_-]+$" + }, + "color": { + "type": "string", + "pattern": "^#[0-9a-fA-F]{6}$", + "description": "Hex color code", + "example": "#3b82f6" + }, + "category": { + "type": "string", + "enum": ["open", "pending", "on_hold", "solved", "closed"] + }, + "position": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "isDefault": { + "type": "boolean" + } + }, + "required": ["name", "slug", "category"], + "description": "Create ticket-status request body" + } + } + } + }, + "responses": { + "201": { + "description": "Status created", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "ticket_status_01h455vb4pex5vsknk084sn02q" + }, + "name": { + "type": "string" + }, + "slug": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "description": "URL-friendly identifier", + "example": "feature-requests" + }, + "color": { + "anyOf": [ + { + "type": "string", + "pattern": "^#[0-9a-fA-F]{6}$", + "description": "Hex color code", + "example": "#3b82f6" + }, + { + "type": "null" + } + ] + }, + "category": { + "type": "string", + "enum": ["open", "pending", "on_hold", "solved", "closed"] + }, + "position": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + }, + "isDefault": { + "type": "boolean" + }, + "isSystem": { + "type": "boolean", + "description": "System statuses cannot be archived" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + }, + "deletedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ], + "description": "ISO 8601 timestamp or null", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": [ + "id", + "name", + "slug", + "color", + "category", + "position", + "isDefault", + "isSystem", + "createdAt", + "deletedAt" + ], + "additionalProperties": false, + "description": "Ticket workflow status (workflow state)" + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Status" + } + } + } + }, + "400": { + "description": "Validation error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "VALIDATION_ERROR" + }, + "message": { + "type": "string" + }, + "details": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Request validation failed" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + }, + "409": { + "description": "Slug already in use" + } + } + } + }, + "/ticket-statuses/{statusId}": { + "get": { + "tags": ["Ticket Statuses"], + "summary": "Get a ticket status", + "parameters": [ + { + "name": "statusId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "responses": { + "200": { + "description": "Status", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "ticket_status_01h455vb4pex5vsknk084sn02q" + }, + "name": { + "type": "string" + }, + "slug": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "description": "URL-friendly identifier", + "example": "feature-requests" + }, + "color": { + "anyOf": [ + { + "type": "string", + "pattern": "^#[0-9a-fA-F]{6}$", + "description": "Hex color code", + "example": "#3b82f6" + }, + { + "type": "null" + } + ] + }, + "category": { + "type": "string", + "enum": ["open", "pending", "on_hold", "solved", "closed"] + }, + "position": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + }, + "isDefault": { + "type": "boolean" + }, + "isSystem": { + "type": "boolean", + "description": "System statuses cannot be archived" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + }, + "deletedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ], + "description": "ISO 8601 timestamp or null", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": [ + "id", + "name", + "slug", + "color", + "category", + "position", + "isDefault", + "isSystem", + "createdAt", + "deletedAt" + ], + "additionalProperties": false, + "description": "Ticket workflow status (workflow state)" + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Status" + } + } + } + }, + "404": { + "description": "Status not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "NOT_FOUND" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Resource not found" + } + } + } + } + } + }, + "patch": { + "tags": ["Ticket Statuses"], + "summary": "Update a ticket status (admin)", + "parameters": [ + { + "name": "statusId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 50 + }, + "color": { + "type": "string", + "pattern": "^#[0-9a-fA-F]{6}$", + "description": "Hex color code", + "example": "#3b82f6" + }, + "category": { + "type": "string", + "enum": ["open", "pending", "on_hold", "solved", "closed"] + }, + "position": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "isDefault": { + "type": "boolean" + } + }, + "description": "Update ticket-status request body" + } + } + } + }, + "responses": { + "200": { + "description": "Status updated", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "ticket_status_01h455vb4pex5vsknk084sn02q" + }, + "name": { + "type": "string" + }, + "slug": { + "type": "string", + "minLength": 1, + "maxLength": 100, + "description": "URL-friendly identifier", + "example": "feature-requests" + }, + "color": { + "anyOf": [ + { + "type": "string", + "pattern": "^#[0-9a-fA-F]{6}$", + "description": "Hex color code", + "example": "#3b82f6" + }, + { + "type": "null" + } + ] + }, + "category": { + "type": "string", + "enum": ["open", "pending", "on_hold", "solved", "closed"] + }, + "position": { + "type": "integer", + "minimum": -9007199254740991, + "maximum": 9007199254740991 + }, + "isDefault": { + "type": "boolean" + }, + "isSystem": { + "type": "boolean", + "description": "System statuses cannot be archived" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + }, + "deletedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ], + "description": "ISO 8601 timestamp or null", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": [ + "id", + "name", + "slug", + "color", + "category", + "position", + "isDefault", + "isSystem", + "createdAt", + "deletedAt" + ], + "additionalProperties": false, + "description": "Ticket workflow status (workflow state)" + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Status" + } + } + } + }, + "400": { + "description": "Validation error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "VALIDATION_ERROR" + }, + "message": { + "type": "string" + }, + "details": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Request validation failed" + } + } + } + }, + "404": { + "description": "Status not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "NOT_FOUND" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Resource not found" + } + } + } + } + } + }, + "delete": { + "tags": ["Ticket Statuses"], + "summary": "Archive a ticket status (admin)", + "description": "Soft-archives the status (sets `deletedAt`). Returns 409 if any active ticket still references the status, and rejects with 400 for system statuses.", + "parameters": [ + { + "name": "statusId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "responses": { + "204": { + "description": "Archived" + }, + "400": { + "description": "System status — cannot archive", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "VALIDATION_ERROR" + }, + "message": { + "type": "string" + }, + "details": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Request validation failed" + } + } + } + }, + "404": { + "description": "Status not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "NOT_FOUND" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Resource not found" + } + } + } + }, + "409": { + "description": "Status is still referenced by active tickets" + } + } + } + }, + "/inboxes": { + "get": { + "tags": ["Support Config"], + "summary": "List inboxes", + "responses": { + "200": { + "description": "Inboxes", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "inbox_01h455vb4pex5vsknk084sn02q" + }, + "name": { + "type": "string" + }, + "slug": { + "type": "string" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "primaryTeamId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "defaultVisibilityScope": { + "type": "string", + "enum": ["team", "org", "shared", "private"] + }, + "defaultPriority": { + "type": "string", + "enum": ["low", "normal", "high", "urgent"] + }, + "defaultStatusId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "color": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "icon": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "archivedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ], + "description": "ISO 8601 timestamp or null", + "example": "2024-01-15T10:30:00.000Z" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": [ + "id", + "name", + "slug", + "description", + "primaryTeamId", + "defaultVisibilityScope", + "defaultPriority", + "defaultStatusId", + "color", + "icon", + "archivedAt", + "createdAt", + "updatedAt" + ], + "additionalProperties": false + } + }, + "meta": { + "type": "object", + "properties": { + "pagination": { + "type": "object", + "properties": { + "cursor": { + "description": "Cursor for next page, null if no more pages", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "hasMore": { + "type": "boolean", + "description": "Whether there are more items" + }, + "total": { + "description": "Total count (when available)", + "type": "number" + } + }, + "required": ["hasMore"], + "additionalProperties": false + } + }, + "required": ["pagination"], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Inboxes" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + } + } + }, + "post": { + "tags": ["Support Config"], + "summary": "Create an inbox", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + "slug": { + "type": "string", + "pattern": "^[a-z0-9-]+$" + }, + "description": { + "type": "string", + "maxLength": 500 + }, + "primaryTeamId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "defaultVisibilityScope": { + "type": "string", + "enum": ["team", "org", "shared", "private"] + }, + "defaultPriority": { + "type": "string", + "enum": ["low", "normal", "high", "urgent"] + }, + "color": { + "type": "string" + }, + "icon": { + "type": "string" + } + }, + "required": ["name", "slug"] + } + } + } + }, + "responses": { + "201": { + "description": "Inbox created", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "inbox_01h455vb4pex5vsknk084sn02q" + }, + "name": { + "type": "string" + }, + "slug": { + "type": "string" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "primaryTeamId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "defaultVisibilityScope": { + "type": "string", + "enum": ["team", "org", "shared", "private"] + }, + "defaultPriority": { + "type": "string", + "enum": ["low", "normal", "high", "urgent"] + }, + "defaultStatusId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "color": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "icon": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "archivedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ], + "description": "ISO 8601 timestamp or null", + "example": "2024-01-15T10:30:00.000Z" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": [ + "id", + "name", + "slug", + "description", + "primaryTeamId", + "defaultVisibilityScope", + "defaultPriority", + "defaultStatusId", + "color", + "icon", + "archivedAt", + "createdAt", + "updatedAt" + ], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Inbox" + } + } + } + } + } + } + }, + "/inboxes/{inboxId}": { + "get": { + "tags": ["Support Config"], + "summary": "Get an inbox", + "parameters": [ + { + "name": "inboxId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "responses": { + "200": { + "description": "Inbox", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "inbox_01h455vb4pex5vsknk084sn02q" + }, + "name": { + "type": "string" + }, + "slug": { + "type": "string" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "primaryTeamId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "defaultVisibilityScope": { + "type": "string", + "enum": ["team", "org", "shared", "private"] + }, + "defaultPriority": { + "type": "string", + "enum": ["low", "normal", "high", "urgent"] + }, + "defaultStatusId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "color": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "icon": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "archivedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ], + "description": "ISO 8601 timestamp or null", + "example": "2024-01-15T10:30:00.000Z" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": [ + "id", + "name", + "slug", + "description", + "primaryTeamId", + "defaultVisibilityScope", + "defaultPriority", + "defaultStatusId", + "color", + "icon", + "archivedAt", + "createdAt", + "updatedAt" + ], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Inbox" + } + } + } + } + } + }, + "patch": { + "tags": ["Support Config"], + "summary": "Update an inbox", + "parameters": [ + { + "name": "inboxId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "responses": { + "200": { + "description": "Updated" + } + } + }, + "delete": { + "tags": ["Support Config"], + "summary": "Archive an inbox", + "parameters": [ + { + "name": "inboxId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "responses": { + "204": { + "description": "Archived" + } + } + } + }, + "/inboxes/{inboxId}/members": { + "get": { + "tags": ["Support Config"], + "summary": "List inbox members", + "parameters": [ + { + "name": "inboxId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "responses": { + "200": { + "description": "Members" + } + } + }, + "post": { + "tags": ["Support Config"], + "summary": "Add inbox member", + "parameters": [ + { + "name": "inboxId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "responses": { + "201": { + "description": "Added" + } + } + } + }, + "/inboxes/{inboxId}/channels": { + "get": { + "tags": ["Support Config"], + "summary": "List channels", + "parameters": [ + { + "name": "inboxId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "responses": { + "200": { + "description": "Channels" + } + } + }, + "post": { + "tags": ["Support Config"], + "summary": "Create a channel", + "parameters": [ + { + "name": "inboxId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "responses": { + "201": { + "description": "Created" + } + } + } + }, + "/routing-rules": { + "get": { + "tags": ["Routing"], + "summary": "List routing rules (priority-ordered)", + "responses": { + "200": { + "description": "Rules", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "route_rule_01h455vb4pex5vsknk084sn02q" + }, + "name": { + "type": "string" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "priority": { + "type": "number" + }, + "enabled": { + "type": "boolean" + }, + "conditions": {}, + "actions": {}, + "inboxIdScope": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "matchCount": { + "type": "number" + }, + "lastMatchedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ], + "description": "ISO 8601 timestamp or null", + "example": "2024-01-15T10:30:00.000Z" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": [ + "id", + "name", + "description", + "priority", + "enabled", + "conditions", + "actions", + "inboxIdScope", + "matchCount", + "lastMatchedAt", + "createdAt", + "updatedAt" + ], + "additionalProperties": false + } + }, + "meta": { + "type": "object", + "properties": { + "pagination": { + "type": "object", + "properties": { + "cursor": { + "description": "Cursor for next page, null if no more pages", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "hasMore": { + "type": "boolean", + "description": "Whether there are more items" + }, + "total": { + "description": "Total count (when available)", + "type": "number" + } + }, + "required": ["hasMore"], + "additionalProperties": false + } + }, + "required": ["pagination"], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Rules" + } + } + } + } + } + }, + "post": { + "tags": ["Routing"], + "summary": "Create a routing rule", + "responses": { + "201": { + "description": "Created" + } + } + } + }, + "/routing-rules/{ruleId}": { + "get": { + "tags": ["Routing"], + "summary": "Get a routing rule", + "parameters": [ + { + "name": "ruleId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "responses": { + "200": { + "description": "Rule" + } + } + }, + "patch": { + "tags": ["Routing"], + "summary": "Update a routing rule", + "parameters": [ + { + "name": "ruleId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "responses": { + "200": { + "description": "Updated" + } + } + }, + "delete": { + "tags": ["Routing"], + "summary": "Delete a routing rule", + "parameters": [ + { + "name": "ruleId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "responses": { + "204": { + "description": "Deleted" + } + } + } + }, + "/routing-rules/reorder": { + "post": { + "tags": ["Routing"], + "summary": "Reorder routing rules", + "description": "Replace routing-rule evaluation order by passing the rule IDs in desired order.", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "orderedIds": { + "minItems": 1, + "type": "array", + "items": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + } + } + }, + "required": ["orderedIds"], + "description": "Routing-rule IDs in desired evaluation order" + } + } + } + }, + "responses": { + "200": { + "description": "Rules reordered" + } + } + } + }, + "/business-hours": { + "get": { + "tags": ["SLA"], + "summary": "List business-hours calendars", + "responses": { + "200": { + "description": "Calendars", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "bizhrs_01h455vb4pex5vsknk084sn02q" + }, + "name": { + "type": "string" + }, + "timezone": { + "type": "string", + "example": "America/New_York" + }, + "schedule": {}, + "holidays": {}, + "archivedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ], + "description": "ISO 8601 timestamp or null", + "example": "2024-01-15T10:30:00.000Z" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": [ + "id", + "name", + "timezone", + "schedule", + "holidays", + "archivedAt", + "createdAt", + "updatedAt" + ], + "additionalProperties": false + } + }, + "meta": { + "type": "object", + "properties": { + "pagination": { + "type": "object", + "properties": { + "cursor": { + "description": "Cursor for next page, null if no more pages", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "hasMore": { + "type": "boolean", + "description": "Whether there are more items" + }, + "total": { + "description": "Total count (when available)", + "type": "number" + } + }, + "required": ["hasMore"], + "additionalProperties": false + } + }, + "required": ["pagination"], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Calendars" + } + } + } + } + } + }, + "post": { + "tags": ["SLA"], + "summary": "Create a business-hours calendar", + "responses": { + "201": { + "description": "Created" + } + } + } + }, + "/business-hours/{id}": { + "get": { + "tags": ["SLA"], + "summary": "Get a calendar", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "responses": { + "200": { + "description": "Calendar" + } + } + }, + "patch": { + "tags": ["SLA"], + "summary": "Update a calendar", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "responses": { + "200": { + "description": "Updated" + } + } + }, + "delete": { + "tags": ["SLA"], + "summary": "Archive a calendar", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "responses": { + "204": { + "description": "Archived" + } + } + } + }, + "/sla-policies": { + "get": { + "tags": ["SLA"], + "summary": "List SLA policies", + "responses": { + "200": { + "description": "Policies", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "sla_pol_01h455vb4pex5vsknk084sn02q" + }, + "name": { + "type": "string" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "scope": { + "type": "string", + "enum": ["workspace", "team", "inbox"] + }, + "scopeTeamId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "scopeInboxId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "priority": { + "type": "number" + }, + "enabled": { + "type": "boolean" + }, + "appliesToPriorities": { + "type": "array", + "items": { + "type": "string" + } + }, + "businessHoursId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "pauseOnPending": { + "type": "boolean" + }, + "pauseOnOnHold": { + "type": "boolean" + }, + "archivedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ], + "description": "ISO 8601 timestamp or null", + "example": "2024-01-15T10:30:00.000Z" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": [ + "id", + "name", + "description", + "scope", + "scopeTeamId", + "scopeInboxId", + "priority", + "enabled", + "appliesToPriorities", + "businessHoursId", + "pauseOnPending", + "pauseOnOnHold", + "archivedAt", + "createdAt", + "updatedAt" + ], + "additionalProperties": false + } + }, + "meta": { + "type": "object", + "properties": { + "pagination": { + "type": "object", + "properties": { + "cursor": { + "description": "Cursor for next page, null if no more pages", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "hasMore": { + "type": "boolean", + "description": "Whether there are more items" + }, + "total": { + "description": "Total count (when available)", + "type": "number" + } + }, + "required": ["hasMore"], + "additionalProperties": false + } + }, + "required": ["pagination"], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Policies" + } + } + } + } + } + }, + "post": { + "tags": ["SLA"], + "summary": "Create an SLA policy", + "responses": { + "201": { + "description": "Created" + } + } + } + }, + "/sla-policies/{policyId}": { + "get": { + "tags": ["SLA"], + "summary": "Get a policy", + "parameters": [ + { + "name": "policyId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "responses": { + "200": { + "description": "Policy" + } + } + }, + "patch": { + "tags": ["SLA"], + "summary": "Update a policy", + "parameters": [ + { + "name": "policyId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "responses": { + "200": { + "description": "Updated" + } + } + }, + "delete": { + "tags": ["SLA"], + "summary": "Archive a policy", + "parameters": [ + { + "name": "policyId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "responses": { + "204": { + "description": "Archived" + } + } + } + }, + "/sla-policies/{policyId}/targets": { + "get": { + "tags": ["SLA"], + "summary": "List targets for a policy", + "parameters": [ + { + "name": "policyId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "responses": { + "200": { + "description": "Targets" + } + } + }, + "post": { + "tags": ["SLA"], + "summary": "Add a target", + "parameters": [ + { + "name": "policyId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "responses": { + "201": { + "description": "Created" + } + } + }, + "patch": { + "tags": ["SLA"], + "summary": "Replace target set", + "parameters": [ + { + "name": "policyId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "responses": { + "200": { + "description": "Replaced" + } + } + }, + "put": { + "tags": ["SLA"], + "summary": "Replace target set", + "parameters": [ + { + "name": "policyId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "targets": { + "type": "array", + "items": { + "type": "object", + "properties": { + "kind": { + "type": "string", + "enum": ["first_response", "next_response", "resolution"] + }, + "minutes": { + "type": "integer", + "exclusiveMinimum": 0, + "maximum": 9007199254740991 + } + }, + "required": ["kind", "minutes"] + } + } + }, + "required": ["targets"] + } + } + } + }, + "responses": { + "200": { + "description": "Targets replaced" + }, + "400": { + "description": "Validation error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "VALIDATION_ERROR" + }, + "message": { + "type": "string" + }, + "details": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Request validation failed" + } + } + } + } + } + } + }, + "/sla-policies/{policyId}/escalation-rules": { + "get": { + "tags": ["SLA"], + "summary": "List escalation rules for a policy", + "parameters": [ + { + "name": "policyId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "responses": { + "200": { + "description": "Rules" + } + } + }, + "post": { + "tags": ["SLA"], + "summary": "Create an escalation rule", + "parameters": [ + { + "name": "policyId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "responses": { + "201": { + "description": "Created" + } + } + } + }, + "/sla-policies/{policyId}/escalation-rules/{ruleId}": { + "patch": { + "tags": ["SLA"], + "summary": "Update an escalation rule", + "parameters": [ + { + "name": "policyId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + }, + { + "name": "ruleId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "responses": { + "200": { + "description": "Updated" + } + } + }, + "delete": { + "tags": ["SLA"], + "summary": "Delete an escalation rule", + "parameters": [ + { + "name": "policyId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + }, + { + "name": "ruleId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "responses": { + "204": { + "description": "Deleted" + } + } + } + }, + "/internal/sla-tick": { + "post": { + "tags": ["SLA"], + "summary": "Run one SLA escalation tick (internal cron endpoint)", + "description": "Protected by `x-internal-secret` header. Designed for pg_cron or external scheduler. Idempotent; safe to call concurrently.", + "responses": { + "200": { + "description": "Counters: { escalated, breached }" + }, + "401": { + "description": "Bad shared secret" + } + } + } + }, + "/organizations": { + "get": { + "tags": ["Organizations"], + "summary": "List organizations", + "parameters": [ + { + "name": "search", + "in": "query", + "schema": { + "def": { + "type": "optional", + "innerType": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + }, + "type": "optional" + } + }, + { + "name": "cursor", + "in": "query", + "schema": { + "def": { + "type": "optional", + "innerType": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + }, + "type": "optional" + } + } + ], + "responses": { + "200": { + "description": "Organizations", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "org_01h455vb4pex5vsknk084sn02q" + }, + "name": { + "type": "string" + }, + "domain": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "externalId": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "website": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "notes": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "archivedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ], + "description": "ISO 8601 timestamp or null", + "example": "2024-01-15T10:30:00.000Z" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": [ + "id", + "name", + "domain", + "externalId", + "website", + "notes", + "archivedAt", + "createdAt", + "updatedAt" + ], + "additionalProperties": false + } + }, + "meta": { + "type": "object", + "properties": { + "pagination": { + "type": "object", + "properties": { + "cursor": { + "description": "Cursor for next page, null if no more pages", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "hasMore": { + "type": "boolean", + "description": "Whether there are more items" + }, + "total": { + "description": "Total count (when available)", + "type": "number" + } + }, + "required": ["hasMore"], + "additionalProperties": false + } + }, + "required": ["pagination"], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Organizations" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + } + } + }, + "post": { + "tags": ["Organizations"], + "summary": "Create an organization", + "responses": { + "201": { + "description": "Created" + } + } + } + }, + "/organizations/{organizationId}": { + "get": { + "tags": ["Organizations"], + "summary": "Get organization", + "parameters": [ + { + "name": "organizationId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "responses": { + "200": { + "description": "Organization" + } + } + }, + "patch": { + "tags": ["Organizations"], + "summary": "Update organization", + "parameters": [ + { + "name": "organizationId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "responses": { + "200": { + "description": "Updated" + } + } + }, + "delete": { + "tags": ["Organizations"], + "summary": "Archive organization", + "parameters": [ + { + "name": "organizationId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "responses": { + "204": { + "description": "Archived" + } + } + } + }, + "/organizations/{organizationId}/contacts": { + "get": { + "tags": ["Organizations"], + "summary": "List contacts for an organization", + "parameters": [ + { + "name": "organizationId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "responses": { + "200": { + "description": "Contacts" + } + } + } + }, + "/contacts": { + "get": { + "tags": ["Contacts"], + "summary": "List contacts", + "parameters": [ + { + "name": "q", + "in": "query", + "schema": { + "def": { + "type": "optional", + "innerType": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + }, + "type": "optional" + } + }, + { + "name": "email", + "in": "query", + "schema": { + "def": { + "type": "optional", + "innerType": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + }, + "type": "optional" + } + }, + { + "name": "organizationId", + "in": "query", + "schema": { + "def": { + "type": "optional", + "innerType": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + }, + "type": "optional" + } + } + ], + "responses": { + "200": { + "description": "Contacts", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "contact_01h455vb4pex5vsknk084sn02q" + }, + "name": { + "type": "string" + }, + "email": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "phone": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "title": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "externalId": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "organizationId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "avatarUrl": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "archivedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ], + "description": "ISO 8601 timestamp or null", + "example": "2024-01-15T10:30:00.000Z" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": [ + "id", + "name", + "email", + "phone", + "title", + "externalId", + "organizationId", + "avatarUrl", + "archivedAt", + "createdAt", + "updatedAt" + ], + "additionalProperties": false + } + }, + "meta": { + "type": "object", + "properties": { + "pagination": { + "type": "object", + "properties": { + "cursor": { + "description": "Cursor for next page, null if no more pages", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "hasMore": { + "type": "boolean", + "description": "Whether there are more items" + }, + "total": { + "description": "Total count (when available)", + "type": "number" + } + }, + "required": ["hasMore"], + "additionalProperties": false + } + }, + "required": ["pagination"], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Contacts" + } + } + } + } + } + }, + "post": { + "tags": ["Contacts"], + "summary": "Create a contact", + "responses": { + "201": { + "description": "Created" + } + } + } + }, + "/contacts/{contactId}": { + "get": { + "tags": ["Contacts"], + "summary": "Get contact", + "parameters": [ + { + "name": "contactId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "responses": { + "200": { + "description": "Contact" + } + } + }, + "patch": { + "tags": ["Contacts"], + "summary": "Update contact", + "parameters": [ + { + "name": "contactId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "responses": { + "200": { + "description": "Updated" + } + } + }, + "delete": { + "tags": ["Contacts"], + "summary": "Archive contact", + "parameters": [ + { + "name": "contactId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "responses": { + "204": { + "description": "Archived" + } + } + } + }, + "/contacts/{contactId}/links": { + "post": { + "tags": ["Contacts"], + "summary": "Link contact to a portal user", + "parameters": [ + { + "name": "contactId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "responses": { + "201": { + "description": "Linked" + } + } + }, + "delete": { + "tags": ["Contacts"], + "summary": "Unlink contact from a portal user", + "parameters": [ + { + "name": "contactId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "responses": { + "204": { + "description": "Unlinked" + } + } + }, + "get": { + "tags": ["Contacts"], + "summary": "List portal-user links for a contact", + "parameters": [ + { + "name": "contactId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "responses": { + "200": { + "description": "Contact links" + } + } + } + }, + "/audit-events": { + "get": { + "tags": ["Audit"], + "summary": "List audit events", + "parameters": [ + { + "name": "principalId", + "in": "query", + "schema": { + "def": { + "type": "optional", + "innerType": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + }, + "type": "optional" + } + }, + { + "name": "action", + "in": "query", + "schema": { + "def": { + "type": "optional", + "innerType": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + }, + "type": "optional" + } + }, + { + "name": "actionPrefix", + "in": "query", + "schema": { + "def": { + "type": "optional", + "innerType": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + }, + "type": "optional" + } + }, + { + "name": "targetType", + "in": "query", + "schema": { + "def": { + "type": "optional", + "innerType": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + }, + "type": "optional" + } + }, + { + "name": "targetId", + "in": "query", + "schema": { + "def": { + "type": "optional", + "innerType": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + }, + "type": "optional" + } + }, + { + "name": "source", + "in": "query", + "schema": { + "def": { + "type": "optional", + "innerType": { + "def": { + "type": "enum", + "entries": { + "web": "web", + "api": "api", + "integration": "integration", + "system": "system", + "mcp": "mcp" + } + }, + "type": "enum", + "enum": { + "web": "web", + "api": "api", + "integration": "integration", + "system": "system", + "mcp": "mcp" + }, + "options": ["web", "api", "integration", "system", "mcp"] + } + }, + "type": "optional" + } + }, + { + "name": "from", + "in": "query", + "schema": { + "def": { + "type": "optional", + "innerType": { + "def": { + "type": "string", + "checks": [ + { + "def": { + "type": "string", + "format": "datetime", + "check": "string_format", + "offset": false, + "local": false, + "precision": null, + "pattern": {} + }, + "type": "string", + "format": "datetime", + "minLength": null, + "maxLength": null + } + ] + }, + "type": "string", + "format": "datetime", + "minLength": null, + "maxLength": null + } + }, + "type": "optional" + } + }, + { + "name": "to", + "in": "query", + "schema": { + "def": { + "type": "optional", + "innerType": { + "def": { + "type": "string", + "checks": [ + { + "def": { + "type": "string", + "format": "datetime", + "check": "string_format", + "offset": false, + "local": false, + "precision": null, + "pattern": {} + }, + "type": "string", + "format": "datetime", + "minLength": null, + "maxLength": null + } + ] + }, + "type": "string", + "format": "datetime", + "minLength": null, + "maxLength": null + } + }, + "type": "optional" + } + }, + { + "name": "cursor", + "in": "query", + "schema": { + "def": { + "type": "optional", + "innerType": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + }, + "type": "optional" + } + }, + { + "name": "limit", + "in": "query", + "schema": { + "def": { + "type": "optional", + "innerType": { + "def": { + "type": "number", + "coerce": true, + "checks": [{}, {}] + }, + "type": "number", + "minValue": 1, + "maxValue": 200, + "isInt": false, + "isFinite": true, + "format": null + } + }, + "type": "optional" + } + } + ], + "responses": { + "200": { + "description": "Audit events", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "audit_01h455vb4pex5vsknk084sn02q" + }, + "principalId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "action": { + "type": "string" + }, + "targetType": { + "type": "string" + }, + "targetId": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "source": { + "type": "string", + "enum": ["web", "api", "integration", "system", "mcp"] + }, + "ipAddress": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "userAgent": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "diff": {}, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": [ + "id", + "principalId", + "action", + "targetType", + "targetId", + "source", + "ipAddress", + "userAgent", + "diff", + "createdAt" + ], + "additionalProperties": false + } + }, + "meta": { + "type": "object", + "properties": { + "pagination": { + "type": "object", + "properties": { + "cursor": { + "description": "Cursor for next page, null if no more pages", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "hasMore": { + "type": "boolean", + "description": "Whether there are more items" + }, + "total": { + "description": "Total count (when available)", + "type": "number" + } + }, + "required": ["hasMore"], + "additionalProperties": false + } + }, + "required": ["pagination"], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Audit events" + } + } + } + } + } + } + }, + "/api-keys": { + "get": { + "tags": ["API Keys"], + "summary": "List API keys", + "responses": { + "200": { + "description": "API keys", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "apikey_01h455vb4pex5vsknk084sn02q" + }, + "name": { + "type": "string" + }, + "keyPrefix": { + "type": "string" + }, + "scopes": { + "type": "array", + "items": { + "type": "string" + } + }, + "allowedTeamIds": { + "type": "array", + "items": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + } + }, + "allowedInboxIds": { + "type": "array", + "items": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + } + }, + "compatLegacyFullAccess": { + "type": "boolean" + }, + "compatAcknowledgedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ], + "description": "ISO 8601 timestamp or null", + "example": "2024-01-15T10:30:00.000Z" + }, + "lastIp": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "lastUserAgent": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "rotatedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ], + "description": "ISO 8601 timestamp or null", + "example": "2024-01-15T10:30:00.000Z" + }, + "expiresAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ], + "description": "ISO 8601 timestamp or null", + "example": "2024-01-15T10:30:00.000Z" + }, + "revokedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ], + "description": "ISO 8601 timestamp or null", + "example": "2024-01-15T10:30:00.000Z" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + }, + "lastUsedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ], + "description": "ISO 8601 timestamp or null", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": [ + "id", + "name", + "keyPrefix", + "scopes", + "allowedTeamIds", + "allowedInboxIds", + "compatLegacyFullAccess", + "compatAcknowledgedAt", + "lastIp", + "lastUserAgent", + "rotatedAt", + "expiresAt", + "revokedAt", + "createdAt", + "lastUsedAt" + ], + "additionalProperties": false + } + }, + "meta": { + "type": "object", + "properties": { + "pagination": { + "type": "object", + "properties": { + "cursor": { + "description": "Cursor for next page, null if no more pages", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "hasMore": { + "type": "boolean", + "description": "Whether there are more items" + }, + "total": { + "description": "Total count (when available)", + "type": "number" + } + }, + "required": ["hasMore"], + "additionalProperties": false + } + }, + "required": ["pagination"], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "API keys" + } + } + } + } + } + }, + "post": { + "tags": ["API Keys"], + "summary": "Create a scoped API key", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + "expiresAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ] + }, + "scopes": { + "type": "array", + "items": { + "type": "string" + } + }, + "allowedTeamIds": { + "type": "array", + "items": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + } + }, + "allowedInboxIds": { + "type": "array", + "items": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + } + } + }, + "required": ["name"] + } + } + } + }, + "responses": { + "201": { + "description": "API key created (plaintext returned ONCE)", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "apikey_01h455vb4pex5vsknk084sn02q" + }, + "name": { + "type": "string" + }, + "keyPrefix": { + "type": "string" + }, + "scopes": { + "type": "array", + "items": { + "type": "string" + } + }, + "allowedTeamIds": { + "type": "array", + "items": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + } + }, + "allowedInboxIds": { + "type": "array", + "items": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + } + }, + "compatLegacyFullAccess": { + "type": "boolean" + }, + "compatAcknowledgedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ], + "description": "ISO 8601 timestamp or null", + "example": "2024-01-15T10:30:00.000Z" + }, + "lastIp": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "lastUserAgent": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "rotatedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ], + "description": "ISO 8601 timestamp or null", + "example": "2024-01-15T10:30:00.000Z" + }, + "expiresAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ], + "description": "ISO 8601 timestamp or null", + "example": "2024-01-15T10:30:00.000Z" + }, + "revokedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ], + "description": "ISO 8601 timestamp or null", + "example": "2024-01-15T10:30:00.000Z" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + }, + "lastUsedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ], + "description": "ISO 8601 timestamp or null", + "example": "2024-01-15T10:30:00.000Z" + }, + "plaintextKey": { + "type": "string" + } + }, + "required": [ + "id", + "name", + "keyPrefix", + "scopes", + "allowedTeamIds", + "allowedInboxIds", + "compatLegacyFullAccess", + "compatAcknowledgedAt", + "lastIp", + "lastUserAgent", + "rotatedAt", + "expiresAt", + "revokedAt", + "createdAt", + "lastUsedAt", + "plaintextKey" + ], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "API key + plaintext" + } + } + } + } + } + } + }, + "/api-keys/{apiKeyId}": { + "get": { + "tags": ["API Keys"], + "summary": "Get API key", + "parameters": [ + { + "name": "apiKeyId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "responses": { + "200": { + "description": "API key" + } + } + }, + "patch": { + "tags": ["API Keys"], + "summary": "Update API key (name + scopes + allow-lists)", + "parameters": [ + { + "name": "apiKeyId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "responses": { + "200": { + "description": "Updated" + } + } + }, + "delete": { + "tags": ["API Keys"], + "summary": "Revoke API key", + "parameters": [ + { + "name": "apiKeyId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "responses": { + "204": { + "description": "Revoked" + } + } + } + }, + "/api-keys/{apiKeyId}/rotate": { + "post": { + "tags": ["API Keys"], + "summary": "Rotate API key (returns new plaintext)", + "parameters": [ + { + "name": "apiKeyId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "responses": { + "200": { + "description": "Rotated" + } + } + } + }, + "/api-keys/{apiKeyId}/acknowledge-legacy": { + "post": { + "tags": ["API Keys"], + "summary": "Acknowledge legacy unscoped status", + "parameters": [ + { + "name": "apiKeyId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "responses": { + "200": { + "description": "Acknowledged" + } + } + } + }, + "/webhooks/{webhookId}/deliveries": { + "get": { + "tags": ["Webhooks"], + "summary": "List delivery attempts for a webhook", + "parameters": [ + { + "name": "webhookId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + }, + { + "name": "status", + "in": "query", + "schema": { + "def": { + "type": "optional", + "innerType": { + "def": { + "type": "enum", + "entries": { + "queued": "queued", + "success": "success", + "failed_retryable": "failed_retryable", + "failed_terminal": "failed_terminal", + "blocked_ssrf": "blocked_ssrf" + } + }, + "type": "enum", + "enum": { + "queued": "queued", + "success": "success", + "failed_retryable": "failed_retryable", + "failed_terminal": "failed_terminal", + "blocked_ssrf": "blocked_ssrf" + }, + "options": [ + "queued", + "success", + "failed_retryable", + "failed_terminal", + "blocked_ssrf" + ] + } + }, + "type": "optional" + } + }, + { + "name": "cursor", + "in": "query", + "schema": { + "def": { + "type": "optional", + "innerType": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + }, + "type": "optional" + } + }, + { + "name": "cursorAttemptedAt", + "in": "query", + "schema": { + "def": { + "type": "optional", + "innerType": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + }, + "type": "optional" + } + }, + { + "name": "cursorId", + "in": "query", + "schema": { + "def": { + "type": "optional", + "innerType": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + }, + "type": "optional" + } + }, + { + "name": "limit", + "in": "query", + "schema": { + "def": { + "type": "optional", + "innerType": { + "def": { + "type": "number", + "coerce": true, + "checks": [{}, {}] + }, + "type": "number", + "minValue": 1, + "maxValue": 200, + "isInt": false, + "isFinite": true, + "format": null + } + }, + "type": "optional" + } + } + ], + "responses": { + "200": { + "description": "Delivery attempts (newest first)", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "wh_deliv_01h455vb4pex5vsknk084sn02q" + }, + "webhookId": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "eventId": { + "type": "string" + }, + "eventType": { + "type": "string" + }, + "attemptNumber": { + "type": "number" + }, + "status": { + "type": "string", + "enum": [ + "queued", + "success", + "failed_retryable", + "failed_terminal", + "blocked_ssrf" + ] + }, + "httpStatus": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "errorMessage": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "requestUrl": { + "type": "string" + }, + "requestPayloadBytes": { + "type": "number" + }, + "responseBodySnippet": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "latencyMs": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ] + }, + "signatureTimestamp": { + "type": "number" + }, + "attemptedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + }, + "nextRetryAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ], + "description": "ISO 8601 timestamp or null", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": [ + "id", + "webhookId", + "eventId", + "eventType", + "attemptNumber", + "status", + "httpStatus", + "errorMessage", + "requestUrl", + "requestPayloadBytes", + "responseBodySnippet", + "latencyMs", + "signatureTimestamp", + "attemptedAt", + "nextRetryAt" + ], + "additionalProperties": false + } + }, + "meta": { + "type": "object", + "properties": { + "pagination": { + "type": "object", + "properties": { + "cursor": { + "description": "Cursor for next page, null if no more pages", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "hasMore": { + "type": "boolean", + "description": "Whether there are more items" + }, + "total": { + "description": "Total count (when available)", + "type": "number" + } + }, + "required": ["hasMore"], + "additionalProperties": false + } + }, + "required": ["pagination"], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Deliveries" + } + } + } + } + } + } + }, + "/admin/usage": { + "get": { + "tags": ["Admin"], + "summary": "Get workspace usage counters", + "description": "Trusted endpoint authenticated by ADMIN_API_TOKEN for external billing meters and usage tooling.", + "security": [], + "responses": { + "200": { + "description": "Usage counters", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "aiTokensThisMonth": { + "type": "number" + }, + "postCount": { + "type": "number" + }, + "boardCount": { + "type": "number" + }, + "teamSeatCount": { + "type": "number" + } + }, + "required": ["aiTokensThisMonth", "postCount", "boardCount", "teamSeatCount"], + "additionalProperties": false + } + } + } + }, + "404": { + "description": "ADMIN_API_TOKEN is not configured" + } + } + } + }, + "/principals": { + "get": { + "tags": ["Principals"], + "summary": "List team principals", + "description": "Lists human team members with admin or member roles.", + "responses": { + "200": { + "description": "Team principals", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "userId": { + "type": "string" + }, + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "email": { + "anyOf": [ + { + "type": "string", + "format": "email", + "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$" + }, + { + "type": "null" + } + ] + }, + "image": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "role": { + "type": "string", + "enum": ["admin", "member"] + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": ["id", "userId", "name", "email", "image", "role", "createdAt"], + "additionalProperties": false + } + } + }, + "required": ["data"], + "additionalProperties": false + } + } + } + } + } + } + }, + "/principals/{principalId}": { + "get": { + "tags": ["Principals"], + "summary": "Get a team principal", + "parameters": [ + { + "name": "principalId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "responses": { + "200": { + "description": "Team principal" + } + } + }, + "patch": { + "tags": ["Principals"], + "summary": "Update a team principal role", + "parameters": [ + { + "name": "principalId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "role": { + "type": "string", + "enum": ["admin", "member"] + } + }, + "required": ["role"] + } + } + } + }, + "responses": { + "200": { + "description": "Updated team principal" + }, + "400": { + "description": "Validation error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "VALIDATION_ERROR" + }, + "message": { + "type": "string" + }, + "details": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Request validation failed" + } + } + } + } + } + }, + "delete": { + "tags": ["Principals"], + "summary": "Remove a team principal", + "description": "Converts the team member to a portal user.", + "parameters": [ + { + "name": "principalId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "responses": { + "204": { + "description": "Removed" + } + } + } + }, + "/webhooks": { + "get": { + "tags": ["Webhooks"], + "summary": "List webhooks", + "responses": { + "200": { + "description": "Webhooks", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "webhook_01h455vb4pex5vsknk084sn02q" + }, + "url": { + "type": "string", + "format": "uri" + }, + "events": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "post.created", + "post.status_changed", + "post.updated", + "post.deleted", + "post.restored", + "post.merged", + "post.unmerged", + "post.mentioned", + "comment.created", + "comment.updated", + "comment.deleted", + "changelog.published", + "ticket.created", + "ticket.updated", + "ticket.deleted", + "ticket.restored", + "ticket.assigned", + "ticket.unassigned", + "ticket.status_changed", + "ticket.first_response", + "ticket.thread_added", + "ticket.thread_updated", + "ticket.thread_deleted", + "ticket.participant_added", + "ticket.participant_removed", + "ticket.shared", + "ticket.unshared", + "ticket.sla_warning", + "ticket.sla_breach", + "ticket.attachment_added", + "ticket.attachment_removed", + "inbox.created", + "inbox.updated", + "inbox.archived", + "inbox.unarchived", + "team.created", + "team.updated", + "team.archived", + "ticket_status.created", + "ticket_status.updated", + "contact.created", + "contact.updated", + "contact.archived", + "contact.linked", + "contact.unlinked", + "organization.created", + "organization.updated", + "organization.archived", + "organization.unarchived", + "conversation.created", + "conversation.status_changed", + "conversation.assigned", + "conversation.priority_changed", + "conversation.csat_submitted", + "message.created", + "message.note_created", + "message.deleted", + "help_center.category.created", + "help_center.category.updated", + "help_center.category.deleted", + "help_center.article.created", + "help_center.article.updated", + "help_center.article.published", + "help_center.article.unpublished", + "help_center.article.deleted", + "changelog.created", + "changelog.updated", + "changelog.deleted", + "segment.created", + "segment.updated", + "segment.deleted", + "user_attribute.created", + "user_attribute.updated", + "user_attribute.deleted", + "board.created", + "board.updated", + "board.deleted", + "tag.created", + "tag.updated", + "tag.deleted", + "status.created", + "status.updated", + "status.deleted", + "roadmap.created", + "roadmap.updated", + "roadmap.deleted", + "sla_policy.created", + "sla_policy.updated", + "sla_policy.archived", + "routing_rule.created", + "routing_rule.updated", + "routing_rule.deleted", + "business_hours.created", + "business_hours.updated", + "business_hours.archived", + "inbox_channel.created", + "inbox_channel.updated", + "inbox_channel.archived", + "inbox_membership.added", + "inbox_membership.updated", + "inbox_membership.removed", + "api_key.created", + "api_key.rotated", + "api_key.revoked", + "role.created", + "role.updated", + "role.deleted", + "role_assignment.created", + "role_assignment.revoked" + ] + } + }, + "boardIds": { + "type": "array", + "items": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + } + }, + "status": { + "type": "string", + "enum": ["active", "disabled"] + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": ["id", "url", "events", "boardIds", "status", "createdAt"], + "additionalProperties": false + } + } + }, + "required": ["data"], + "additionalProperties": false + } + } + } + } + } + }, + "post": { + "tags": ["Webhooks"], + "summary": "Create a webhook", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "url": { + "type": "string", + "format": "uri" + }, + "events": { + "minItems": 1, + "type": "array", + "items": { + "type": "string", + "enum": [ + "post.created", + "post.status_changed", + "post.updated", + "post.deleted", + "post.restored", + "post.merged", + "post.unmerged", + "post.mentioned", + "comment.created", + "comment.updated", + "comment.deleted", + "changelog.published", + "ticket.created", + "ticket.updated", + "ticket.deleted", + "ticket.restored", + "ticket.assigned", + "ticket.unassigned", + "ticket.status_changed", + "ticket.first_response", + "ticket.thread_added", + "ticket.thread_updated", + "ticket.thread_deleted", + "ticket.participant_added", + "ticket.participant_removed", + "ticket.shared", + "ticket.unshared", + "ticket.sla_warning", + "ticket.sla_breach", + "ticket.attachment_added", + "ticket.attachment_removed", + "inbox.created", + "inbox.updated", + "inbox.archived", + "inbox.unarchived", + "team.created", + "team.updated", + "team.archived", + "ticket_status.created", + "ticket_status.updated", + "contact.created", + "contact.updated", + "contact.archived", + "contact.linked", + "contact.unlinked", + "organization.created", + "organization.updated", + "organization.archived", + "organization.unarchived", + "conversation.created", + "conversation.status_changed", + "conversation.assigned", + "conversation.priority_changed", + "conversation.csat_submitted", + "message.created", + "message.note_created", + "message.deleted", + "help_center.category.created", + "help_center.category.updated", + "help_center.category.deleted", + "help_center.article.created", + "help_center.article.updated", + "help_center.article.published", + "help_center.article.unpublished", + "help_center.article.deleted", + "changelog.created", + "changelog.updated", + "changelog.deleted", + "segment.created", + "segment.updated", + "segment.deleted", + "user_attribute.created", + "user_attribute.updated", + "user_attribute.deleted", + "board.created", + "board.updated", + "board.deleted", + "tag.created", + "tag.updated", + "tag.deleted", + "status.created", + "status.updated", + "status.deleted", + "roadmap.created", + "roadmap.updated", + "roadmap.deleted", + "sla_policy.created", + "sla_policy.updated", + "sla_policy.archived", + "routing_rule.created", + "routing_rule.updated", + "routing_rule.deleted", + "business_hours.created", + "business_hours.updated", + "business_hours.archived", + "inbox_channel.created", + "inbox_channel.updated", + "inbox_channel.archived", + "inbox_membership.added", + "inbox_membership.updated", + "inbox_membership.removed", + "api_key.created", + "api_key.rotated", + "api_key.revoked", + "role.created", + "role.updated", + "role.deleted", + "role_assignment.created", + "role_assignment.revoked" + ] + } + }, + "boardIds": { + "type": "array", + "items": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + } + } + }, + "required": ["url", "events"] + } + } + } + }, + "responses": { + "201": { + "description": "Webhook created; signing secret is returned once", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "webhook_01h455vb4pex5vsknk084sn02q" + }, + "url": { + "type": "string", + "format": "uri" + }, + "events": { + "type": "array", + "items": { + "type": "string", + "enum": [ + "post.created", + "post.status_changed", + "post.updated", + "post.deleted", + "post.restored", + "post.merged", + "post.unmerged", + "post.mentioned", + "comment.created", + "comment.updated", + "comment.deleted", + "changelog.published", + "ticket.created", + "ticket.updated", + "ticket.deleted", + "ticket.restored", + "ticket.assigned", + "ticket.unassigned", + "ticket.status_changed", + "ticket.first_response", + "ticket.thread_added", + "ticket.thread_updated", + "ticket.thread_deleted", + "ticket.participant_added", + "ticket.participant_removed", + "ticket.shared", + "ticket.unshared", + "ticket.sla_warning", + "ticket.sla_breach", + "ticket.attachment_added", + "ticket.attachment_removed", + "inbox.created", + "inbox.updated", + "inbox.archived", + "inbox.unarchived", + "team.created", + "team.updated", + "team.archived", + "ticket_status.created", + "ticket_status.updated", + "contact.created", + "contact.updated", + "contact.archived", + "contact.linked", + "contact.unlinked", + "organization.created", + "organization.updated", + "organization.archived", + "organization.unarchived", + "conversation.created", + "conversation.status_changed", + "conversation.assigned", + "conversation.priority_changed", + "conversation.csat_submitted", + "message.created", + "message.note_created", + "message.deleted", + "help_center.category.created", + "help_center.category.updated", + "help_center.category.deleted", + "help_center.article.created", + "help_center.article.updated", + "help_center.article.published", + "help_center.article.unpublished", + "help_center.article.deleted", + "changelog.created", + "changelog.updated", + "changelog.deleted", + "segment.created", + "segment.updated", + "segment.deleted", + "user_attribute.created", + "user_attribute.updated", + "user_attribute.deleted", + "board.created", + "board.updated", + "board.deleted", + "tag.created", + "tag.updated", + "tag.deleted", + "status.created", + "status.updated", + "status.deleted", + "roadmap.created", + "roadmap.updated", + "roadmap.deleted", + "sla_policy.created", + "sla_policy.updated", + "sla_policy.archived", + "routing_rule.created", + "routing_rule.updated", + "routing_rule.deleted", + "business_hours.created", + "business_hours.updated", + "business_hours.archived", + "inbox_channel.created", + "inbox_channel.updated", + "inbox_channel.archived", + "inbox_membership.added", + "inbox_membership.updated", + "inbox_membership.removed", + "api_key.created", + "api_key.rotated", + "api_key.revoked", + "role.created", + "role.updated", + "role.deleted", + "role_assignment.created", + "role_assignment.revoked" + ] + } + }, + "boardIds": { + "type": "array", + "items": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + } + }, + "status": { + "type": "string", + "enum": ["active", "disabled"] + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + }, + "secret": { + "type": "string" + } + }, + "required": [ + "id", + "url", + "events", + "boardIds", + "status", + "createdAt", + "secret" + ], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Webhook" + } + } + } + }, + "400": { + "description": "Validation error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "VALIDATION_ERROR" + }, + "message": { + "type": "string" + }, + "details": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Request validation failed" + } + } + } + } + } + } + }, + "/webhooks/{webhookId}": { + "get": { + "tags": ["Webhooks"], + "summary": "Get a webhook", + "parameters": [ + { + "name": "webhookId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "responses": { + "200": { + "description": "Webhook" + } + } + }, + "patch": { + "tags": ["Webhooks"], + "summary": "Update a webhook", + "parameters": [ + { + "name": "webhookId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "url": { + "type": "string", + "format": "uri" + }, + "events": { + "minItems": 1, + "type": "array", + "items": { + "type": "string", + "enum": [ + "post.created", + "post.status_changed", + "post.updated", + "post.deleted", + "post.restored", + "post.merged", + "post.unmerged", + "post.mentioned", + "comment.created", + "comment.updated", + "comment.deleted", + "changelog.published", + "ticket.created", + "ticket.updated", + "ticket.deleted", + "ticket.restored", + "ticket.assigned", + "ticket.unassigned", + "ticket.status_changed", + "ticket.first_response", + "ticket.thread_added", + "ticket.thread_updated", + "ticket.thread_deleted", + "ticket.participant_added", + "ticket.participant_removed", + "ticket.shared", + "ticket.unshared", + "ticket.sla_warning", + "ticket.sla_breach", + "ticket.attachment_added", + "ticket.attachment_removed", + "inbox.created", + "inbox.updated", + "inbox.archived", + "inbox.unarchived", + "team.created", + "team.updated", + "team.archived", + "ticket_status.created", + "ticket_status.updated", + "contact.created", + "contact.updated", + "contact.archived", + "contact.linked", + "contact.unlinked", + "organization.created", + "organization.updated", + "organization.archived", + "organization.unarchived", + "conversation.created", + "conversation.status_changed", + "conversation.assigned", + "conversation.priority_changed", + "conversation.csat_submitted", + "message.created", + "message.note_created", + "message.deleted", + "help_center.category.created", + "help_center.category.updated", + "help_center.category.deleted", + "help_center.article.created", + "help_center.article.updated", + "help_center.article.published", + "help_center.article.unpublished", + "help_center.article.deleted", + "changelog.created", + "changelog.updated", + "changelog.deleted", + "segment.created", + "segment.updated", + "segment.deleted", + "user_attribute.created", + "user_attribute.updated", + "user_attribute.deleted", + "board.created", + "board.updated", + "board.deleted", + "tag.created", + "tag.updated", + "tag.deleted", + "status.created", + "status.updated", + "status.deleted", + "roadmap.created", + "roadmap.updated", + "roadmap.deleted", + "sla_policy.created", + "sla_policy.updated", + "sla_policy.archived", + "routing_rule.created", + "routing_rule.updated", + "routing_rule.deleted", + "business_hours.created", + "business_hours.updated", + "business_hours.archived", + "inbox_channel.created", + "inbox_channel.updated", + "inbox_channel.archived", + "inbox_membership.added", + "inbox_membership.updated", + "inbox_membership.removed", + "api_key.created", + "api_key.rotated", + "api_key.revoked", + "role.created", + "role.updated", + "role.deleted", + "role_assignment.created", + "role_assignment.revoked" + ] + } + }, + "boardIds": { + "anyOf": [ + { + "type": "array", + "items": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + } + }, + { + "type": "null" + } + ] + }, + "status": { + "type": "string", + "enum": ["active", "disabled"] + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Updated webhook" + } + } + }, + "delete": { + "tags": ["Webhooks"], + "summary": "Delete a webhook", + "parameters": [ + { + "name": "webhookId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "responses": { + "204": { + "description": "Deleted" + } + } + } + }, + "/webhooks/{webhookId}/rotate": { + "post": { + "tags": ["Webhooks"], + "summary": "Rotate a webhook signing secret", + "parameters": [ + { + "name": "webhookId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "responses": { + "200": { + "description": "New signing secret", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "secret": { + "type": "string" + }, + "rotatedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": ["id", "secret", "rotatedAt"], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Rotated secret" + } + } + } + } + } + } + }, + "/webhooks/{webhookId}/test": { + "post": { + "tags": ["Webhooks"], + "summary": "Deliver a sample payload to a webhook", + "parameters": [ + { + "name": "webhookId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "eventType": { + "type": "string", + "minLength": 1 + } + }, + "required": ["eventType"] + } + } + } + }, + "responses": { + "200": { + "description": "Test delivery outcome" + } + } + } + }, + "/webhooks/sample-payloads": { + "get": { + "tags": ["Webhooks"], + "summary": "List sample webhook payloads", + "responses": { + "200": { + "description": "Sample payloads keyed by event type", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + "required": ["data"], + "additionalProperties": false + } + } + } + } + } + } + }, + "/webhooks/{webhookId}/deliveries/{deliveryId}/redeliver": { + "post": { + "tags": ["Webhooks"], + "summary": "Redeliver a stored webhook delivery", + "parameters": [ + { + "name": "webhookId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + }, + { + "name": "deliveryId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "responses": { + "200": { + "description": "Redelivery outcome" + }, + "422": { + "description": "Original payload is unavailable" + } + } + } + }, + "/conversations": { + "get": { + "tags": ["Conversations"], + "summary": "List conversations", + "description": "Returns a paginated list of support conversations. Requires a team-role API key.", + "parameters": [ + { + "name": "status", + "in": "query", + "schema": { + "type": "string", + "enum": ["open", "pending", "closed"] + }, + "description": "Filter by conversation status" + }, + { + "name": "priority", + "in": "query", + "schema": { + "type": "string", + "enum": ["none", "low", "medium", "high", "urgent"] + }, + "description": "Filter by triage priority" + }, + { + "name": "assignedAgentPrincipalId", + "in": "query", + "schema": { + "type": "string" + }, + "description": "Filter by assigned agent principal ID" + }, + { + "name": "cursor", + "in": "query", + "schema": { + "type": "string" + }, + "description": "Pagination cursor from previous response" + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "default": 20, + "maximum": 100 + }, + "description": "Items per page (max 100)" + } + ], + "responses": { + "200": { + "description": "List of conversations", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "conversation_01h455vb4pex5vsknk084sn02q" + }, + "status": { + "type": "string", + "enum": ["open", "pending", "closed"], + "description": "Current conversation status", + "example": "open" + }, + "channel": { + "type": "string", + "enum": ["live_chat", "email", "web_form"], + "description": "Channel the conversation arrived on", + "example": "live_chat" + }, + "priority": { + "type": "string", + "enum": ["none", "low", "medium", "high", "urgent"], + "description": "Agent-set triage priority", + "example": "none" + }, + "subject": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Conversation subject line, null for live-chat threads", + "example": null + }, + "visitorPrincipalId": { + "type": "string", + "description": "Principal ID of the visiting user", + "example": "principal_01h455vb4pex5vsknk084sn02q" + }, + "visitorEmail": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Captured contact email for the visitor, null if not provided", + "example": "visitor@example.com" + }, + "assignedAgentPrincipalId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ], + "description": "Principal ID of the assigned agent, null if unassigned", + "example": null + }, + "lastMessageAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + }, + "resolvedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ], + "description": "When the conversation was resolved, null while still active", + "example": "2024-01-15T10:30:00.000Z" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": [ + "id", + "status", + "channel", + "priority", + "subject", + "visitorPrincipalId", + "visitorEmail", + "assignedAgentPrincipalId", + "lastMessageAt", + "resolvedAt", + "createdAt" + ], + "additionalProperties": false + } + }, + "meta": { + "type": "object", + "properties": { + "pagination": { + "type": "object", + "properties": { + "cursor": { + "description": "Cursor for next page, null if no more pages", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "hasMore": { + "type": "boolean", + "description": "Whether there are more items" + }, + "total": { + "description": "Total count (when available)", + "type": "number" + } + }, + "required": ["hasMore"], + "additionalProperties": false + } + }, + "required": ["pagination"], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Paginated conversations list" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + } + } + } + }, + "/conversations/{conversationId}": { + "get": { + "tags": ["Conversations"], + "summary": "Get a conversation", + "description": "Get a single conversation by ID. Requires a team-role API key.", + "parameters": [ + { + "name": "conversationId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Conversation ID" + } + ], + "responses": { + "200": { + "description": "Conversation details", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "conversation_01h455vb4pex5vsknk084sn02q" + }, + "status": { + "type": "string", + "enum": ["open", "pending", "closed"], + "description": "Current conversation status", + "example": "open" + }, + "channel": { + "type": "string", + "enum": ["live_chat", "email", "web_form"], + "description": "Channel the conversation arrived on", + "example": "live_chat" + }, + "priority": { + "type": "string", + "enum": ["none", "low", "medium", "high", "urgent"], + "description": "Agent-set triage priority", + "example": "none" + }, + "subject": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Conversation subject line, null for live-chat threads", + "example": null + }, + "visitorPrincipalId": { + "type": "string", + "description": "Principal ID of the visiting user", + "example": "principal_01h455vb4pex5vsknk084sn02q" + }, + "visitorEmail": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Captured contact email for the visitor, null if not provided", + "example": "visitor@example.com" + }, + "assignedAgentPrincipalId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ], + "description": "Principal ID of the assigned agent, null if unassigned", + "example": null + }, + "lastMessageAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + }, + "resolvedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ], + "description": "When the conversation was resolved, null while still active", + "example": "2024-01-15T10:30:00.000Z" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": [ + "id", + "status", + "channel", + "priority", + "subject", + "visitorPrincipalId", + "visitorEmail", + "assignedAgentPrincipalId", + "lastMessageAt", + "resolvedAt", + "createdAt" + ], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Conversation details" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + }, + "404": { + "description": "Conversation not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "NOT_FOUND" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Resource not found" + } + } + } + } + } + } + }, + "/conversations/{conversationId}/messages": { + "get": { + "tags": ["Conversations"], + "summary": "List messages in a conversation", + "description": "Returns a paginated list of messages in a conversation. Internal agent notes are excluded by default. Requires a team-role API key.", + "parameters": [ + { + "name": "conversationId", + "in": "path", + "required": true, + "schema": { + "type": "string" + }, + "description": "Conversation ID" + }, + { + "name": "includeInternal", + "in": "query", + "schema": { + "type": "boolean" + }, + "description": "Include internal agent notes (default: false)" + }, + { + "name": "cursor", + "in": "query", + "schema": { + "type": "string" + }, + "description": "Pagination cursor from previous response" + }, + { + "name": "limit", + "in": "query", + "schema": { + "type": "integer", + "default": 30, + "maximum": 100 + }, + "description": "Items per page (max 100)" + } + ], + "responses": { + "200": { + "description": "List of messages", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "chat_msg_01h455vb4pex5vsknk084sn02q" + }, + "conversationId": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "conversation_01h455vb4pex5vsknk084sn02q" + }, + "senderType": { + "type": "string", + "enum": ["visitor", "agent", "system"], + "description": "Who sent the message", + "example": "visitor" + }, + "isInternal": { + "type": "boolean", + "description": "Whether this is an internal agent note not visible to the visitor", + "example": false + }, + "authorPrincipalId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ], + "description": "Principal ID of the author, null for system messages", + "example": "principal_01h455vb4pex5vsknk084sn02q" + }, + "authorName": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Display name of the author, null for system messages", + "example": "Jane Doe" + }, + "content": { + "type": "string", + "example": "Hello, I need help with my account." + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": [ + "id", + "conversationId", + "senderType", + "isInternal", + "authorPrincipalId", + "authorName", + "content", + "createdAt" + ], + "additionalProperties": false + } + }, + "meta": { + "type": "object", + "properties": { + "pagination": { + "type": "object", + "properties": { + "cursor": { + "description": "Cursor for next page, null if no more pages", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "hasMore": { + "type": "boolean", + "description": "Whether there are more items" + }, + "total": { + "description": "Total count (when available)", + "type": "number" + } + }, + "required": ["hasMore"], + "additionalProperties": false + } + }, + "required": ["pagination"], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Paginated messages list" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + }, + "404": { + "description": "Conversation not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "NOT_FOUND" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Resource not found" + } + } + } + } + } + } + }, + "/conversations/{conversationId}/reply": { + "post": { + "tags": ["Conversations"], + "summary": "Send a public agent reply", + "parameters": [ + { + "name": "conversationId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + }, + "description": "Conversation ID" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "content": { + "type": "string", + "minLength": 1, + "maxLength": 4000 + } + }, + "required": ["content"] + } + } + } + }, + "responses": { + "201": { + "description": "Reply created", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "conversationId": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "status": { + "type": "string", + "enum": ["open", "pending", "closed"] + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": ["id", "conversationId", "createdAt"], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Created reply" + } + } + } + }, + "400": { + "description": "Validation error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "VALIDATION_ERROR" + }, + "message": { + "type": "string" + }, + "details": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Request validation failed" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + }, + "404": { + "description": "Conversation not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "NOT_FOUND" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Resource not found" + } + } + } + } + } + } + }, + "/conversations/{conversationId}/note": { + "post": { + "tags": ["Conversations"], + "summary": "Add an internal agent note", + "parameters": [ + { + "name": "conversationId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + }, + "description": "Conversation ID" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "content": { + "type": "string", + "minLength": 1, + "maxLength": 10000 + } + }, + "required": ["content"] + } + } + } + }, + "responses": { + "201": { + "description": "Internal note created", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "conversationId": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "status": { + "type": "string", + "enum": ["open", "pending", "closed"] + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": ["id", "conversationId", "createdAt"], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Created note" + } + } + } + }, + "400": { + "description": "Validation error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "VALIDATION_ERROR" + }, + "message": { + "type": "string" + }, + "details": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Request validation failed" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + }, + "404": { + "description": "Conversation not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "NOT_FOUND" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Resource not found" + } + } + } + } + } + } + }, + "/conversations/{conversationId}/status": { + "patch": { + "tags": ["Conversations"], + "summary": "Set conversation status", + "parameters": [ + { + "name": "conversationId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + }, + "description": "Conversation ID" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "status": { + "type": "string", + "enum": ["open", "pending", "closed"] + } + }, + "required": ["status"] + } + } + } + }, + "responses": { + "200": { + "description": "Conversation status updated", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "status": { + "type": "string", + "enum": ["open", "pending", "closed"] + } + }, + "required": ["id", "status"], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Status update" + } + } + } + }, + "400": { + "description": "Validation error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "VALIDATION_ERROR" + }, + "message": { + "type": "string" + }, + "details": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Request validation failed" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + }, + "404": { + "description": "Conversation not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "NOT_FOUND" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Resource not found" + } + } + } + } + } + } + }, + "/conversations/{conversationId}/priority": { + "patch": { + "tags": ["Conversations"], + "summary": "Set conversation priority", + "parameters": [ + { + "name": "conversationId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + }, + "description": "Conversation ID" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "priority": { + "type": "string", + "enum": ["none", "low", "medium", "high", "urgent"] + } + }, + "required": ["priority"] + } + } + } + }, + "responses": { + "200": { + "description": "Conversation priority updated", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "priority": { + "type": "string", + "enum": ["none", "low", "medium", "high", "urgent"] + } + }, + "required": ["id", "priority"], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Priority update" + } + } + } + }, + "400": { + "description": "Validation error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "VALIDATION_ERROR" + }, + "message": { + "type": "string" + }, + "details": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Request validation failed" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + }, + "404": { + "description": "Conversation not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "NOT_FOUND" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Resource not found" + } + } + } + } + } + } + }, + "/conversations/{conversationId}/assign": { + "post": { + "tags": ["Conversations"], + "summary": "Assign or unassign a conversation", + "parameters": [ + { + "name": "conversationId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + }, + "description": "Conversation ID" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "agentPrincipalId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + } + }, + "required": ["agentPrincipalId"] + } + } + } + }, + "responses": { + "200": { + "description": "Conversation assignment updated", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "assignedAgentPrincipalId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + } + }, + "required": ["id", "assignedAgentPrincipalId"], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Assignment update" + } + } + } + }, + "400": { + "description": "Validation error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "VALIDATION_ERROR" + }, + "message": { + "type": "string" + }, + "details": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Request validation failed" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + }, + "404": { + "description": "Conversation not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "NOT_FOUND" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Resource not found" + } + } + } + } + } + } + }, + "/conversations/{conversationId}/end": { + "post": { + "tags": ["Conversations"], + "summary": "End a conversation", + "parameters": [ + { + "name": "conversationId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + }, + "description": "Conversation ID" + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "reason": { + "type": "string", + "enum": [ + "resolved", + "tracked_as_feedback", + "duplicate", + "no_response", + "spam", + "other" + ] + }, + "note": { + "anyOf": [ + { + "type": "string", + "maxLength": 2000 + }, + { + "type": "null" + } + ] + } + }, + "required": ["reason"] + } + } + } + }, + "responses": { + "200": { + "description": "Conversation ended", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "status": { + "type": "string", + "enum": ["open", "pending", "closed"] + }, + "channel": { + "type": "string", + "enum": ["live_chat", "email", "web_form"] + }, + "priority": { + "type": "string", + "enum": ["none", "low", "medium", "high", "urgent"] + }, + "subject": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "visitorPrincipalId": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "visitorEmail": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "assignedAgentPrincipalId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "lastMessageAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + }, + "resolvedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + }, + { + "type": "null" + } + ] + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": [ + "id", + "status", + "channel", + "priority", + "subject", + "visitorPrincipalId", + "visitorEmail", + "assignedAgentPrincipalId", + "lastMessageAt", + "resolvedAt", + "createdAt" + ], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Ended conversation" + } + } + } + }, + "400": { + "description": "Validation error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "VALIDATION_ERROR" + }, + "message": { + "type": "string" + }, + "details": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Request validation failed" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + }, + "404": { + "description": "Conversation not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "NOT_FOUND" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Resource not found" + } + } + } + } + } + } + }, + "/conversations/{conversationId}/read": { + "post": { + "tags": ["Conversations"], + "summary": "Mark a conversation read for the calling agent", + "parameters": [ + { + "name": "conversationId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + }, + "description": "Conversation ID" + } + ], + "responses": { + "204": { + "description": "Conversation marked read" + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + }, + "404": { + "description": "Conversation not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "NOT_FOUND" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Resource not found" + } + } + } + } + } + } + }, + "/chat-tags": { + "get": { + "tags": ["Conversations"], + "summary": "List conversation tags", + "responses": { + "200": { + "description": "Conversation tags", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "chat_tag_01h455vb4pex5vsknk084sn02q" + }, + "name": { + "type": "string", + "example": "VIP" + }, + "color": { + "type": "string", + "example": "#6b7280" + }, + "count": { + "type": "number", + "description": "Number of open conversations currently using the tag" + } + }, + "required": ["id", "name", "color", "count"], + "additionalProperties": false + } + }, + "meta": { + "type": "object", + "properties": { + "pagination": { + "type": "object", + "properties": { + "cursor": { + "description": "Cursor for next page, null if no more pages", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "hasMore": { + "type": "boolean", + "description": "Whether there are more items" + }, + "total": { + "description": "Total count (when available)", + "type": "number" + } + }, + "required": ["hasMore"], + "additionalProperties": false + } + }, + "required": ["pagination"], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Conversation tags" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + } + } + }, + "post": { + "tags": ["Conversations"], + "summary": "Create a conversation tag", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 80 + }, + "color": { + "type": "string", + "pattern": "^#[0-9A-Fa-f]{6}$" + } + }, + "required": ["name"] + } + } + } + }, + "responses": { + "201": { + "description": "Conversation tag created", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "chat_tag_01h455vb4pex5vsknk084sn02q" + }, + "name": { + "type": "string", + "example": "VIP" + }, + "color": { + "type": "string", + "example": "#6b7280" + } + }, + "required": ["id", "name", "color"], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Created conversation tag" + } + } + } + }, + "400": { + "description": "Validation error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "VALIDATION_ERROR" + }, + "message": { + "type": "string" + }, + "details": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Request validation failed" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + } + } + } + }, + "/chat-tags/{tagId}": { + "patch": { + "tags": ["Conversations"], + "summary": "Update a conversation tag", + "parameters": [ + { + "name": "tagId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 80 + }, + "color": { + "type": "string", + "pattern": "^#[0-9A-Fa-f]{6}$" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Conversation tag updated", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "chat_tag_01h455vb4pex5vsknk084sn02q" + }, + "name": { + "type": "string", + "example": "VIP" + }, + "color": { + "type": "string", + "example": "#6b7280" + } + }, + "required": ["id", "name", "color"], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Updated conversation tag" + } + } + } + }, + "400": { + "description": "Validation error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "VALIDATION_ERROR" + }, + "message": { + "type": "string" + }, + "details": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Request validation failed" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + }, + "404": { + "description": "Resource not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "NOT_FOUND" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Resource not found" + } + } + } + } + } + }, + "delete": { + "tags": ["Conversations"], + "summary": "Delete a conversation tag", + "parameters": [ + { + "name": "tagId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "Conversation tag deleted" + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + }, + "404": { + "description": "Resource not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "NOT_FOUND" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Resource not found" + } + } + } + } + } + } + }, + "/conversations/{conversationId}/tags": { + "get": { + "tags": ["Conversations"], + "summary": "List tags on a conversation", + "parameters": [ + { + "name": "conversationId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Tags on the conversation", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "chat_tag_01h455vb4pex5vsknk084sn02q" + }, + "name": { + "type": "string", + "example": "VIP" + }, + "color": { + "type": "string", + "example": "#6b7280" + } + }, + "required": ["id", "name", "color"], + "additionalProperties": false + } + }, + "meta": { + "type": "object", + "properties": { + "pagination": { + "type": "object", + "properties": { + "cursor": { + "description": "Cursor for next page, null if no more pages", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "hasMore": { + "type": "boolean", + "description": "Whether there are more items" + }, + "total": { + "description": "Total count (when available)", + "type": "number" + } + }, + "required": ["hasMore"], + "additionalProperties": false + } + }, + "required": ["pagination"], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Conversation tags" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + }, + "404": { + "description": "Resource not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "NOT_FOUND" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Resource not found" + } + } + } + } + } + }, + "post": { + "tags": ["Conversations"], + "summary": "Attach a tag to a conversation", + "parameters": [ + { + "name": "conversationId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "chatTagId": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + } + }, + "required": ["chatTagId"] + } + } + } + }, + "responses": { + "200": { + "description": "Tag attached", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "chat_tag_01h455vb4pex5vsknk084sn02q" + }, + "name": { + "type": "string", + "example": "VIP" + }, + "color": { + "type": "string", + "example": "#6b7280" + } + }, + "required": ["id", "name", "color"], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Attached conversation tag" + } + } + } + }, + "400": { + "description": "Validation error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "VALIDATION_ERROR" + }, + "message": { + "type": "string" + }, + "details": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Request validation failed" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + }, + "404": { + "description": "Resource not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "NOT_FOUND" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Resource not found" + } + } + } + } + } + } + }, + "/conversations/{conversationId}/tags/{chatTagId}": { + "delete": { + "tags": ["Conversations"], + "summary": "Detach a tag from a conversation", + "parameters": [ + { + "name": "conversationId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "chatTagId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Tag detached", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "chat_tag_01h455vb4pex5vsknk084sn02q" + }, + "name": { + "type": "string", + "example": "VIP" + }, + "color": { + "type": "string", + "example": "#6b7280" + } + }, + "required": ["id", "name", "color"], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Detached conversation tag" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + }, + "404": { + "description": "Resource not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "NOT_FOUND" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Resource not found" + } + } + } + } + } + } + }, + "/settings/features": { + "get": { + "tags": ["Settings"], + "summary": "Read workspace feature flags", + "responses": { + "200": { + "description": "Feature flags", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "helpCenter": { + "type": "boolean" + }, + "aiFeedbackExtraction": { + "type": "boolean" + }, + "tickets": { + "type": "boolean" + }, + "supportInbox": { + "type": "boolean" + }, + "linkPreviews": { + "type": "boolean" + } + }, + "required": [ + "helpCenter", + "aiFeedbackExtraction", + "tickets", + "supportInbox", + "linkPreviews" + ], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Feature flags" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + } + } + }, + "patch": { + "tags": ["Settings"], + "summary": "Toggle workspace feature flags", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "helpCenter": { + "type": "boolean" + }, + "aiFeedbackExtraction": { + "type": "boolean" + }, + "tickets": { + "type": "boolean" + }, + "supportInbox": { + "type": "boolean" + }, + "linkPreviews": { + "type": "boolean" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Updated feature flags", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "helpCenter": { + "type": "boolean" + }, + "aiFeedbackExtraction": { + "type": "boolean" + }, + "tickets": { + "type": "boolean" + }, + "supportInbox": { + "type": "boolean" + }, + "linkPreviews": { + "type": "boolean" + } + }, + "required": [ + "helpCenter", + "aiFeedbackExtraction", + "tickets", + "supportInbox", + "linkPreviews" + ], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Feature flags" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + } + } + } + }, + "/settings/help-center": { + "get": { + "tags": ["Settings"], + "summary": "Read help-center configuration", + "responses": { + "200": { + "description": "Help-center configuration", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + }, + "homepageTitle": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "homepageDescription": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": ["enabled", "homepageTitle", "homepageDescription"], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Help-center configuration" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + } + } + }, + "patch": { + "tags": ["Settings"], + "summary": "Update help-center configuration", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + }, + "homepageTitle": { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + "homepageDescription": { + "type": "string", + "maxLength": 500 + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Updated help-center configuration", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean" + }, + "homepageTitle": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "homepageDescription": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": ["enabled", "homepageTitle", "homepageDescription"], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Help-center configuration" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + } + } + } + }, + "/moderation/posts": { + "get": { + "tags": ["Moderation"], + "summary": "List posts awaiting moderation", + "description": "Requires moderation.view scope and permission.", + "responses": { + "200": { + "description": "Pending posts", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "title": { + "type": "string" + }, + "content": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "authorPrincipalId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": ["id", "title"], + "additionalProperties": {} + } + }, + "meta": { + "type": "object", + "properties": { + "pagination": { + "type": "object", + "properties": { + "cursor": { + "description": "Cursor for next page, null if no more pages", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "hasMore": { + "type": "boolean", + "description": "Whether there are more items" + }, + "total": { + "description": "Total count (when available)", + "type": "number" + } + }, + "required": ["hasMore"], + "additionalProperties": false + } + }, + "required": ["pagination"], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Pending moderation posts" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + } + } + } + }, + "/moderation/posts/{postId}/approve": { + "post": { + "tags": ["Moderation"], + "summary": "Approve a pending post", + "parameters": [ + { + "name": "postId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Post approved", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "ok": { + "type": "boolean", + "const": true + }, + "postId": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "commentId": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + } + }, + "required": ["ok"], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Moderation result" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + }, + "404": { + "description": "Post not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "NOT_FOUND" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Resource not found" + } + } + } + } + } + } + }, + "/moderation/posts/{postId}/reject": { + "post": { + "tags": ["Moderation"], + "summary": "Reject a pending post", + "parameters": [ + { + "name": "postId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "reason": { + "type": "string", + "maxLength": 1000 + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Post rejected", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "ok": { + "type": "boolean", + "const": true + }, + "postId": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "commentId": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + } + }, + "required": ["ok"], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Moderation result" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + }, + "404": { + "description": "Post not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "NOT_FOUND" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Resource not found" + } + } + } + } + } + } + }, + "/moderation/comments": { + "get": { + "tags": ["Moderation"], + "summary": "List comments awaiting moderation", + "description": "Requires moderation.view scope and permission.", + "responses": { + "200": { + "description": "Pending comments", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "postId": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "content": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "principalId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": ["id"], + "additionalProperties": {} + } + }, + "meta": { + "type": "object", + "properties": { + "pagination": { + "type": "object", + "properties": { + "cursor": { + "description": "Cursor for next page, null if no more pages", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "hasMore": { + "type": "boolean", + "description": "Whether there are more items" + }, + "total": { + "description": "Total count (when available)", + "type": "number" + } + }, + "required": ["hasMore"], + "additionalProperties": false + } + }, + "required": ["pagination"], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Pending moderation comments" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + } + } + } + }, + "/moderation/comments/{commentId}/approve": { + "post": { + "tags": ["Moderation"], + "summary": "Approve a pending comment", + "parameters": [ + { + "name": "commentId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Comment approved", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "ok": { + "type": "boolean", + "const": true + }, + "postId": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "commentId": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + } + }, + "required": ["ok"], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Moderation result" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + }, + "404": { + "description": "Comment not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "NOT_FOUND" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Resource not found" + } + } + } + } + } + } + }, + "/moderation/comments/{commentId}/reject": { + "post": { + "tags": ["Moderation"], + "summary": "Reject a pending comment", + "parameters": [ + { + "name": "commentId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": false, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "reason": { + "type": "string", + "maxLength": 1000 + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Comment rejected", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "ok": { + "type": "boolean", + "const": true + }, + "postId": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "commentId": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + } + }, + "required": ["ok"], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Moderation result" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + }, + "404": { + "description": "Comment not found", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "NOT_FOUND" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Resource not found" + } + } + } + } + } + } + }, + "/mentions/suggest": { + "get": { + "tags": ["Mentions"], + "summary": "Suggest principals for @-mention typeahead", + "security": [], + "parameters": [ + { + "name": "q", + "in": "query", + "schema": { + "def": { + "type": "optional", + "innerType": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + }, + "type": "optional" + } + }, + { + "name": "scope", + "in": "query", + "schema": { + "def": { + "type": "optional", + "innerType": { + "def": { + "type": "enum", + "entries": { + "team": "team" + } + }, + "type": "enum", + "enum": { + "team": "team" + }, + "options": ["team"] + } + }, + "type": "optional" + } + } + ], + "responses": { + "200": { + "description": "Mention suggestions", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "principalId": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "displayName": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "avatarUrl": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "role": { + "type": "string" + } + }, + "required": ["principalId", "displayName", "avatarUrl", "role"], + "additionalProperties": false + } + } + } + } + }, + "403": { + "description": "Session user required" + }, + "429": { + "description": "Rate limit exceeded" + } + } + } + }, + "/users/{principalId}/card": { + "get": { + "tags": ["Users"], + "summary": "Get a principal hover-card payload", + "security": [], + "parameters": [ + { + "name": "principalId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "responses": { + "200": { + "description": "Principal card", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "principalId": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "displayName": { + "type": "string" + }, + "avatarUrl": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "role": { + "type": "string" + }, + "joinedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": ["principalId", "displayName", "avatarUrl", "role", "joinedAt"], + "additionalProperties": false + } + } + } + }, + "403": { + "description": "Session user required" + }, + "404": { + "description": "Principal not found" + } + } + } + }, + "/internal/portal-tabs": { + "get": { + "tags": ["Internal"], + "summary": "Get the effective portal tab config for the current user", + "security": [], + "responses": { + "200": { + "description": "Effective portal tab config", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "config": { + "type": "object", + "properties": { + "feedback": { + "type": "boolean" + }, + "roadmap": { + "type": "boolean" + }, + "changelog": { + "type": "boolean" + }, + "myTickets": { + "type": "boolean" + }, + "helpCenter": { + "type": "boolean" + }, + "support": { + "type": "boolean" + } + }, + "additionalProperties": false + } + }, + "required": ["config"], + "additionalProperties": false + } + } + } + }, + "401": { + "description": "Session required" + } + } + }, + "post": { + "tags": ["Internal"], + "summary": "Update organization portal tab config", + "security": [], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "config": { + "type": "object", + "properties": { + "feedback": { + "type": "boolean" + }, + "roadmap": { + "type": "boolean" + }, + "changelog": { + "type": "boolean" + }, + "myTickets": { + "type": "boolean" + }, + "helpCenter": { + "type": "boolean" + }, + "support": { + "type": "boolean" + } + } + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Portal tab config updated" + }, + "400": { + "description": "Invalid configuration" + }, + "401": { + "description": "Session required" + }, + "403": { + "description": "Admin only" + } + } + } + }, + "/help-center/categories": { + "get": { + "tags": ["Help Center"], + "summary": "List knowledge-base categories", + "description": "Each category includes its `articleCount`.", + "responses": { + "200": { + "description": "Categories", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "category_01h455vb4pex5vsknk084sn02q" + }, + "slug": { + "type": "string" + }, + "name": { + "type": "string" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "icon": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "parentId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "isPublic": { + "type": "boolean" + }, + "visibility": { + "type": "string", + "enum": ["public", "targeted"] + }, + "allowedSegmentIds": { + "type": "array", + "items": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + } + }, + "allowedPrincipalIds": { + "type": "array", + "items": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + } + }, + "position": { + "type": "number" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + }, + "articleCount": { + "type": "number" + } + }, + "required": [ + "id", + "slug", + "name", + "description", + "icon", + "parentId", + "isPublic", + "visibility", + "allowedSegmentIds", + "allowedPrincipalIds", + "position", + "createdAt", + "updatedAt", + "articleCount" + ], + "additionalProperties": false + } + }, + "meta": { + "type": "object", + "properties": { + "pagination": { + "type": "object", + "properties": { + "cursor": { + "description": "Cursor for next page, null if no more pages", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "hasMore": { + "type": "boolean", + "description": "Whether there are more items" + }, + "total": { + "description": "Total count (when available)", + "type": "number" + } + }, + "required": ["hasMore"], + "additionalProperties": false + } + }, + "required": ["pagination"], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Categories" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + } + } + }, + "post": { + "tags": ["Help Center"], + "summary": "Create a category (admin)", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + "slug": { + "type": "string", + "maxLength": 200 + }, + "description": { + "type": "string", + "maxLength": 2000 + }, + "isPublic": { + "type": "boolean" + }, + "visibility": { + "type": "string", + "enum": ["public", "targeted"] + }, + "allowedSegmentIds": { + "maxItems": 200, + "type": "array", + "items": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + } + }, + "allowedPrincipalIds": { + "maxItems": 200, + "type": "array", + "items": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + } + }, + "position": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "parentId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "icon": { + "anyOf": [ + { + "type": "string", + "maxLength": 50 + }, + { + "type": "null" + } + ] + } + }, + "required": ["name"] + } + } + } + }, + "responses": { + "201": { + "description": "Category created", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "category_01h455vb4pex5vsknk084sn02q" + }, + "slug": { + "type": "string" + }, + "name": { + "type": "string" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "icon": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "parentId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "isPublic": { + "type": "boolean" + }, + "visibility": { + "type": "string", + "enum": ["public", "targeted"] + }, + "allowedSegmentIds": { + "type": "array", + "items": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + } + }, + "allowedPrincipalIds": { + "type": "array", + "items": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + } + }, + "position": { + "type": "number" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": [ + "id", + "slug", + "name", + "description", + "icon", + "parentId", + "isPublic", + "visibility", + "allowedSegmentIds", + "allowedPrincipalIds", + "position", + "createdAt", + "updatedAt" + ], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Category" + } + } + } + } + } + } + }, + "/help-center/categories/{categoryId}": { + "get": { + "tags": ["Help Center"], + "summary": "Get a category", + "parameters": [ + { + "name": "categoryId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "responses": { + "200": { + "description": "Category", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "category_01h455vb4pex5vsknk084sn02q" + }, + "slug": { + "type": "string" + }, + "name": { + "type": "string" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "icon": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "parentId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "isPublic": { + "type": "boolean" + }, + "visibility": { + "type": "string", + "enum": ["public", "targeted"] + }, + "allowedSegmentIds": { + "type": "array", + "items": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + } + }, + "allowedPrincipalIds": { + "type": "array", + "items": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + } + }, + "position": { + "type": "number" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": [ + "id", + "slug", + "name", + "description", + "icon", + "parentId", + "isPublic", + "visibility", + "allowedSegmentIds", + "allowedPrincipalIds", + "position", + "createdAt", + "updatedAt" + ], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Category" + } + } + } + } + } + }, + "patch": { + "tags": ["Help Center"], + "summary": "Update a category (admin)", + "parameters": [ + { + "name": "categoryId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + "slug": { + "type": "string", + "maxLength": 200 + }, + "description": { + "anyOf": [ + { + "type": "string", + "maxLength": 2000 + }, + { + "type": "null" + } + ] + }, + "isPublic": { + "type": "boolean" + }, + "visibility": { + "type": "string", + "enum": ["public", "targeted"] + }, + "allowedSegmentIds": { + "maxItems": 200, + "type": "array", + "items": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + } + }, + "allowedPrincipalIds": { + "maxItems": 200, + "type": "array", + "items": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + } + }, + "position": { + "type": "integer", + "minimum": 0, + "maximum": 9007199254740991 + }, + "parentId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + }, + "icon": { + "anyOf": [ + { + "type": "string", + "maxLength": 50 + }, + { + "type": "null" + } + ] + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Updated" + } + } + }, + "delete": { + "tags": ["Help Center"], + "summary": "Delete a category (admin)", + "parameters": [ + { + "name": "categoryId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "responses": { + "204": { + "description": "Deleted" + } + } + } + }, + "/help-center/articles": { + "get": { + "tags": ["Help Center"], + "summary": "List articles", + "parameters": [ + { + "name": "categoryId", + "in": "query", + "schema": { + "def": { + "type": "optional", + "innerType": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + }, + "type": "optional" + } + }, + { + "name": "status", + "in": "query", + "schema": { + "def": { + "type": "optional", + "innerType": { + "def": { + "type": "enum", + "entries": { + "draft": "draft", + "published": "published", + "all": "all" + } + }, + "type": "enum", + "enum": { + "draft": "draft", + "published": "published", + "all": "all" + }, + "options": ["draft", "published", "all"] + } + }, + "type": "optional" + } + }, + { + "name": "search", + "in": "query", + "schema": { + "def": { + "type": "optional", + "innerType": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + }, + "type": "optional" + } + }, + { + "name": "cursor", + "in": "query", + "schema": { + "def": { + "type": "optional", + "innerType": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + }, + "type": "optional" + } + }, + { + "name": "limit", + "in": "query", + "schema": { + "def": { + "type": "optional", + "innerType": { + "def": { + "type": "number", + "coerce": true, + "checks": [{}, {}] + }, + "type": "number", + "minValue": 1, + "maxValue": 100, + "isInt": false, + "isFinite": true, + "format": null + } + }, + "type": "optional" + } + } + ], + "responses": { + "200": { + "description": "Articles", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "article_01h455vb4pex5vsknk084sn02q" + }, + "slug": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "content": { + "type": "string" + }, + "publishedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ], + "description": "ISO 8601 timestamp or null", + "example": "2024-01-15T10:30:00.000Z" + }, + "viewCount": { + "type": "number" + }, + "helpfulCount": { + "type": "number" + }, + "notHelpfulCount": { + "type": "number" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + }, + "category": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "slug": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": ["id", "slug", "name"], + "additionalProperties": false + }, + "author": { + "anyOf": [ + { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "name": { + "type": "string" + }, + "avatarUrl": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": ["id", "name", "avatarUrl"], + "additionalProperties": false + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "id", + "slug", + "title", + "description", + "content", + "publishedAt", + "viewCount", + "helpfulCount", + "notHelpfulCount", + "createdAt", + "updatedAt", + "category", + "author" + ], + "additionalProperties": false + } + }, + "meta": { + "type": "object", + "properties": { + "pagination": { + "type": "object", + "properties": { + "cursor": { + "description": "Cursor for next page, null if no more pages", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "hasMore": { + "type": "boolean", + "description": "Whether there are more items" + }, + "total": { + "description": "Total count (when available)", + "type": "number" + } + }, + "required": ["hasMore"], + "additionalProperties": false + } + }, + "required": ["pagination"], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Articles" + } + } + } + } + } + }, + "post": { + "tags": ["Help Center"], + "summary": "Create an article", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "categoryId": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "title": { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + "content": { + "type": "string", + "minLength": 1 + }, + "slug": { + "type": "string", + "maxLength": 200 + }, + "description": { + "type": "string", + "maxLength": 300 + }, + "authorId": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + } + }, + "required": ["categoryId", "title", "content"] + } + } + } + }, + "responses": { + "201": { + "description": "Article created", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "article_01h455vb4pex5vsknk084sn02q" + }, + "slug": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "content": { + "type": "string" + }, + "publishedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ], + "description": "ISO 8601 timestamp or null", + "example": "2024-01-15T10:30:00.000Z" + }, + "viewCount": { + "type": "number" + }, + "helpfulCount": { + "type": "number" + }, + "notHelpfulCount": { + "type": "number" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + }, + "category": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "slug": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": ["id", "slug", "name"], + "additionalProperties": false + }, + "author": { + "anyOf": [ + { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "name": { + "type": "string" + }, + "avatarUrl": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": ["id", "name", "avatarUrl"], + "additionalProperties": false + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "id", + "slug", + "title", + "description", + "content", + "publishedAt", + "viewCount", + "helpfulCount", + "notHelpfulCount", + "createdAt", + "updatedAt", + "category", + "author" + ], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Article" + } + } + } + } + } + } + }, + "/help-center/articles/{articleId}": { + "get": { + "tags": ["Help Center"], + "summary": "Get an article", + "parameters": [ + { + "name": "articleId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "responses": { + "200": { + "description": "Article", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "article_01h455vb4pex5vsknk084sn02q" + }, + "slug": { + "type": "string" + }, + "title": { + "type": "string" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "content": { + "type": "string" + }, + "publishedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ], + "description": "ISO 8601 timestamp or null", + "example": "2024-01-15T10:30:00.000Z" + }, + "viewCount": { + "type": "number" + }, + "helpfulCount": { + "type": "number" + }, + "notHelpfulCount": { + "type": "number" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + }, + "category": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "slug": { + "type": "string" + }, + "name": { + "type": "string" + } + }, + "required": ["id", "slug", "name"], + "additionalProperties": false + }, + "author": { + "anyOf": [ + { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "name": { + "type": "string" + }, + "avatarUrl": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + } + }, + "required": ["id", "name", "avatarUrl"], + "additionalProperties": false + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "id", + "slug", + "title", + "description", + "content", + "publishedAt", + "viewCount", + "helpfulCount", + "notHelpfulCount", + "createdAt", + "updatedAt", + "category", + "author" + ], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Article" + } + } + } + } + } + }, + "patch": { + "tags": ["Help Center"], + "summary": "Update an article", + "description": "Setting `publishedAt` to a timestamp publishes the article; setting it to null unpublishes it.", + "parameters": [ + { + "name": "articleId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "categoryId": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "title": { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + "content": { + "type": "string", + "minLength": 1 + }, + "slug": { + "type": "string", + "maxLength": 200 + }, + "description": { + "type": "string", + "maxLength": 300 + }, + "publishedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ] + }, + "authorId": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Updated" + } + } + }, + "delete": { + "tags": ["Help Center"], + "summary": "Soft-delete an article", + "parameters": [ + { + "name": "articleId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "responses": { + "204": { + "description": "Deleted" + } + } + } + }, + "/help-center/articles/{articleId}/feedback": { + "post": { + "tags": ["Help Center"], + "summary": "Record helpful / not-helpful feedback on an article", + "parameters": [ + { + "name": "articleId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "helpful": { + "type": "boolean" + } + }, + "required": ["helpful"] + } + } + } + }, + "responses": { + "200": { + "description": "Recorded", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "success": { + "type": "boolean" + } + }, + "required": ["success"], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Result" + } + } + } + } + } + } + }, + "/segments": { + "get": { + "tags": ["Segments"], + "summary": "List audience segments (with member counts)", + "description": "Requires the `segment.view` scope/permission.", + "responses": { + "200": { + "description": "Segments", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "segment_01h455vb4pex5vsknk084sn02q" + }, + "name": { + "type": "string" + }, + "slug": { + "type": "string" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "type": { + "type": "string", + "enum": ["manual", "dynamic"] + }, + "color": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "rules": { + "anyOf": [ + { + "type": "object", + "properties": { + "match": { + "type": "string", + "enum": ["all", "any"] + }, + "conditions": { + "type": "array", + "items": { + "type": "object", + "properties": { + "attribute": { + "type": "string" + }, + "operator": { + "type": "string", + "enum": [ + "eq", + "neq", + "lt", + "lte", + "gt", + "gte", + "contains", + "starts_with", + "ends_with", + "in", + "is_set", + "is_not_set" + ] + }, + "value": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "array", + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "number" + } + ] + } + } + ] + }, + "metadataKey": { + "type": "string" + } + }, + "required": ["attribute", "operator"], + "additionalProperties": false + } + } + }, + "required": ["match", "conditions"], + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "evaluationSchedule": {}, + "weightConfig": {}, + "memberCount": { + "type": "number" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": [ + "id", + "name", + "slug", + "description", + "type", + "color", + "rules", + "evaluationSchedule", + "weightConfig", + "createdAt", + "updatedAt" + ], + "additionalProperties": false + } + }, + "meta": { + "type": "object", + "properties": { + "pagination": { + "type": "object", + "properties": { + "cursor": { + "description": "Cursor for next page, null if no more pages", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "hasMore": { + "type": "boolean", + "description": "Whether there are more items" + }, + "total": { + "description": "Total count (when available)", + "type": "number" + } + }, + "required": ["hasMore"], + "additionalProperties": false + } + }, + "required": ["pagination"], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Segments" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + }, + "403": { + "description": "segment.view permission required" + } + } + }, + "post": { + "tags": ["Segments"], + "summary": "Create a segment (manual or dynamic)", + "description": "Requires the `segment.manage` scope/permission.", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1 + }, + "description": { + "type": "string" + }, + "type": { + "type": "string", + "enum": ["manual", "dynamic"] + }, + "color": { + "type": "string" + }, + "rules": { + "type": "object", + "properties": { + "match": { + "type": "string", + "enum": ["all", "any"] + }, + "conditions": { + "type": "array", + "items": { + "type": "object", + "properties": { + "attribute": { + "type": "string" + }, + "operator": { + "type": "string", + "enum": [ + "eq", + "neq", + "lt", + "lte", + "gt", + "gte", + "contains", + "starts_with", + "ends_with", + "in", + "is_set", + "is_not_set" + ] + }, + "value": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "array", + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "number" + } + ] + } + } + ] + }, + "metadataKey": { + "type": "string" + } + }, + "required": ["attribute", "operator"] + } + } + }, + "required": ["match", "conditions"] + }, + "evaluationSchedule": {}, + "weightConfig": {} + }, + "required": ["name", "type"] + } + } + } + }, + "responses": { + "201": { + "description": "Segment created", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "segment_01h455vb4pex5vsknk084sn02q" + }, + "name": { + "type": "string" + }, + "slug": { + "type": "string" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "type": { + "type": "string", + "enum": ["manual", "dynamic"] + }, + "color": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "rules": { + "anyOf": [ + { + "type": "object", + "properties": { + "match": { + "type": "string", + "enum": ["all", "any"] + }, + "conditions": { + "type": "array", + "items": { + "type": "object", + "properties": { + "attribute": { + "type": "string" + }, + "operator": { + "type": "string", + "enum": [ + "eq", + "neq", + "lt", + "lte", + "gt", + "gte", + "contains", + "starts_with", + "ends_with", + "in", + "is_set", + "is_not_set" + ] + }, + "value": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "array", + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "number" + } + ] + } + } + ] + }, + "metadataKey": { + "type": "string" + } + }, + "required": ["attribute", "operator"], + "additionalProperties": false + } + } + }, + "required": ["match", "conditions"], + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "evaluationSchedule": {}, + "weightConfig": {}, + "memberCount": { + "type": "number" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": [ + "id", + "name", + "slug", + "description", + "type", + "color", + "rules", + "evaluationSchedule", + "weightConfig", + "createdAt", + "updatedAt" + ], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Segment" + } + } + } + }, + "403": { + "description": "segment.manage permission required" + } + } + } + }, + "/segments/{segmentId}": { + "get": { + "tags": ["Segments"], + "summary": "Get a segment", + "parameters": [ + { + "name": "segmentId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "responses": { + "200": { + "description": "Segment", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "segment_01h455vb4pex5vsknk084sn02q" + }, + "name": { + "type": "string" + }, + "slug": { + "type": "string" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "type": { + "type": "string", + "enum": ["manual", "dynamic"] + }, + "color": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "rules": { + "anyOf": [ + { + "type": "object", + "properties": { + "match": { + "type": "string", + "enum": ["all", "any"] + }, + "conditions": { + "type": "array", + "items": { + "type": "object", + "properties": { + "attribute": { + "type": "string" + }, + "operator": { + "type": "string", + "enum": [ + "eq", + "neq", + "lt", + "lte", + "gt", + "gte", + "contains", + "starts_with", + "ends_with", + "in", + "is_set", + "is_not_set" + ] + }, + "value": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "array", + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "number" + } + ] + } + } + ] + }, + "metadataKey": { + "type": "string" + } + }, + "required": ["attribute", "operator"], + "additionalProperties": false + } + } + }, + "required": ["match", "conditions"], + "additionalProperties": false + }, + { + "type": "null" + } + ] + }, + "evaluationSchedule": {}, + "weightConfig": {}, + "memberCount": { + "type": "number" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": [ + "id", + "name", + "slug", + "description", + "type", + "color", + "rules", + "evaluationSchedule", + "weightConfig", + "createdAt", + "updatedAt" + ], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Segment" + } + } + } + }, + "404": { + "description": "Segment not found" + } + } + }, + "patch": { + "tags": ["Segments"], + "summary": "Update a segment", + "description": "Requires the `segment.manage` scope/permission.", + "parameters": [ + { + "name": "segmentId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1 + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "color": { + "type": "string" + }, + "rules": { + "anyOf": [ + { + "type": "object", + "properties": { + "match": { + "type": "string", + "enum": ["all", "any"] + }, + "conditions": { + "type": "array", + "items": { + "type": "object", + "properties": { + "attribute": { + "type": "string" + }, + "operator": { + "type": "string", + "enum": [ + "eq", + "neq", + "lt", + "lte", + "gt", + "gte", + "contains", + "starts_with", + "ends_with", + "in", + "is_set", + "is_not_set" + ] + }, + "value": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "number" + }, + { + "type": "boolean" + }, + { + "type": "array", + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "number" + } + ] + } + } + ] + }, + "metadataKey": { + "type": "string" + } + }, + "required": ["attribute", "operator"] + } + } + }, + "required": ["match", "conditions"] + }, + { + "type": "null" + } + ] + }, + "evaluationSchedule": {}, + "weightConfig": {} + } + } + } + } + }, + "responses": { + "200": { + "description": "Updated" + } + } + }, + "delete": { + "tags": ["Segments"], + "summary": "Soft-delete a segment", + "description": "Requires the `segment.manage` scope/permission.", + "parameters": [ + { + "name": "segmentId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "responses": { + "204": { + "description": "Deleted" + } + } + } + }, + "/segments/{slug}/members": { + "post": { + "tags": ["Segments"], + "summary": "Add principals to a segment (by slug)", + "description": "Batch add up to 1000 principal IDs. Unknown / soft-deleted principals are reported back in `failed`.", + "parameters": [ + { + "name": "slug", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "principalIds": { + "minItems": 1, + "maxItems": 1000, + "type": "array", + "items": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + } + } + }, + "required": ["principalIds"] + } + } + } + }, + "responses": { + "200": { + "description": "Result counts", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "added": { + "type": "number" + }, + "failed": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": ["added", "failed"], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Add result" + } + } + } + }, + "404": { + "description": "Segment not found" + } + } + }, + "delete": { + "tags": ["Segments"], + "summary": "Remove principals from a segment (by slug)", + "description": "Batch remove up to 1000 principal IDs. Unknown / failed principals are reported back in `failed`.", + "parameters": [ + { + "name": "slug", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "principalIds": { + "minItems": 1, + "maxItems": 1000, + "type": "array", + "items": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + } + } + }, + "required": ["principalIds"] + } + } + } + }, + "responses": { + "200": { + "description": "Result counts", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "removed": { + "type": "number" + }, + "failed": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": ["removed", "failed"], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Remove result" + } + } + } + }, + "404": { + "description": "Segment not found" + } + } + } + }, + "/user-attributes": { + "get": { + "tags": ["User Attributes"], + "summary": "List custom user-attribute definitions", + "description": "Requires the `user_attribute.view` scope/permission.", + "responses": { + "200": { + "description": "User attributes", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "user_attr_01h455vb4pex5vsknk084sn02q" + }, + "key": { + "type": "string" + }, + "label": { + "type": "string" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "type": { + "type": "string", + "enum": ["string", "number", "boolean", "date", "currency"] + }, + "currencyCode": { + "anyOf": [ + { + "type": "string", + "enum": [ + "USD", + "EUR", + "GBP", + "JPY", + "CAD", + "AUD", + "CHF", + "CNY", + "INR", + "BRL" + ] + }, + { + "type": "null" + } + ] + }, + "externalKey": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": [ + "id", + "key", + "label", + "description", + "type", + "currencyCode", + "externalKey", + "createdAt", + "updatedAt" + ], + "additionalProperties": false + } + }, + "meta": { + "type": "object", + "properties": { + "pagination": { + "type": "object", + "properties": { + "cursor": { + "description": "Cursor for next page, null if no more pages", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "hasMore": { + "type": "boolean", + "description": "Whether there are more items" + }, + "total": { + "description": "Total count (when available)", + "type": "number" + } + }, + "required": ["hasMore"], + "additionalProperties": false + } + }, + "required": ["pagination"], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "User attributes" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + }, + "403": { + "description": "user_attribute.view permission required" + } + } + }, + "post": { + "tags": ["User Attributes"], + "summary": "Create a custom user-attribute definition", + "description": "Requires the `user_attribute.manage` scope/permission.", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "key": { + "type": "string", + "minLength": 1, + "maxLength": 100 + }, + "label": { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + "description": { + "anyOf": [ + { + "type": "string", + "maxLength": 1000 + }, + { + "type": "null" + } + ] + }, + "type": { + "type": "string", + "enum": ["string", "number", "boolean", "date", "currency"] + }, + "currencyCode": { + "anyOf": [ + { + "type": "string", + "enum": [ + "USD", + "EUR", + "GBP", + "JPY", + "CAD", + "AUD", + "CHF", + "CNY", + "INR", + "BRL" + ] + }, + { + "type": "null" + } + ] + }, + "externalKey": { + "anyOf": [ + { + "type": "string", + "maxLength": 200 + }, + { + "type": "null" + } + ] + } + }, + "required": ["key", "label", "type"] + } + } + } + }, + "responses": { + "201": { + "description": "User attribute created", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "user_attr_01h455vb4pex5vsknk084sn02q" + }, + "key": { + "type": "string" + }, + "label": { + "type": "string" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "type": { + "type": "string", + "enum": ["string", "number", "boolean", "date", "currency"] + }, + "currencyCode": { + "anyOf": [ + { + "type": "string", + "enum": [ + "USD", + "EUR", + "GBP", + "JPY", + "CAD", + "AUD", + "CHF", + "CNY", + "INR", + "BRL" + ] + }, + { + "type": "null" + } + ] + }, + "externalKey": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": [ + "id", + "key", + "label", + "description", + "type", + "currencyCode", + "externalKey", + "createdAt", + "updatedAt" + ], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "User attribute" + } + } + } + }, + "403": { + "description": "user_attribute.manage permission required" + } + } + } + }, + "/user-attributes/{attributeId}": { + "get": { + "tags": ["User Attributes"], + "summary": "Get a user-attribute definition", + "parameters": [ + { + "name": "attributeId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "responses": { + "200": { + "description": "User attribute", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "user_attr_01h455vb4pex5vsknk084sn02q" + }, + "key": { + "type": "string" + }, + "label": { + "type": "string" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "type": { + "type": "string", + "enum": ["string", "number", "boolean", "date", "currency"] + }, + "currencyCode": { + "anyOf": [ + { + "type": "string", + "enum": [ + "USD", + "EUR", + "GBP", + "JPY", + "CAD", + "AUD", + "CHF", + "CNY", + "INR", + "BRL" + ] + }, + { + "type": "null" + } + ] + }, + "externalKey": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": [ + "id", + "key", + "label", + "description", + "type", + "currencyCode", + "externalKey", + "createdAt", + "updatedAt" + ], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "User attribute" + } + } + } + }, + "404": { + "description": "User attribute not found" + } + } + }, + "patch": { + "tags": ["User Attributes"], + "summary": "Update a user-attribute definition", + "description": "Requires the `user_attribute.manage` scope/permission.", + "parameters": [ + { + "name": "attributeId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "label": { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + "description": { + "anyOf": [ + { + "type": "string", + "maxLength": 1000 + }, + { + "type": "null" + } + ] + }, + "type": { + "type": "string", + "enum": ["string", "number", "boolean", "date", "currency"] + }, + "currencyCode": { + "anyOf": [ + { + "type": "string", + "enum": [ + "USD", + "EUR", + "GBP", + "JPY", + "CAD", + "AUD", + "CHF", + "CNY", + "INR", + "BRL" + ] + }, + { + "type": "null" + } + ] + }, + "externalKey": { + "anyOf": [ + { + "type": "string", + "maxLength": 200 + }, + { + "type": "null" + } + ] + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Updated" + } + } + }, + "delete": { + "tags": ["User Attributes"], + "summary": "Delete a user-attribute definition", + "description": "Requires the `user_attribute.manage` scope/permission.", + "parameters": [ + { + "name": "attributeId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "responses": { + "204": { + "description": "Deleted" + } + } + } + }, + "/changelog/visibility": { + "get": { + "tags": ["Changelog Visibility"], + "summary": "Read org-level changelog visibility config", + "responses": { + "200": { + "description": "Org visibility config", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "restrictCategories": { + "type": "boolean" + }, + "allowedCategoryIds": { + "maxItems": 500, + "type": "array", + "items": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + } + }, + "restrictProducts": { + "type": "boolean" + }, + "allowedProductIds": { + "maxItems": 500, + "type": "array", + "items": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + } + } + }, + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Config" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + } + } + }, + "put": { + "tags": ["Changelog Visibility"], + "summary": "Replace org-level changelog visibility config (admin)", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "restrictCategories": { + "type": "boolean" + }, + "allowedCategoryIds": { + "maxItems": 500, + "type": "array", + "items": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + } + }, + "restrictProducts": { + "type": "boolean" + }, + "allowedProductIds": { + "maxItems": 500, + "type": "array", + "items": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + } + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Updated config", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "restrictCategories": { + "type": "boolean" + }, + "allowedCategoryIds": { + "maxItems": 500, + "type": "array", + "items": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + } + }, + "restrictProducts": { + "type": "boolean" + }, + "allowedProductIds": { + "maxItems": 500, + "type": "array", + "items": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + } + } + }, + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Config" + } + } + } + } + } + } + }, + "/changelog/visibility/segments": { + "get": { + "tags": ["Changelog Visibility"], + "summary": "List every per-segment visibility override", + "responses": { + "200": { + "description": "Segment overrides", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "segmentId": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "config": { + "type": "object", + "properties": { + "restrictCategories": { + "type": "boolean" + }, + "allowedCategoryIds": { + "maxItems": 500, + "type": "array", + "items": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + } + }, + "restrictProducts": { + "type": "boolean" + }, + "allowedProductIds": { + "maxItems": 500, + "type": "array", + "items": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + } + } + }, + "additionalProperties": false + }, + "segmentName": { + "type": "string" + } + }, + "required": ["segmentId", "config", "segmentName"], + "additionalProperties": false + } + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Overrides" + } + } + } + } + } + } + }, + "/changelog/visibility/segments/{segmentId}": { + "get": { + "tags": ["Changelog Visibility"], + "summary": "Read one per-segment override", + "parameters": [ + { + "name": "segmentId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "responses": { + "200": { + "description": "Override", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "segmentId": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "config": { + "type": "object", + "properties": { + "restrictCategories": { + "type": "boolean" + }, + "allowedCategoryIds": { + "maxItems": 500, + "type": "array", + "items": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + } + }, + "restrictProducts": { + "type": "boolean" + }, + "allowedProductIds": { + "maxItems": 500, + "type": "array", + "items": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + } + } + }, + "additionalProperties": false + } + }, + "required": ["segmentId", "config"], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Override" + } + } + } + }, + "404": { + "description": "Override not found" + } + } + }, + "put": { + "tags": ["Changelog Visibility"], + "summary": "Upsert one per-segment override (admin)", + "parameters": [ + { + "name": "segmentId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "restrictCategories": { + "type": "boolean" + }, + "allowedCategoryIds": { + "maxItems": 500, + "type": "array", + "items": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + } + }, + "restrictProducts": { + "type": "boolean" + }, + "allowedProductIds": { + "maxItems": 500, + "type": "array", + "items": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + } + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Upserted override", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "segmentId": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "config": { + "type": "object", + "properties": { + "restrictCategories": { + "type": "boolean" + }, + "allowedCategoryIds": { + "maxItems": 500, + "type": "array", + "items": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + } + }, + "restrictProducts": { + "type": "boolean" + }, + "allowedProductIds": { + "maxItems": 500, + "type": "array", + "items": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + } + } + }, + "additionalProperties": false + } + }, + "required": ["segmentId", "config"], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Override" + } + } + } + } + } + }, + "delete": { + "tags": ["Changelog Visibility"], + "summary": "Remove one per-segment override (admin)", + "parameters": [ + { + "name": "segmentId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "responses": { + "204": { + "description": "Removed" + } + } + } + }, + "/portal-tabs": { + "get": { + "tags": ["Portal Tabs"], + "summary": "Read org-level portal tab config", + "responses": { + "200": { + "description": "Org portal tab config", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "feedback": { + "type": "boolean" + }, + "roadmap": { + "type": "boolean" + }, + "changelog": { + "type": "boolean" + }, + "myTickets": { + "type": "boolean" + }, + "helpCenter": { + "type": "boolean" + }, + "support": { + "type": "boolean" + } + }, + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Config" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + } + } + }, + "put": { + "tags": ["Portal Tabs"], + "summary": "Replace org-level portal tab config", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "feedback": { + "type": "boolean" + }, + "roadmap": { + "type": "boolean" + }, + "changelog": { + "type": "boolean" + }, + "myTickets": { + "type": "boolean" + }, + "helpCenter": { + "type": "boolean" + }, + "support": { + "type": "boolean" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Updated config", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "feedback": { + "type": "boolean" + }, + "roadmap": { + "type": "boolean" + }, + "changelog": { + "type": "boolean" + }, + "myTickets": { + "type": "boolean" + }, + "helpCenter": { + "type": "boolean" + }, + "support": { + "type": "boolean" + } + }, + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Config" + } + } + } + } + } + } + }, + "/portal-tabs/segments": { + "get": { + "tags": ["Portal Tabs"], + "summary": "List every per-segment portal tab override", + "responses": { + "200": { + "description": "Segment overrides", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "segmentId": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "segmentName": { + "type": "string" + }, + "overrides": { + "type": "object", + "properties": { + "feedback": { + "type": "boolean" + }, + "roadmap": { + "type": "boolean" + }, + "changelog": { + "type": "boolean" + }, + "myTickets": { + "type": "boolean" + }, + "helpCenter": { + "type": "boolean" + }, + "support": { + "type": "boolean" + } + }, + "additionalProperties": false + } + }, + "required": ["segmentId", "segmentName", "overrides"], + "additionalProperties": false + } + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Overrides" + } + } + } + } + } + } + }, + "/portal-tabs/segments/{segmentId}": { + "get": { + "tags": ["Portal Tabs"], + "summary": "Read one per-segment portal tab override", + "parameters": [ + { + "name": "segmentId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "responses": { + "200": { + "description": "Override", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "feedback": { + "type": "boolean" + }, + "roadmap": { + "type": "boolean" + }, + "changelog": { + "type": "boolean" + }, + "myTickets": { + "type": "boolean" + }, + "helpCenter": { + "type": "boolean" + }, + "support": { + "type": "boolean" + } + }, + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Override" + } + } + } + }, + "404": { + "description": "Override not found" + } + } + }, + "put": { + "tags": ["Portal Tabs"], + "summary": "Upsert one per-segment portal tab override", + "parameters": [ + { + "name": "segmentId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "feedback": { + "type": "boolean" + }, + "roadmap": { + "type": "boolean" + }, + "changelog": { + "type": "boolean" + }, + "myTickets": { + "type": "boolean" + }, + "helpCenter": { + "type": "boolean" + }, + "support": { + "type": "boolean" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Upserted override", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "feedback": { + "type": "boolean" + }, + "roadmap": { + "type": "boolean" + }, + "changelog": { + "type": "boolean" + }, + "myTickets": { + "type": "boolean" + }, + "helpCenter": { + "type": "boolean" + }, + "support": { + "type": "boolean" + } + }, + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Override" + } + } + } + } + } + }, + "delete": { + "tags": ["Portal Tabs"], + "summary": "Remove one per-segment portal tab override (revert to org defaults)", + "parameters": [ + { + "name": "segmentId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "responses": { + "204": { + "description": "Removed" + } + } + } + }, + "/widget-profiles": { + "get": { + "tags": ["Widget Profiles"], + "summary": "List widget applications (each with its environment profiles)", + "description": "Requires the `widget.view` scope/permission.", + "responses": { + "200": { + "description": "Widget applications", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "widget_app_01h455vb4pex5vsknk084sn02q" + }, + "key": { + "type": "string" + }, + "name": { + "type": "string" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "archivedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ], + "description": "ISO 8601 timestamp or null", + "example": "2024-01-15T10:30:00.000Z" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + }, + "profiles": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "widget_profile_01h455vb4pex5vsknk084sn02q" + }, + "applicationId": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "environment": { + "type": "string", + "example": "production" + }, + "displayName": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "allowedOrigins": { + "type": "array", + "items": { + "type": "string" + } + }, + "configOverrides": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + "contentFilters": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + "supportConfig": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + "archivedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ], + "description": "ISO 8601 timestamp or null", + "example": "2024-01-15T10:30:00.000Z" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": [ + "id", + "applicationId", + "environment", + "displayName", + "enabled", + "allowedOrigins", + "configOverrides", + "contentFilters", + "supportConfig", + "archivedAt", + "createdAt", + "updatedAt" + ], + "additionalProperties": false + } + } + }, + "required": [ + "id", + "key", + "name", + "description", + "archivedAt", + "createdAt", + "updatedAt", + "profiles" + ], + "additionalProperties": false + } + }, + "meta": { + "type": "object", + "properties": { + "pagination": { + "type": "object", + "properties": { + "cursor": { + "description": "Cursor for next page, null if no more pages", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "hasMore": { + "type": "boolean", + "description": "Whether there are more items" + }, + "total": { + "description": "Total count (when available)", + "type": "number" + } + }, + "required": ["hasMore"], + "additionalProperties": false + } + }, + "required": ["pagination"], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Applications" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + }, + "403": { + "description": "widget.view permission required" + } + } + }, + "post": { + "tags": ["Widget Profiles"], + "summary": "Create or update a widget application", + "description": "Requires the `widget.manage` scope/permission. When `id` is supplied the matching application is updated; otherwise a new one is created.", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "key": { + "type": "string", + "minLength": 1, + "maxLength": 120 + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + "description": { + "anyOf": [ + { + "type": "string", + "maxLength": 1000 + }, + { + "type": "null" + } + ] + } + }, + "required": ["key", "name"] + } + } + } + }, + "responses": { + "201": { + "description": "Application created or updated", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "widget_app_01h455vb4pex5vsknk084sn02q" + }, + "key": { + "type": "string" + }, + "name": { + "type": "string" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "archivedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ], + "description": "ISO 8601 timestamp or null", + "example": "2024-01-15T10:30:00.000Z" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + }, + "profiles": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "widget_profile_01h455vb4pex5vsknk084sn02q" + }, + "applicationId": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "environment": { + "type": "string", + "example": "production" + }, + "displayName": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "allowedOrigins": { + "type": "array", + "items": { + "type": "string" + } + }, + "configOverrides": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + "contentFilters": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + "supportConfig": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + "archivedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ], + "description": "ISO 8601 timestamp or null", + "example": "2024-01-15T10:30:00.000Z" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": [ + "id", + "applicationId", + "environment", + "displayName", + "enabled", + "allowedOrigins", + "configOverrides", + "contentFilters", + "supportConfig", + "archivedAt", + "createdAt", + "updatedAt" + ], + "additionalProperties": false + } + } + }, + "required": [ + "id", + "key", + "name", + "description", + "archivedAt", + "createdAt", + "updatedAt", + "profiles" + ], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Application" + } + } + } + }, + "403": { + "description": "widget.manage permission required" + } + } + } + }, + "/widget-profiles/environments": { + "post": { + "tags": ["Widget Profiles"], + "summary": "Create or update a widget environment profile", + "description": "Requires the `widget.manage` scope/permission. When `id` is supplied the matching profile is updated; otherwise a new one is created. The environment is normalized and an absent `displayName` defaults to it.", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "applicationId": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "environment": { + "type": "string", + "minLength": 1, + "maxLength": 80 + }, + "displayName": { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + "enabled": { + "type": "boolean" + }, + "allowedOrigins": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 300 + } + }, + "configOverrides": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + "contentFilters": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + "supportConfig": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + "required": ["applicationId", "environment"] + } + } + } + }, + "responses": { + "201": { + "description": "Environment profile created or updated", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "widget_profile_01h455vb4pex5vsknk084sn02q" + }, + "applicationId": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "environment": { + "type": "string", + "example": "production" + }, + "displayName": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "allowedOrigins": { + "type": "array", + "items": { + "type": "string" + } + }, + "configOverrides": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + "contentFilters": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + "supportConfig": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + "archivedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ], + "description": "ISO 8601 timestamp or null", + "example": "2024-01-15T10:30:00.000Z" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": [ + "id", + "applicationId", + "environment", + "displayName", + "enabled", + "allowedOrigins", + "configOverrides", + "contentFilters", + "supportConfig", + "archivedAt", + "createdAt", + "updatedAt" + ], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Profile" + } + } + } + }, + "403": { + "description": "widget.manage permission required" + } + } + } + }, + "/widget-profiles/{applicationId}/environments": { + "post": { + "tags": ["Widget Profiles"], + "summary": "Create a widget environment profile for an application", + "parameters": [ + { + "name": "applicationId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "environment": { + "type": "string", + "minLength": 1, + "maxLength": 80 + }, + "displayName": { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + "enabled": { + "type": "boolean" + }, + "allowedOrigins": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 300 + } + }, + "configOverrides": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + "contentFilters": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + "supportConfig": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + "required": ["environment"] + } + } + } + }, + "responses": { + "201": { + "description": "Environment profile created", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "widget_profile_01h455vb4pex5vsknk084sn02q" + }, + "applicationId": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "environment": { + "type": "string" + }, + "displayName": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "allowedOrigins": { + "type": "array", + "items": { + "type": "string" + } + }, + "configOverrides": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + "contentFilters": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + "supportConfig": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + "archivedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ], + "description": "ISO 8601 timestamp or null", + "example": "2024-01-15T10:30:00.000Z" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": [ + "id", + "applicationId", + "environment", + "displayName", + "enabled", + "allowedOrigins", + "configOverrides", + "contentFilters", + "supportConfig", + "archivedAt", + "createdAt", + "updatedAt" + ], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Profile" + } + } + } + }, + "400": { + "description": "Validation error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "VALIDATION_ERROR" + }, + "message": { + "type": "string" + }, + "details": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Request validation failed" + } + } + } + } + } + }, + "put": { + "tags": ["Widget Profiles"], + "summary": "Create or update a widget environment profile for an application", + "parameters": [ + { + "name": "applicationId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "environment": { + "type": "string", + "minLength": 1, + "maxLength": 80 + }, + "displayName": { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + "enabled": { + "type": "boolean" + }, + "allowedOrigins": { + "type": "array", + "items": { + "type": "string", + "minLength": 1, + "maxLength": 300 + } + }, + "configOverrides": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + "contentFilters": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + "supportConfig": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + "required": ["environment"] + } + } + } + }, + "responses": { + "200": { + "description": "Environment profile upserted", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "widget_profile_01h455vb4pex5vsknk084sn02q" + }, + "applicationId": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "environment": { + "type": "string" + }, + "displayName": { + "type": "string" + }, + "enabled": { + "type": "boolean" + }, + "allowedOrigins": { + "type": "array", + "items": { + "type": "string" + } + }, + "configOverrides": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + "contentFilters": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + "supportConfig": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + "archivedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ], + "description": "ISO 8601 timestamp or null", + "example": "2024-01-15T10:30:00.000Z" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": [ + "id", + "applicationId", + "environment", + "displayName", + "enabled", + "allowedOrigins", + "configOverrides", + "contentFilters", + "supportConfig", + "archivedAt", + "createdAt", + "updatedAt" + ], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Profile" + } + } + } + }, + "400": { + "description": "Validation error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "VALIDATION_ERROR" + }, + "message": { + "type": "string" + }, + "details": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Request validation failed" + } + } + } + } + } + } + }, + "/inboxes/{inboxId}/memberships": { + "get": { + "tags": ["Support Config"], + "summary": "List inbox memberships", + "parameters": [ + { + "name": "inboxId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "responses": { + "200": { + "description": "Inbox memberships" + } + } + }, + "post": { + "tags": ["Support Config"], + "summary": "Add an inbox membership", + "parameters": [ + { + "name": "inboxId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "principalId": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "role": { + "type": "string", + "enum": ["owner", "agent", "viewer"] + } + }, + "required": ["principalId", "role"] + } + } + } + }, + "responses": { + "201": { + "description": "Membership created" + }, + "400": { + "description": "Validation error", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "VALIDATION_ERROR" + }, + "message": { + "type": "string" + }, + "details": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Request validation failed" + } + } + } + } + } + } + }, + "/inboxes/{inboxId}/memberships/{membershipId}": { + "patch": { + "tags": ["Support Config"], + "summary": "Update an inbox membership role", + "parameters": [ + { + "name": "inboxId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + }, + { + "name": "membershipId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "role": { + "type": "string", + "enum": ["owner", "agent", "viewer"] + } + }, + "required": ["role"] + } + } + } + }, + "responses": { + "200": { + "description": "Membership updated" + } + } + }, + "delete": { + "tags": ["Support Config"], + "summary": "Remove an inbox membership", + "parameters": [ + { + "name": "inboxId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + }, + { + "name": "membershipId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "responses": { + "204": { + "description": "Membership removed" + } + } + } + }, + "/inboxes/{inboxId}/channels/{channelId}": { + "patch": { + "tags": ["Support Config"], + "summary": "Update an inbox channel", + "parameters": [ + { + "name": "inboxId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + }, + { + "name": "channelId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "label": { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + "config": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": {} + }, + "externalId": { + "anyOf": [ + { + "type": "string", + "maxLength": 200 + }, + { + "type": "null" + } + ] + }, + "enabled": { + "type": "boolean" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Channel updated" + } + } + }, + "delete": { + "tags": ["Support Config"], + "summary": "Archive an inbox channel", + "parameters": [ + { + "name": "inboxId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + }, + { + "name": "channelId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "responses": { + "204": { + "description": "Channel archived" + } + } + } + }, + "/teams": { + "get": { + "tags": ["Teams"], + "summary": "List teams", + "description": "Requires the `team.view` scope/permission. Pass `?includeArchived=true` to include archived teams.", + "parameters": [ + { + "name": "includeArchived", + "in": "query", + "schema": { + "def": { + "type": "optional", + "innerType": { + "def": { + "type": "enum", + "entries": { + "true": "true", + "false": "false" + } + }, + "type": "enum", + "enum": { + "true": "true", + "false": "false" + }, + "options": ["true", "false"] + } + }, + "type": "optional" + } + } + ], + "responses": { + "200": { + "description": "Teams", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "team_01h455vb4pex5vsknk084sn02q" + }, + "slug": { + "type": "string" + }, + "name": { + "type": "string" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "shortLabel": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "color": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "archivedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ], + "description": "ISO 8601 timestamp or null", + "example": "2024-01-15T10:30:00.000Z" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": [ + "id", + "slug", + "name", + "description", + "shortLabel", + "color", + "archivedAt", + "createdAt", + "updatedAt" + ], + "additionalProperties": false + } + }, + "meta": { + "type": "object", + "properties": { + "pagination": { + "type": "object", + "properties": { + "cursor": { + "description": "Cursor for next page, null if no more pages", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "hasMore": { + "type": "boolean", + "description": "Whether there are more items" + }, + "total": { + "description": "Total count (when available)", + "type": "number" + } + }, + "required": ["hasMore"], + "additionalProperties": false + } + }, + "required": ["pagination"], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Teams" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + }, + "403": { + "description": "team.view permission required" + } + } + }, + "post": { + "tags": ["Teams"], + "summary": "Create a team", + "description": "Requires the `team.manage` scope/permission.", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "slug": { + "type": "string", + "pattern": "^[a-z0-9](?:[a-z0-9-]{0,62}[a-z0-9])?$" + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + "description": { + "anyOf": [ + { + "type": "string", + "maxLength": 1000 + }, + { + "type": "null" + } + ] + }, + "shortLabel": { + "anyOf": [ + { + "type": "string", + "maxLength": 40 + }, + { + "type": "null" + } + ] + }, + "color": { + "anyOf": [ + { + "type": "string", + "maxLength": 16 + }, + { + "type": "null" + } + ] + } + }, + "required": ["slug", "name"] + } + } + } + }, + "responses": { + "201": { + "description": "Team created", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "team_01h455vb4pex5vsknk084sn02q" + }, + "slug": { + "type": "string" + }, + "name": { + "type": "string" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "shortLabel": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "color": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "archivedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ], + "description": "ISO 8601 timestamp or null", + "example": "2024-01-15T10:30:00.000Z" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": [ + "id", + "slug", + "name", + "description", + "shortLabel", + "color", + "archivedAt", + "createdAt", + "updatedAt" + ], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Team" + } + } + } + }, + "403": { + "description": "team.manage permission required" + } + } + } + }, + "/teams/{teamId}": { + "get": { + "tags": ["Teams"], + "summary": "Get a team", + "parameters": [ + { + "name": "teamId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "responses": { + "200": { + "description": "Team", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "team_01h455vb4pex5vsknk084sn02q" + }, + "slug": { + "type": "string" + }, + "name": { + "type": "string" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "shortLabel": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "color": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "archivedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ], + "description": "ISO 8601 timestamp or null", + "example": "2024-01-15T10:30:00.000Z" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": [ + "id", + "slug", + "name", + "description", + "shortLabel", + "color", + "archivedAt", + "createdAt", + "updatedAt" + ], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Team" + } + } + } + }, + "404": { + "description": "Team not found" + } + } + }, + "patch": { + "tags": ["Teams"], + "summary": "Update a team", + "description": "Requires the `team.manage` scope/permission.", + "parameters": [ + { + "name": "teamId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + "description": { + "anyOf": [ + { + "type": "string", + "maxLength": 1000 + }, + { + "type": "null" + } + ] + }, + "shortLabel": { + "anyOf": [ + { + "type": "string", + "maxLength": 40 + }, + { + "type": "null" + } + ] + }, + "color": { + "anyOf": [ + { + "type": "string", + "maxLength": 16 + }, + { + "type": "null" + } + ] + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Updated" + } + } + }, + "delete": { + "tags": ["Teams"], + "summary": "Archive a team (use POST /unarchive to restore)", + "description": "Requires the `team.manage` scope/permission.", + "parameters": [ + { + "name": "teamId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "responses": { + "204": { + "description": "Archived" + } + } + } + }, + "/teams/{teamId}/unarchive": { + "post": { + "tags": ["Teams"], + "summary": "Restore an archived team", + "description": "Requires the `team.manage` scope/permission.", + "parameters": [ + { + "name": "teamId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "responses": { + "200": { + "description": "Restored", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "team_01h455vb4pex5vsknk084sn02q" + }, + "slug": { + "type": "string" + }, + "name": { + "type": "string" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "shortLabel": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "color": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "archivedAt": { + "anyOf": [ + { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$" + }, + { + "type": "null" + } + ], + "description": "ISO 8601 timestamp or null", + "example": "2024-01-15T10:30:00.000Z" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": [ + "id", + "slug", + "name", + "description", + "shortLabel", + "color", + "archivedAt", + "createdAt", + "updatedAt" + ], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Team" + } + } + } + }, + "404": { + "description": "Team not found" + } + } + } + }, + "/teams/{teamId}/members": { + "get": { + "tags": ["Teams"], + "summary": "List team members", + "parameters": [ + { + "name": "teamId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "responses": { + "200": { + "description": "Members", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "teamId": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "principalId": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "role": { + "type": "string", + "enum": ["lead", "member"] + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": ["teamId", "principalId", "role", "createdAt"], + "additionalProperties": false + } + }, + "meta": { + "type": "object", + "properties": { + "pagination": { + "type": "object", + "properties": { + "cursor": { + "description": "Cursor for next page, null if no more pages", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "hasMore": { + "type": "boolean", + "description": "Whether there are more items" + }, + "total": { + "description": "Total count (when available)", + "type": "number" + } + }, + "required": ["hasMore"], + "additionalProperties": false + } + }, + "required": ["pagination"], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Members" + } + } + } + } + } + }, + "post": { + "tags": ["Teams"], + "summary": "Add or update a team member", + "description": "Requires the `team.manage` scope/permission.", + "parameters": [ + { + "name": "teamId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "principalId": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "role": { + "type": "string", + "enum": ["lead", "member"] + } + }, + "required": ["principalId"] + } + } + } + }, + "responses": { + "201": { + "description": "Member added", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "teamId": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "principalId": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "role": { + "type": "string", + "enum": ["lead", "member"] + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": ["teamId", "principalId", "role", "createdAt"], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Member" + } + } + } + } + } + } + }, + "/teams/{teamId}/members/{principalId}": { + "delete": { + "tags": ["Teams"], + "summary": "Remove a team member", + "description": "Requires the `team.manage` scope/permission.", + "parameters": [ + { + "name": "teamId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + }, + { + "name": "principalId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "responses": { + "204": { + "description": "Removed" + } + } + } + }, + "/roles": { + "get": { + "tags": ["RBAC"], + "summary": "List roles (with permission counts)", + "description": "Requires the `admin.manage_roles` scope/permission.", + "responses": { + "200": { + "description": "Roles", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "role_01h455vb4pex5vsknk084sn02q" + }, + "key": { + "type": "string" + }, + "name": { + "type": "string" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "isSystem": { + "type": "boolean" + }, + "permissionCount": { + "type": "number" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + } + }, + "required": [ + "id", + "key", + "name", + "description", + "isSystem", + "createdAt", + "updatedAt" + ], + "additionalProperties": false + } + }, + "meta": { + "type": "object", + "properties": { + "pagination": { + "type": "object", + "properties": { + "cursor": { + "description": "Cursor for next page, null if no more pages", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "hasMore": { + "type": "boolean", + "description": "Whether there are more items" + }, + "total": { + "description": "Total count (when available)", + "type": "number" + } + }, + "required": ["hasMore"], + "additionalProperties": false + } + }, + "required": ["pagination"], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Roles" + } + } + } + }, + "401": { + "description": "Unauthorized", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "error": { + "type": "object", + "properties": { + "code": { + "type": "string", + "const": "UNAUTHORIZED" + }, + "message": { + "type": "string" + } + }, + "required": ["code", "message"], + "additionalProperties": false + } + }, + "required": ["error"], + "additionalProperties": false, + "description": "Authentication required or invalid API key" + } + } + } + }, + "403": { + "description": "admin.manage_roles permission required" + } + } + }, + "post": { + "tags": ["RBAC"], + "summary": "Create a custom role", + "description": "Requires the `admin.manage_roles` scope/permission.", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "key": { + "type": "string", + "minLength": 1, + "maxLength": 64 + }, + "name": { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + "description": { + "anyOf": [ + { + "type": "string", + "maxLength": 1000 + }, + { + "type": "null" + } + ] + }, + "permissionKeys": { + "maxItems": 200, + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": ["key", "name"] + } + } + } + }, + "responses": { + "201": { + "description": "Role created", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "role_01h455vb4pex5vsknk084sn02q" + }, + "key": { + "type": "string" + }, + "name": { + "type": "string" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "isSystem": { + "type": "boolean" + }, + "permissionCount": { + "type": "number" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + }, + "permissionKeys": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "id", + "key", + "name", + "description", + "isSystem", + "createdAt", + "updatedAt", + "permissionKeys" + ], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Role" + } + } + } + } + } + } + }, + "/roles/{roleId}": { + "get": { + "tags": ["RBAC"], + "summary": "Get a role with its permissions", + "parameters": [ + { + "name": "roleId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "responses": { + "200": { + "description": "Role", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "role_01h455vb4pex5vsknk084sn02q" + }, + "key": { + "type": "string" + }, + "name": { + "type": "string" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "isSystem": { + "type": "boolean" + }, + "permissionCount": { + "type": "number" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + }, + "permissionKeys": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "id", + "key", + "name", + "description", + "isSystem", + "createdAt", + "updatedAt", + "permissionKeys" + ], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Role" + } + } + } + } + } + }, + "patch": { + "tags": ["RBAC"], + "summary": "Rename / re-describe a role", + "parameters": [ + { + "name": "roleId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 1, + "maxLength": 200 + }, + "description": { + "anyOf": [ + { + "type": "string", + "maxLength": 1000 + }, + { + "type": "null" + } + ] + } + }, + "required": ["name"] + } + } + } + }, + "responses": { + "200": { + "description": "Updated" + } + } + }, + "delete": { + "tags": ["RBAC"], + "summary": "Delete a custom role (system roles are rejected)", + "parameters": [ + { + "name": "roleId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "responses": { + "204": { + "description": "Deleted" + } + } + } + }, + "/roles/{roleId}/permissions": { + "put": { + "tags": ["RBAC"], + "summary": "Replace a role's permission set", + "parameters": [ + { + "name": "roleId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "permissionKeys": { + "maxItems": 200, + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": ["permissionKeys"] + } + } + } + }, + "responses": { + "200": { + "description": "Updated", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "role_01h455vb4pex5vsknk084sn02q" + }, + "key": { + "type": "string" + }, + "name": { + "type": "string" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "isSystem": { + "type": "boolean" + }, + "permissionCount": { + "type": "number" + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + }, + "updatedAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + }, + "permissionKeys": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "id", + "key", + "name", + "description", + "isSystem", + "createdAt", + "updatedAt", + "permissionKeys" + ], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Role" + } + } + } + } + } + } + }, + "/permissions": { + "get": { + "tags": ["RBAC"], + "summary": "List the RBAC permission catalogue", + "description": "Reference data: every permission key plus the category → keys grouping. Requires the `admin.manage_roles` scope/permission.", + "responses": { + "200": { + "description": "Permission catalogue", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "permissions": { + "type": "array", + "items": { + "type": "string" + } + }, + "categories": { + "type": "object", + "propertyNames": { + "type": "string" + }, + "additionalProperties": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "required": ["permissions", "categories"], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Catalogue" + } + } + } + } + } + } + }, + "/principals/{principalId}/roles": { + "get": { + "tags": ["RBAC"], + "summary": "List a principal's role assignments", + "parameters": [ + { + "name": "principalId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "responses": { + "200": { + "description": "Assignments", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "role_asgn_01h455vb4pex5vsknk084sn02q" + }, + "principalId": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "roleId": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "teamId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + } + }, + "required": ["id", "principalId", "roleId", "teamId"], + "additionalProperties": false + } + }, + "meta": { + "type": "object", + "properties": { + "pagination": { + "type": "object", + "properties": { + "cursor": { + "description": "Cursor for next page, null if no more pages", + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "hasMore": { + "type": "boolean", + "description": "Whether there are more items" + }, + "total": { + "description": "Total count (when available)", + "type": "number" + } + }, + "required": ["hasMore"], + "additionalProperties": false + } + }, + "required": ["pagination"], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Assignments" + } + } + } + } + } + }, + "post": { + "tags": ["RBAC"], + "summary": "Assign a role to a principal (optionally team-scoped)", + "parameters": [ + { + "name": "principalId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "roleId": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "teamId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + } + }, + "required": ["roleId"] + } + } + } + }, + "responses": { + "201": { + "description": "Assigned", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "role_asgn_01h455vb4pex5vsknk084sn02q" + }, + "principalId": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "roleId": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "teamId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ] + } + }, + "required": ["id", "principalId", "roleId", "teamId"], + "additionalProperties": false + } + }, + "required": ["data"], + "additionalProperties": false, + "description": "Assignment" + } + } + } + } + } + } + }, + "/role-assignments/{assignmentId}": { + "delete": { + "tags": ["RBAC"], + "summary": "Revoke a role assignment", + "parameters": [ + { + "name": "assignmentId", + "in": "path", + "required": true, + "schema": { + "def": { + "type": "string" + }, + "type": "string", + "format": null, + "minLength": null, + "maxLength": null + } + } + ], + "responses": { + "204": { + "description": "Revoked" + } + } + } + } + }, + "components": { + "schemas": { + "__schema0": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "comment_01h455vb4pex5vsknk084sn02q" + }, + "postId": { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + "parentId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ], + "description": "Parent comment ID for replies" + }, + "content": { + "type": "string", + "example": "Great idea! This would be very useful." + }, + "authorName": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "example": "Jane Doe" + }, + "principalId": { + "anyOf": [ + { + "type": "string", + "description": "TypeID - a type-prefixed UUID", + "example": "post_01h455vb4pex5vsknk084sn02q" + }, + { + "type": "null" + } + ], + "description": "Principal ID of the comment author" + }, + "isTeamMember": { + "type": "boolean", + "description": "Whether the author is a team member", + "example": false + }, + "isPrivate": { + "type": "boolean", + "description": "Whether the comment is only visible to team members", + "example": false + }, + "createdAt": { + "type": "string", + "format": "date-time", + "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$", + "description": "ISO 8601 timestamp", + "example": "2024-01-15T10:30:00.000Z" + }, + "reactions": { + "type": "array", + "items": { + "type": "object", + "properties": { + "emoji": { + "type": "string", + "example": "👍" + }, + "count": { + "type": "number", + "example": 3 + }, + "hasReacted": { + "type": "boolean", + "description": "Whether the authenticated user has reacted with this emoji" + } + }, + "required": ["emoji", "count", "hasReacted"], + "additionalProperties": false + }, + "description": "Aggregated reaction counts" + }, + "replies": { + "type": "array", + "items": { + "$ref": "#/components/schemas/__schema0" + }, + "description": "Nested reply comments" + } + }, + "required": [ + "id", + "postId", + "parentId", + "content", + "authorName", + "principalId", + "isTeamMember", + "isPrivate", + "createdAt", + "reactions", + "replies" + ], + "additionalProperties": false + } + }, + "securitySchemes": { + "bearerAuth": { + "type": "http", + "scheme": "bearer", + "bearerFormat": "API Key", + "description": "API key authentication. Format: Bearer qb_xxx" + } + } + } +} diff --git a/apps/web/package.json b/apps/web/package.json index 86bc9fb31..5326ddd94 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -61,27 +61,28 @@ "@tanstack/react-start": "^1.168.25", "@tanstack/react-table": "^8.21.3", "@tanstack/react-virtual": "^3.13.26", - "@tiptap/core": "^3.22.3", - "@tiptap/extension-bubble-menu": "^3.22.3", - "@tiptap/extension-code-block-lowlight": "^3.22.3", - "@tiptap/extension-emoji": "3.22.3", - "@tiptap/extension-image": "^3.22.3", - "@tiptap/extension-link": "^3.22.3", - "@tiptap/extension-mention": "3.22.3", - "@tiptap/extension-placeholder": "^3.22.3", - "@tiptap/extension-table": "^3.22.3", - "@tiptap/extension-table-cell": "^3.22.3", - "@tiptap/extension-table-header": "^3.22.3", - "@tiptap/extension-table-row": "^3.22.3", - "@tiptap/extension-task-item": "^3.22.3", - "@tiptap/extension-task-list": "^3.22.3", - "@tiptap/extension-underline": "^3.22.3", - "@tiptap/extension-youtube": "^3.22.3", - "@tiptap/markdown": "^3.22.3", - "@tiptap/pm": "^3.22.3", - "@tiptap/react": "^3.22.3", - "@tiptap/starter-kit": "^3.22.3", - "@tiptap/suggestion": "^3.22.3", + "@tanstack/start": "^1.120.20", + "@tiptap/core": "3.23.4", + "@tiptap/extension-bubble-menu": "3.23.4", + "@tiptap/extension-code-block-lowlight": "3.23.4", + "@tiptap/extension-emoji": "3.23.4", + "@tiptap/extension-image": "3.23.4", + "@tiptap/extension-link": "3.23.4", + "@tiptap/extension-mention": "3.23.4", + "@tiptap/extension-placeholder": "3.23.4", + "@tiptap/extension-table": "3.23.4", + "@tiptap/extension-table-cell": "3.23.4", + "@tiptap/extension-table-header": "3.23.4", + "@tiptap/extension-table-row": "3.23.4", + "@tiptap/extension-task-item": "3.23.4", + "@tiptap/extension-task-list": "3.23.4", + "@tiptap/extension-underline": "3.23.4", + "@tiptap/extension-youtube": "3.23.4", + "@tiptap/markdown": "3.23.4", + "@tiptap/pm": "3.23.4", + "@tiptap/react": "3.23.4", + "@tiptap/starter-kit": "3.23.4", + "@tiptap/suggestion": "3.23.4", "bcryptjs": "^3.0.3", "better-auth": "^1.6.16", "bullmq": "^5.74.1", diff --git a/apps/web/scripts/backfill-github-ticket-comments.ts b/apps/web/scripts/backfill-github-ticket-comments.ts new file mode 100644 index 000000000..35fe872c2 --- /dev/null +++ b/apps/web/scripts/backfill-github-ticket-comments.ts @@ -0,0 +1,580 @@ +#!/usr/bin/env bun +/** + * Backfill two-way GitHub issue comment sync for linked tickets. + * + * Usage: + * bun apps/web/scripts/backfill-github-ticket-comments.ts --dry-run + * bun apps/web/scripts/backfill-github-ticket-comments.ts --integration-id=integration_... + * bun apps/web/scripts/backfill-github-ticket-comments.ts --ticket-id=ticket_... + * bun apps/web/scripts/backfill-github-ticket-comments.ts --direction=github-to-quackback + * bun apps/web/scripts/backfill-github-ticket-comments.ts --direction=quackback-to-github + * bun apps/web/scripts/backfill-github-ticket-comments.ts --since=2026-06-01T00:00:00Z + * + * Environment: + * DATABASE_URL — Required. PostgreSQL connection string. + */ + +try { + const { config } = await import('dotenv') + config({ path: '.env', quiet: true }) +} catch { + // dotenv not available; rely on environment variables. +} + +import { + db, + eq, + and, + integrations, + ticketExternalLinks, + ticketThreadExternalLinks, + ticketThreads, +} from '@/lib/server/db' +import type { IntegrationId, PrincipalId, TicketId, TicketThreadId } from '@quackback/ids' +import { decryptSecrets } from '@/lib/server/integrations/encryption' +import type { GitHubIntegrationConfig } from '@/lib/server/integrations/github/types' +import { + buildInboundTicketThreadBody, + buildOutboundGitHubCommentBody, + createGitHubIssueComment, + deleteGitHubIssueComment, + findThreadLinkByExternalComment, + findThreadLinkByThread, + getThreadAuthorName, + listGitHubIssueComments, + loadThreadForSync, + markThreadLinkDeleted, + parseQuackbackThreadMarker, + upsertThreadExternalLink, + type GitHubIssueComment, +} from '@/lib/server/integrations/github/ticket-comments' +import { + addThread, + editThread, + listPublicThreadsForTicket, +} from '@/lib/server/domains/tickets/ticket.threads' + +type Direction = 'both' | 'github-to-quackback' | 'quackback-to-github' + +interface Flags { + dryRun: boolean + integrationId: string | null + ticketId: string | null + direction: Direction + since: string | null +} + +interface Counters { + created: number + updated: number + deleted: number + linked: number + skipped: number +} + +interface GitHubIntegrationRow { + id: IntegrationId + principalId: PrincipalId | null + config: GitHubIntegrationConfig + accessToken: string +} + +interface TicketIssueLink { + ticketId: TicketId + externalId: string + externalUrl: string | null +} + +function printUsage(): void { + console.log(`Backfill GitHub ticket comment sync. + +Usage: + bun apps/web/scripts/backfill-github-ticket-comments.ts [flags] + +Flags: + --dry-run Preview without writing. + --integration-id=ID Limit to one GitHub integration. + --ticket-id=ID Limit to one linked ticket. + --direction=DIR both | github-to-quackback | quackback-to-github (default both). + --since=ISO_TIMESTAMP Only fetch GitHub comments updated since this timestamp. + --help Show this message. + +Notes: + Deletes are only reconciled when --since is omitted, because GitHub's list endpoint + cannot report deleted comments inside a since window. +`) +} + +function parseFlags(argv: string[]): Flags { + const direction = readArg(argv, '--direction') ?? 'both' + if (!['both', 'github-to-quackback', 'quackback-to-github'].includes(direction)) { + throw new Error('--direction must be both, github-to-quackback, or quackback-to-github') + } + const since = readArg(argv, '--since') + if (since && Number.isNaN(Date.parse(since))) { + throw new Error('--since must be an ISO timestamp') + } + return { + dryRun: argv.includes('--dry-run'), + integrationId: readArg(argv, '--integration-id'), + ticketId: readArg(argv, '--ticket-id'), + direction: direction as Direction, + since: since ? new Date(since).toISOString() : null, + } +} + +function readArg(argv: string[], name: string): string | null { + const prefix = `${name}=` + const value = argv.find((arg) => arg.startsWith(prefix)) + return value ? value.slice(prefix.length) : null +} + +async function loadIntegrations(flags: Flags): Promise { + const conditions = [eq(integrations.integrationType, 'github'), eq(integrations.status, 'active')] + if (flags.integrationId) { + conditions.push(eq(integrations.id, flags.integrationId as IntegrationId)) + } + + const rows = await db.query.integrations.findMany({ + where: and(...conditions), + }) + + return rows.flatMap((row) => { + const config = (row.config ?? {}) as GitHubIntegrationConfig + const ownerRepo = config.channelId + if (!ownerRepo) { + console.warn(`[github-comments-backfill] skip integration ${row.id}: missing channelId`) + return [] + } + if (!row.secrets) { + console.warn(`[github-comments-backfill] skip integration ${row.id}: missing secrets`) + return [] + } + const secrets = decryptSecrets<{ accessToken?: string }>(row.secrets) + if (!secrets.accessToken) { + console.warn(`[github-comments-backfill] skip integration ${row.id}: missing access token`) + return [] + } + return [ + { + id: row.id as IntegrationId, + principalId: (row.principalId as PrincipalId | null) ?? null, + config, + accessToken: secrets.accessToken, + }, + ] + }) +} + +async function loadIssueLinks( + integration: GitHubIntegrationRow, + flags: Flags +): Promise { + const conditions = [ + eq(ticketExternalLinks.integrationId, integration.id), + eq(ticketExternalLinks.status, 'active'), + ] + if (flags.ticketId) { + conditions.push(eq(ticketExternalLinks.ticketId, flags.ticketId as TicketId)) + } + return db + .select({ + ticketId: ticketExternalLinks.ticketId, + externalId: ticketExternalLinks.externalId, + externalUrl: ticketExternalLinks.externalUrl, + }) + .from(ticketExternalLinks) + .where(and(...conditions)) +} + +async function backfillGitHubToQuackback( + integration: GitHubIntegrationRow, + issueLink: TicketIssueLink, + flags: Flags, + counters: Counters +): Promise { + const comments = await listGitHubIssueComments({ + ownerRepo: integration.config.channelId, + issueNumber: issueLink.externalId, + accessToken: integration.accessToken, + since: flags.since ?? undefined, + }) + const remoteIds = new Set(comments.map((comment) => String(comment.id))) + + for (const comment of comments) { + const externalCommentId = String(comment.id) + const marker = parseQuackbackThreadMarker(comment.body) + if (marker) { + if (marker.integrationId === integration.id) { + await maybeLinkMarker(integration, issueLink, comment, marker, flags, counters) + } else { + counters.skipped++ + } + continue + } + + if (!commentBody(comment)) { + counters.skipped++ + continue + } + + const link = await findThreadLinkByExternalComment({ + integrationId: integration.id, + externalCommentId, + }) + const nextBody = buildInboundTicketThreadBody(comment) + + if (link?.status === 'active') { + const thread = await loadThreadForSync(link.threadId) + if (!thread || thread.deletedAt) { + await createInboundThread(integration, issueLink, comment, flags, counters) + continue + } + if (thread.bodyText !== nextBody) { + if (flags.dryRun) { + console.log( + `[dry-run] update ticket thread ${thread.id} from GitHub comment ${externalCommentId}` + ) + } else { + await editThread({ + threadId: thread.id, + actorPrincipalId: (thread.principalId as PrincipalId | null) ?? integration.principalId, + bodyText: nextBody, + syncSourceIntegrationId: integration.id, + }) + await upsertThreadExternalLink({ + ticketId: issueLink.ticketId, + threadId: thread.id, + integrationId: integration.id, + externalIssueId: issueLink.externalId, + externalCommentId, + externalUrl: comment.html_url ?? null, + syncDirection: 'inbound', + }) + } + counters.updated++ + } else { + counters.skipped++ + } + continue + } + + await createInboundThread(integration, issueLink, comment, flags, counters) + } + + if (!flags.since) { + await deleteMissingInboundThreads(integration, issueLink, remoteIds, flags, counters) + } +} + +async function backfillQuackbackToGitHub( + integration: GitHubIntegrationRow, + issueLink: TicketIssueLink, + flags: Flags, + counters: Counters +): Promise { + const comments = await listGitHubIssueComments({ + ownerRepo: integration.config.channelId, + issueNumber: issueLink.externalId, + accessToken: integration.accessToken, + }) + const markerByThreadId = new Map() + for (const comment of comments) { + const marker = parseQuackbackThreadMarker(comment.body) + if (marker?.integrationId === integration.id && marker.ticketId === issueLink.ticketId) { + markerByThreadId.set(marker.threadId, comment) + } + } + + const publicThreads = await listPublicThreadsForTicket(issueLink.ticketId) + const sinceMs = flags.since ? Date.parse(flags.since) : null + + for (const thread of publicThreads) { + if ( + sinceMs && + thread.createdAt.getTime() < sinceMs && + (thread.editedAt?.getTime() ?? 0) < sinceMs + ) { + counters.skipped++ + continue + } + + const link = await findThreadLinkByThread({ + integrationId: integration.id, + threadId: thread.id, + }) + if (link?.status === 'active') { + counters.skipped++ + continue + } + if (link?.status === 'deleted') { + counters.skipped++ + continue + } + + const markerComment = markerByThreadId.get(thread.id) + if (markerComment) { + if (flags.dryRun) { + console.log( + `[dry-run] link existing GitHub comment ${markerComment.id} to ticket thread ${thread.id}` + ) + } else { + await upsertThreadExternalLink({ + ticketId: issueLink.ticketId, + threadId: thread.id, + integrationId: integration.id, + externalIssueId: issueLink.externalId, + externalCommentId: String(markerComment.id), + externalUrl: markerComment.html_url ?? null, + syncDirection: 'outbound', + }) + } + counters.linked++ + continue + } + + const authorName = await getThreadAuthorName(thread.principalId) + const body = buildOutboundGitHubCommentBody({ + ticketId: issueLink.ticketId, + threadId: thread.id, + integrationId: integration.id, + bodyText: thread.bodyText, + authorName, + isFromRequester: false, + }) + + if (flags.dryRun) { + console.log( + `[dry-run] create GitHub comment for ticket ${issueLink.ticketId} thread ${thread.id}` + ) + } else { + const comment = await createGitHubIssueComment({ + ownerRepo: integration.config.channelId, + issueNumber: issueLink.externalId, + accessToken: integration.accessToken, + body, + }) + await upsertThreadExternalLink({ + ticketId: issueLink.ticketId, + threadId: thread.id, + integrationId: integration.id, + externalIssueId: issueLink.externalId, + externalCommentId: comment.id, + externalUrl: comment.htmlUrl, + syncDirection: 'outbound', + }) + } + counters.created++ + } + + if (!flags.since) { + await deleteRemoteCommentsForDeletedThreads(integration, issueLink, flags, counters) + } +} + +async function maybeLinkMarker( + integration: GitHubIntegrationRow, + issueLink: TicketIssueLink, + comment: GitHubIssueComment, + marker: { ticketId: string; threadId: string; integrationId: string }, + flags: Flags, + counters: Counters +): Promise { + if (flags.dryRun) { + console.log(`[dry-run] link marker GitHub comment ${comment.id} to thread ${marker.threadId}`) + } else { + await upsertThreadExternalLink({ + ticketId: marker.ticketId, + threadId: marker.threadId, + integrationId: integration.id, + externalIssueId: issueLink.externalId, + externalCommentId: String(comment.id), + externalUrl: comment.html_url ?? null, + syncDirection: 'outbound', + }) + } + counters.linked++ +} + +async function createInboundThread( + integration: GitHubIntegrationRow, + issueLink: TicketIssueLink, + comment: GitHubIssueComment, + flags: Flags, + counters: Counters +): Promise { + const externalCommentId = String(comment.id) + if (flags.dryRun) { + console.log( + `[dry-run] create public ticket thread on ${issueLink.ticketId} from GitHub comment ${externalCommentId}` + ) + } else { + const thread = await addThread({ + ticketId: issueLink.ticketId, + principalId: integration.principalId, + audience: 'public', + bodyText: buildInboundTicketThreadBody(comment), + syncSourceIntegrationId: integration.id, + }) + await upsertThreadExternalLink({ + ticketId: issueLink.ticketId, + threadId: thread.id, + integrationId: integration.id, + externalIssueId: issueLink.externalId, + externalCommentId, + externalUrl: comment.html_url ?? null, + syncDirection: 'inbound', + }) + } + counters.created++ +} + +async function deleteMissingInboundThreads( + integration: GitHubIntegrationRow, + issueLink: TicketIssueLink, + remoteIds: Set, + flags: Flags, + counters: Counters +): Promise { + const links = await db + .select({ + threadId: ticketThreadExternalLinks.threadId, + externalCommentId: ticketThreadExternalLinks.externalCommentId, + syncDirection: ticketThreadExternalLinks.syncDirection, + }) + .from(ticketThreadExternalLinks) + .where( + and( + eq(ticketThreadExternalLinks.integrationId, integration.id), + eq(ticketThreadExternalLinks.externalIssueId, issueLink.externalId), + eq(ticketThreadExternalLinks.status, 'active') + ) + ) + + for (const link of links) { + if (link.syncDirection === 'outbound') continue + if (remoteIds.has(link.externalCommentId)) continue + if (flags.dryRun) { + console.log( + `[dry-run] delete local thread ${link.threadId} because GitHub comment ${link.externalCommentId} is gone` + ) + } else { + await softDeleteLinkedThread(integration, link.threadId, link.externalCommentId) + } + counters.deleted++ + } +} + +async function deleteRemoteCommentsForDeletedThreads( + integration: GitHubIntegrationRow, + issueLink: TicketIssueLink, + flags: Flags, + counters: Counters +): Promise { + const links = await db + .select({ + threadId: ticketThreadExternalLinks.threadId, + externalCommentId: ticketThreadExternalLinks.externalCommentId, + }) + .from(ticketThreadExternalLinks) + .where( + and( + eq(ticketThreadExternalLinks.integrationId, integration.id), + eq(ticketThreadExternalLinks.externalIssueId, issueLink.externalId), + eq(ticketThreadExternalLinks.status, 'active') + ) + ) + + for (const link of links) { + const thread = await loadThreadForSync(link.threadId) + if (!thread?.deletedAt) continue + if (flags.dryRun) { + console.log( + `[dry-run] delete GitHub comment ${link.externalCommentId} for deleted thread ${link.threadId}` + ) + } else { + await deleteGitHubIssueComment({ + ownerRepo: integration.config.channelId, + commentId: link.externalCommentId, + accessToken: integration.accessToken, + }) + await markThreadLinkDeleted({ + integrationId: integration.id, + externalCommentId: link.externalCommentId, + }) + } + counters.deleted++ + } +} + +async function softDeleteLinkedThread( + integration: GitHubIntegrationRow, + threadId: TicketThreadId, + externalCommentId: string +): Promise { + const { softDeleteThread } = await import('@/lib/server/domains/tickets/ticket.threads') + const thread = await db.query.ticketThreads.findFirst({ + where: eq(ticketThreads.id, threadId), + columns: { principalId: true, deletedAt: true }, + }) + if (!thread || thread.deletedAt) { + await markThreadLinkDeleted({ integrationId: integration.id, externalCommentId }) + return + } + await softDeleteThread( + threadId, + (thread.principalId as PrincipalId | null) ?? integration.principalId, + integration.id + ) + await markThreadLinkDeleted({ integrationId: integration.id, externalCommentId }) +} + +function commentBody(comment: GitHubIssueComment): string { + return (comment.body ?? '').trim() +} + +async function main(): Promise { + const argv = process.argv.slice(2) + if (argv.includes('--help')) { + printUsage() + return + } + if (!process.env.DATABASE_URL) { + throw new Error('DATABASE_URL is required') + } + + const flags = parseFlags(argv) + const counters: Counters = { created: 0, updated: 0, deleted: 0, linked: 0, skipped: 0 } + console.log('[github-comments-backfill] start', flags) + + const integrationsToProcess = await loadIntegrations(flags) + if (integrationsToProcess.length === 0) { + console.log('[github-comments-backfill] no matching active GitHub integrations') + return + } + + for (const integration of integrationsToProcess) { + const links = await loadIssueLinks(integration, flags) + console.log('[github-comments-backfill] integration', { + integrationId: integration.id, + repo: integration.config.channelId, + linkedTickets: links.length, + }) + + for (const issueLink of links) { + if (flags.direction === 'both' || flags.direction === 'github-to-quackback') { + await backfillGitHubToQuackback(integration, issueLink, flags, counters) + } + if (flags.direction === 'both' || flags.direction === 'quackback-to-github') { + await backfillQuackbackToGitHub(integration, issueLink, flags, counters) + } + } + } + + console.log('[github-comments-backfill] summary', counters) +} + +main().catch((error) => { + console.error('[github-comments-backfill] failed', error) + process.exit(1) +}) diff --git a/apps/web/scripts/backfill-ticket-contacts.ts b/apps/web/scripts/backfill-ticket-contacts.ts new file mode 100644 index 000000000..d457edb97 --- /dev/null +++ b/apps/web/scripts/backfill-ticket-contacts.ts @@ -0,0 +1,319 @@ +#!/usr/bin/env bun +/** + * Backfill historical data for the portal-tickets identity model. + * + * Closes the gap left by Phases A + B for rows that already existed before + * those landed: + * + * Phase 1 — link existing portal users to CRM contacts via + * `contact_user_links`. Mirrors the better-auth + * `databaseHooks.user.create.after` behavior, applied + * retroactively. Gated on `emailVerified = true` so we only + * link identities the user has demonstrably proven they own. + * + * Phase 2 — populate `tickets.requesterContactId` for tickets that were + * filed with a `requesterPrincipalId` but no contact (the + * pre-Phase-B portal-creation path). Resolves principal → user + * → email → contact and writes the contact id back. + * + * Both phases are idempotent — re-running is a no-op for rows already in + * the desired state. Use `--dry-run` for a preview that reports candidate + * counts without writing. + * + * Usage: + * bun apps/web/scripts/backfill-ticket-contacts.ts # Run both phases + * bun apps/web/scripts/backfill-ticket-contacts.ts --dry-run # Preview + * bun apps/web/scripts/backfill-ticket-contacts.ts --users-only + * bun apps/web/scripts/backfill-ticket-contacts.ts --tickets-only + * bun apps/web/scripts/backfill-ticket-contacts.ts --batch-size=200 + * bun apps/web/scripts/backfill-ticket-contacts.ts --limit=1000 + * bun apps/web/scripts/backfill-ticket-contacts.ts --help + * + * Environment: + * DATABASE_URL — Required. PostgreSQL connection string. + */ + +// Load .env if available — same pattern as sibling backfill scripts. +try { + const { config } = await import('dotenv') + config({ path: '.env', quiet: true }) +} catch { + // dotenv not available, rely on environment variables +} + +import { db, eq, and, isNull, isNotNull, gt, asc, tickets, principal, user } from '@/lib/server/db' +import type { ContactId, PrincipalId, TicketId, UserId } from '@quackback/ids' +import { linkContactForUser } from '@/lib/server/auth/link-contact' +import { + findOrCreateByEmail, + linkContactToUser, +} from '@/lib/server/domains/organizations/contact.service' + +// --------------------------------------------------------------------------- +// CLI parsing +// --------------------------------------------------------------------------- + +interface Flags { + dryRun: boolean + usersOnly: boolean + ticketsOnly: boolean + batchSize: number + limit: number | null +} + +function printUsage(): void { + console.log(`Backfill ticket-contact identity links. + +Usage: + bun apps/web/scripts/backfill-ticket-contacts.ts [flags] + +Flags: + --dry-run Preview without writing. + --users-only Skip the ticket backfill phase. + --tickets-only Skip the user-link phase. + --batch-size=N Rows per batch (default 100, max 500). + --limit=N Cap total processed rows per phase. + --help Show this message. + +Environment: + DATABASE_URL Required. PostgreSQL connection string. +`) +} + +function parseFlags(argv: string[]): Flags { + const dryRun = argv.includes('--dry-run') + const usersOnly = argv.includes('--users-only') + const ticketsOnly = argv.includes('--tickets-only') + if (usersOnly && ticketsOnly) { + console.error( + '[backfill-ticket-contacts] --users-only and --tickets-only are mutually exclusive' + ) + process.exit(2) + } + const batchSizeArg = argv.find((a) => a.startsWith('--batch-size=')) + let batchSize = batchSizeArg ? parseInt(batchSizeArg.split('=')[1] ?? '', 10) : 100 + if (!Number.isFinite(batchSize) || batchSize <= 0) batchSize = 100 + if (batchSize > 500) batchSize = 500 + const limitArg = argv.find((a) => a.startsWith('--limit=')) + const limit = limitArg ? parseInt(limitArg.split('=')[1] ?? '', 10) : NaN + return { + dryRun, + usersOnly, + ticketsOnly, + batchSize, + limit: Number.isFinite(limit) && limit > 0 ? limit : null, + } +} + +// --------------------------------------------------------------------------- +// Phase 1 — link existing verified users to contacts +// --------------------------------------------------------------------------- + +interface UserPhaseStats { + candidates: number + processed: number + errors: number +} + +async function phase1LinkUsers(flags: Flags): Promise { + const stats: UserPhaseStats = { candidates: 0, processed: 0, errors: 0 } + + let cursor: string | null = null + while (true) { + if (flags.limit != null && stats.candidates >= flags.limit) break + const remaining = flags.limit != null ? flags.limit - stats.candidates : flags.batchSize + const take = Math.min(flags.batchSize, remaining) + + const rows = await db + .select({ id: user.id, email: user.email, isAnonymous: user.isAnonymous }) + .from(user) + .where( + and( + isNotNull(user.email), + eq(user.emailVerified, true), + eq(user.isAnonymous, false), + cursor ? gt(user.id, cursor) : undefined + ) + ) + .orderBy(asc(user.id)) + .limit(take) + if (rows.length === 0) break + + for (const row of rows) { + stats.candidates += 1 + cursor = row.id + if (!row.email) continue // belt-and-suspenders; filtered above + if (flags.dryRun) continue + try { + await linkContactForUser({ + userId: row.id as UserId, + email: row.email, + emailVerified: true, + anonymous: row.isAnonymous, + }) + stats.processed += 1 + } catch (err) { + stats.errors += 1 + console.error('[backfill-ticket-contacts] phase1 user error', { + userId: row.id, + error: err instanceof Error ? err.message : err, + }) + } + } + + if (rows.length < take) break + } + + return stats +} + +// --------------------------------------------------------------------------- +// Phase 2 — backfill tickets.requesterContactId +// --------------------------------------------------------------------------- + +interface TicketPhaseStats { + candidates: number + processed: number + missingEmail: number + errors: number +} + +async function phase2BackfillTickets(flags: Flags): Promise { + const stats: TicketPhaseStats = { candidates: 0, processed: 0, missingEmail: 0, errors: 0 } + + let cursor: string | null = null + while (true) { + if (flags.limit != null && stats.candidates >= flags.limit) break + const remaining = flags.limit != null ? flags.limit - stats.candidates : flags.batchSize + const take = Math.min(flags.batchSize, remaining) + + const rows = await db + .select({ + id: tickets.id, + requesterPrincipalId: tickets.requesterPrincipalId, + }) + .from(tickets) + .where( + and( + isNotNull(tickets.requesterPrincipalId), + isNull(tickets.requesterContactId), + isNull(tickets.deletedAt), + cursor ? gt(tickets.id, cursor) : undefined + ) + ) + .orderBy(asc(tickets.id)) + .limit(take) + if (rows.length === 0) break + + for (const row of rows) { + stats.candidates += 1 + cursor = row.id + try { + const contactId = await resolveContactForTicket(row.requesterPrincipalId as PrincipalId) + if (!contactId) { + stats.missingEmail += 1 + continue + } + if (flags.dryRun) { + console.log( + `[backfill-ticket-contacts] [dry-run] would set ticket ${row.id}.requesterContactId = ${contactId}` + ) + continue + } + await db + .update(tickets) + .set({ requesterContactId: contactId }) + .where(eq(tickets.id, row.id as TicketId)) + stats.processed += 1 + } catch (err) { + stats.errors += 1 + console.error('[backfill-ticket-contacts] phase2 ticket error', { + ticketId: row.id, + error: err instanceof Error ? err.message : err, + }) + } + } + + if (rows.length < take) break + } + + return stats +} + +/** + * Resolve the contact id for a ticket's requester principal. + * + * Mirrors `resolveRequesterContactId` in `ticket.service.ts` but exposed at + * script level so we can decide policy (skip vs. error) per row. Returns + * `null` when the principal isn't a user, has no email, or any link + * step fails — those rows are reported as `missingEmail` and skipped. + */ +async function resolveContactForTicket(principalId: PrincipalId): Promise { + const principalRow = await db.query.principal.findFirst({ + where: eq(principal.id, principalId), + columns: { userId: true, type: true }, + }) + if (!principalRow || !principalRow.userId || principalRow.type !== 'user') return null + const userRow = await db.query.user.findFirst({ + where: eq(user.id, principalRow.userId as UserId), + columns: { email: true }, + }) + if (!userRow?.email) return null + const contact = await findOrCreateByEmail({ email: userRow.email }) + // Best-effort link — keeps the user/contact pair joined for future queries. + await linkContactToUser({ + contactId: contact.id, + userId: principalRow.userId as UserId, + linkedByPrincipalId: null, + }) + return contact.id +} + +// --------------------------------------------------------------------------- +// Entrypoint +// --------------------------------------------------------------------------- + +async function main(): Promise { + const argv = process.argv.slice(2) + if (argv.includes('--help') || argv.includes('-h')) { + printUsage() + return + } + if (!process.env.DATABASE_URL) { + console.error('[backfill-ticket-contacts] DATABASE_URL is required') + process.exit(1) + } + const flags = parseFlags(argv) + + console.log('[backfill-ticket-contacts] start', { + dryRun: flags.dryRun, + usersOnly: flags.usersOnly, + ticketsOnly: flags.ticketsOnly, + batchSize: flags.batchSize, + limit: flags.limit, + }) + + let userStats: UserPhaseStats | null = null + let ticketStats: TicketPhaseStats | null = null + + if (!flags.ticketsOnly) { + console.log('[backfill-ticket-contacts] phase 1 — link users to contacts') + userStats = await phase1LinkUsers(flags) + console.log('[backfill-ticket-contacts] phase 1 done', userStats) + } + if (!flags.usersOnly) { + console.log('[backfill-ticket-contacts] phase 2 — backfill ticket.requesterContactId') + ticketStats = await phase2BackfillTickets(flags) + console.log('[backfill-ticket-contacts] phase 2 done', ticketStats) + } + + console.log('[backfill-ticket-contacts] summary', { + users: userStats, + tickets: ticketStats, + }) + + const errors = (userStats?.errors ?? 0) + (ticketStats?.errors ?? 0) + process.exit(errors > 0 ? 1 : 0) +} + +await main() diff --git a/apps/web/src/components/admin/changelog/__tests__/changelog-components.test.tsx b/apps/web/src/components/admin/changelog/__tests__/changelog-components.test.tsx new file mode 100644 index 000000000..9d059458a --- /dev/null +++ b/apps/web/src/components/admin/changelog/__tests__/changelog-components.test.tsx @@ -0,0 +1,864 @@ +// @vitest-environment happy-dom +import type { ReactNode } from 'react' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { fireEvent, render, screen, waitFor } from '@testing-library/react' +import type { ChangelogId, PostId, PrincipalId } from '@quackback/ids' +import { CreateChangelogDialog } from '../create-changelog-dialog' +import { ChangelogModal } from '../changelog-modal' +import { ChangelogListItem } from '../changelog-list-item' +import { ChangelogMetadataSidebar } from '../changelog-metadata-sidebar' +import { ChangelogMetadataSidebarContent } from '../changelog-metadata-sidebar-content' + +type ShippedPost = { + id: PostId + title: string + voteCount: number + boardSlug: string + authorName: string | null + createdAt: string +} + +const pickedDate = new Date('2026-06-22T09:30:00.000Z') + +const mocks = vi.hoisted(() => ({ + posts: [] as ShippedPost[], + taxonomy: { + categories: [ + { id: 'cat_1', name: 'Feature' }, + { id: 'cat_2', name: 'Fixes' }, + ], + products: [ + { id: 'prod_1', name: 'Core app' }, + { id: 'prod_2', name: 'Widget' }, + ], + }, + detailQuery: { + data: null as null | { + id: ChangelogId + title: string + content: string + contentJson: unknown + linkedPosts: Array<{ id: PostId; title: string; voteCount: number }> + category: { name: string } | null + product: { name: string } | null + status: 'draft' | 'scheduled' | 'published' + publishedAt: string | null + author: { name: string | null } | null + }, + isLoading: false, + }, + createMutation: { + mutate: vi.fn(), + reset: vi.fn(), + isPending: false, + isError: false, + error: new Error('create failed'), + }, + updateMutation: { + mutate: vi.fn(), + isPending: false, + isError: false, + error: new Error('update failed'), + }, + closeUrlModal: vi.fn(), + openCreateDialog: null as null | ((open: boolean) => void), +})) + +vi.mock('@tanstack/react-query', () => ({ + useQuery: (options: { queryKey?: readonly unknown[] }) => { + if (options.queryKey?.[0] === 'changelog-taxonomy') { + return { data: mocks.taxonomy } + } + if (options.queryKey?.[0] === 'changelog-detail') { + return mocks.detailQuery + } + return { data: mocks.posts, isLoading: false } + }, +})) + +vi.mock('@/lib/client/queries/changelog', () => ({ + changelogQueries: { + detail: (id: ChangelogId) => ({ queryKey: ['changelog-detail', id] }), + taxonomy: () => ({ queryKey: ['changelog-taxonomy'] }), + }, +})) + +vi.mock('@/lib/client/mutations/changelog', () => ({ + useCreateChangelog: () => mocks.createMutation, + useUpdateChangelog: () => mocks.updateMutation, +})) + +vi.mock('@hookform/resolvers/standard-schema', () => ({ + standardSchemaResolver: () => async (values: Record) => ({ + values, + errors: {}, + }), +})) + +vi.mock('@/lib/client/hooks/use-keyboard-submit', () => ({ + useKeyboardSubmit: + (submit: () => void) => (event: { key?: string; metaKey?: boolean; ctrlKey?: boolean }) => { + if (event.key === 'Enter' && (event.metaKey || event.ctrlKey)) { + submit() + } + }, +})) + +vi.mock('@/lib/client/hooks/use-url-modal', () => ({ + useUrlModal: ({ urlId }: { urlId?: ChangelogId }) => ({ + open: Boolean(urlId), + validatedId: urlId, + close: mocks.closeUrlModal, + }), +})) + +vi.mock('@/routes/admin/changelog', () => ({ + Route: { + useSearch: () => ({}), + }, +})) + +vi.mock('@/components/shared/modal-footer', () => ({ + ModalFooter: ({ + children, + onCancel, + submitLabel, + isPending, + }: { + children?: ReactNode + onCancel: () => void + submitLabel: string + isPending?: boolean + }) => ( +
+ {children} + + +
+ ), +})) + +vi.mock('@/components/shared/modal-header', () => ({ + ModalHeader: ({ + section, + title, + viewUrl, + onClose, + }: { + section: string + title: string + viewUrl: string | null + onClose: () => void + }) => ( +
+ {section} +

{title}

+ {viewUrl && View entry} + +
+ ), +})) + +vi.mock('@/components/shared/url-modal-shell', () => ({ + UrlModalShell: ({ + children, + open, + hasValidId, + }: { + children: ReactNode + open: boolean + hasValidId: boolean + onOpenChange?: (open: boolean) => void + srTitle?: string + }) => (open && hasValidId ?
{children}
: null), +})) + +vi.mock('@/components/ui/form', () => ({ + Form: ({ children }: { children: ReactNode }) => <>{children}, +})) + +vi.mock('../changelog-form-fields', () => ({ + ChangelogFormFields: ({ + form, + onContentChange, + error, + }: { + form: { + watch: (field: string) => string + setValue: (field: string, value: string, options?: { shouldValidate?: boolean }) => void + } + onContentChange: (json: unknown, html: string, markdown: string) => void + error?: string + }) => ( +
+ + + {error &&

{error}

} +
+ ), +})) + +vi.mock('@/components/ui/dialog', async () => { + const React = await import('react') + const DialogContext = React.createContext<{ open: boolean; setOpen: (open: boolean) => void }>({ + open: false, + setOpen: () => {}, + }) + return { + Dialog: ({ + children, + open, + onOpenChange, + }: { + children: ReactNode + open?: boolean + onOpenChange?: (open: boolean) => void + }) => { + const [internalOpen, setInternalOpen] = React.useState(open ?? false) + const isOpen = open ?? internalOpen + const setOpen = (nextOpen: boolean) => { + if (open === undefined) { + setInternalOpen(nextOpen) + mocks.openCreateDialog = setInternalOpen + } + onOpenChange?.(nextOpen) + } + return ( + +
{children}
+
+ ) + }, + DialogContent: ({ children }: { children: ReactNode }) => { + const context = React.useContext(DialogContext) + return context.open ? ( +
+ {children} + +
+ ) : null + }, + DialogTitle: ({ children }: { children: ReactNode; className?: string }) =>

{children}

, + DialogTrigger: ({ children }: { children: React.ReactElement; asChild?: boolean }) => { + const context = React.useContext(DialogContext) + return React.cloneElement(children as React.ReactElement<{ onClick?: () => void }>, { + onClick: () => context.setOpen(true), + }) + }, + } +}) + +vi.mock('@/components/ui/sheet', async () => { + const React = await import('react') + const SheetContext = React.createContext<{ open: boolean; setOpen: (open: boolean) => void }>({ + open: false, + setOpen: () => {}, + }) + return { + Sheet: ({ + children, + open, + onOpenChange, + }: { + children: ReactNode + open?: boolean + onOpenChange?: (open: boolean) => void + }) => { + const [internalOpen, setInternalOpen] = React.useState(open ?? false) + const isOpen = open ?? internalOpen + const setOpen = (nextOpen: boolean) => { + if (open === undefined) { + setInternalOpen(nextOpen) + } + onOpenChange?.(nextOpen) + } + return ( + +
{children}
+
+ ) + }, + SheetContent: ({ children }: { children: ReactNode }) => { + const context = React.useContext(SheetContext) + return context.open ?
{children}
: null + }, + SheetHeader: ({ children }: { children: ReactNode }) =>
{children}
, + SheetTitle: ({ children }: { children: ReactNode }) =>

{children}

, + SheetTrigger: ({ children }: { children: React.ReactElement; asChild?: boolean }) => { + const context = React.useContext(SheetContext) + return React.cloneElement(children as React.ReactElement<{ onClick?: () => void }>, { + onClick: () => context.setOpen(true), + }) + }, + } +}) + +vi.mock('@/lib/server/functions/changelog', () => ({ + searchShippedPostsFn: vi.fn(), +})) + +vi.mock('@/components/ui/status-badge', () => ({ + StatusBadge: ({ name, color }: { name: string; color: string | null }) => ( + {name} + ), +})) + +vi.mock('@/components/ui/time-ago', () => ({ + TimeAgo: ({ date }: { date: string; className?: string }) => , +})) + +vi.mock('@/components/ui/button', () => ({ + Button: ({ + children, + onClick, + }: { + children: ReactNode + onClick?: () => void + variant?: string + size?: string + className?: string + type?: 'button' | 'submit' | 'reset' + }) => ( + + ), +})) + +vi.mock('@/components/ui/dropdown-menu', () => ({ + DropdownMenu: ({ children }: { children: ReactNode }) =>
{children}
, + DropdownMenuContent: ({ children }: { children: ReactNode; align?: string }) => ( +
{children}
+ ), + DropdownMenuItem: ({ + children, + onClick, + }: { + children: ReactNode + onClick?: () => void + className?: string + }) => ( + + ), + DropdownMenuSeparator: () =>
, + DropdownMenuTrigger: ({ children }: { children: ReactNode; asChild?: boolean }) => ( + <>{children} + ), +})) + +vi.mock('@/components/ui/popover', () => ({ + Popover: ({ + children, + }: { + children: ReactNode + open?: boolean + onOpenChange?: (open: boolean) => void + }) =>
{children}
, + PopoverContent: ({ + children, + }: { + children: ReactNode + className?: string + align?: string + sideOffset?: number + }) =>
{children}
, + PopoverTrigger: ({ children }: { children: ReactNode; asChild?: boolean }) => <>{children}, +})) + +vi.mock('@/components/ui/scroll-area', () => ({ + ScrollArea: ({ children }: { children: ReactNode; className?: string }) =>
{children}
, +})) + +vi.mock('@/components/ui/checkbox', () => ({ + Checkbox: ({ checked }: { checked?: boolean; className?: string }) => ( + {checked ? 'checked' : 'unchecked'} + ), +})) + +vi.mock('@/components/ui/datetime-picker', () => ({ + DateTimePicker: ({ + onChange, + }: { + value?: Date + onChange?: (date: Date | undefined) => void + minDate?: Date + className?: string + }) => ( + + ), +})) + +vi.mock('@/components/ui/input', () => ({ + Input: ({ + value, + onChange, + placeholder, + list, + }: { + value?: string + onChange?: (event: { target: { value: string } }) => void + placeholder?: string + list?: string + className?: string + }) => ( + onChange?.({ target: { value: event.currentTarget.value } })} + /> + ), +})) + +vi.mock('@/components/shared/sidebar-primitives', () => ({ + SidebarRow: ({ + label, + children, + }: { + icon?: ReactNode + label: string + alignTop?: boolean + children: ReactNode + }) => ( +
+ {label} + {children} +
+ ), + SidebarContainer: ({ children, className }: { children: ReactNode; className?: string }) => ( + + ), + SidebarSkeleton: () =>
sidebar skeleton
, + StatusSelect: ({ + value, + options, + onChange, + }: { + value: string + options: ReadonlyArray<{ value: string; label: string }> + onChange: (value: string) => void + }) => ( +
+ {options.map((option) => ( + + ))} +
+ ), + ListItem: ({ + title, + meta, + action, + left, + }: { + left?: ReactNode + title: string + meta?: ReactNode[] + action?: ReactNode + }) => ( +
+ {left} + {title} + {meta} + {action} +
+ ), + VoteCount: ({ count }: { count: number }) => {count} votes, + ListItemRemoveButton: ({ onClick, label }: { onClick: () => void; label: string }) => ( + + ), +})) + +vi.mock('@heroicons/react/24/outline', () => ({ + ChevronUpIcon: () => , + CubeIcon: () => , + DocumentTextIcon: () => , + EllipsisHorizontalIcon: () => , + LinkIcon: () => , + MagnifyingGlassIcon: () => , + PencilIcon: () => , + PlusIcon: () => , + Squares2X2Icon: () => , + TrashIcon: () => , + UserIcon: () => , +})) + +vi.mock('@heroicons/react/24/solid', () => ({ + CheckIcon: () => , + Cog6ToothIcon: () => , + PlusIcon: () => , +})) + +function post(overrides: Partial = {}): ShippedPost { + return { + id: 'post_1' as PostId, + title: 'First shipped post', + voteCount: 12, + boardSlug: 'roadmap', + authorName: 'Dana', + createdAt: '2026-06-20T10:00:00.000Z', + ...overrides, + } +} + +beforeEach(() => { + vi.clearAllMocks() + mocks.detailQuery = { data: null, isLoading: false } + mocks.createMutation.isPending = false + mocks.createMutation.isError = false + mocks.createMutation.error = new Error('create failed') + mocks.createMutation.mutate.mockImplementation( + (_payload, options?: { onSuccess?: () => void }) => { + options?.onSuccess?.() + } + ) + mocks.updateMutation.isPending = false + mocks.updateMutation.isError = false + mocks.updateMutation.error = new Error('update failed') + mocks.updateMutation.mutate.mockImplementation( + (_payload, options?: { onSuccess?: () => void }) => { + options?.onSuccess?.() + } + ) + mocks.posts = [ + post(), + post({ + id: 'post_2' as PostId, + title: 'Second shipped post', + voteCount: 3, + boardSlug: 'ideas', + authorName: null, + }), + ] +}) + +describe('ChangelogListItem', () => { + it('renders a published entry with taxonomy, author, linked post count, and actions', () => { + const onEdit = vi.fn() + const onDelete = vi.fn() + + render( + + ) + + expect(screen.getAllByText('Published')).toHaveLength(2) + expect(screen.getByText('Feature')).toBeInTheDocument() + expect(screen.getByText('Widget')).toBeInTheDocument() + expect(screen.getByText('Launch notes')).toBeInTheDocument() + expect(screen.getByText(/Markdown body/)).toBeInTheDocument() + expect(screen.getByText('Ada')).toBeInTheDocument() + expect(screen.getByText('2026-06-20T09:00:00.000Z')).toBeInTheDocument() + expect(screen.getByText('1')).toBeInTheDocument() + + fireEvent.click(screen.getByText('Launch notes')) + expect(onEdit).toHaveBeenCalledWith('changelog_1') + + fireEvent.click(screen.getByRole('button', { name: /Delete/ })) + expect(onDelete).toHaveBeenCalledWith('changelog_1') + }) + + it('renders scheduled and draft time labels without optional metadata', () => { + const { rerender } = render( + + ) + + expect(screen.getByText('Scheduled')).toBeInTheDocument() + expect(screen.getByText(/Scheduled for/)).toBeInTheDocument() + + rerender( + + ) + + expect(screen.getByText('Draft')).toBeInTheDocument() + expect(screen.getByText('2026-06-18T09:00:00.000Z')).toBeInTheDocument() + }) +}) + +describe('ChangelogMetadataSidebarContent', () => { + it('updates status, schedule, taxonomy fields, and linked posts', () => { + const onPublishStateChange = vi.fn() + const onLinkedPostsChange = vi.fn() + const onCategoryNameChange = vi.fn() + const onProductNameChange = vi.fn() + + render( + + ) + + expect(screen.getByText('Status')).toBeInTheDocument() + expect(screen.getByText('Author')).toBeInTheDocument() + expect(screen.getByText('Ada')).toBeInTheDocument() + expect(screen.getByText('Schedule')).toBeInTheDocument() + expect(screen.getAllByText('First shipped post')).toHaveLength(2) + expect(screen.getByText('12 votes')).toBeInTheDocument() + expect(screen.getByText('Dana')).toBeInTheDocument() + expect(screen.getByDisplayValue('Feature')).toHaveAttribute( + 'list', + 'changelog-category-options' + ) + expect(screen.getByDisplayValue('Core app')).toHaveAttribute( + 'list', + 'changelog-product-options' + ) + + fireEvent.click(screen.getByRole('button', { name: 'Draft' })) + expect(onPublishStateChange).toHaveBeenCalledWith({ type: 'draft' }) + + fireEvent.click(screen.getByRole('button', { name: 'Published' })) + expect(onPublishStateChange).toHaveBeenCalledWith({ type: 'published' }) + + fireEvent.click(screen.getByRole('button', { name: 'Pick date' })) + expect(onPublishStateChange).toHaveBeenCalledWith({ + type: 'scheduled', + publishAt: pickedDate, + }) + + fireEvent.change(screen.getByDisplayValue('Feature'), { target: { value: 'Fixes' } }) + expect(onCategoryNameChange).toHaveBeenCalledWith('Fixes') + + fireEvent.change(screen.getByDisplayValue('Core app'), { target: { value: 'Widget' } }) + expect(onProductNameChange).toHaveBeenCalledWith('Widget') + + fireEvent.click(screen.getByText('Second shipped post')) + expect(onLinkedPostsChange).toHaveBeenCalledWith(['post_1', 'post_2']) + + fireEvent.click(screen.getByRole('button', { name: 'Remove First shipped post' })) + expect(onLinkedPostsChange).toHaveBeenCalledWith([]) + }) + + it('shows empty and search-empty post states', () => { + mocks.posts = [] + const props = { + publishState: { type: 'draft' as const }, + onPublishStateChange: vi.fn(), + linkedPostIds: [] as PostId[], + onLinkedPostsChange: vi.fn(), + categoryName: '', + onCategoryNameChange: vi.fn(), + productName: '', + onProductNameChange: vi.fn(), + } + + render() + + expect(screen.getByText('No shipped posts yet.')).toBeInTheDocument() + expect(screen.getByText('No posts linked yet')).toBeInTheDocument() + + fireEvent.change(screen.getByPlaceholderText('Search shipped posts...'), { + target: { value: 'missing' }, + }) + + expect(screen.getByText('No shipped posts found.')).toBeInTheDocument() + }) +}) + +describe('ChangelogMetadataSidebar', () => { + it('wraps metadata content in the shared sidebar container', () => { + render( + + ) + + expect(screen.getByText('Status')).toBeInTheDocument() + expect(screen.getByText('Author')).toBeInTheDocument() + expect(screen.getByText('Ada')).toBeInTheDocument() + expect(screen.getByText('No posts linked yet')).toBeInTheDocument() + }) +}) + +describe('CreateChangelogDialog', () => { + it('creates a published changelog, resets local state, and calls the completion hook', async () => { + const onChangelogCreated = vi.fn() + render() + + fireEvent.click(screen.getByRole('button', { name: /New Entry/ })) + fireEvent.change(screen.getByLabelText('Title'), { target: { value: 'Fresh launch' } }) + fireEvent.click(screen.getByRole('button', { name: 'Set content' })) + fireEvent.click(screen.getByRole('button', { name: 'Published' })) + fireEvent.change(screen.getByPlaceholderText('Feature'), { target: { value: 'Launches' } }) + fireEvent.change(screen.getByPlaceholderText('Core app'), { target: { value: 'Widget' } }) + fireEvent.click(screen.getByText('Second shipped post')) + fireEvent.click(screen.getByRole('button', { name: 'Publish Now' })) + + await waitFor(() => { + expect(mocks.createMutation.mutate).toHaveBeenCalledWith( + { + title: 'Fresh launch', + content: 'Updated markdown', + contentJson: { type: 'doc', content: [{ type: 'paragraph' }] }, + categoryName: 'Launches', + productName: 'Widget', + linkedPostIds: ['post_2'], + publishState: expect.objectContaining({ type: 'published' }), + }, + expect.objectContaining({ onSuccess: expect.any(Function) }) + ) + }) + expect(onChangelogCreated).toHaveBeenCalled() + expect(mocks.createMutation.reset).not.toHaveBeenCalled() + }) + + it('shows pending and error states, then resets mutation state when closed', () => { + mocks.createMutation.isPending = true + mocks.createMutation.isError = true + mocks.createMutation.error = new Error('Unable to create') + + render() + + fireEvent.click(screen.getByRole('button', { name: /New Entry/ })) + expect(screen.getByRole('button', { name: 'Saving...' })).toBeDisabled() + expect(screen.getByText('Unable to create')).toBeInTheDocument() + + fireEvent.click(screen.getByRole('button', { name: 'Close dialog' })) + expect(mocks.createMutation.reset).toHaveBeenCalled() + }) +}) + +describe('ChangelogModal', () => { + it('initializes fetched data, updates content, and closes after saving', async () => { + mocks.detailQuery = { + data: { + id: 'changelog_1' as ChangelogId, + title: 'Existing release', + content: 'Saved markdown', + contentJson: { type: 'doc' }, + linkedPosts: [{ id: 'post_1' as PostId, title: 'First shipped post', voteCount: 12 }], + category: { name: 'Feature' }, + product: { name: 'Core app' }, + status: 'published', + publishedAt: '2026-06-20T09:00:00.000Z', + author: { name: 'Ada' }, + }, + isLoading: false, + } + + render() + + expect(screen.getByRole('dialog')).toBeInTheDocument() + await waitFor(() => { + expect(screen.getByDisplayValue('Existing release')).toBeInTheDocument() + }) + expect(screen.getByRole('link', { name: 'View entry' })).toHaveAttribute( + 'href', + '/changelog/changelog_1' + ) + + fireEvent.change(screen.getByLabelText('Title'), { target: { value: 'Updated release' } }) + fireEvent.click(screen.getByRole('button', { name: 'Set content' })) + fireEvent.click(screen.getByRole('button', { name: 'Update & Publish' })) + + await waitFor(() => { + expect(mocks.updateMutation.mutate).toHaveBeenCalledWith( + { + id: 'changelog_1', + title: 'Updated release', + content: 'Updated markdown', + contentJson: { type: 'doc', content: [{ type: 'paragraph' }] }, + categoryName: 'Feature', + productName: 'Core app', + linkedPostIds: ['post_1'], + publishState: expect.objectContaining({ type: 'published' }), + }, + expect.objectContaining({ onSuccess: expect.any(Function) }) + ) + }) + expect(mocks.closeUrlModal).toHaveBeenCalled() + }) + + it('renders the loading state and hides content for invalid modal ids', () => { + mocks.detailQuery = { data: null, isLoading: true } + const { rerender } = render() + + expect(document.querySelector('.animate-spin')).toBeTruthy() + + rerender() + expect(screen.queryByRole('dialog')).not.toBeInTheDocument() + }) +}) diff --git a/apps/web/src/components/admin/changelog/__tests__/changelog-list.test.tsx b/apps/web/src/components/admin/changelog/__tests__/changelog-list.test.tsx new file mode 100644 index 000000000..18e1efefe --- /dev/null +++ b/apps/web/src/components/admin/changelog/__tests__/changelog-list.test.tsx @@ -0,0 +1,376 @@ +// @vitest-environment happy-dom +import type { ReactNode } from 'react' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { fireEvent, render, screen } from '@testing-library/react' +import { ChangelogList } from '../changelog-list' + +type ChangelogEntryFixture = { + id: string + title: string + content: string + status: 'draft' | 'scheduled' | 'published' + publishedAt: string | null + createdAt: string + author: { name: string } | null + category: { name: string; color?: string | null } | null + product: { name: string } | null + linkedPosts: Array<{ id: string; title: string; voteCount: number }> +} + +const mocks = vi.hoisted(() => ({ + navigate: vi.fn(), + setFilters: vi.fn(), + setSearchValue: vi.fn(), + fetchNextPage: vi.fn(), + deleteChangelog: vi.fn(), + filters: { + status: 'all', + search: '', + } as { status: string; search: string }, + hasActiveFilters: false, + query: { + data: null as null | { pages: Array<{ items: ChangelogEntryFixture[] }> }, + hasNextPage: false, + isFetchingNextPage: false, + isLoading: false, + }, +})) + +vi.mock('@tanstack/react-query', () => ({ + useInfiniteQuery: () => ({ + data: mocks.query.data, + fetchNextPage: mocks.fetchNextPage, + hasNextPage: mocks.query.hasNextPage, + isFetchingNextPage: mocks.query.isFetchingNextPage, + isLoading: mocks.query.isLoading, + }), +})) + +vi.mock('@tanstack/react-router', () => ({ + useNavigate: () => mocks.navigate, +})) + +vi.mock('@/routes/admin/changelog', () => ({ + Route: { + fullPath: '/admin/changelog', + useSearch: () => ({ tab: 'all' }), + }, +})) + +vi.mock('@/lib/client/queries/changelog', () => ({ + changelogQueries: { + list: (params: { status: string }) => ({ queryKey: ['changelogs', params] }), + }, +})) + +vi.mock('@/lib/client/mutations/changelog', () => ({ + useDeleteChangelog: () => ({ + isPending: false, + mutate: mocks.deleteChangelog, + }), +})) + +vi.mock('../use-changelog-filters', () => ({ + useChangelogFilters: () => ({ + filters: mocks.filters, + setFilters: mocks.setFilters, + hasActiveFilters: mocks.hasActiveFilters, + }), +})) + +vi.mock('@/lib/client/hooks/use-debounced-search', () => ({ + useDebouncedSearch: ({ + externalValue, + }: { + externalValue: string + onChange: (value: string) => void + }) => ({ + value: externalValue, + setValue: mocks.setSearchValue, + }), +})) + +vi.mock('@/lib/client/hooks/use-infinite-scroll', () => ({ + useInfiniteScroll: () => vi.fn(), +})) + +vi.mock('@/components/admin/feedback/inbox-layout', () => ({ + InboxLayout: ({ + headerTitle, + filters, + children, + hasActiveFilters, + }: { + headerIcon: unknown + headerTitle: string + filters: ReactNode + children: ReactNode + hasActiveFilters: boolean + }) => ( +
+

{headerTitle}

+ {filters} + {children} +
+ ), +})) + +vi.mock('@/components/admin/admin-list-header', () => ({ + AdminListHeader: ({ + searchValue, + onSearchChange, + action, + }: { + searchValue: string + onSearchChange: (value: string) => void + action?: ReactNode + }) => ( +
+ onSearchChange(event.currentTarget.value)} + /> + {action} +
+ ), +})) + +vi.mock('../changelog-filters', () => ({ + ChangelogFiltersPanel: ({ + status, + onStatusChange, + }: { + status: string + onStatusChange: (status: string) => void + }) => ( +
+ Status {status} + +
+ ), +})) + +vi.mock('../create-changelog-dialog', () => ({ + CreateChangelogDialog: () => , +})) + +vi.mock('../changelog-list-item', () => ({ + ChangelogListItem: ({ + id, + title, + category, + product, + onEdit, + onDelete, + }: { + id: string + title: string + category: { name: string } | null + product: { name: string } | null + onEdit?: (id: string) => void + onDelete?: (id: string) => void + }) => ( +
+

{title}

+ Category {category?.name ?? 'none'} + Product {product?.name ?? 'none'} + + +
+ ), +})) + +vi.mock('@/components/shared/confirm-dialog', () => ({ + ConfirmDialog: ({ + open, + title, + description, + confirmLabel, + onConfirm, + onOpenChange, + }: { + open: boolean + title: string + description: string + confirmLabel: string + isPending?: boolean + variant?: string + onConfirm: () => void + onOpenChange: (open: boolean) => void + }) => + open ? ( +
+

{title}

+

{description}

+ + +
+ ) : null, +})) + +vi.mock('@/components/shared/empty-state', () => ({ + EmptyState: ({ + title, + action, + }: { + icon: unknown + title: string + className?: string + action?: ReactNode + }) => ( +
+

{title}

+ {action} +
+ ), +})) + +vi.mock('@/components/ui/skeleton', () => ({ + Skeleton: () =>
Skeleton row
, +})) + +vi.mock('@/components/shared/spinner', () => ({ + Spinner: () => Spinner, +})) + +vi.mock('@/components/ui/button', () => ({ + Button: ({ + children, + onClick, + }: { + children: ReactNode + onClick?: () => void + variant?: string + size?: string + className?: string + }) => ( + + ), +})) + +vi.mock('@heroicons/react/24/solid', () => ({ + DocumentTextIcon: () => , +})) + +function entry(overrides: Partial = {}): ChangelogEntryFixture { + return { + id: 'changelog_1', + title: 'Launch notes', + content: 'Important launch body', + status: 'published', + publishedAt: '2026-06-20T09:00:00.000Z', + createdAt: '2026-06-19T09:00:00.000Z', + author: { name: 'Ada' }, + category: { name: 'Feature' }, + product: { name: 'Widget' }, + linkedPosts: [], + ...overrides, + } +} + +beforeEach(() => { + vi.clearAllMocks() + mocks.filters = { status: 'all', search: '' } + mocks.hasActiveFilters = false + mocks.query = { + data: { pages: [{ items: [entry()] }] }, + hasNextPage: false, + isFetchingNextPage: false, + isLoading: false, + } + mocks.deleteChangelog.mockImplementation((_id, options?: { onSuccess?: () => void }) => { + options?.onSuccess?.() + }) +}) + +describe('ChangelogList', () => { + it('passes taxonomy props, filters, navigates edits, and confirms deletion', () => { + render() + + expect(screen.getByText('Changelog')).toBeInTheDocument() + expect(screen.getByText('Category Feature')).toBeInTheDocument() + expect(screen.getByText('Product Widget')).toBeInTheDocument() + + fireEvent.change(screen.getByDisplayValue(''), { target: { value: 'roadmap' } }) + expect(mocks.setSearchValue).toHaveBeenCalledWith('roadmap') + + fireEvent.click(screen.getByRole('button', { name: 'Published filter' })) + expect(mocks.setFilters).toHaveBeenCalledWith({ status: 'published' }) + + fireEvent.click(screen.getByRole('button', { name: 'Edit Launch notes' })) + expect(mocks.navigate).toHaveBeenCalledWith({ + to: '/admin/changelog', + search: { tab: 'all', entry: 'changelog_1' }, + }) + + fireEvent.click(screen.getByRole('button', { name: 'Delete Launch notes' })) + expect(screen.getByRole('heading', { name: 'Delete changelog entry?' })).toBeInTheDocument() + fireEvent.click(screen.getByRole('button', { name: 'Delete' })) + expect(mocks.deleteChangelog).toHaveBeenCalledWith( + 'changelog_1', + expect.objectContaining({ onSuccess: expect.any(Function) }) + ) + }) + + it('renders loading, empty search, empty filtered, empty initial, and pagination states', () => { + mocks.query = { data: null, hasNextPage: false, isFetchingNextPage: false, isLoading: true } + const { rerender } = render() + expect(screen.getAllByText('Skeleton row').length).toBeGreaterThan(0) + + mocks.query = { + data: { pages: [{ items: [] }] }, + hasNextPage: false, + isFetchingNextPage: false, + isLoading: false, + } + mocks.filters = { status: 'all', search: 'missing' } + rerender() + expect( + screen.getByRole('heading', { name: 'No changelog entries match your search' }) + ).toBeInTheDocument() + + mocks.filters = { status: 'published', search: '' } + mocks.hasActiveFilters = true + rerender() + expect( + screen.getByRole('heading', { name: 'No changelog entries match your filters' }) + ).toBeInTheDocument() + + mocks.filters = { status: 'all', search: '' } + mocks.hasActiveFilters = false + rerender() + expect(screen.getByRole('heading', { name: 'No changelog entries yet' })).toBeInTheDocument() + + mocks.query = { + data: { pages: [{ items: [entry({ id: 'changelog_2', title: 'Roadmap update' })] }] }, + hasNextPage: true, + isFetchingNextPage: false, + isLoading: false, + } + rerender() + fireEvent.click(screen.getByRole('button', { name: 'Load more' })) + expect(mocks.fetchNextPage).toHaveBeenCalled() + + mocks.query = { + data: { pages: [{ items: [entry({ id: 'changelog_3', title: 'Spinner update' })] }] }, + hasNextPage: true, + isFetchingNextPage: true, + isLoading: false, + } + rerender() + expect(screen.getByText('Spinner')).toBeInTheDocument() + }) +}) diff --git a/apps/web/src/components/admin/changelog/changelog-list-item.tsx b/apps/web/src/components/admin/changelog/changelog-list-item.tsx index 8f3b79f5c..251cde8a8 100644 --- a/apps/web/src/components/admin/changelog/changelog-list-item.tsx +++ b/apps/web/src/components/admin/changelog/changelog-list-item.tsx @@ -29,6 +29,13 @@ interface ChangelogListItemProps { name: string avatarUrl: string | null } | null + category?: { + name: string + color: string | null + } | null + product?: { + name: string + } | null linkedPosts: Array<{ id: PostId title: string @@ -52,6 +59,8 @@ export function ChangelogListItem({ publishedAt, createdAt, author, + category, + product, linkedPosts, onEdit, onDelete, @@ -67,7 +76,17 @@ export function ChangelogListItem({ {/* Content */}
{/* Status badge */} - +
+ + {category && ( + + )} + {product && ( + + {product.name} + + )} +
{/* Title */}

{title}

diff --git a/apps/web/src/components/admin/changelog/changelog-list.tsx b/apps/web/src/components/admin/changelog/changelog-list.tsx index 67d200024..fc1bc530d 100644 --- a/apps/web/src/components/admin/changelog/changelog-list.tsx +++ b/apps/web/src/components/admin/changelog/changelog-list.tsx @@ -186,6 +186,8 @@ export function ChangelogList() { publishedAt={entry.publishedAt} createdAt={entry.createdAt} author={entry.author} + category={entry.category} + product={entry.product} linkedPosts={entry.linkedPosts} onEdit={handleEdit} onDelete={handleDelete} diff --git a/apps/web/src/components/admin/changelog/changelog-metadata-sidebar-content.tsx b/apps/web/src/components/admin/changelog/changelog-metadata-sidebar-content.tsx index 698b3de5b..a1351b650 100644 --- a/apps/web/src/components/admin/changelog/changelog-metadata-sidebar-content.tsx +++ b/apps/web/src/components/admin/changelog/changelog-metadata-sidebar-content.tsx @@ -5,6 +5,8 @@ import { MagnifyingGlassIcon, ChevronUpIcon, UserIcon, + Squares2X2Icon, + CubeIcon, } from '@heroicons/react/24/outline' import { CheckIcon } from '@heroicons/react/24/solid' import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover' @@ -13,6 +15,7 @@ import { Checkbox } from '@/components/ui/checkbox' import { DateTimePicker } from '@/components/ui/datetime-picker' import { useQuery } from '@tanstack/react-query' import { searchShippedPostsFn } from '@/lib/server/functions/changelog' +import { changelogQueries } from '@/lib/client/queries/changelog' import { TimeAgo } from '@/components/ui/time-ago' import { SidebarRow, @@ -23,6 +26,7 @@ import { type StatusOption, } from '@/components/shared/sidebar-primitives' import { cn } from '@/lib/shared/utils' +import { Input } from '@/components/ui/input' import type { PostId } from '@quackback/ids' import type { PublishState } from '@/lib/shared/schemas/changelog' @@ -31,6 +35,10 @@ interface ChangelogMetadataSidebarContentProps { onPublishStateChange: (state: PublishState) => void linkedPostIds: PostId[] onLinkedPostsChange: (postIds: PostId[]) => void + categoryName: string + onCategoryNameChange: (name: string) => void + productName: string + onProductNameChange: (name: string) => void authorName?: string | null } @@ -45,6 +53,10 @@ export function ChangelogMetadataSidebarContent({ onPublishStateChange, linkedPostIds, onLinkedPostsChange, + categoryName, + onCategoryNameChange, + productName, + onProductNameChange, authorName, }: ChangelogMetadataSidebarContentProps) { const [postsOpen, setPostsOpen] = useState(false) @@ -67,6 +79,7 @@ export function ChangelogMetadataSidebarContent({ queryFn: () => searchShippedPostsFn({ data: { query: search || undefined, limit: 30 } }), staleTime: 30 * 1000, }) + const { data: taxonomy } = useQuery(changelogQueries.taxonomy()) // Get selected post details const selectedPosts = posts.filter((p) => linkedPostIds.includes(p.id)) @@ -139,6 +152,38 @@ export function ChangelogMetadataSidebarContent({
)} +
+ } label="Category" alignTop> + onCategoryNameChange(event.target.value)} + placeholder="Feature" + className="h-8 w-36 text-xs" + /> + + + {(taxonomy?.categories ?? []).map((category) => ( + + + } label="Product" alignTop> + onProductNameChange(event.target.value)} + placeholder="Core app" + className="h-8 w-36 text-xs" + /> + + + {(taxonomy?.products ?? []).map((product) => ( + +
+ {/* Linked Posts - single unified section */}
} label="Linked Posts"> diff --git a/apps/web/src/components/admin/changelog/changelog-metadata-sidebar.tsx b/apps/web/src/components/admin/changelog/changelog-metadata-sidebar.tsx index 3769d9fee..7fe9b40e4 100644 --- a/apps/web/src/components/admin/changelog/changelog-metadata-sidebar.tsx +++ b/apps/web/src/components/admin/changelog/changelog-metadata-sidebar.tsx @@ -10,6 +10,10 @@ interface ChangelogMetadataSidebarProps { onPublishStateChange: (state: PublishState) => void linkedPostIds: PostId[] onLinkedPostsChange: (postIds: PostId[]) => void + categoryName: string + onCategoryNameChange: (name: string) => void + productName: string + onProductNameChange: (name: string) => void authorName?: string | null } @@ -18,6 +22,10 @@ export function ChangelogMetadataSidebar({ onPublishStateChange, linkedPostIds, onLinkedPostsChange, + categoryName, + onCategoryNameChange, + productName, + onProductNameChange, authorName, }: ChangelogMetadataSidebarProps) { return ( @@ -27,6 +35,10 @@ export function ChangelogMetadataSidebar({ onPublishStateChange={onPublishStateChange} linkedPostIds={linkedPostIds} onLinkedPostsChange={onLinkedPostsChange} + categoryName={categoryName} + onCategoryNameChange={onCategoryNameChange} + productName={productName} + onProductNameChange={onProductNameChange} authorName={authorName} /> diff --git a/apps/web/src/components/admin/changelog/changelog-modal.tsx b/apps/web/src/components/admin/changelog/changelog-modal.tsx index 369469b79..6fa6d96a5 100644 --- a/apps/web/src/components/admin/changelog/changelog-modal.tsx +++ b/apps/web/src/components/admin/changelog/changelog-modal.tsx @@ -36,6 +36,8 @@ interface ChangelogModalContentProps { function ChangelogModalContent({ entryId, onClose }: ChangelogModalContentProps) { const [contentJson, setContentJson] = useState(null) const [linkedPostIds, setLinkedPostIds] = useState([]) + const [categoryName, setCategoryName] = useState('') + const [productName, setProductName] = useState('') const [publishState, setPublishState] = useState({ type: 'draft' }) const [mobileSettingsOpen, setMobileSettingsOpen] = useState(false) const [hasInitialized, setHasInitialized] = useState(false) @@ -65,6 +67,8 @@ function ChangelogModalContent({ entryId, onClose }: ChangelogModalContentProps) form.setValue('content', entry.content) setContentJson(entry.contentJson as JSONContent | null) setLinkedPostIds(entry.linkedPosts.map((p) => p.id)) + setCategoryName(entry.category?.name ?? '') + setProductName(entry.product?.name ?? '') setPublishState(toPublishState(entry.status, entry.publishedAt)) setHasInitialized(true) } @@ -85,6 +89,8 @@ function ChangelogModalContent({ entryId, onClose }: ChangelogModalContentProps) title: data.title, content: data.content, contentJson: contentJson as TiptapContent | null, + categoryName: categoryName.trim() || null, + productName: productName.trim() || null, linkedPostIds, publishState, }, @@ -151,6 +157,10 @@ function ChangelogModalContent({ entryId, onClose }: ChangelogModalContentProps) onPublishStateChange={setPublishState} linkedPostIds={linkedPostIds} onLinkedPostsChange={setLinkedPostIds} + categoryName={categoryName} + onCategoryNameChange={setCategoryName} + productName={productName} + onProductNameChange={setProductName} authorName={entry?.author?.name} />
@@ -179,6 +189,10 @@ function ChangelogModalContent({ entryId, onClose }: ChangelogModalContentProps) onPublishStateChange={setPublishState} linkedPostIds={linkedPostIds} onLinkedPostsChange={setLinkedPostIds} + categoryName={categoryName} + onCategoryNameChange={setCategoryName} + productName={productName} + onProductNameChange={setProductName} authorName={entry?.author?.name} /> diff --git a/apps/web/src/components/admin/changelog/create-changelog-dialog.tsx b/apps/web/src/components/admin/changelog/create-changelog-dialog.tsx index 5acd6a8a1..311187376 100644 --- a/apps/web/src/components/admin/changelog/create-changelog-dialog.tsx +++ b/apps/web/src/components/admin/changelog/create-changelog-dialog.tsx @@ -28,6 +28,8 @@ export function CreateChangelogDialog({ onChangelogCreated }: CreateChangelogDia const [open, setOpen] = useState(false) const [contentJson, setContentJson] = useState(null) const [linkedPostIds, setLinkedPostIds] = useState([]) + const [categoryName, setCategoryName] = useState('') + const [productName, setProductName] = useState('') const [publishState, setPublishState] = useState({ type: 'draft' }) const [mobileSettingsOpen, setMobileSettingsOpen] = useState(false) const createChangelogMutation = useCreateChangelog() @@ -56,6 +58,8 @@ export function CreateChangelogDialog({ onChangelogCreated }: CreateChangelogDia title: data.title, content: data.content, contentJson: contentJson as TiptapContent | null, + categoryName: categoryName.trim() || null, + productName: productName.trim() || null, linkedPostIds, publishState, }, @@ -65,6 +69,8 @@ export function CreateChangelogDialog({ onChangelogCreated }: CreateChangelogDia form.reset() setContentJson(null) setLinkedPostIds([]) + setCategoryName('') + setProductName('') setPublishState({ type: 'draft' }) onChangelogCreated?.() }, @@ -78,6 +84,8 @@ export function CreateChangelogDialog({ onChangelogCreated }: CreateChangelogDia form.reset() setContentJson(null) setLinkedPostIds([]) + setCategoryName('') + setProductName('') setPublishState({ type: 'draft' }) createChangelogMutation.reset() } @@ -138,6 +146,10 @@ export function CreateChangelogDialog({ onChangelogCreated }: CreateChangelogDia onPublishStateChange={setPublishState} linkedPostIds={linkedPostIds} onLinkedPostsChange={setLinkedPostIds} + categoryName={categoryName} + onCategoryNameChange={setCategoryName} + productName={productName} + onProductNameChange={setProductName} /> @@ -165,6 +177,10 @@ export function CreateChangelogDialog({ onChangelogCreated }: CreateChangelogDia onPublishStateChange={setPublishState} linkedPostIds={linkedPostIds} onLinkedPostsChange={setLinkedPostIds} + categoryName={categoryName} + onCategoryNameChange={setCategoryName} + productName={productName} + onProductNameChange={setProductName} /> diff --git a/apps/web/src/components/admin/contacts/__tests__/contact-create-dialog.test.tsx b/apps/web/src/components/admin/contacts/__tests__/contact-create-dialog.test.tsx new file mode 100644 index 000000000..96a279c04 --- /dev/null +++ b/apps/web/src/components/admin/contacts/__tests__/contact-create-dialog.test.tsx @@ -0,0 +1,202 @@ +// @vitest-environment happy-dom +import type { ChangeEvent, ReactNode } from 'react' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { fireEvent, render, screen, waitFor } from '@testing-library/react' +import { ContactCreateDialog } from '../contact-create-dialog' + +type MutationOptions = { + mutationFn: () => Promise + onSuccess?: (result: T) => void + onError?: (error: Error) => void +} + +const mocks = vi.hoisted(() => ({ + createContactFn: vi.fn(), + invalidateQueries: vi.fn(), + navigate: vi.fn(), + toastSuccess: vi.fn(), + toastError: vi.fn(), +})) + +vi.mock('@tanstack/react-query', () => ({ + useQueryClient: () => ({ + invalidateQueries: mocks.invalidateQueries, + }), + useMutation: (options: MutationOptions) => ({ + isPending: false, + mutate: async () => { + try { + const result = await options.mutationFn() + options.onSuccess?.(result) + } catch (error) { + options.onError?.(error instanceof Error ? error : new Error(String(error))) + } + }, + }), +})) + +vi.mock('@tanstack/react-router', () => ({ + useRouter: () => ({ + navigate: mocks.navigate, + }), +})) + +vi.mock('@/lib/server/functions/contacts', () => ({ + createContactFn: mocks.createContactFn, +})) + +vi.mock('sonner', () => ({ + toast: { + success: mocks.toastSuccess, + error: mocks.toastError, + }, +})) + +vi.mock('@/components/ui/button', () => ({ + Button: ({ + children, + disabled, + onClick, + type = 'button', + }: { + children: ReactNode + disabled?: boolean + onClick?: () => void + type?: 'button' | 'submit' | 'reset' + variant?: string + }) => ( + + ), +})) + +vi.mock('@/components/ui/input', () => ({ + Input: ({ + id, + type = 'text', + value, + onChange, + }: { + id?: string + type?: string + value?: string + onChange?: (event: ChangeEvent) => void + maxLength?: number + className?: string + }) => , +})) + +vi.mock('@/components/ui/label', () => ({ + Label: ({ children, htmlFor }: { children: ReactNode; htmlFor?: string }) => ( + + ), +})) + +vi.mock('@/components/ui/dialog', () => ({ + Dialog: ({ + children, + }: { + children: ReactNode + open?: boolean + onOpenChange?: (open: boolean) => void + }) =>
{children}
, + DialogContent: ({ children }: { children: ReactNode; className?: string }) => ( +
{children}
+ ), + DialogDescription: ({ children }: { children: ReactNode }) =>

{children}

, + DialogFooter: ({ children }: { children: ReactNode }) =>
{children}
, + DialogHeader: ({ children }: { children: ReactNode }) =>
{children}
, + DialogTitle: ({ children }: { children: ReactNode }) =>

{children}

, + DialogTrigger: ({ children }: { children: ReactNode; asChild?: boolean }) => <>{children}, +})) + +vi.mock('@/components/admin/shared/org-picker', () => ({ + OrgPicker: ({ + value, + onValueChange, + }: { + value: string | null + onValueChange: (value: string | null) => void + allowClear?: boolean + }) => ( + + ), +})) + +beforeEach(() => { + vi.clearAllMocks() + mocks.createContactFn.mockResolvedValue({ id: 'contact_created' }) +}) + +describe('ContactCreateDialog', () => { + it('requires either a name or an email before creating a contact', () => { + render(Open} />) + + fireEvent.submit(screen.getByRole('button', { name: 'Create' }).closest('form')!) + + expect(mocks.toastError).toHaveBeenCalledWith('Name or email is required') + expect(mocks.createContactFn).not.toHaveBeenCalled() + }) + + it('submits trimmed fields, invalidates the list, resets and navigates to the contact', async () => { + render( + Open} + defaultOrganizationId={'org_default' as never} + /> + ) + + fireEvent.change(screen.getByLabelText('Name'), { target: { value: ' Ada Lovelace ' } }) + fireEvent.change(screen.getByLabelText('Email'), { target: { value: ' ada@example.com ' } }) + fireEvent.change(screen.getByLabelText('Phone'), { target: { value: ' ' } }) + fireEvent.change(screen.getByLabelText('Title'), { target: { value: ' Engineer ' } }) + fireEvent.change(screen.getByLabelText('Organization'), { target: { value: 'org_beta' } }) + fireEvent.change(screen.getByLabelText('External ID'), { target: { value: ' crm-42 ' } }) + fireEvent.submit(screen.getByRole('button', { name: 'Create' }).closest('form')!) + + await waitFor(() => { + expect(mocks.createContactFn).toHaveBeenCalledWith({ + data: { + name: 'Ada Lovelace', + email: 'ada@example.com', + phone: null, + title: 'Engineer', + externalId: 'crm-42', + organizationId: 'org_beta', + }, + }) + }) + expect(mocks.invalidateQueries).toHaveBeenCalledWith({ queryKey: ['contacts'] }) + expect(mocks.toastSuccess).toHaveBeenCalledWith('Contact created') + expect(mocks.navigate).toHaveBeenCalledWith({ + to: '/admin/contacts/people/$contactId', + params: { contactId: 'contact_created' }, + }) + expect(screen.getByLabelText('Name')).toHaveValue('') + expect(screen.getByLabelText('Organization')).toHaveValue('org_default') + }) + + it('reports create failures from the server function', async () => { + mocks.createContactFn.mockRejectedValueOnce(new Error('Duplicate contact')) + render(Open} />) + + fireEvent.change(screen.getByLabelText('Email'), { target: { value: 'ada@example.com' } }) + fireEvent.submit(screen.getByRole('button', { name: 'Create' }).closest('form')!) + + await waitFor(() => { + expect(mocks.toastError).toHaveBeenCalledWith('Duplicate contact') + }) + expect(mocks.navigate).not.toHaveBeenCalled() + }) +}) diff --git a/apps/web/src/components/admin/contacts/__tests__/contact-linked-users-tab.test.tsx b/apps/web/src/components/admin/contacts/__tests__/contact-linked-users-tab.test.tsx new file mode 100644 index 000000000..c25764fac --- /dev/null +++ b/apps/web/src/components/admin/contacts/__tests__/contact-linked-users-tab.test.tsx @@ -0,0 +1,320 @@ +// @vitest-environment happy-dom +import type { ChangeEvent, ReactNode } from 'react' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react' +import { ContactLinkedUsersTab } from '../contact-linked-users-tab' + +type QueryOptions = { + queryKey: readonly unknown[] + queryFn: () => T + staleTime?: number +} + +type MutationOptions = { + mutationFn: (vars: TVars) => Promise + onSuccess?: (result: TResult) => void + onError?: (error: Error) => void +} + +type Link = { + id: string + userId: string + linkedAt: string +} + +type Principal = { + id: string + userId: string | null + displayName: string | null + email: string | null +} + +const mocks = vi.hoisted(() => ({ + invalidateQueries: vi.fn(), + linkContactToUserFn: vi.fn(), + unlinkContactFromUserFn: vi.fn(), + searchPrincipalsFn: vi.fn(), + toastSuccess: vi.fn(), + toastError: vi.fn(), + permissionAllowed: true, + links: [] as Link[], + principals: [] as Principal[], +})) + +vi.mock('@tanstack/react-query', () => ({ + useQueryClient: () => ({ + invalidateQueries: mocks.invalidateQueries, + }), + useSuspenseQuery: () => ({ + data: mocks.links, + }), + useQuery: (options: QueryOptions) => ({ + data: options.queryFn(), + }), + useMutation: (options: MutationOptions) => ({ + isPending: false, + mutate: async (vars: TVars) => { + try { + const result = await options.mutationFn(vars) + options.onSuccess?.(result) + } catch (error) { + options.onError?.(error instanceof Error ? error : new Error(String(error))) + } + }, + }), +})) + +vi.mock('@/components/admin/shared/permission-gate', () => ({ + PermissionGate: ({ + children, + fallback = null, + }: { + children: ReactNode + fallback?: ReactNode + permission: string + }) => (mocks.permissionAllowed ? <>{children} : <>{fallback}), +})) + +vi.mock('@/components/ui/button', () => ({ + Button: ({ + children, + disabled, + onClick, + type = 'button', + 'aria-label': ariaLabel, + }: { + children: ReactNode + disabled?: boolean + onClick?: () => void + type?: 'button' | 'submit' | 'reset' + variant?: string + size?: string + className?: string + 'aria-label'?: string + }) => ( + + ), +})) + +vi.mock('@/components/ui/avatar', () => ({ + Avatar: ({ children }: { children: ReactNode; className?: string }) => {children}, +})) + +vi.mock('@/components/ui/table', () => ({ + Table: ({ children }: { children: ReactNode }) => {children}
, + TableBody: ({ children }: { children: ReactNode }) => {children}, + TableCell: ({ + children, + colSpan, + }: { + children?: ReactNode + colSpan?: number + className?: string + }) => {children}, + TableHead: ({ children }: { children?: ReactNode; className?: string }) => {children}, + TableHeader: ({ children }: { children: ReactNode }) => {children}, + TableRow: ({ children }: { children: ReactNode }) => {children}, +})) + +vi.mock('@/components/ui/alert-dialog', () => ({ + AlertDialog: ({ children }: { children: ReactNode }) =>
{children}
, + AlertDialogAction: ({ children, onClick }: { children: ReactNode; onClick?: () => void }) => ( + + ), + AlertDialogCancel: ({ children }: { children: ReactNode }) => ( + + ), + AlertDialogContent: ({ children }: { children: ReactNode }) =>
{children}
, + AlertDialogDescription: ({ children }: { children: ReactNode }) =>

{children}

, + AlertDialogFooter: ({ children }: { children: ReactNode }) =>
{children}
, + AlertDialogHeader: ({ children }: { children: ReactNode }) =>
{children}
, + AlertDialogTitle: ({ children }: { children: ReactNode }) =>

{children}

, + AlertDialogTrigger: ({ children }: { children: ReactNode; asChild?: boolean }) => <>{children}, +})) + +vi.mock('@/components/admin/shared/principal-picker', () => ({ + PrincipalPicker: ({ + value, + onValueChange, + excludeIds, + placeholder, + }: { + value: string | null + onValueChange: (value: string | null) => void + roleFilter?: string[] + excludeIds: string[] + placeholder?: string + }) => ( + + ), +})) + +vi.mock('@heroicons/react/24/outline', () => ({ + TrashIcon: () => , +})) + +vi.mock('@/lib/server/functions/contacts', () => ({ + linkContactToUserFn: mocks.linkContactToUserFn, + unlinkContactFromUserFn: mocks.unlinkContactFromUserFn, +})) + +vi.mock('@/lib/server/functions/principals', () => ({ + searchPrincipalsFn: mocks.searchPrincipalsFn, +})) + +vi.mock('@/lib/client/queries/contacts', () => ({ + contactQueries: { + links: (contactId: string) => ({ queryKey: ['contacts', contactId, 'links'] }), + }, +})) + +vi.mock('@/lib/server/domains/authz', () => ({ + PERMISSIONS: { + ORG_MANAGE: 'org.manage', + }, +})) + +vi.mock('sonner', () => ({ + toast: { + success: mocks.toastSuccess, + error: mocks.toastError, + }, +})) + +beforeEach(() => { + vi.clearAllMocks() + mocks.permissionAllowed = true + mocks.links = [ + { id: 'link_1', userId: 'user_existing', linkedAt: '2026-06-18T12:00:00.000Z' }, + { id: 'link_2', userId: 'user_unknown', linkedAt: '2026-06-18T13:00:00.000Z' }, + ] + mocks.principals = [ + { + id: 'principal_existing', + userId: 'user_existing', + displayName: 'Existing User', + email: 'existing@example.com', + }, + { + id: 'principal_new', + userId: 'user_new', + displayName: 'New User', + email: 'new@example.com', + }, + { + id: 'principal_without_user', + userId: null, + displayName: 'No User', + email: null, + }, + ] + mocks.searchPrincipalsFn.mockImplementation(() => mocks.principals) + mocks.linkContactToUserFn.mockResolvedValue({ id: 'link_new' }) + mocks.unlinkContactFromUserFn.mockResolvedValue(undefined) +}) + +describe('ContactLinkedUsersTab', () => { + it('renders linked users with principal enrichment and picker exclusions', () => { + render() + + expect(mocks.searchPrincipalsFn).toHaveBeenCalledWith({ + data: { roleFilter: ['user'], limit: 50 }, + }) + expect(screen.getByText('Existing User')).toBeInTheDocument() + expect(screen.getByText('existing@example.com')).toBeInTheDocument() + expect(screen.getByText('user_unknown')).toBeInTheDocument() + expect(screen.getByLabelText('Add user')).toHaveAttribute( + 'data-exclude-ids', + 'principal_existing' + ) + }) + + it('links a selected user and rejects principals that do not resolve to a user', async () => { + render() + + expect(screen.getByRole('button', { name: 'Link' })).toBeDisabled() + fireEvent.change(screen.getByLabelText('Add user'), { + target: { value: 'principal_without_user' }, + }) + fireEvent.click(screen.getByRole('button', { name: 'Link' })) + expect(mocks.toastError).toHaveBeenCalledWith('Selected principal has no associated user') + expect(mocks.linkContactToUserFn).not.toHaveBeenCalled() + + fireEvent.change(screen.getByLabelText('Add user'), { target: { value: 'principal_new' } }) + fireEvent.click(screen.getByRole('button', { name: 'Link' })) + + await waitFor(() => { + expect(mocks.linkContactToUserFn).toHaveBeenCalledWith({ + data: { + contactId: 'contact_1', + userId: 'user_new', + }, + }) + }) + expect(mocks.invalidateQueries).toHaveBeenCalledWith({ + queryKey: ['contacts', 'contact_1', 'links'], + }) + expect(mocks.toastSuccess).toHaveBeenCalledWith('User linked') + }) + + it('unlinks existing users and reports link/unlink failures', async () => { + mocks.linkContactToUserFn.mockRejectedValueOnce(new Error('Link denied')) + mocks.unlinkContactFromUserFn.mockRejectedValueOnce(new Error('Unlink denied')) + + render() + + fireEvent.change(screen.getByLabelText('Add user'), { target: { value: 'principal_new' } }) + fireEvent.click(screen.getByRole('button', { name: 'Link' })) + await waitFor(() => { + expect(mocks.toastError).toHaveBeenCalledWith('Link denied') + }) + + fireEvent.click(screen.getAllByRole('button', { name: 'Unlink' })[0]) + await waitFor(() => { + expect(mocks.unlinkContactFromUserFn).toHaveBeenCalledWith({ + data: { + contactId: 'contact_1', + userId: 'user_existing', + }, + }) + }) + expect(mocks.toastError).toHaveBeenCalledWith('Unlink denied') + + fireEvent.click(screen.getAllByRole('button', { name: 'Unlink' })[0]) + await waitFor(() => { + expect(mocks.toastSuccess).toHaveBeenCalledWith('User unlinked') + }) + }) + + it('renders empty and permission-denied states without mutation controls', () => { + mocks.links = [] + render() + + expect(screen.getByText('No linked users yet.')).toBeInTheDocument() + + cleanup() + mocks.links = [{ id: 'link_1', userId: 'user_existing', linkedAt: '2026-06-18T12:00:00.000Z' }] + mocks.permissionAllowed = false + render() + + expect(screen.queryByLabelText('Add user')).not.toBeInTheDocument() + expect(screen.queryByRole('button', { name: 'Unlink user' })).not.toBeInTheDocument() + expect(screen.queryByRole('button', { name: 'Unlink' })).not.toBeInTheDocument() + expect(screen.getByText('Existing User')).toBeInTheDocument() + }) +}) diff --git a/apps/web/src/components/admin/contacts/__tests__/contact-list-and-tabs.test.tsx b/apps/web/src/components/admin/contacts/__tests__/contact-list-and-tabs.test.tsx new file mode 100644 index 000000000..120e3c249 --- /dev/null +++ b/apps/web/src/components/admin/contacts/__tests__/contact-list-and-tabs.test.tsx @@ -0,0 +1,355 @@ +// @vitest-environment happy-dom +import type { ReactNode } from 'react' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { render, screen } from '@testing-library/react' +import { ContactList } from '../contact-list' +import { ContactTicketsTab } from '../contact-tickets-tab' +import { OrganizationList } from '../organization-list' +import { OrganizationContactsTab } from '../organization-contacts-tab' +import { OrganizationTicketsTab } from '../organization-tickets-tab' + +type ContactRow = { + id: string + name: string | null + email: string | null + phone?: string | null + title: string | null + organizationId: string | null + archivedAt: string | null +} + +type TicketRow = { + id: string + subject: string + priority: string + channel: string + lastActivityAt: string | null +} + +type OrganizationRow = { + id: string + name: string + domain: string | null + website: string | null + externalId: string | null + archivedAt: string | null +} + +const mocks = vi.hoisted(() => ({ + searchContacts: [] as ContactRow[], + orgContacts: [] as ContactRow[], + organizations: [] as Array<{ id: string; name: string }>, + organizationList: [] as OrganizationRow[], + organizationListParams: undefined as unknown, + ticketRows: [] as TicketRow[], + listTicketsFn: vi.fn(), + permissionAllowed: true, +})) + +vi.mock('@tanstack/react-query', () => ({ + useSuspenseQuery: (options: { queryKey?: unknown[]; queryFn?: () => { rows: TicketRow[] } }) => { + if (options.queryFn) return { data: options.queryFn() } + if (options.queryKey?.[1] === 'byOrg') return { data: mocks.orgContacts } + if (options.queryKey?.[0] === 'organizations') return { data: mocks.organizationList } + return { data: mocks.searchContacts } + }, + useQuery: () => ({ + data: mocks.organizations, + }), +})) + +vi.mock('@tanstack/react-router', () => ({ + Link: ({ + children, + params, + to, + }: { + children: ReactNode + params?: Record + to: string + }) => { + const href = Object.entries(params ?? {}).reduce( + (path, [key, value]) => path.replace(`$${key}`, value), + to + ) + return {children} + }, +})) + +vi.mock('@/lib/client/queries/contacts', () => ({ + contactQueries: { + search: (params: unknown) => ({ queryKey: ['contacts', 'search', params] }), + byOrg: (organizationId: string, params: unknown) => ({ + queryKey: ['contacts', 'byOrg', organizationId, params], + }), + }, +})) + +vi.mock('@/lib/client/queries/organizations', () => ({ + organizationQueries: { + list: (params: unknown) => { + mocks.organizationListParams = params + return { queryKey: ['organizations', 'list', params] } + }, + }, +})) + +vi.mock('@/lib/server/functions/tickets', () => ({ + listTicketsFn: mocks.listTicketsFn, +})) + +vi.mock('@/components/admin/shared/permission-gate', () => ({ + PermissionGate: ({ children }: { children: ReactNode; permission: string }) => + mocks.permissionAllowed ? <>{children} : null, +})) + +vi.mock('@/components/admin/contacts/contact-create-dialog', () => ({ + ContactCreateDialog: ({ + defaultOrganizationId, + trigger, + }: { + defaultOrganizationId: string + trigger: ReactNode + }) =>
{trigger}
, +})) + +vi.mock('@/lib/server/domains/authz', () => ({ + PERMISSIONS: { + ORG_MANAGE: 'org.manage', + }, +})) + +function contact(overrides: Partial = {}): ContactRow { + return { + id: 'contact_1', + name: 'Ada Lovelace', + email: 'ada@example.com', + phone: '+1 555 1000', + title: 'Engineer', + organizationId: 'org_acme', + archivedAt: null, + ...overrides, + } +} + +function organization(overrides: Partial = {}): OrganizationRow { + return { + id: 'org_acme', + name: 'Acme', + domain: 'acme.com', + website: 'https://acme.com', + externalId: 'crm-acme', + archivedAt: null, + ...overrides, + } +} + +beforeEach(() => { + vi.clearAllMocks() + mocks.searchContacts = [] + mocks.orgContacts = [] + mocks.organizations = [] + mocks.organizationList = [] + mocks.organizationListParams = undefined + mocks.ticketRows = [] + mocks.permissionAllowed = true + mocks.listTicketsFn.mockImplementation(() => ({ rows: mocks.ticketRows })) +}) + +describe('ContactList', () => { + it('renders search rows with organization names, fallbacks and status badges', () => { + mocks.searchContacts = [ + contact(), + contact({ + id: 'contact_email', + name: null, + email: 'fallback@example.com', + organizationId: 'org_unknown', + title: null, + archivedAt: '2026-06-20T10:00:00.000Z', + }), + contact({ + id: 'contact_id', + name: null, + email: null, + organizationId: null, + }), + ] + mocks.organizations = [{ id: 'org_acme', name: 'Acme' }] + + render() + + expect(screen.getByRole('link', { name: 'Ada Lovelace' })).toHaveAttribute( + 'href', + '/admin/contacts/people/contact_1' + ) + expect(screen.getByText('Acme')).toBeInTheDocument() + expect(screen.getByText('org_unknown')).toBeInTheDocument() + expect(screen.getByRole('link', { name: 'fallback@example.com' })).toBeInTheDocument() + expect(screen.getByRole('link', { name: 'contact_id' })).toBeInTheDocument() + expect(screen.getAllByText('Active')).toHaveLength(2) + expect(screen.getByText('Archived')).toBeInTheDocument() + }) + + it('renders an empty contact-list row', () => { + render() + + expect(screen.getByText('No contacts.')).toBeInTheDocument() + }) +}) + +describe('ContactTicketsTab', () => { + it('requests tickets for the contact and renders ticket rows', () => { + mocks.ticketRows = [ + { + id: 'ticket_1', + subject: 'Cannot sign in', + priority: 'urgent', + channel: 'portal', + lastActivityAt: '2026-06-20T10:00:00.000Z', + }, + { + id: 'ticket_2', + subject: 'Billing question', + priority: 'normal', + channel: 'email', + lastActivityAt: null, + }, + ] + + render() + + expect(mocks.listTicketsFn).toHaveBeenCalledWith({ + data: { scope: 'all', requesterContactId: 'contact_1', limit: 100 }, + }) + expect(screen.getByRole('link', { name: 'Cannot sign in' })).toHaveAttribute( + 'href', + '/admin/tickets/ticket_1' + ) + expect(screen.getByText('urgent')).toBeInTheDocument() + expect(screen.getByText('portal')).toBeInTheDocument() + expect(screen.getByRole('link', { name: 'Billing question' })).toBeInTheDocument() + expect(screen.getByText('email')).toBeInTheDocument() + }) + + it('renders an empty ticket row', () => { + render() + + expect(screen.getByText('No tickets for this contact.')).toBeInTheDocument() + }) +}) + +describe('OrganizationContactsTab', () => { + it('renders organization contacts and passes the organization id to the create dialog', () => { + mocks.orgContacts = [ + contact(), + contact({ + id: 'contact_archived', + name: null, + email: null, + phone: null, + title: null, + organizationId: 'org_acme', + archivedAt: '2026-06-20T10:00:00.000Z', + }), + ] + + const { container } = render() + + expect(screen.getByRole('button', { name: /Add contact/ })).toBeInTheDocument() + expect(container.querySelector('[data-default-organization-id="org_acme"]')).not.toBeNull() + expect(screen.getByRole('link', { name: 'Ada Lovelace' })).toHaveAttribute( + 'href', + '/admin/contacts/people/contact_1' + ) + expect(screen.getByRole('link', { name: 'contact_archived' })).toBeInTheDocument() + expect(screen.getByText('+1 555 1000')).toBeInTheDocument() + expect(screen.getByText('Engineer')).toBeInTheDocument() + expect(screen.getByText('Archived')).toBeInTheDocument() + }) + + it('hides the add-contact action when the actor lacks org management permission', () => { + mocks.permissionAllowed = false + + render() + + expect(screen.queryByRole('button', { name: /Add contact/ })).not.toBeInTheDocument() + expect(screen.getByText('No contacts in this organization yet.')).toBeInTheDocument() + }) +}) + +describe('OrganizationList', () => { + it('trims search, filters archived rows and renders organization fallbacks', () => { + mocks.organizationList = [ + organization(), + organization({ + id: 'org_archived', + name: 'Archived org', + domain: null, + website: null, + externalId: null, + archivedAt: '2026-06-20T10:00:00.000Z', + }), + ] + + const { rerender } = render() + + expect(mocks.organizationListParams).toEqual({ + includeArchived: true, + search: 'acme', + }) + expect(screen.getByRole('link', { name: 'Acme' })).toHaveAttribute( + 'href', + '/admin/contacts/organizations/org_acme' + ) + expect(screen.getByText('acme.com')).toBeInTheDocument() + expect(screen.getByText('https://acme.com')).toBeInTheDocument() + expect(screen.getByText('crm-acme')).toBeInTheDocument() + expect(screen.getByText('Active')).toBeInTheDocument() + expect(screen.queryByRole('link', { name: 'Archived org' })).not.toBeInTheDocument() + + rerender() + + expect(mocks.organizationListParams).toEqual({ includeArchived: true }) + expect(screen.getByRole('link', { name: 'Archived org' })).toBeInTheDocument() + expect(screen.getByText('Archived')).toBeInTheDocument() + }) + + it('renders an empty organization-list row', () => { + render() + + expect(screen.getByText('No organizations.')).toBeInTheDocument() + }) +}) + +describe('OrganizationTicketsTab', () => { + it('requests tickets for the organization and renders ticket rows', () => { + mocks.ticketRows = [ + { + id: 'ticket_1', + subject: 'SLA escalation', + priority: 'high', + channel: 'email', + lastActivityAt: '2026-06-20T10:00:00.000Z', + }, + ] + + render() + + expect(mocks.listTicketsFn).toHaveBeenCalledWith({ + data: { scope: 'all', organizationId: 'org_acme', limit: 100 }, + }) + expect(screen.getByRole('link', { name: 'SLA escalation' })).toHaveAttribute( + 'href', + '/admin/tickets/ticket_1' + ) + expect(screen.getByText('high')).toBeInTheDocument() + expect(screen.getByText('email')).toBeInTheDocument() + }) + + it('renders an empty organization-ticket row', () => { + render() + + expect(screen.getByText('No tickets for this organization.')).toBeInTheDocument() + }) +}) diff --git a/apps/web/src/components/admin/contacts/__tests__/contact-overview-tab.test.tsx b/apps/web/src/components/admin/contacts/__tests__/contact-overview-tab.test.tsx new file mode 100644 index 000000000..9bec0da9e --- /dev/null +++ b/apps/web/src/components/admin/contacts/__tests__/contact-overview-tab.test.tsx @@ -0,0 +1,295 @@ +// @vitest-environment happy-dom +import type { ChangeEvent, ReactNode } from 'react' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { fireEvent, render, screen, waitFor } from '@testing-library/react' +import { ContactOverviewTab } from '../contact-overview-tab' + +type ContactProp = Parameters[0]['contact'] + +type MutationOptions = { + mutationFn: () => Promise + onSuccess?: (result: T) => void + onError?: (error: Error) => void +} + +const mocks = vi.hoisted(() => ({ + invalidateQueries: vi.fn(), + updateContactFn: vi.fn(), + archiveContactFn: vi.fn(), + toastSuccess: vi.fn(), + toastError: vi.fn(), + permissionAllowed: true, +})) + +vi.mock('@tanstack/react-query', () => ({ + useQueryClient: () => ({ + invalidateQueries: mocks.invalidateQueries, + }), + useMutation: (options: MutationOptions) => ({ + isPending: false, + mutate: async () => { + try { + const result = await options.mutationFn() + options.onSuccess?.(result) + } catch (error) { + options.onError?.(error instanceof Error ? error : new Error(String(error))) + } + }, + }), +})) + +vi.mock('@/components/admin/shared/permission-gate', () => ({ + PermissionGate: ({ + children, + fallback = null, + }: { + children: ReactNode + fallback?: ReactNode + permission: string + }) => (mocks.permissionAllowed ? <>{children} : <>{fallback}), +})) + +vi.mock('@/components/ui/button', () => ({ + Button: ({ + children, + disabled, + onClick, + type = 'button', + }: { + children: ReactNode + disabled?: boolean + onClick?: () => void + type?: 'button' | 'submit' | 'reset' + variant?: string + size?: string + }) => ( + + ), +})) + +vi.mock('@/components/ui/input', () => ({ + Input: ({ + id, + type = 'text', + value, + onChange, + }: { + id?: string + type?: string + value?: string + onChange?: (event: ChangeEvent) => void + maxLength?: number + className?: string + }) => , +})) + +vi.mock('@/components/ui/label', () => ({ + Label: ({ children, htmlFor }: { children: ReactNode; htmlFor?: string }) => ( + + ), +})) + +vi.mock('@/components/ui/alert-dialog', () => ({ + AlertDialog: ({ children }: { children: ReactNode }) =>
{children}
, + AlertDialogAction: ({ children, onClick }: { children: ReactNode; onClick?: () => void }) => ( + + ), + AlertDialogCancel: ({ children }: { children: ReactNode }) => ( + + ), + AlertDialogContent: ({ children }: { children: ReactNode }) =>
{children}
, + AlertDialogDescription: ({ children }: { children: ReactNode }) =>

{children}

, + AlertDialogFooter: ({ children }: { children: ReactNode }) =>
{children}
, + AlertDialogHeader: ({ children }: { children: ReactNode }) =>
{children}
, + AlertDialogTitle: ({ children }: { children: ReactNode }) =>

{children}

, + AlertDialogTrigger: ({ children }: { children: ReactNode; asChild?: boolean }) => <>{children}, +})) + +vi.mock('@/components/admin/shared/org-picker', () => ({ + OrgPicker: ({ + value, + onValueChange, + }: { + value: string | null + onValueChange: (value: string | null) => void + allowClear?: boolean + }) => ( + + ), +})) + +vi.mock('@/lib/server/functions/contacts', () => ({ + updateContactFn: mocks.updateContactFn, + archiveContactFn: mocks.archiveContactFn, +})) + +vi.mock('@/lib/client/queries/contacts', () => ({ + contactQueries: { + detail: (contactId: string) => ({ queryKey: ['contacts', 'detail', contactId] }), + }, +})) + +vi.mock('@/lib/server/domains/authz', () => ({ + PERMISSIONS: { + ORG_MANAGE: 'org.manage', + }, +})) + +vi.mock('sonner', () => ({ + toast: { + success: mocks.toastSuccess, + error: mocks.toastError, + }, +})) + +function contact(overrides: Partial = {}): ContactProp { + return { + id: 'contact_1', + name: 'Ada Lovelace', + email: 'ada@example.com', + phone: '+1 555 1000', + title: 'Engineer', + externalId: 'crm-123', + organizationId: 'org_acme', + archivedAt: null, + ...overrides, + } as ContactProp +} + +beforeEach(() => { + vi.clearAllMocks() + mocks.permissionAllowed = true + mocks.updateContactFn.mockResolvedValue({ id: 'contact_1' }) + mocks.archiveContactFn.mockResolvedValue({ id: 'contact_1' }) +}) + +describe('ContactOverviewTab', () => { + it('saves trimmed contact fields, invalidates detail/list queries and shows success', async () => { + render() + + fireEvent.change(screen.getByLabelText('Name'), { target: { value: ' Grace Hopper ' } }) + fireEvent.change(screen.getByLabelText('Email'), { target: { value: ' grace@example.com ' } }) + fireEvent.change(screen.getByLabelText('Phone'), { target: { value: ' ' } }) + fireEvent.change(screen.getByLabelText('Title'), { target: { value: ' Admiral ' } }) + fireEvent.change(screen.getByLabelText('Organization'), { target: { value: 'org_beta' } }) + fireEvent.change(screen.getByLabelText('External ID'), { target: { value: ' crm-456 ' } }) + fireEvent.click(screen.getByRole('button', { name: 'Save changes' })) + + await waitFor(() => { + expect(mocks.updateContactFn).toHaveBeenCalledWith({ + data: { + contactId: 'contact_1', + name: 'Grace Hopper', + email: 'grace@example.com', + phone: null, + title: 'Admiral', + externalId: 'crm-456', + organizationId: 'org_beta', + }, + }) + }) + expect(mocks.invalidateQueries).toHaveBeenCalledWith({ + queryKey: ['contacts', 'detail', 'contact_1'], + }) + expect(mocks.invalidateQueries).toHaveBeenCalledWith({ queryKey: ['contacts'] }) + expect(mocks.toastSuccess).toHaveBeenCalledWith('Contact updated') + }) + + it('requires at least a name or email before saving and reports save failures', async () => { + render() + + fireEvent.change(screen.getByLabelText('Name'), { target: { value: ' ' } }) + fireEvent.change(screen.getByLabelText('Email'), { target: { value: ' ' } }) + fireEvent.click(screen.getByRole('button', { name: 'Save changes' })) + + expect(mocks.toastError).toHaveBeenCalledWith('Name or email is required') + expect(mocks.updateContactFn).not.toHaveBeenCalled() + + fireEvent.change(screen.getByLabelText('Email'), { target: { value: 'ada@example.com' } }) + mocks.updateContactFn.mockRejectedValueOnce(new Error('Duplicate contact')) + fireEvent.click(screen.getByRole('button', { name: 'Save changes' })) + + await waitFor(() => { + expect(mocks.toastError).toHaveBeenCalledWith('Duplicate contact') + }) + }) + + it('resets local fields when the contact prop changes', () => { + const { rerender } = render() + + fireEvent.change(screen.getByLabelText('Name'), { target: { value: 'Unsaved edit' } }) + expect(screen.getByLabelText('Name')).toHaveValue('Unsaved edit') + + rerender( + + ) + + expect(screen.getByLabelText('Name')).toHaveValue('') + expect(screen.getByLabelText('Email')).toHaveValue('new@example.com') + expect(screen.getByLabelText('Phone')).toHaveValue('') + expect(screen.getByLabelText('Title')).toHaveValue('') + expect(screen.getByLabelText('External ID')).toHaveValue('') + expect(screen.getByLabelText('Organization')).toHaveValue('') + }) + + it('archives active contacts and reports archive failures', async () => { + render() + + fireEvent.click(screen.getAllByRole('button', { name: 'Archive' })[1]) + + await waitFor(() => { + expect(mocks.archiveContactFn).toHaveBeenCalledWith({ + data: { contactId: 'contact_1' }, + }) + }) + expect(mocks.toastSuccess).toHaveBeenCalledWith('Contact archived') + + mocks.archiveContactFn.mockRejectedValueOnce(new Error('Archive denied')) + fireEvent.click(screen.getAllByRole('button', { name: 'Archive' })[1]) + + await waitFor(() => { + expect(mocks.toastError).toHaveBeenCalledWith('Archive denied') + }) + }) + + it('renders archived and denied-management states without mutation controls', () => { + const { rerender } = render( + + ) + + expect(screen.getByText('Archived')).toBeInTheDocument() + expect( + screen.getByText('This contact is archived. Restoring is not currently supported.') + ).toBeInTheDocument() + + mocks.permissionAllowed = false + rerender() + + expect(screen.queryByRole('button', { name: 'Save changes' })).not.toBeInTheDocument() + expect(screen.queryByRole('button', { name: 'Archive' })).not.toBeInTheDocument() + }) +}) diff --git a/apps/web/src/components/admin/contacts/__tests__/organization-create-dialog.test.tsx b/apps/web/src/components/admin/contacts/__tests__/organization-create-dialog.test.tsx new file mode 100644 index 000000000..2debdd7fc --- /dev/null +++ b/apps/web/src/components/admin/contacts/__tests__/organization-create-dialog.test.tsx @@ -0,0 +1,201 @@ +// @vitest-environment happy-dom +import type { ChangeEvent, ReactNode } from 'react' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { fireEvent, render, screen, waitFor } from '@testing-library/react' +import { OrganizationCreateDialog } from '../organization-create-dialog' + +type MutationOptions = { + mutationFn: () => Promise + onSuccess?: (result: T) => void + onError?: (error: Error) => void +} + +const mocks = vi.hoisted(() => ({ + createOrganizationFn: vi.fn(), + invalidateQueries: vi.fn(), + navigate: vi.fn(), + toastSuccess: vi.fn(), + toastError: vi.fn(), +})) + +vi.mock('@tanstack/react-query', () => ({ + useQueryClient: () => ({ + invalidateQueries: mocks.invalidateQueries, + }), + useMutation: (options: MutationOptions) => ({ + isPending: false, + mutate: async () => { + try { + const result = await options.mutationFn() + options.onSuccess?.(result) + } catch (error) { + options.onError?.(error instanceof Error ? error : new Error(String(error))) + } + }, + }), +})) + +vi.mock('@tanstack/react-router', () => ({ + useRouter: () => ({ + navigate: mocks.navigate, + }), +})) + +vi.mock('@/lib/server/functions/organizations', () => ({ + createOrganizationFn: mocks.createOrganizationFn, +})) + +vi.mock('sonner', () => ({ + toast: { + success: mocks.toastSuccess, + error: mocks.toastError, + }, +})) + +vi.mock('@/components/ui/button', () => ({ + Button: ({ + children, + disabled, + onClick, + type = 'button', + }: { + children: ReactNode + disabled?: boolean + onClick?: () => void + type?: 'button' | 'submit' | 'reset' + variant?: string + }) => ( + + ), +})) + +vi.mock('@/components/ui/input', () => ({ + Input: ({ + id, + value, + onChange, + placeholder, + }: { + id?: string + value?: string + onChange?: (event: ChangeEvent) => void + required?: boolean + maxLength?: number + placeholder?: string + className?: string + }) => , +})) + +vi.mock('@/components/ui/textarea', () => ({ + Textarea: ({ + id, + value, + onChange, + rows, + }: { + id?: string + value?: string + onChange?: (event: ChangeEvent) => void + rows?: number + maxLength?: number + }) =>