{"openapi":"3.1.0","info":{"title":"Codex Titan API","version":"1.0.0","description":"Codex Titan API — full public reference. Authentication via the `X-CT-API-Key` header. Per-key rate limits and plan tiers apply at runtime (see https://www.codextitan.com/#pricing). Generate an API key from the dashboard at https://app.codextitan.com.","contact":{"email":"support@codextitan.com"}},"servers":[{"url":"https://api.codextitan.com","description":"API Server"}],"security":[{"apiKey":[]}],"paths":{"/api/v1/tickets":{"get":{"operationId":"listTickets","summary":"List tickets with filtering and pagination","tags":["Tickets"],"parameters":[{"name":"X-CT-External-User-Id","in":"header","required":true,"schema":{"type":"string"},"description":"**Required.** Scopes the result to a single end user's tickets. Supply this header *or* the equivalent `externalUserId` query parameter below — either one satisfies the requirement.\n\n**Deprecated behaviour:** when neither is supplied the request currently falls through to a tenant-wide read and returns every ticket in the tenant. That fall-through is scheduled to fail closed in a future major version, which will return an empty list instead. Do not build against the unscoped response."},{"name":"status","in":"query","schema":{"type":"string","enum":["open","ai_responding","waiting_on_user","waiting_on_admin","escalated","resolved","closed","deleted"]},"description":"Filter by status. Pass `deleted` to retrieve soft-deleted tickets (excluded by default)."},{"name":"priority","in":"query","schema":{"type":"string","enum":["low","medium","high","urgent"]},"description":"Filter by priority"},{"name":"category","in":"query","schema":{"type":"string"},"description":"Filter by category"},{"name":"externalUserId","in":"query","schema":{"type":"string"},"description":"Filter by external user ID. Equivalent to the `X-CT-External-User-Id` header above — supplying either one scopes the read and satisfies the requirement."},{"name":"search","in":"query","schema":{"type":"string"},"description":"Search by subject, email, category, or ticket number"},{"name":"sortBy","in":"query","schema":{"type":"string","enum":["updatedAt","createdAt","priority"],"default":"updatedAt"}},{"name":"sortDir","in":"query","schema":{"type":"string","enum":["asc","desc"],"default":"desc"}},{"name":"limit","in":"query","schema":{"type":"integer","default":50,"maximum":200}},{"name":"startAfter","in":"query","schema":{"type":"string"},"description":"Ticket ID cursor for pagination"}],"responses":{"200":{"description":"Ticket list with pagination"}}},"post":{"operationId":"createTicket","summary":"Create a support ticket","description":"Create a new support ticket with an initial message. Creating a ticket with `message` enqueues AI triage when enabled; do not re-post that message via POST /tickets/{id}/messages. No monthly limit.","tags":["Tickets"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateTicketRequest"},"example":{"category":"billing","subject":"Subscription question","message":"How do I upgrade my plan?","externalUserId":"user-123","externalUserEmail":"user@example.com","priority":"medium","context":{"device":{"appVersion":"1.0.0","platform":"ios"}}}}}},"responses":{"201":{"description":"Ticket created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateTicketResponse"}}}},"400":{"description":"Validation error"},"429":{"description":"Quota or rate limit exceeded"}}}},"/api/v1/tickets/{ticketId}":{"get":{"operationId":"getTicket","summary":"Get a ticket by ID","tags":["Tickets"],"parameters":[{"name":"ticketId","in":"path","required":true,"schema":{"type":"string"}},{"name":"X-CT-External-User-Id","in":"header","required":true,"schema":{"type":"string"},"description":"**Required.** The end user the ticket must belong to. A ticket owned by anyone else returns 404. There is no query-parameter equivalent on this route.\n\n**Deprecated behaviour:** when the header is absent the ownership check does not run and any ticket in the tenant is returned. That fall-through is scheduled to fail closed in a future major version, which will return 404 instead. Do not build against the unscoped response."}],"responses":{"200":{"description":"Ticket details"},"404":{"description":"Ticket not found"}}},"patch":{"operationId":"updateTicket","summary":"Update ticket status, priority, or resolution","tags":["Tickets"],"parameters":[{"name":"ticketId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateTicketRequest"},"example":{"status":"resolved","resolutionSource":"human"}}}},"responses":{"200":{"description":"Ticket updated"},"404":{"description":"Ticket not found"}}}},"/api/v1/tickets/user/{externalUserId}":{"get":{"operationId":"listTicketsByUser","summary":"List tickets for a specific external user","tags":["Tickets"],"parameters":[{"name":"externalUserId","in":"path","required":true,"schema":{"type":"string","maxLength":200,"pattern":"^[A-Za-z0-9][A-Za-z0-9._:-]{0,199}$","description":"Letters, numbers, '.', '_', '-', ':'. Max 200."}},{"name":"limit","in":"query","schema":{"type":"integer","default":50,"maximum":200}}],"responses":{"200":{"description":"Tickets for the user with count"},"400":{"description":"externalUserId failed charset or length validation"}}}},"/api/v1/tickets/batch":{"post":{"operationId":"batchCreateTickets","summary":"Create up to 25 tickets in a single request","tags":["Tickets"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["items"],"properties":{"items":{"type":"array","minItems":1,"maxItems":25,"items":{"type":"object"}}}}}}},"responses":{"200":{"description":"Batch processed; per-item succeeded/failed arrays returned"},"400":{"description":"Invalid envelope (empty array, too large, missing items)"},"429":{"description":"Batch would exceed monthly tickets quota"}}},"patch":{"operationId":"batchUpdateTickets","summary":"Update status or priority on up to 50 tickets","tags":["Tickets"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["items"],"properties":{"items":{"type":"array","minItems":1,"maxItems":50,"items":{"type":"object","required":["id"],"properties":{"id":{"type":"string"},"status":{"type":"string"},"priority":{"type":"string"}}}}}}}}},"responses":{"200":{"description":"Batch processed; per-item succeeded/failed arrays returned"},"400":{"description":"Invalid envelope"}}}},"/api/v1/tickets/{ticketId}/messages":{"get":{"operationId":"getMessages","summary":"Get message thread for a ticket","tags":["Messages"],"parameters":[{"name":"ticketId","in":"path","required":true,"schema":{"type":"string"}},{"name":"X-CT-External-User-Id","in":"header","required":true,"schema":{"type":"string"},"description":"**Required.** The end user the ticket must belong to. A ticket owned by anyone else returns 404. There is no query-parameter equivalent on this route.\n\n**Deprecated behaviour:** when the header is absent the ownership check does not run and the full conversation of any ticket in the tenant is returned. That fall-through is scheduled to fail closed in a future major version, which will return 404 instead. Do not build against the unscoped response."}],"responses":{"200":{"description":"Message list"}}},"post":{"operationId":"addMessage","summary":"Add a follow-up user message to a ticket thread","description":"Follow-up turns only. Messages sent via API are always role='user'. Do not re-post the opening create message to kick AI — create-time triage is async; poll GET messages. triggerAI requests another AI turn while the ticket is still AI-owned. Admin/AI roles are set via the dashboard only.","tags":["Messages"],"parameters":[{"name":"ticketId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateMessageRequest"},"example":{"content":"I already tried that, it didn't work"}}}},"responses":{"201":{"description":"Message added"},"404":{"description":"Ticket not found"}}}},"/api/v1/tickets/{ticketId}/messages/{messageId}/rate":{"post":{"operationId":"rateMessage","summary":"Rate a ticket message as helpful or not","tags":["Messages"],"parameters":[{"name":"ticketId","in":"path","required":true,"schema":{"type":"string"}},{"name":"messageId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["helpful"],"properties":{"helpful":{"type":"boolean"},"reason":{"type":"string","maxLength":500}}},"example":{"helpful":true,"reason":"This resolved my issue"}}}},"responses":{"200":{"description":"Rating recorded"},"400":{"description":"Validation error"},"404":{"description":"Message or ticket not found (incl. ticket not owned by X-CT-External-User-Id)"}}}},"/api/v1/kb/articles":{"get":{"operationId":"listKBArticles","summary":"List knowledge base articles","tags":["Knowledge Base"],"parameters":[{"name":"category","in":"query","required":false,"schema":{"type":"string"},"description":"Filter by category"}],"responses":{"200":{"description":"Article list with total count"}}},"post":{"summary":"Create a KB article","description":"Persists a knowledge-base article. Body content is sanitized through @codextitan/content-versioning before write.","tags":["Knowledge Base"],"security":[{"apiKey":[]}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string","minLength":1,"maxLength":200},"content":{"type":"string","minLength":1,"maxLength":500000},"slug":{"type":"string","maxLength":200},"excerpt":{"type":"string","maxLength":500},"category":{"type":"string","maxLength":100},"status":{"type":"string","enum":["draft","published","archived"]},"tags":{"type":"array","items":{"type":"string","maxLength":50},"maxItems":20},"searchKeywords":{"type":"array","items":{"type":"string","maxLength":100},"maxItems":20},"order":{"type":"integer"},"relatedArticles":{"type":"array","items":{"type":"string","maxLength":200},"maxItems":20},"authorName":{"type":"string","maxLength":200}},"required":["title","content"]}}}},"responses":{"201":{"description":"KB article created","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"articleId":{"type":"string"}},"required":["success","articleId"]}}}},"429":{"description":"Plan limit reached","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"status":{"type":"number"}},"required":["code","message","status"]}},"required":["error"]}}}}}}},"/api/v1/kb/articles/search":{"get":{"operationId":"searchKBArticles","summary":"Search KB articles by keyword","tags":["Knowledge Base"],"parameters":[{"name":"q","in":"query","required":true,"schema":{"type":"string"},"description":"Search query (matches title, excerpt, tags)"},{"name":"category","in":"query","schema":{"type":"string"},"description":"Filter by category"},{"name":"limit","in":"query","schema":{"type":"integer","default":20,"maximum":100}}],"responses":{"200":{"description":"Matching articles with count and query echo"}}}},"/api/v1/kb/articles/{articleId}":{"get":{"operationId":"getKBArticle","summary":"Get a single KB article (increments view count)","tags":["Knowledge Base"],"parameters":[{"name":"articleId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Article details"},"404":{"description":"Not found"}}}},"/api/v1/kb/articles/{articleId}/rate":{"post":{"operationId":"rateKBArticle","summary":"Rate a KB article as helpful or not","tags":["Knowledge Base"],"parameters":[{"name":"articleId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"helpful":{"type":"boolean"}},"required":["helpful"]}}}},"responses":{"200":{"description":"Rating recorded"},"404":{"description":"Not found"}}}},"/api/v1/faqs":{"get":{"operationId":"listFAQs","summary":"List published FAQs","tags":["FAQs"],"parameters":[{"name":"category","in":"query","required":false,"schema":{"type":"string"}}],"responses":{"200":{"description":"FAQ list"}}},"post":{"summary":"Create a FAQ","description":"Creates a FAQ. Answer body is sanitized through @codextitan/content-versioning before write.","tags":["FAQs"],"security":[{"apiKey":[]}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"question":{"type":"string","minLength":1,"maxLength":500},"answer":{"type":"string","minLength":1,"maxLength":50000},"category":{"type":"string","maxLength":100},"categoryOrder":{"type":"integer"},"order":{"type":"integer"},"published":{"type":"boolean"},"audience":{"type":"string","enum":["all","user","admin"]},"tags":{"type":"array","items":{"type":"string","maxLength":50},"maxItems":20},"searchKeywords":{"type":"array","items":{"type":"string","maxLength":100},"maxItems":20}},"required":["question","answer"]}}}},"responses":{"201":{"description":"FAQ created","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"faqId":{"type":"string"}},"required":["success","faqId"]}}}},"429":{"description":"Plan limit reached","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"status":{"type":"number"}},"required":["code","message","status"]}},"required":["error"]}}}}}}},"/api/v1/faqs/search":{"get":{"operationId":"searchFAQs","summary":"Search FAQs by keyword","tags":["FAQs"],"parameters":[{"name":"q","in":"query","required":true,"schema":{"type":"string"},"description":"Search query (matches question, tags)"},{"name":"category","in":"query","schema":{"type":"string"},"description":"Filter by category"},{"name":"audience","in":"query","schema":{"type":"string"},"description":"Filter by target audience"},{"name":"limit","in":"query","schema":{"type":"integer","default":20,"maximum":100}}],"responses":{"200":{"description":"Matching FAQs with count and query echo"}}}},"/api/v1/faqs/{faqId}":{"get":{"operationId":"getFAQ","summary":"Get a single published FAQ","tags":["FAQs"],"parameters":[{"name":"faqId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"FAQ details"},"404":{"description":"Not found"}}}},"/api/v1/faqs/{faqId}/rate":{"post":{"operationId":"rateFAQ","summary":"Rate a FAQ as helpful or not","tags":["FAQs"],"parameters":[{"name":"faqId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"helpful":{"type":"boolean"}},"required":["helpful"]}}}},"responses":{"200":{"description":"Rating recorded"},"404":{"description":"Not found"}}}},"/api/v1/feedback":{"post":{"operationId":"submitFeedback","summary":"Submit user feedback (NPS, CSAT, or general)","description":"Collect user feedback.","tags":["Feedback"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateFeedbackRequest"},"example":{"type":"nps","score":9,"comment":"Great app!","externalUserId":"user-123"}}}},"responses":{"201":{"description":"Feedback submitted"},"400":{"description":"Validation error"},"429":{"description":"Quota exceeded"}}},"get":{"operationId":"listFeedback","summary":"List feedback with filtering and pagination","tags":["Feedback"],"parameters":[{"name":"type","in":"query","schema":{"type":"string","enum":["nps","csat","general"]},"description":"Filter by feedback type"},{"name":"sentiment","in":"query","schema":{"type":"string"},"description":"Filter by sentiment"},{"name":"limit","in":"query","schema":{"type":"integer","default":50,"maximum":200}},{"name":"startAfter","in":"query","schema":{"type":"string"},"description":"Feedback ID cursor for pagination"}],"responses":{"200":{"description":"Feedback list with pagination"}}}},"/api/v1/feedback/batch":{"post":{"operationId":"batchSubmitFeedback","summary":"Submit up to 50 feedback records in a single request","tags":["Feedback"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["items"],"properties":{"items":{"type":"array","minItems":1,"maxItems":50,"items":{"$ref":"#/components/schemas/CreateFeedbackRequest"}}}}}}},"responses":{"200":{"description":"Batch processed; per-item succeeded/failed arrays returned"},"400":{"description":"Invalid envelope"},"429":{"description":"Batch would exceed monthly feedback quota"}}}},"/api/v1/feedback/{feedbackId}":{"patch":{"operationId":"updateFeedback","summary":"Update feedback status or admin notes","tags":["Feedback"],"parameters":[{"name":"feedbackId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["new","reviewed","actioned"]},"adminNotes":{"type":"string","maxLength":5000,"nullable":true}}}}}},"responses":{"200":{"description":"Feedback updated"},"404":{"description":"Feedback not found"}}}},"/api/v1/bugs":{"post":{"operationId":"reportBug","summary":"Submit a bug report","description":"Report a bug with severity and reproduction steps.","tags":["Bug Reports"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateBugReportRequest"},"example":{"title":"App crashes on launch","description":"Crashes after splash screen","severity":"critical"}}}},"responses":{"201":{"description":"Bug report submitted"},"400":{"description":"Validation error"},"429":{"description":"Quota exceeded"}}},"get":{"operationId":"listBugReports","summary":"List bug reports with filtering and pagination","tags":["Bug Reports"],"parameters":[{"name":"status","in":"query","schema":{"type":"string","enum":["new","investigating","fixed","wont_fix","duplicate"]},"description":"Filter by status"},{"name":"severity","in":"query","schema":{"type":"string","enum":["low","medium","high","critical"]},"description":"Filter by severity"},{"name":"search","in":"query","schema":{"type":"string"},"description":"Search by title or description"},{"name":"limit","in":"query","schema":{"type":"integer","default":50,"maximum":200}},{"name":"startAfter","in":"query","schema":{"type":"string"},"description":"Bug report ID cursor for pagination"}],"responses":{"200":{"description":"Bug report list with pagination"}}}},"/api/v1/bugs/{bugId}":{"get":{"operationId":"getBugReport","summary":"Get a single bug report by ID","tags":["Bug Reports"],"parameters":[{"name":"bugId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Bug report details"},"404":{"description":"Bug report not found"}}},"patch":{"operationId":"updateBugReport","summary":"Update bug report status, severity, or admin notes","tags":["Bug Reports"],"parameters":[{"name":"bugId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["new","investigating","fixed","wont_fix","duplicate"]},"severity":{"type":"string","enum":["low","medium","high","critical"]},"adminNotes":{"type":"string","maxLength":5000,"nullable":true},"adminResponse":{"type":"string","maxLength":5000,"nullable":true},"linkedTicketId":{"type":"string","nullable":true},"duplicateOfId":{"type":"string","nullable":true}}}}}},"responses":{"200":{"description":"Bug report updated"},"404":{"description":"Bug report not found"}}}},"/api/v1/bugs/batch":{"patch":{"operationId":"batchUpdateBugs","summary":"Update status/severity on up to 50 bug reports","tags":["Bug Reports"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["items"],"properties":{"items":{"type":"array","minItems":1,"maxItems":50,"items":{"type":"object","required":["id"],"properties":{"id":{"type":"string"},"status":{"type":"string"},"severity":{"type":"string"}}}}}}}}},"responses":{"200":{"description":"Batch processed; per-item succeeded/failed arrays returned"},"400":{"description":"Invalid envelope"}}}},"/api/v1/features":{"post":{"operationId":"submitFeatureRequest","summary":"Submit a feature request","description":"Submit a feature request from a user.","tags":["Feature Requests"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateFeatureRequest"},"example":{"title":"Dark mode","description":"Please add dark mode support","externalUserId":"user-123"}}}},"responses":{"201":{"description":"Feature request submitted"},"400":{"description":"Validation error"},"429":{"description":"Quota exceeded"}}},"get":{"operationId":"listFeatureRequests","summary":"List feature requests with filtering and pagination","tags":["Feature Requests"],"parameters":[{"name":"status","in":"query","schema":{"type":"string","enum":["submitted","under_review","planned","in_progress","shipped","completed","declined"]},"description":"Filter by status"},{"name":"visibility","in":"query","schema":{"type":"string"},"description":"Filter by visibility (e.g. public, private)"},{"name":"search","in":"query","schema":{"type":"string"},"description":"Search by title or description"},{"name":"sortBy","in":"query","schema":{"type":"string","enum":["voteCount","createdAt"],"default":"voteCount"}},{"name":"limit","in":"query","schema":{"type":"integer","default":50,"maximum":200}},{"name":"startAfter","in":"query","schema":{"type":"string"},"description":"Feature request ID cursor for pagination"}],"responses":{"200":{"description":"Feature request list with pagination"}}}},"/api/v1/features/{featureId}/vote":{"post":{"operationId":"voteOnFeature","summary":"Vote on a feature request","tags":["Feature Requests"],"parameters":[{"name":"featureId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"externalUserId":{"type":"string"}},"required":["externalUserId"]}}}},"responses":{"200":{"description":"Vote recorded"},"404":{"description":"Not found"},"409":{"description":"Already voted"}}}},"/api/v1/features/{featureId}":{"get":{"operationId":"getFeatureRequest","summary":"Get a single feature request by ID","tags":["Feature Requests"],"parameters":[{"name":"featureId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Feature request details"},"404":{"description":"Feature request not found"}}},"patch":{"operationId":"updateFeatureRequest","summary":"Update feature request status, priority, visibility, or admin notes","tags":["Feature Requests"],"parameters":[{"name":"featureId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"status":{"type":"string","enum":["submitted","under_review","planned","in_progress","shipped","completed","declined"]},"priority":{"type":"string"},"visibility":{"type":"string"},"category":{"type":"string"},"tags":{"type":"array","items":{"type":"string"}},"adminResponse":{"type":"string","maxLength":5000},"adminNotes":{"type":"string","maxLength":5000}}}}}},"responses":{"200":{"description":"Feature request updated"},"404":{"description":"Feature request not found"}}}},"/api/v1/features/batch":{"patch":{"operationId":"batchUpdateFeatures","summary":"Update status on up to 50 feature requests","tags":["Feature Requests"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["items"],"properties":{"items":{"type":"array","minItems":1,"maxItems":50,"items":{"type":"object","required":["id"],"properties":{"id":{"type":"string"},"status":{"type":"string"}}}}}}}}},"responses":{"200":{"description":"Batch processed; per-item succeeded/failed arrays returned"},"400":{"description":"Invalid envelope"}}}},"/api/v1/users/{userId}":{"put":{"operationId":"upsertUser","summary":"Create or update a user profile","description":"Sync user profiles from your app. Users are also auto-created when they submit tickets.","tags":["Users"],"parameters":[{"name":"userId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpsertUserRequest"},"example":{"email":"user@example.com","displayName":"Jane Smith","metadata":{"subscriptionTier":"pro","platform":"ios"}}}}},"responses":{"200":{"description":"User updated"},"201":{"description":"User created"}}},"get":{"operationId":"getUser","summary":"Get a single user profile","tags":["Users"],"parameters":[{"name":"userId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"User details"},"404":{"description":"Not found"}}},"delete":{"operationId":"deleteUser","summary":"Delete a user profile","tags":["Users"],"parameters":[{"name":"userId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"User deleted"},"404":{"description":"Not found"}}}},"/api/v1/users":{"get":{"operationId":"listUsers","summary":"List tenant users with pagination","tags":["Users"],"parameters":[{"name":"limit","in":"query","schema":{"type":"integer","default":50,"maximum":100}},{"name":"startAfter","in":"query","schema":{"type":"string"},"description":"User ID to start after for pagination"}],"responses":{"200":{"description":"User list"}}}},"/api/v1/users/batch":{"post":{"operationId":"batchUpsertUsers","summary":"Batch create/update up to 100 user profiles","tags":["Users"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"users":{"type":"array","maxItems":100,"items":{"$ref":"#/components/schemas/UpsertUserRequest"}}},"required":["users"]}}}},"responses":{"200":{"description":"Batch result with created/updated counts"}}}},"/api/v1/users/{userId}/profile":{"get":{"operationId":"getUnifiedUserProfile","summary":"Get unified user profile combining tenant user and widget user data","tags":["Users"],"parameters":[{"name":"userId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Unified profile"},"404":{"description":"User not found"}}}},"/api/v1/users/{userId}/dedup-candidates":{"get":{"operationId":"getDedupCandidates","summary":"Find candidate widget users for deduplication","description":"Returns widget users that may be the same person as the given tenant user, based on email matching.","tags":["Users"],"parameters":[{"name":"userId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"List of dedup candidates"},"404":{"description":"User not found"}}}},"/api/v1/users/{userId}/link":{"post":{"operationId":"linkUserToWidgetUser","summary":"Manually link a tenant user to a widget user","description":"Merge a widget user identity into an existing tenant user for deduplication.","tags":["Users"],"parameters":[{"name":"userId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["widgetUserId"],"properties":{"widgetUserId":{"type":"string","description":"ID of the widget user to link"}}}}}},"responses":{"200":{"description":"User linked successfully"},"400":{"description":"Validation error"},"404":{"description":"User or widget user not found"}}}},"/api/v1/legal":{"get":{"operationId":"listPublishedLegalDocs","summary":"List all published legal documents","tags":["Legal Documents"],"responses":{"200":{"description":"Array of published legal documents with type, title, effective date, version, and URL","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"tenant_name":{"type":"string"},"documents":{"type":"array","items":{"type":"object","properties":{"type":{"type":"string","enum":["privacy_policy","terms_of_service","cookie_policy","dpa"]},"title":{"type":"string"},"effective_date":{"type":"string","nullable":true},"version":{"type":"string"},"url":{"type":"string","description":"Relative URL to fetch the full document"}}}},"count":{"type":"integer"}}}}}}}}}}},"/api/v1/legal/published/{docType}":{"get":{"operationId":"getPublishedLegalDoc","summary":"Get a specific published legal document","tags":["Legal Documents"],"parameters":[{"name":"docType","in":"path","required":true,"schema":{"type":"string","enum":["privacy-policy","terms-of-service","cookie-policy","dpa"]},"description":"Document type slug"}],"responses":{"200":{"description":"Published legal document with markdown and HTML content","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"type":{"type":"string"},"title":{"type":"string"},"content_markdown":{"type":"string","description":"Original markdown content"},"content_html":{"type":"string","description":"Rendered and sanitized HTML"},"effective_date":{"type":"string","nullable":true},"version":{"type":"string"},"tenant_name":{"type":"string"}}}}}}}},"404":{"description":"No published document found for this type"},"410":{"description":"Document has been archived"}}},"delete":{"summary":"Unpublish (archive) the published document for a docType","tags":["Legal Documents"],"security":[{"apiKey":[]}],"parameters":[{"schema":{"type":"string","enum":["privacy-policy","terms-of-service","cookie-policy","dpa"]},"required":true,"name":"docType","in":"path"}],"responses":{"200":{"description":"Unpublished (or none was published)","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"docId":{"type":["string","null"]},"alreadyArchived":{"type":"boolean"}},"required":["success","docId"]}}}},"404":{"description":"No published doc found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"status":{"type":"number"},"existingDocId":{"type":"string"},"hint":{"type":"string"}},"required":["code","message","status"]}},"required":["error"]}}}}}}},"/api/v1/dsar/public":{"post":{"operationId":"submitPublicDSAR","summary":"Submit a public DSAR (browser / embed)","description":"Subject-facing intake. Authenticate with a publishable `apiKey` in the JSON body (prefer `permissions: []`). CT emails a verification link; the request is persisted only after click. Rate-limited per client IP (5/hour). Do not proxy through shared egress. Do not use `/api/v1/dsar-api` from public HTML forms. See docs/guides/dsar-intake.md.","tags":["Data Requests"],"security":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreatePublicDSARRequest"},"example":{"apiKey":"tnk_xxxxxxxx_…","type":"access","subjectEmail":"user@example.com","subjectName":"Jane Doe","honeypot":""}}}},"responses":{"200":{"description":"Verification email sent (or honeypot no-op). Message only — no requestId.","content":{"application/json":{"schema":{"type":"object","properties":{"message":{"type":"string"}}}}}},"400":{"description":"Validation error"},"401":{"description":"Invalid or inactive apiKey"},"429":{"description":"Per-IP rate limit exceeded"}}}},"/api/v1/embed/dsar-form":{"get":{"operationId":"embedDsarForm","summary":"Embeddable DSAR form HTML","description":"HTML page for iframe embedding. Pass the publishable key as `key`. Optional `theme=light|dark`. Prefer empty-scope keys.","tags":["Data Requests"],"security":[],"parameters":[{"name":"key","in":"query","required":true,"schema":{"type":"string"},"description":"Publishable API key"},{"name":"theme","in":"query","schema":{"type":"string","enum":["light","dark"]}}],"responses":{"200":{"description":"HTML document"},"400":{"description":"Missing key"},"401":{"description":"Invalid key"}}}},"/api/v1/dsar-api":{"get":{"operationId":"listDSARs","summary":"List data subject access requests","tags":["Data Requests"],"parameters":[{"name":"status","in":"query","schema":{"type":"string","enum":["pending","verified","assigned","in_progress","data_gathered","review","processor_erased","completed","denied","closed"]}},{"name":"type","in":"query","schema":{"type":"string","enum":["access","deletion","portability","rectification"]}},{"name":"limit","in":"query","schema":{"type":"integer","default":50,"maximum":100}},{"name":"startAfter","in":"query","schema":{"type":"string"},"description":"Request ID cursor for pagination"},{"name":"subjectEmail","in":"query","schema":{"type":"string","maxLength":320},"description":"ZD-589: Filter by subject email. Requires dsar:update permission (operator-level). Writes one audit log entry per filtered query (action: read, resource: dsar, 1-year retention)."}],"responses":{"200":{"description":"DSAR list with pagination","content":{"application/json":{"schema":{"type":"object","properties":{"requests":{"type":"array","items":{"$ref":"#/components/schemas/DSARRequest"}},"pagination":{"type":"object","properties":{"count":{"type":"integer"},"hasMore":{"type":"boolean"},"nextCursor":{"type":"string"}}}}}}}},"400":{"description":"Validation error (e.g. subjectEmail empty/whitespace/missing @ or exceeds 320 chars)"},"403":{"description":"subjectEmail filter present but key lacks dsar:update permission"}}},"post":{"operationId":"createDSAR","summary":"Submit a DSAR via API (server only)","description":"Trusted backend after you authenticated the subject. Requires `X-CT-API-Key` + `dsar:create`. No CT verification email (`source: api`). Never call from a public HTML form — use POST /api/v1/dsar/public. Property proxies to /dsar/public are deprecated (shared egress collapses per-IP limits). Deadlines: 30 days (GDPR) or 45 days (CCPA for California).","tags":["Data Requests"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateDSARRequest"},"example":{"type":"access","subjectEmail":"user@example.com","subjectName":"Jane Doe","jurisdiction":"california","description":"I would like a copy of all my personal data"}}}},"responses":{"201":{"description":"DSAR created","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DSARRequest"}}}},"400":{"description":"Validation error"}}}},"/api/v1/dsar-api/{requestId}":{"get":{"operationId":"getDSAR","summary":"Get DSAR details with activity timeline","tags":["Data Requests"],"parameters":[{"name":"requestId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"DSAR details including activity log"},"404":{"description":"Request not found"}}},"patch":{"operationId":"updateDSAR","summary":"Update DSAR description","description":"API key users can update the description only. Status changes must be made via the dashboard.","tags":["Data Requests"],"parameters":[{"name":"requestId","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"description":{"type":"string","maxLength":5000}}}}}},"responses":{"200":{"description":"DSAR updated"},"400":{"description":"Cannot update completed/denied/closed requests"},"404":{"description":"Request not found"}}}},"/api/v1/dsar-api/{requestId}/status":{"get":{"operationId":"getDSARStatus","summary":"Lightweight DSAR status check","tags":["Data Requests"],"parameters":[{"name":"requestId","in":"path","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Status, type, deadline, and completion date","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string"},"type":{"type":"string"},"deadline":{"type":"string","format":"date-time"},"completedAt":{"type":"string","format":"date-time","nullable":true}}}}}},"404":{"description":"Request not found"}}}},"/api/v1/credits/balance":{"get":{"summary":"Get the tenant's current AI credit balance","description":"Returns the tenant's monthly credit allocation, usage, remaining balance, and any overage reserve state. Internal tenants are flagged as unlimited.","tags":["Credits"],"security":[{"apiKey":[]}],"responses":{"200":{"description":"Credit balance and overage reserve state","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"monthlyAllocation":{"type":"number"},"monthlyUsed":{"type":"number"},"monthlyRemaining":{"type":"number"},"totalAvailable":{"type":"number"},"isUnlimited":{"type":"boolean"},"isInternal":{"type":"boolean"},"overageReserve":{"type":["object","null"],"properties":{"enabled":{"type":"boolean"},"balanceCents":{"type":"number"},"costPerCreditCents":{"type":"number"},"currentMonthSpendCents":{"type":"number"},"maxMonthlySpendCents":{"type":["number","null"]}},"required":["enabled","balanceCents","costPerCreditCents","currentMonthSpendCents","maxMonthlySpendCents"]}},"required":["monthlyAllocation","monthlyUsed","monthlyRemaining","totalAvailable","isUnlimited","isInternal","overageReserve"]}},"required":["data"]}}}},"500":{"description":"Internal error fetching credit balance","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"status":{"type":"number"}},"required":["code","message","status"]}},"required":["error"]}}}}}}},"/api/v1/credits/ledger":{"get":{"operationId":"getCreditLedger","summary":"Get recent credit usage history","tags":["Credits"],"parameters":[{"name":"limit","in":"query","schema":{"type":"integer","default":50,"maximum":200}}],"responses":{"200":{"description":"Ledger entries with operation, credits consumed, and timestamps"}}}},"/api/v1/credits/config":{"get":{"operationId":"getCreditConfig","summary":"Get AI credit pricing and operation costs","tags":["Credits"],"responses":{"200":{"description":"Operation costs, tier multipliers, and plan credit allocation"}}}},"/api/v1/consent-log":{"get":{"operationId":"listConsentEvents","summary":"List consent log events","tags":["Consent"],"parameters":[{"name":"property","in":"query","schema":{"type":"string"},"description":"Filter by property/domain"},{"name":"action","in":"query","schema":{"type":"string","enum":["grant","deny","revoke"]}},{"name":"startDate","in":"query","schema":{"type":"string","format":"date"}},{"name":"endDate","in":"query","schema":{"type":"string","format":"date"}},{"name":"limit","in":"query","schema":{"type":"integer","default":50,"maximum":200}},{"name":"startAfter","in":"query","schema":{"type":"string"},"description":"Event ID cursor for pagination"}],"responses":{"200":{"description":"Consent events with pagination"}}},"post":{"operationId":"logConsentEvent","summary":"Log a consent event from a consent banner","description":"Records a consent event in the tenant's append-only consent log.\n\n**Synthetic monitoring:** send `X-CT-Dry-Run: 1` to run the full validation path — auth, permissions, rate limit, and every field check — and receive the result WITHOUT writing to the audit trail. A valid payload returns `200 { data: { dryRun: true, valid: true } }`; an invalid one returns the same 4xx status, code, and message a real write would. Probe this endpoint with the dry-run header rather than posting real events, so uptime checks never fabricate consent records.","tags":["Consent"],"parameters":[{"name":"X-CT-Dry-Run","in":"header","required":false,"schema":{"type":"string","enum":["1","true"]},"description":"Validate only — returns 200 on success and persists nothing. Use for synthetic probes."}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateConsentLogRequest"},"example":{"subjectId":"visitor-abc123","action":"grant","categories":{"necessary":true,"analytics":true,"marketing":false},"property":"example.com","bannerRevision":1,"gpcDetected":false,"secGpcDetected":false,"region":"US-CA","consentRegime":"opt-out","gcmSignals":{"analytics_storage":"granted","ad_storage":"denied"}}}}},"responses":{"200":{"description":"Dry run only — payload valid, nothing persisted"},"201":{"description":"Consent event logged. `data.warnings.unknownFields` lists any body fields CT did not recognise."},"400":{"description":"Validation error"}}}},"/api/v1/consent-log/stats":{"get":{"operationId":"getConsentStats","summary":"Get aggregate consent statistics","tags":["Consent"],"parameters":[{"name":"property","in":"query","schema":{"type":"string"},"description":"Filter by property/domain"},{"name":"startDate","in":"query","schema":{"type":"string","format":"date"}},{"name":"endDate","in":"query","schema":{"type":"string","format":"date"}}],"responses":{"200":{"description":"Aggregate stats: total events, grant rate, breakdown by category, GPC detection count"}}}},"/api/v1/consent/status/{externalUserId}":{"get":{"operationId":"getUserConsentStatus","summary":"Get current consent status for a user","tags":["Consent"],"parameters":[{"name":"externalUserId","in":"path","required":true,"schema":{"type":"string","maxLength":200,"pattern":"^[A-Za-z0-9][A-Za-z0-9._:-]{0,199}$","description":"Letters, numbers, '.', '_', '-', ':'. Max 200."}}],"responses":{"200":{"description":"Current consent record and all historical records by config ID"},"404":{"description":"No consent records found"}}},"delete":{"operationId":"deleteUserConsentRecords","summary":"Delete all consent records for a user (DSAR fulfillment)","description":"Purges every consent_records document with userId == externalUserId for the tenant. Writes a compliance audit log entry (action=consent_deleted, 1-year retention). Does NOT touch consent_log — the audit trail is preserved per retention policy.","tags":["Consent"],"parameters":[{"name":"externalUserId","in":"path","required":true,"schema":{"type":"string","maxLength":200,"pattern":"^[A-Za-z0-9][A-Za-z0-9._:-]{0,199}$","description":"Letters, numbers, '.', '_', '-', ':'. Max 200."}}],"responses":{"200":{"description":"Records deleted with count and list of configIds affected","content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"type":"integer"},"externalUserId":{"type":"string"},"deletedConfigIds":{"type":"array","items":{"type":"string"}}}}}}},"400":{"description":"externalUserId missing"},"404":{"description":"No consent records found for the user"}}}},"/api/v1/consent/status/{externalUserId}/{category}":{"get":{"operationId":"checkUserCategoryConsent","summary":"Check if a specific consent category is granted for a user","tags":["Consent"],"parameters":[{"name":"externalUserId","in":"path","required":true,"schema":{"type":"string","maxLength":200,"pattern":"^[A-Za-z0-9][A-Za-z0-9._:-]{0,199}$","description":"Letters, numbers, '.', '_', '-', ':'. Max 200."}},{"name":"category","in":"path","required":true,"schema":{"type":"string","enum":["necessary","analytics","marketing","functional"]}}],"responses":{"200":{"description":"Consent status for the category","content":{"application/json":{"schema":{"type":"object","properties":{"granted":{"type":"boolean"},"category":{"type":"string"},"userId":{"type":"string"}}}}}}}}},"/api/v1/consent/status/batch":{"post":{"operationId":"batchCheckConsent","summary":"Check consent status for multiple users","tags":["Consent"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["userIds"],"properties":{"userIds":{"type":"array","items":{"type":"string"},"minItems":1,"maxItems":100},"category":{"type":"string","description":"Optional: check a specific category only"}}}}}},"responses":{"200":{"description":"Consent status per user ID"},"400":{"description":"Validation error"}}}},"/api/v1/consent/status":{"post":{"operationId":"upsertConsentRecord","summary":"Create or update a consent record","description":"Called by the widget when a user updates their consent preferences. Creates a new record or updates existing.","tags":["Consent"],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpsertConsentRequest"},"example":{"userId":"visitor-abc123","configId":"consent-config-1","categories":{"necessary":true,"analytics":true,"marketing":false,"functional":true},"revision":1,"gpcDetected":false,"source":"banner"}}}},"responses":{"200":{"description":"Consent record upserted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"consentId":{"type":"string"},"action":{"type":"string","enum":["granted","updated","revoked"]},"categories":{"type":"object"}}}}}},"400":{"description":"Validation error"}}}},"/api/v1/tickets/{id}":{"delete":{"summary":"Soft-delete a ticket","description":"Marks a ticket as deleted (status → 'deleted', deletedAt/deletedBy set). Excluded from default list queries; reachable via direct GET /:id for audit lookups. Idempotent — a second call returns { alreadyDeleted: true }.","tags":["Tickets"],"security":[{"apiKey":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":200},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Ticket soft-deleted (or already deleted)","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"ticketId":{"type":"string"},"bugReportId":{"type":"string"},"featureRequestId":{"type":"string"},"feedbackId":{"type":"string"},"alreadyDeleted":{"type":"boolean"},"deleted":{"type":"boolean"}},"required":["success"]}}}},"404":{"description":"Ticket not found in this tenant","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"status":{"type":"number"}},"required":["code","message","status"]}},"required":["error"]}}}}}}},"/api/v1/kb/articles/{id}":{"put":{"summary":"Replace a KB article (partial accepted)","tags":["Knowledge Base"],"security":[{"apiKey":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":200},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string","minLength":1,"maxLength":200},"content":{"type":"string","minLength":1,"maxLength":500000},"slug":{"type":"string","maxLength":200},"excerpt":{"type":"string","maxLength":500},"category":{"type":"string","maxLength":100},"status":{"type":"string","enum":["draft","published","archived"]},"tags":{"type":"array","items":{"type":"string","maxLength":50},"maxItems":20},"searchKeywords":{"type":"array","items":{"type":"string","maxLength":100},"maxItems":20},"order":{"type":"integer"},"relatedArticles":{"type":"array","items":{"type":"string","maxLength":200},"maxItems":20},"authorName":{"type":"string","maxLength":200}}}}}},"responses":{"200":{"description":"Article updated","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"articleId":{"type":"string"}},"required":["success","articleId"]}}}},"400":{"description":"No updatable fields","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"status":{"type":"number"}},"required":["code","message","status"]}},"required":["error"]}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"status":{"type":"number"}},"required":["code","message","status"]}},"required":["error"]}}}}}},"patch":{"summary":"Update a KB article","tags":["Knowledge Base"],"security":[{"apiKey":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":200},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string","minLength":1,"maxLength":200},"content":{"type":"string","minLength":1,"maxLength":500000},"slug":{"type":"string","maxLength":200},"excerpt":{"type":"string","maxLength":500},"category":{"type":"string","maxLength":100},"status":{"type":"string","enum":["draft","published","archived"]},"tags":{"type":"array","items":{"type":"string","maxLength":50},"maxItems":20},"searchKeywords":{"type":"array","items":{"type":"string","maxLength":100},"maxItems":20},"order":{"type":"integer"},"relatedArticles":{"type":"array","items":{"type":"string","maxLength":200},"maxItems":20},"authorName":{"type":"string","maxLength":200}}}}}},"responses":{"200":{"description":"Article updated","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"articleId":{"type":"string"}},"required":["success","articleId"]}}}},"400":{"description":"No updatable fields","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"status":{"type":"number"}},"required":["code","message","status"]}},"required":["error"]}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"status":{"type":"number"}},"required":["code","message","status"]}},"required":["error"]}}}}}},"delete":{"summary":"Soft-delete a KB article","description":"Marks the article as archived (status → 'archived', published → false). Preserves version history; the article disappears from public list queries because they filter on status == 'published'. Idempotent.","tags":["Knowledge Base"],"security":[{"apiKey":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":200},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Article archived (or already archived)","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"articleId":{"type":"string"},"alreadyArchived":{"type":"boolean"}},"required":["success","articleId"]}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"status":{"type":"number"}},"required":["code","message","status"]}},"required":["error"]}}}}}}},"/api/v1/feedback/{id}":{"get":{"summary":"Fetch a single feedback item by ID","description":"Returns one feedback record scoped to the authenticated tenant. Responds 404 when the id does not exist within the tenant.","tags":["Feedback"],"security":[{"apiKey":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":200},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Feedback record","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"type":{"type":"string","enum":["nps","csat","general"]},"score":{"type":["number","null"]},"comment":{"type":"string"},"externalUserId":{"type":["string","null"]},"ticketId":{"type":["string","null"]},"context":{"type":"object","additionalProperties":{}},"status":{"type":"string","enum":["new","reviewed","actioned"]},"sentiment":{"type":["string","null"],"enum":["positive","neutral","negative",null]},"adminNotes":{"type":["string","null"]},"createdAt":{},"updatedAt":{}},"required":["id","tenantId","type","score","comment","externalUserId","ticketId","context","status","sentiment","adminNotes"]}},"required":["data"]}}}},"404":{"description":"Feedback not found in this tenant","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"status":{"type":"number"}},"required":["code","message","status"]}},"required":["error"]}}}}}},"delete":{"summary":"Hard-delete a feedback record","description":"Permanently removes a feedback record (no soft-delete — feedback may contain free-text PII and DSAR deletion requires hard removal). The feedback.deleted webhook fires BEFORE the document is removed; the payload includes the feedback type but no comment/score/subjectEmail.","tags":["Feedback"],"security":[{"apiKey":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":200},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Feedback hard-deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"feedbackId":{"type":"string"},"deleted":{"type":"boolean"}},"required":["success","feedbackId","deleted"]}}}},"404":{"description":"Feedback not found in this tenant","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"status":{"type":"number"}},"required":["code","message","status"]}},"required":["error"]}}}}}}},"/api/v1/bugs/{id}":{"delete":{"summary":"Soft-delete a bug report","description":"Marks a bug report as deleted. Excluded from default list queries; reachable via direct GET /:id for audit lookups. Idempotent.","tags":["Bug Reports"],"security":[{"apiKey":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":200},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Bug report soft-deleted (or already deleted)","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"bugReportId":{"type":"string"},"alreadyDeleted":{"type":"boolean"}},"required":["success","bugReportId"]}}}},"404":{"description":"Bug report not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"status":{"type":"number"}},"required":["code","message","status"]}},"required":["error"]}}}}}}},"/api/v1/features/{id}":{"delete":{"summary":"Soft-delete a feature request","description":"Marks a feature request as deleted. Excluded from default list queries; idempotent.","tags":["Feature Requests"],"security":[{"apiKey":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":200},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Feature request soft-deleted (or already deleted)","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"featureRequestId":{"type":"string"},"alreadyDeleted":{"type":"boolean"}},"required":["success","featureRequestId"]}}}},"404":{"description":"Feature request not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"status":{"type":"number"}},"required":["code","message","status"]}},"required":["error"]}}}}}}},"/api/v1/faqs/{id}":{"put":{"summary":"Replace a FAQ (partial accepted)","tags":["FAQs"],"security":[{"apiKey":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":200},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"question":{"type":"string","minLength":1,"maxLength":500},"answer":{"type":"string","minLength":1,"maxLength":50000},"category":{"type":"string","maxLength":100},"categoryOrder":{"type":"integer"},"order":{"type":"integer"},"published":{"type":"boolean"},"audience":{"type":"string","enum":["all","user","admin"]},"tags":{"type":"array","items":{"type":"string","maxLength":50},"maxItems":20},"searchKeywords":{"type":"array","items":{"type":"string","maxLength":100},"maxItems":20}}}}}},"responses":{"200":{"description":"FAQ updated","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"faqId":{"type":"string"}},"required":["success","faqId"]}}}},"400":{"description":"No updatable fields","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"status":{"type":"number"}},"required":["code","message","status"]}},"required":["error"]}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"status":{"type":"number"}},"required":["code","message","status"]}},"required":["error"]}}}}}},"patch":{"summary":"Update a FAQ","tags":["FAQs"],"security":[{"apiKey":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":200},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"question":{"type":"string","minLength":1,"maxLength":500},"answer":{"type":"string","minLength":1,"maxLength":50000},"category":{"type":"string","maxLength":100},"categoryOrder":{"type":"integer"},"order":{"type":"integer"},"published":{"type":"boolean"},"audience":{"type":"string","enum":["all","user","admin"]},"tags":{"type":"array","items":{"type":"string","maxLength":50},"maxItems":20},"searchKeywords":{"type":"array","items":{"type":"string","maxLength":100},"maxItems":20}}}}}},"responses":{"200":{"description":"FAQ updated","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"faqId":{"type":"string"}},"required":["success","faqId"]}}}},"400":{"description":"No updatable fields","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"status":{"type":"number"}},"required":["code","message","status"]}},"required":["error"]}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"status":{"type":"number"}},"required":["code","message","status"]}},"required":["error"]}}}}}},"delete":{"summary":"Delete (archive) a FAQ","description":"Soft-deletes by setting published: false (mirrors the dashboard archive action). Version history is preserved; the FAQ disappears from public list queries because they filter on published == true. Use the dashboard for hard deletes.","tags":["FAQs"],"security":[{"apiKey":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":200},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"FAQ archived (or already archived)","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"faqId":{"type":"string"},"alreadyArchived":{"type":"boolean"}},"required":["success","faqId"]}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"status":{"type":"number"}},"required":["code","message","status"]}},"required":["error"]}}}}}}},"/api/v1/me":{"get":{"summary":"Get metadata for the calling API key","description":"Returns the calling key's permissions, plan tier, effective rate limit, monthly resource usage, AI credit balance, and lifecycle timestamps. Available to any valid API key — no specific permission required.","tags":["Account"],"security":[{"apiKey":[]}],"responses":{"200":{"description":"Key metadata + quota snapshot","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"keyPrefix":{"type":"string"},"label":{"type":"string"},"permissions":{"type":"array","items":{"type":"string"}},"plan":{"type":"string"},"rateLimitPerMinute":{"type":"number"},"quotas":{"type":"object","properties":{"tickets":{"type":"object","properties":{"used":{"type":"number"},"limit":{"type":["number","null"]},"isUnlimited":{"type":"boolean"},"period":{"type":"string","enum":["monthly"]}},"required":["used","limit","isUnlimited","period"]},"bugReports":{"type":"object","properties":{"used":{"type":"number"},"limit":{"type":["number","null"]},"isUnlimited":{"type":"boolean"},"period":{"type":"string","enum":["monthly"]}},"required":["used","limit","isUnlimited","period"]},"featureRequests":{"type":"object","properties":{"used":{"type":"number"},"limit":{"type":["number","null"]},"isUnlimited":{"type":"boolean"},"period":{"type":"string","enum":["monthly"]}},"required":["used","limit","isUnlimited","period"]},"feedback":{"type":"object","properties":{"used":{"type":"number"},"limit":{"type":["number","null"]},"isUnlimited":{"type":"boolean"},"period":{"type":"string","enum":["monthly"]}},"required":["used","limit","isUnlimited","period"]},"aiCredits":{"type":"object","properties":{"used":{"type":"number"},"allocated":{"type":"number"},"remaining":{"type":"number"},"onboardingGrantRemaining":{"type":"number"},"isUnlimited":{"type":"boolean"}},"required":["used","allocated","remaining","onboardingGrantRemaining","isUnlimited"]}},"required":["tickets","bugReports","featureRequests","feedback","aiCredits"]},"createdAt":{"type":["string","null"]},"lastUsedAt":{"type":["string","null"]}},"required":["keyPrefix","label","permissions","plan","rateLimitPerMinute","quotas","createdAt","lastUsedAt"]}},"required":["data"]}}}},"404":{"description":"Key record not found (revoked or deleted)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"status":{"type":"number"}},"required":["code","message","status"]}},"required":["error"]}}}},"500":{"description":"Failed to read key metadata","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"status":{"type":"number"}},"required":["code","message","status"]}},"required":["error"]}}}}}}},"/api/v1/widgets":{"get":{"summary":"List widget configurations","description":"Returns all widget configurations for the tenant, ordered by priority.","tags":["Widgets"],"security":[{"apiKey":[]}],"responses":{"200":{"description":"Configurations list","content":{"application/json":{"schema":{"type":"object","properties":{"configs":{"type":"array","items":{"type":"object","additionalProperties":{}}}},"required":["configs"]}}}}}},"post":{"summary":"Create a widget configuration","description":"Creates a new widget configuration. When isDefault is true, the existing default config (if any) is demoted in the same transaction.","tags":["Widgets"],"security":[{"apiKey":[]}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100},"appearance":{"type":"object","properties":{"position":{"type":"string","enum":["bottom-right","bottom-left"]},"primaryColor":{"type":"string"},"theme":{"type":"string","enum":["light","dark","auto"]},"fontFamily":{"type":["string","null"]},"headerText":{"type":"string"},"greetingMessage":{"type":"string"},"defaultTab":{"type":"string","enum":["home","tickets","kb","faq","contact"]},"tabs":{"type":"array","items":{"type":"string"}},"autoOpenDelayMs":{"type":["number","null"]},"hideOnMobile":{"type":"boolean"},"zIndex":{"type":"number"},"launcherIcon":{"type":"string","enum":["chat","help","question"]},"labels":{"type":"object","additionalProperties":{"type":"string"}}},"additionalProperties":{}},"targeting":{"type":"object","properties":{"matchMode":{"type":"string","enum":["all","any"]},"rules":{"type":"array","items":{"type":"object","additionalProperties":{}}}},"additionalProperties":{}},"enabled":{"type":"boolean"},"isDefault":{"type":"boolean"},"priority":{"type":"number"}},"required":["name"]}}}},"responses":{"201":{"description":"Created config","content":{"application/json":{"schema":{"type":"object","additionalProperties":{}}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"status":{"type":"number"}},"required":["code","message","status"]}},"required":["error"]}}}},"403":{"description":"Quota exceeded","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"status":{"type":"number"}},"required":["code","message","status"]}},"required":["error"]}}}}}}},"/api/v1/widgets/{configId}":{"get":{"summary":"Get a widget configuration","tags":["Widgets"],"security":[{"apiKey":[]}],"parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"configId","in":"path"}],"responses":{"200":{"description":"The configuration","content":{"application/json":{"schema":{"type":"object","additionalProperties":{}}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"status":{"type":"number"}},"required":["code","message","status"]}},"required":["error"]}}}}}},"patch":{"summary":"Update a widget configuration","description":"Partial update of a widget configuration. Setting isDefault=true demotes the existing default in the same transaction. isDefault=false on the current default is rejected (promote another config to default instead).","tags":["Widgets"],"security":[{"apiKey":[]}],"parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"configId","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":100},"appearance":{"type":"object","properties":{"position":{"type":"string","enum":["bottom-right","bottom-left"]},"primaryColor":{"type":"string"},"theme":{"type":"string","enum":["light","dark","auto"]},"fontFamily":{"type":["string","null"]},"headerText":{"type":"string"},"greetingMessage":{"type":"string"},"defaultTab":{"type":"string","enum":["home","tickets","kb","faq","contact"]},"tabs":{"type":"array","items":{"type":"string"}},"autoOpenDelayMs":{"type":["number","null"]},"hideOnMobile":{"type":"boolean"},"zIndex":{"type":"number"},"launcherIcon":{"type":"string","enum":["chat","help","question"]},"labels":{"type":"object","additionalProperties":{"type":"string"}}},"additionalProperties":{}},"targeting":{"type":"object","properties":{"matchMode":{"type":"string","enum":["all","any"]},"rules":{"type":"array","items":{"type":"object","additionalProperties":{}}}},"additionalProperties":{}},"enabled":{"type":"boolean"},"isDefault":{"type":"boolean"},"priority":{"type":"number","minimum":0}}}}}},"responses":{"200":{"description":"Updated config","content":{"application/json":{"schema":{"type":"object","additionalProperties":{}}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"status":{"type":"number"}},"required":["code","message","status"]}},"required":["error"]}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"status":{"type":"number"}},"required":["code","message","status"]}},"required":["error"]}}}}}},"delete":{"summary":"Delete a widget configuration","description":"Deletes a non-default widget configuration. Default configs cannot be deleted — promote another config to default first.","tags":["Widgets"],"security":[{"apiKey":[]}],"parameters":[{"schema":{"type":"string","minLength":1},"required":true,"name":"configId","in":"path"}],"responses":{"204":{"description":"Deleted","content":{"application/json":{"schema":{}}}},"400":{"description":"Cannot delete default","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"status":{"type":"number"}},"required":["code","message","status"]}},"required":["error"]}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"status":{"type":"number"}},"required":["code","message","status"]}},"required":["error"]}}}}}}},"/api/v1/webhooks":{"get":{"summary":"List webhook subscriptions","description":"Returns all webhook subscriptions for the authenticated tenant.","tags":["Webhooks"],"security":[{"apiKey":[]}],"responses":{"200":{"description":"Webhook subscriptions","content":{"application/json":{"schema":{"type":"object","properties":{"subscriptions":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"url":{"type":"string"},"events":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"description":{"type":"string"},"failureCount":{"type":"number"},"createdAt":{},"updatedAt":{},"lastDeliveryAt":{}},"required":["id","url","events","enabled","description","failureCount"]}}},"required":["subscriptions"]}}}}}},"post":{"summary":"Create a webhook subscription","description":"Subscribes the tenant to one or more event types. Signing secret is returned ONCE; store it securely — it cannot be re-fetched (rotate via POST /:id/rotate-secret).","tags":["Webhooks"],"security":[{"apiKey":[]}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","maxLength":2000,"format":"uri"},"events":{"type":"array","items":{"type":"string"},"minItems":1,"maxItems":28},"description":{"type":"string","maxLength":200}},"required":["url","events"]}}}},"responses":{"201":{"description":"Subscription created","content":{"application/json":{"schema":{"type":"object","properties":{"subscription":{"type":"object","properties":{"id":{"type":"string"},"url":{"type":"string"},"events":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"description":{"type":"string"},"failureCount":{"type":"number"},"createdAt":{},"updatedAt":{},"lastDeliveryAt":{}},"required":["id","url","events","enabled","description","failureCount"]},"signingSecret":{"type":"string"}},"required":["subscription","signingSecret"]}}}},"400":{"description":"Validation error or reserved ID","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"status":{"type":"number"}},"required":["code","message","status"]}},"required":["error"]}}}},"403":{"description":"Plan does not include webhook subscriptions","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"status":{"type":"number"}},"required":["code","message","status"]}},"required":["error"]}}}}}}},"/api/v1/webhooks/{id}":{"get":{"summary":"Get a webhook subscription by ID","tags":["Webhooks"],"security":[{"apiKey":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":200},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Subscription","content":{"application/json":{"schema":{"type":"object","properties":{"subscription":{"type":"object","properties":{"id":{"type":"string"},"url":{"type":"string"},"events":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"description":{"type":"string"},"failureCount":{"type":"number"},"createdAt":{},"updatedAt":{},"lastDeliveryAt":{}},"required":["id","url","events","enabled","description","failureCount"]}},"required":["subscription"]}}}},"404":{"description":"Subscription not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"status":{"type":"number"}},"required":["code","message","status"]}},"required":["error"]}}}}}},"patch":{"summary":"Update a webhook subscription","tags":["Webhooks"],"security":[{"apiKey":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":200},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"url":{"type":"string","maxLength":2000,"format":"uri"},"events":{"type":"array","items":{"type":"string"},"minItems":1,"maxItems":28},"description":{"type":"string","maxLength":200},"enabled":{"type":"boolean"}}}}}},"responses":{"200":{"description":"Subscription updated","content":{"application/json":{"schema":{"type":"object","properties":{"subscription":{"type":"object","properties":{"id":{"type":"string"},"url":{"type":"string"},"events":{"type":"array","items":{"type":"string"}},"enabled":{"type":"boolean"},"description":{"type":"string"},"failureCount":{"type":"number"},"createdAt":{},"updatedAt":{},"lastDeliveryAt":{}},"required":["id","url","events","enabled","description","failureCount"]}},"required":["subscription"]}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"status":{"type":"number"}},"required":["code","message","status"]}},"required":["error"]}}}},"404":{"description":"Subscription not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"status":{"type":"number"}},"required":["code","message","status"]}},"required":["error"]}}}}}},"delete":{"summary":"Delete a webhook subscription","tags":["Webhooks"],"security":[{"apiKey":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":200},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Subscription deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"id":{"type":"string"}},"required":["success","id"]}}}},"404":{"description":"Subscription not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"status":{"type":"number"}},"required":["code","message","status"]}},"required":["error"]}}}}}}},"/api/v1/webhooks/{id}/test":{"post":{"summary":"Send a synthetic test event to verify subscription","description":"POSTs a synthetic { type: 'test.ping' } payload to the subscription URL with the standard Codex-Signature header. Returns the response code and a body preview for verification.","tags":["Webhooks"],"security":[{"apiKey":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":200},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Test event sent","content":{"application/json":{"schema":{"type":"object","properties":{"delivered":{"type":"boolean"},"statusCode":{"type":["number","null"]},"responseBodyPreview":{"type":["string","null"]}},"required":["delivered","statusCode","responseBodyPreview"]}}}},"404":{"description":"Subscription not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"status":{"type":"number"}},"required":["code","message","status"]}},"required":["error"]}}}}}}},"/api/v1/webhooks/{id}/rotate-secret":{"post":{"summary":"Rotate the signing secret","description":"Generates a new signing secret. The previous secret is immediately invalidated — update your verification logic before calling this. The new secret is returned ONCE.","tags":["Webhooks"],"security":[{"apiKey":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":200},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Secret rotated","content":{"application/json":{"schema":{"type":"object","properties":{"signingSecret":{"type":"string"}},"required":["signingSecret"]}}}},"404":{"description":"Subscription not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"status":{"type":"number"}},"required":["code","message","status"]}},"required":["error"]}}}}}}},"/api/v1/canned-responses":{"get":{"summary":"List canned responses","description":"Returns canned responses for the tenant. Supports category and status filters with cursor pagination.","tags":["Canned Responses"],"security":[{"apiKey":[]}],"parameters":[{"schema":{"type":"string","maxLength":100},"required":false,"name":"category","in":"query"},{"schema":{"type":"string","enum":["draft","published","archived"]},"required":false,"name":"status","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":200},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","maxLength":200},"required":false,"name":"startAfter","in":"query"}],"responses":{"200":{"description":"Canned responses","content":{"application/json":{"schema":{"type":"object","properties":{"cannedResponses":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"title":{"type":"string"},"content":{"type":"string"},"category":{"type":"string"},"shortcut":{"type":"string"},"tags":{"type":"array","items":{"type":"string"}},"variables":{"type":"array","items":{}},"status":{"type":"string","enum":["draft","published","archived"]},"published":{"type":"boolean"},"order":{"type":"number"},"sortOrder":{"type":"number"},"version":{"type":"number"},"source":{"type":"string"},"createdBy":{"type":"string"},"createdAt":{},"updatedAt":{}},"required":["id","tenantId","title","content","version"]}},"count":{"type":"number"},"nextStartAfter":{"type":"string"}},"required":["cannedResponses","count"]}}}}}},"post":{"summary":"Create a canned response","description":"Persists a template. Content is sanitized through @codextitan/content-versioning before write.","tags":["Canned Responses"],"security":[{"apiKey":[]}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string","minLength":1,"maxLength":200},"content":{"type":"string","minLength":1,"maxLength":10000},"category":{"type":"string","maxLength":100},"shortcut":{"type":"string","maxLength":50},"tags":{"type":"array","items":{"type":"string","maxLength":50},"maxItems":20},"variables":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"}},"required":["name"],"additionalProperties":{}},"maxItems":20},"status":{"type":"string","enum":["draft","published"]},"published":{"type":"boolean"},"order":{"type":"integer"},"sortOrder":{"type":"integer"}},"required":["title","content"]}}}},"responses":{"201":{"description":"Canned response created","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"cannedResponseId":{"type":"string"}},"required":["success","cannedResponseId"]}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"status":{"type":"number"}},"required":["code","message","status"]}},"required":["error"]}}}},"429":{"description":"Plan limit reached","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"status":{"type":"number"}},"required":["code","message","status"]}},"required":["error"]}}}}}}},"/api/v1/canned-responses/{id}":{"get":{"summary":"Get a canned response by ID","tags":["Canned Responses"],"security":[{"apiKey":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":200},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Canned response","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"title":{"type":"string"},"content":{"type":"string"},"category":{"type":"string"},"shortcut":{"type":"string"},"tags":{"type":"array","items":{"type":"string"}},"variables":{"type":"array","items":{}},"status":{"type":"string","enum":["draft","published","archived"]},"published":{"type":"boolean"},"order":{"type":"number"},"sortOrder":{"type":"number"},"version":{"type":"number"},"source":{"type":"string"},"createdBy":{"type":"string"},"createdAt":{},"updatedAt":{}},"required":["id","tenantId","title","content","version"]}},"required":["data"]}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"status":{"type":"number"}},"required":["code","message","status"]}},"required":["error"]}}}}}},"put":{"summary":"Replace a canned response","description":"Same handler as PATCH — partial updates are accepted. PUT is provided for HTTP-idiomatic clients.","tags":["Canned Responses"],"security":[{"apiKey":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":200},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string","minLength":1,"maxLength":200},"content":{"type":"string","minLength":1,"maxLength":10000},"category":{"type":"string","maxLength":100},"shortcut":{"type":"string","maxLength":50},"tags":{"type":"array","items":{"type":"string","maxLength":50},"maxItems":20},"variables":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"}},"required":["name"],"additionalProperties":{}},"maxItems":20},"status":{"type":"string","enum":["draft","published"]},"published":{"type":"boolean"},"order":{"type":"integer"},"sortOrder":{"type":"integer"}}}}}},"responses":{"200":{"description":"Canned response updated","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"cannedResponseId":{"type":"string"}},"required":["success","cannedResponseId"]}}}},"400":{"description":"No updatable fields supplied (e.g. a data-wrapped body). Nothing was written.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"status":{"type":"number"}},"required":["code","message","status"]}},"required":["error"]}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"status":{"type":"number"}},"required":["code","message","status"]}},"required":["error"]}}}}}},"patch":{"summary":"Update a canned response","tags":["Canned Responses"],"security":[{"apiKey":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":200},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string","minLength":1,"maxLength":200},"content":{"type":"string","minLength":1,"maxLength":10000},"category":{"type":"string","maxLength":100},"shortcut":{"type":"string","maxLength":50},"tags":{"type":"array","items":{"type":"string","maxLength":50},"maxItems":20},"variables":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"description":{"type":"string"}},"required":["name"],"additionalProperties":{}},"maxItems":20},"status":{"type":"string","enum":["draft","published"]},"published":{"type":"boolean"},"order":{"type":"integer"},"sortOrder":{"type":"integer"}}}}}},"responses":{"200":{"description":"Canned response updated","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"cannedResponseId":{"type":"string"}},"required":["success","cannedResponseId"]}}}},"400":{"description":"No updatable fields supplied (e.g. a data-wrapped body). Nothing was written.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"status":{"type":"number"}},"required":["code","message","status"]}},"required":["error"]}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"status":{"type":"number"}},"required":["code","message","status"]}},"required":["error"]}}}}}},"delete":{"summary":"Hard-delete a canned response","description":"Removes the document and all its version snapshots. Hard-delete per epic Pass 1 — no version history to preserve beyond the doc itself.","tags":["Canned Responses"],"security":[{"apiKey":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":200},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"cannedResponseId":{"type":"string"}},"required":["success","cannedResponseId"]}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"status":{"type":"number"}},"required":["code","message","status"]}},"required":["error"]}}}}}}},"/api/v1/support-docs":{"get":{"summary":"List support documents","description":"Returns support documents for the tenant. Supports status (active/archived) and category filters with cursor pagination. Default filter excludes archived.","tags":["Support Documents"],"security":[{"apiKey":[]}],"parameters":[{"schema":{"type":"string","enum":["active","archived"]},"required":false,"name":"status","in":"query"},{"schema":{"type":"string","maxLength":100},"required":false,"name":"category","in":"query"},{"schema":{"type":"integer","minimum":1,"maximum":200},"required":false,"name":"limit","in":"query"},{"schema":{"type":"string","maxLength":200},"required":false,"name":"startAfter","in":"query"}],"responses":{"200":{"description":"Support documents","content":{"application/json":{"schema":{"type":"object","properties":{"docs":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"title":{"type":"string"},"fileName":{"type":"string"},"content":{"type":"string"},"contentSize":{"type":"number"},"category":{"type":"string"},"tags":{"type":"array","items":{"type":"string"}},"source":{"type":"string"},"status":{"type":"string","enum":["active","archived"]},"version":{"type":"number"},"lastVerifiedAt":{},"gateClaims":{"type":"array","items":{"type":"object","properties":{"phrase":{"type":"string","minLength":1,"maxLength":200},"gateType":{"type":"string","enum":["tier","credits","both","none"]},"source":{"type":"string","minLength":1,"maxLength":500}},"required":["phrase","gateType","source"]}},"createdAt":{},"updatedAt":{}},"required":["id","tenantId","title","content","contentSize","status","version"],"additionalProperties":{}}},"count":{"type":"number"},"nextStartAfter":{"type":"string"}},"required":["docs","count"]}}}}}},"post":{"summary":"Create a support document","description":"Default status is 'active' (live for AI triage). ZD-731: requires lastVerifiedAt; gate-language in content requires gateClaims.","tags":["Support Documents"],"security":[{"apiKey":[]}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string","minLength":1,"maxLength":200},"content":{"type":"string","minLength":1,"maxLength":2000000},"fileName":{"type":"string","maxLength":200},"category":{"type":"string","maxLength":100},"tags":{"type":"array","items":{"type":"string","maxLength":50},"maxItems":20},"lastVerifiedAt":{"type":"string","minLength":1,"maxLength":40},"gateClaims":{"type":"array","items":{"type":"object","properties":{"phrase":{"type":"string","minLength":1,"maxLength":200},"gateType":{"type":"string","enum":["tier","credits","both","none"]},"source":{"type":"string","minLength":1,"maxLength":500}},"required":["phrase","gateType","source"]},"maxItems":50}},"required":["title","content","lastVerifiedAt"]}}}},"responses":{"201":{"description":"Support doc created","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"docId":{"type":"string"}},"required":["success","docId"]}}}},"400":{"description":"Claim gate failed","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"status":{"type":"number"}},"required":["code","message","status"]}},"required":["error"]}}}},"429":{"description":"Plan limit reached","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"status":{"type":"number"}},"required":["code","message","status"]}},"required":["error"]}}}}}}},"/api/v1/support-docs/{id}":{"get":{"summary":"Get a support document by ID","tags":["Support Documents"],"security":[{"apiKey":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":200},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Support document","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"title":{"type":"string"},"fileName":{"type":"string"},"content":{"type":"string"},"contentSize":{"type":"number"},"category":{"type":"string"},"tags":{"type":"array","items":{"type":"string"}},"source":{"type":"string"},"status":{"type":"string","enum":["active","archived"]},"version":{"type":"number"},"lastVerifiedAt":{},"gateClaims":{"type":"array","items":{"type":"object","properties":{"phrase":{"type":"string","minLength":1,"maxLength":200},"gateType":{"type":"string","enum":["tier","credits","both","none"]},"source":{"type":"string","minLength":1,"maxLength":500}},"required":["phrase","gateType","source"]}},"createdAt":{},"updatedAt":{}},"required":["id","tenantId","title","content","contentSize","status","version"],"additionalProperties":{}}},"required":["data"]}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"status":{"type":"number"}},"required":["code","message","status"]}},"required":["error"]}}}}}},"put":{"summary":"Replace a support doc (partial accepted)","tags":["Support Documents"],"security":[{"apiKey":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":200},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string","minLength":1,"maxLength":200},"content":{"type":"string","minLength":1,"maxLength":2000000},"fileName":{"type":"string","maxLength":200},"category":{"type":"string","maxLength":100},"tags":{"type":"array","items":{"type":"string","maxLength":50},"maxItems":20},"lastVerifiedAt":{"type":"string","minLength":1,"maxLength":40},"gateClaims":{"type":"array","items":{"type":"object","properties":{"phrase":{"type":"string","minLength":1,"maxLength":200},"gateType":{"type":"string","enum":["tier","credits","both","none"]},"source":{"type":"string","minLength":1,"maxLength":500}},"required":["phrase","gateType","source"]},"maxItems":50},"status":{"type":"string","enum":["active","archived"]}}}}}},"responses":{"200":{"description":"Support doc updated","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"docId":{"type":"string"}},"required":["success","docId"]}}}},"400":{"description":"No updatable fields","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"status":{"type":"number"}},"required":["code","message","status"]}},"required":["error"]}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"status":{"type":"number"}},"required":["code","message","status"]}},"required":["error"]}}}}}},"patch":{"summary":"Update a support doc","tags":["Support Documents"],"security":[{"apiKey":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":200},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string","minLength":1,"maxLength":200},"content":{"type":"string","minLength":1,"maxLength":2000000},"fileName":{"type":"string","maxLength":200},"category":{"type":"string","maxLength":100},"tags":{"type":"array","items":{"type":"string","maxLength":50},"maxItems":20},"lastVerifiedAt":{"type":"string","minLength":1,"maxLength":40},"gateClaims":{"type":"array","items":{"type":"object","properties":{"phrase":{"type":"string","minLength":1,"maxLength":200},"gateType":{"type":"string","enum":["tier","credits","both","none"]},"source":{"type":"string","minLength":1,"maxLength":500}},"required":["phrase","gateType","source"]},"maxItems":50},"status":{"type":"string","enum":["active","archived"]}}}}}},"responses":{"200":{"description":"Support doc updated","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"docId":{"type":"string"}},"required":["success","docId"]}}}},"400":{"description":"No updatable fields","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"status":{"type":"number"}},"required":["code","message","status"]}},"required":["error"]}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"status":{"type":"number"}},"required":["code","message","status"]}},"required":["error"]}}}}}},"delete":{"summary":"Archive a support doc","description":"Soft-deletes by setting status: 'archived'. Mirrors useDocs.archiveDoc. Idempotent. Use POST /:id/restore to undo.","tags":["Support Documents"],"security":[{"apiKey":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":200},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Support doc archived (or already archived)","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"docId":{"type":"string"},"alreadyArchived":{"type":"boolean"}},"required":["success","docId"]}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"status":{"type":"number"}},"required":["code","message","status"]}},"required":["error"]}}}}}}},"/api/v1/support-docs/{id}/versions":{"get":{"summary":"List version snapshots for a support doc","tags":["Support Documents"],"security":[{"apiKey":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":200},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Version snapshots (oldest first)","content":{"application/json":{"schema":{"type":"object","properties":{"versions":{"type":"array","items":{"type":"object","properties":{"version":{"type":"number"}},"required":["version"],"additionalProperties":{}}}},"required":["versions"]}}}},"404":{"description":"Support document not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"status":{"type":"number"}},"required":["code","message","status"]}},"required":["error"]}}}}}}},"/api/v1/support-docs/{id}/versions/{versionId}":{"get":{"summary":"Get a specific version snapshot","tags":["Support Documents"],"security":[{"apiKey":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":200},"required":true,"name":"id","in":"path"},{"schema":{"type":"string","minLength":1,"maxLength":50},"required":true,"name":"versionId","in":"path"}],"responses":{"200":{"description":"Version snapshot","content":{"application/json":{"schema":{"type":"object","properties":{"version":{"type":"object","properties":{"version":{"type":"number"}},"required":["version"],"additionalProperties":{}}},"required":["version"]}}}},"404":{"description":"Version or doc not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"status":{"type":"number"}},"required":["code","message","status"]}},"required":["error"]}}}}}}},"/api/v1/support-docs/{id}/restore":{"post":{"summary":"Restore an archived support doc","description":"Reverses archive: status: 'archived' → 'active'. Idempotent. ZD-731: restoring to active requires lastVerifiedAt (content goes live for AI).","tags":["Support Documents"],"security":[{"apiKey":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":200},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"lastVerifiedAt":{"type":"string","minLength":1,"maxLength":40},"gateClaims":{"type":"array","items":{"type":"object","properties":{"phrase":{"type":"string","minLength":1,"maxLength":200},"gateType":{"type":"string","enum":["tier","credits","both","none"]},"source":{"type":"string","minLength":1,"maxLength":500}},"required":["phrase","gateType","source"]},"maxItems":50}},"required":["lastVerifiedAt"]}}}},"responses":{"200":{"description":"Support doc restored (or already active)","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"docId":{"type":"string"},"alreadyActive":{"type":"boolean"}},"required":["success","docId"]}}}},"400":{"description":"Claim gate failed","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"status":{"type":"number"}},"required":["code","message","status"]}},"required":["error"]}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"status":{"type":"number"}},"required":["code","message","status"]}},"required":["error"]}}}}}}},"/api/v1/sub-processors":{"get":{"summary":"List sub-processors","description":"Returns the tenant's sub-processor registry — the third-party data recipients disclosed in privacy/cookie policies and DPAs. Ordered by sortOrder ascending. Soft-deleted entries are excluded unless `includeDeleted=true`. Filter to a single property with `property=<domain>`. `version`/`lastUpdated` reflect the registry-level change counter.","tags":["Sub-Processors"],"security":[{"apiKey":[]}],"parameters":[{"schema":{"type":"string","enum":["true","false"]},"required":false,"name":"includeDeleted","in":"query"},{"schema":{"type":"string","maxLength":253},"required":false,"name":"property","in":"query"}],"responses":{"200":{"description":"Sub-processor registry","content":{"application/json":{"schema":{"type":"object","properties":{"subProcessors":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"name":{"type":"string"},"purpose":{"type":"string"},"location":{"type":"string"},"dataTypes":{"type":"array","items":{"type":"string"}},"dpaUrl":{"type":["string","null"]},"consentCategory":{"type":["string","null"],"enum":["necessary","analytics","marketing","functional",null]},"setsCookies":{"type":"boolean"},"consentSyncEnabled":{"type":"boolean"},"consentIntegrationName":{"type":["string","null"],"enum":["CookieConsent","Zaraz",null]},"isSystem":{"type":"boolean"},"properties":{"type":"array","items":{"type":"string"}},"sortOrder":{"type":"number"},"effectiveDate":{},"deletedAt":{},"createdAt":{},"updatedAt":{}},"required":["id","tenantId","name","purpose","location","dataTypes","dpaUrl","consentCategory","setsCookies","consentSyncEnabled","consentIntegrationName","isSystem","properties","sortOrder"]}},"total":{"type":"number"},"version":{"type":["number","null"]},"lastUpdated":{}},"required":["subProcessors","total","version"]}}}}}},"post":{"summary":"Create a sub-processor","description":"Adds a third-party data recipient to the registry. Pass `sortOrder` to insert at a position (fractional values slot between neighbours, e.g. 2.5 between 2 and 3); omit it to append. `consentSyncEnabled` requires `consentIntegrationName` (CookieConsent|Zaraz) and a `consentCategory`. System entries cannot be created.","tags":["Sub-Processors"],"security":[{"apiKey":[]}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":200},"purpose":{"type":"string","minLength":1,"maxLength":1000},"location":{"type":"string","minLength":1,"maxLength":200},"dataTypes":{"type":"array","items":{"type":"string","maxLength":100},"maxItems":30},"dpaUrl":{"type":"string","maxLength":2048},"consentCategory":{"type":"string","enum":["necessary","analytics","marketing","functional"]},"setsCookies":{"type":"boolean"},"properties":{"type":"array","items":{"type":"string","minLength":1,"maxLength":253},"maxItems":50},"sortOrder":{"type":"number"},"effectiveDate":{"type":"string","pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"consentSyncEnabled":{"type":"boolean"},"consentIntegrationName":{"type":["string","null"],"enum":["CookieConsent","Zaraz",null]}},"required":["name","purpose","location"]}}}},"responses":{"201":{"description":"Sub-processor created","content":{"application/json":{"schema":{"type":"object","properties":{"subProcessor":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"name":{"type":"string"},"purpose":{"type":"string"},"location":{"type":"string"},"dataTypes":{"type":"array","items":{"type":"string"}},"dpaUrl":{"type":["string","null"]},"consentCategory":{"type":["string","null"],"enum":["necessary","analytics","marketing","functional",null]},"setsCookies":{"type":"boolean"},"consentSyncEnabled":{"type":"boolean"},"consentIntegrationName":{"type":["string","null"],"enum":["CookieConsent","Zaraz",null]},"isSystem":{"type":"boolean"},"properties":{"type":"array","items":{"type":"string"}},"sortOrder":{"type":"number"},"effectiveDate":{},"deletedAt":{},"createdAt":{},"updatedAt":{}},"required":["id","tenantId","name","purpose","location","dataTypes","dpaUrl","consentCategory","setsCookies","consentSyncEnabled","consentIntegrationName","isSystem","properties","sortOrder"]}},"required":["subProcessor"]}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"status":{"type":"number"}},"required":["code","message","status"]}},"required":["error"]}}}}}}},"/api/v1/sub-processors/changes":{"get":{"summary":"List sub-processor registry changes","description":"Returns the append-only change log for the registry (add/update/remove/restore), newest first — the history a controller needs for DPA sub-processor-change notification. Cosmetic reorders are not logged. `version` is the monotonic registry version.","tags":["Sub-Processors"],"security":[{"apiKey":[]}],"parameters":[{"schema":{"type":"string","pattern":"^\\d+$"},"required":false,"name":"limit","in":"query"}],"responses":{"200":{"description":"Registry change log","content":{"application/json":{"schema":{"type":"object","properties":{"changes":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"subProcessorId":{"type":"string"},"name":{"type":"string"},"action":{"type":"string","enum":["created","updated","deleted","restored"]},"changedFields":{"type":"array","items":{"type":"string"}},"consentCategory":{"type":["string","null"],"enum":["necessary","analytics","marketing","functional",null]},"effectiveDate":{},"at":{}},"required":["id","subProcessorId","name","action"]}},"total":{"type":"number"},"version":{"type":["number","null"]},"lastUpdated":{}},"required":["changes","total","version"]}}}}}}},"/api/v1/sub-processors/published":{"get":{"summary":"Get the rendered public sub-processor list","description":"Returns the tenant's sub-processor registry as markdown + sanitized HTML plus a stable JSON array, so a tenant can render a /sub-processors page without building their own renderer. Mirrors GET /api/v1/legal/published/:docType. Filter per property with `property=<domain>`. Cacheable (ETag + short max-age).","tags":["Sub-Processors"],"security":[{"apiKey":[]}],"parameters":[{"schema":{"type":"string","maxLength":253},"required":false,"name":"property","in":"query"}],"responses":{"200":{"description":"Rendered sub-processor view","content":{"application/json":{"schema":{"type":"object","properties":{"tenant_name":{"type":"string"},"generated_at":{"type":"string"},"version":{"type":["number","null"]},"content_markdown":{"type":"string"},"content_html":{"type":"string"},"sub_processors":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"name":{"type":"string"},"purpose":{"type":"string"},"location":{"type":"string"},"dataTypes":{"type":"array","items":{"type":"string"}},"dpaUrl":{"type":["string","null"]},"consentCategory":{"type":["string","null"],"enum":["necessary","analytics","marketing","functional",null]},"setsCookies":{"type":"boolean"},"consentSyncEnabled":{"type":"boolean"},"consentIntegrationName":{"type":["string","null"],"enum":["CookieConsent","Zaraz",null]},"isSystem":{"type":"boolean"},"properties":{"type":"array","items":{"type":"string"}},"sortOrder":{"type":"number"},"effectiveDate":{},"deletedAt":{},"createdAt":{},"updatedAt":{}},"required":["id","tenantId","name","purpose","location","dataTypes","dpaUrl","consentCategory","setsCookies","consentSyncEnabled","consentIntegrationName","isSystem","properties","sortOrder"]}}},"required":["tenant_name","generated_at","version","content_markdown","content_html","sub_processors"]}}}}}}},"/api/v1/sub-processors/parity":{"get":{"summary":"Sub-processor registry parity / drift report","description":"Diffs the registry against (a) the recipients named in the tenant's published legal documents and (b) the tenant's CMP consent categories. Report-only — it never mutates. `legalDocs.missingFromDocs` is deterministic; `legalDocs.missingFromRegistry` is heuristic (review).","tags":["Sub-Processors"],"security":[{"apiKey":[]}],"responses":{"200":{"description":"Parity report","content":{"application/json":{"schema":{"type":"object","properties":{"registryCount":{"type":"number"},"checkedDocs":{"type":"array","items":{"type":"object","properties":{"type":{"type":"string"},"published":{"type":"boolean"}},"required":["type","published"]}},"legalDocs":{"type":"object","properties":{"missingFromDocs":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"}},"required":["id","name"]}},"missingFromRegistry":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"foundIn":{"type":"array","items":{"type":"string"}}},"required":["name","foundIn"]}}},"required":["missingFromDocs","missingFromRegistry"]},"cmp":{"type":"object","properties":{"configured":{"type":"boolean"},"categoryIssues":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"name":{"type":"string"},"consentCategory":{"type":["string","null"]},"issue":{"type":"string"}},"required":["id","name","consentCategory","issue"]}},"orphanedIntegrations":{"type":"array","items":{"type":"object","properties":{"integrationName":{"type":"string"},"linkedSubProcessorId":{"type":"string"}},"required":["integrationName","linkedSubProcessorId"]}}},"required":["configured","categoryIssues","orphanedIntegrations"]}},"required":["registryCount","checkedDocs","legalDocs","cmp"]}}}}}}},"/api/v1/sub-processors/{id}":{"get":{"summary":"Get a sub-processor by ID","tags":["Sub-Processors"],"security":[{"apiKey":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":200},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Sub-processor","content":{"application/json":{"schema":{"type":"object","properties":{"subProcessor":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"name":{"type":"string"},"purpose":{"type":"string"},"location":{"type":"string"},"dataTypes":{"type":"array","items":{"type":"string"}},"dpaUrl":{"type":["string","null"]},"consentCategory":{"type":["string","null"],"enum":["necessary","analytics","marketing","functional",null]},"setsCookies":{"type":"boolean"},"consentSyncEnabled":{"type":"boolean"},"consentIntegrationName":{"type":["string","null"],"enum":["CookieConsent","Zaraz",null]},"isSystem":{"type":"boolean"},"properties":{"type":"array","items":{"type":"string"}},"sortOrder":{"type":"number"},"effectiveDate":{},"deletedAt":{},"createdAt":{},"updatedAt":{}},"required":["id","tenantId","name","purpose","location","dataTypes","dpaUrl","consentCategory","setsCookies","consentSyncEnabled","consentIntegrationName","isSystem","properties","sortOrder"]}},"required":["subProcessor"]}}}},"404":{"description":"Sub-processor not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"status":{"type":"number"}},"required":["code","message","status"]}},"required":["error"]}}}}}},"patch":{"summary":"Update a sub-processor","description":"Updates fields on an existing sub-processor, including `sortOrder` (reposition) and the consent-sync fields. `isSystem`, `tenantId`, and `deletedAt` are not settable here (use DELETE/restore). Pass `dpaUrl`, `consentCategory`, `consentIntegrationName`, or `effectiveDate` as null to clear them.","tags":["Sub-Processors"],"security":[{"apiKey":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":200},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"name":{"type":"string","minLength":1,"maxLength":200},"purpose":{"type":"string","minLength":1,"maxLength":1000},"location":{"type":"string","minLength":1,"maxLength":200},"dataTypes":{"type":"array","items":{"type":"string","maxLength":100},"maxItems":30},"dpaUrl":{"type":["string","null"],"maxLength":2048},"consentCategory":{"type":["string","null"],"enum":["necessary","analytics","marketing","functional",null]},"setsCookies":{"type":"boolean"},"properties":{"type":"array","items":{"type":"string","minLength":1,"maxLength":253},"maxItems":50},"sortOrder":{"type":"number"},"effectiveDate":{"type":["string","null"],"pattern":"^\\d{4}-\\d{2}-\\d{2}$"},"consentSyncEnabled":{"type":"boolean"},"consentIntegrationName":{"type":["string","null"],"enum":["CookieConsent","Zaraz",null]}}}}}},"responses":{"200":{"description":"Sub-processor updated","content":{"application/json":{"schema":{"type":"object","properties":{"subProcessor":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"name":{"type":"string"},"purpose":{"type":"string"},"location":{"type":"string"},"dataTypes":{"type":"array","items":{"type":"string"}},"dpaUrl":{"type":["string","null"]},"consentCategory":{"type":["string","null"],"enum":["necessary","analytics","marketing","functional",null]},"setsCookies":{"type":"boolean"},"consentSyncEnabled":{"type":"boolean"},"consentIntegrationName":{"type":["string","null"],"enum":["CookieConsent","Zaraz",null]},"isSystem":{"type":"boolean"},"properties":{"type":"array","items":{"type":"string"}},"sortOrder":{"type":"number"},"effectiveDate":{},"deletedAt":{},"createdAt":{},"updatedAt":{}},"required":["id","tenantId","name","purpose","location","dataTypes","dpaUrl","consentCategory","setsCookies","consentSyncEnabled","consentIntegrationName","isSystem","properties","sortOrder"]}},"required":["subProcessor"]}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"status":{"type":"number"}},"required":["code","message","status"]}},"required":["error"]}}}},"404":{"description":"Sub-processor not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"status":{"type":"number"}},"required":["code","message","status"]}},"required":["error"]}}}}}},"delete":{"summary":"Delete a sub-processor (soft-delete)","description":"Soft-deletes a sub-processor by setting a `deletedAt` tombstone — the entry is removed from the registry list and the published view but retained for the audit trail and can be restored via POST /:id/restore. System-managed entries (isSystem: true) cannot be deleted. Emits a sub_processor.deleted webhook.","tags":["Sub-Processors"],"security":[{"apiKey":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":200},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Sub-processor soft-deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"id":{"type":"string"},"subProcessor":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"name":{"type":"string"},"purpose":{"type":"string"},"location":{"type":"string"},"dataTypes":{"type":"array","items":{"type":"string"}},"dpaUrl":{"type":["string","null"]},"consentCategory":{"type":["string","null"],"enum":["necessary","analytics","marketing","functional",null]},"setsCookies":{"type":"boolean"},"consentSyncEnabled":{"type":"boolean"},"consentIntegrationName":{"type":["string","null"],"enum":["CookieConsent","Zaraz",null]},"isSystem":{"type":"boolean"},"properties":{"type":"array","items":{"type":"string"}},"sortOrder":{"type":"number"},"effectiveDate":{},"deletedAt":{},"createdAt":{},"updatedAt":{}},"required":["id","tenantId","name","purpose","location","dataTypes","dpaUrl","consentCategory","setsCookies","consentSyncEnabled","consentIntegrationName","isSystem","properties","sortOrder"]}},"required":["success","id","subProcessor"]}}}},"403":{"description":"System sub-processor cannot be deleted","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"status":{"type":"number"}},"required":["code","message","status"]}},"required":["error"]}}}},"404":{"description":"Sub-processor not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"status":{"type":"number"}},"required":["code","message","status"]}},"required":["error"]}}}}}}},"/api/v1/sub-processors/reorder":{"post":{"summary":"Reorder the sub-processor registry","description":"Reassigns sortOrder from the supplied id order. `order` must list exactly the set of active sub-processor ids — no omissions or extras — so a reorder can never silently drop an entry. Returns the registry in its new order.","tags":["Sub-Processors"],"security":[{"apiKey":[]}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"order":{"type":"array","items":{"type":"string","minLength":1,"maxLength":200},"minItems":1,"maxItems":200}},"required":["order"]}}}},"responses":{"200":{"description":"Registry reordered","content":{"application/json":{"schema":{"type":"object","properties":{"subProcessors":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"name":{"type":"string"},"purpose":{"type":"string"},"location":{"type":"string"},"dataTypes":{"type":"array","items":{"type":"string"}},"dpaUrl":{"type":["string","null"]},"consentCategory":{"type":["string","null"],"enum":["necessary","analytics","marketing","functional",null]},"setsCookies":{"type":"boolean"},"consentSyncEnabled":{"type":"boolean"},"consentIntegrationName":{"type":["string","null"],"enum":["CookieConsent","Zaraz",null]},"isSystem":{"type":"boolean"},"properties":{"type":"array","items":{"type":"string"}},"sortOrder":{"type":"number"},"effectiveDate":{},"deletedAt":{},"createdAt":{},"updatedAt":{}},"required":["id","tenantId","name","purpose","location","dataTypes","dpaUrl","consentCategory","setsCookies","consentSyncEnabled","consentIntegrationName","isSystem","properties","sortOrder"]}},"total":{"type":"number"}},"required":["subProcessors","total"]}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"status":{"type":"number"}},"required":["code","message","status"]}},"required":["error"]}}}}}}},"/api/v1/sub-processors/{id}/restore":{"post":{"summary":"Restore a soft-deleted sub-processor","description":"Clears the soft-delete tombstone, re-activating the entry. Emits a sub_processor.updated webhook.","tags":["Sub-Processors"],"security":[{"apiKey":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":200},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Sub-processor restored","content":{"application/json":{"schema":{"type":"object","properties":{"subProcessor":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"name":{"type":"string"},"purpose":{"type":"string"},"location":{"type":"string"},"dataTypes":{"type":"array","items":{"type":"string"}},"dpaUrl":{"type":["string","null"]},"consentCategory":{"type":["string","null"],"enum":["necessary","analytics","marketing","functional",null]},"setsCookies":{"type":"boolean"},"consentSyncEnabled":{"type":"boolean"},"consentIntegrationName":{"type":["string","null"],"enum":["CookieConsent","Zaraz",null]},"isSystem":{"type":"boolean"},"properties":{"type":"array","items":{"type":"string"}},"sortOrder":{"type":"number"},"effectiveDate":{},"deletedAt":{},"createdAt":{},"updatedAt":{}},"required":["id","tenantId","name","purpose","location","dataTypes","dpaUrl","consentCategory","setsCookies","consentSyncEnabled","consentIntegrationName","isSystem","properties","sortOrder"]}},"required":["subProcessor"]}}}},"404":{"description":"Sub-processor not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"status":{"type":"number"}},"required":["code","message","status"]}},"required":["error"]}}}},"409":{"description":"Sub-processor is not deleted","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"status":{"type":"number"}},"required":["code","message","status"]}},"required":["error"]}}}}}}},"/api/v1/legal/drafts":{"get":{"summary":"List legal document drafts","description":"Returns all draft + published compliance documents for the tenant.","tags":["Legal Documents"],"security":[{"apiKey":[]}],"responses":{"200":{"description":"Drafts","content":{"application/json":{"schema":{"type":"object","properties":{"drafts":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"type":{"type":"string"},"title":{"type":"string"},"content":{"type":"string"},"status":{"type":"string"},"version":{"type":"number"}},"required":["id","tenantId","type","title","content","status","version"],"additionalProperties":{}}}},"required":["drafts"]}}}}}},"post":{"summary":"Create a legal document draft (does NOT publish)","description":"Creates or revises the ONE logical document per docType (ZD-660: one record per type, forever). Does NOT make content live — call POST /drafts/:id/publish for that. If none exists, creates the single record (status: 'draft', v1) and charges the compliance-doc quota. If a published doc of this type already exists with no pending revision, this writes the next version into the record's in-record `draft` map (a revise) — it does NOT create a second record and does NOT count against the quota; publishing later promotes the draft in place. If a draft/revision of this type is already in progress, returns 409 DRAFT_EXISTS with the existing record id — update it via PATCH /drafts/:id instead. Content is sanitized through @codextitan/content-versioning. Rejects content containing `[YOUR ...]` placeholder markers (documented scope — does NOT catch `[INSERT ...]`).","tags":["Legal Documents"],"security":[{"apiKey":[]}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"docType":{"type":"string","enum":["privacy_policy","terms_of_service","cookie_policy","dpa"]},"title":{"type":"string","minLength":1,"maxLength":200},"content":{"type":"string","minLength":1,"maxLength":200000},"properties":{"type":"array","items":{"type":"string","maxLength":200},"maxItems":20},"tags":{"type":"array","items":{"type":"string","maxLength":50},"maxItems":20},"jurisdiction":{"type":"string","maxLength":50}},"required":["docType","title","content"]}}}},"responses":{"201":{"description":"Draft created","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"docId":{"type":"string"}},"required":["success","docId"]}}}},"400":{"description":"Validation error or placeholder content","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"status":{"type":"number"},"existingDocId":{"type":"string"},"hint":{"type":"string"}},"required":["code","message","status"]}},"required":["error"]}}}},"409":{"description":"A draft of this docType already exists","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"status":{"type":"number"},"existingDocId":{"type":"string"},"hint":{"type":"string"}},"required":["code","message","status"]}},"required":["error"]}}}},"429":{"description":"Compliance doc limit reached","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"status":{"type":"number"},"existingDocId":{"type":"string"},"hint":{"type":"string"}},"required":["code","message","status"]}},"required":["error"]}}}}}}},"/api/v1/legal/drafts/{id}":{"get":{"summary":"Get a legal document by ID (draft or published)","tags":["Legal Documents"],"security":[{"apiKey":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":200},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Compliance document","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string"},"tenantId":{"type":"string"},"type":{"type":"string"},"title":{"type":"string"},"content":{"type":"string"},"status":{"type":"string"},"version":{"type":"number"}},"required":["id","tenantId","type","title","content","status","version"],"additionalProperties":{}}},"required":["data"]}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"status":{"type":"number"},"existingDocId":{"type":"string"},"hint":{"type":"string"}},"required":["code","message","status"]}},"required":["error"]}}}}}},"put":{"summary":"Save a legal doc draft (does NOT publish)","description":"Saves draft fields only — never makes content live. On an unpublished draft, edits title/content/properties in place. On a published doc, content/title/properties edits are written to the in-record `draft` map (`routedToDraft: true` in the 200 body) while the public continues to see the previous published text. tags/jurisdiction always update live. To make content public, call POST /drafts/:id/publish and confirm its response.","tags":["Legal Documents"],"security":[{"apiKey":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":200},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string","minLength":1,"maxLength":200},"content":{"type":"string","minLength":1,"maxLength":200000},"properties":{"type":"array","items":{"type":"string","maxLength":200},"maxItems":20},"tags":{"type":"array","items":{"type":"string","maxLength":50},"maxItems":20},"jurisdiction":{"type":"string","maxLength":50}}}}}},"responses":{"200":{"description":"Saved (not published). Check routedToDraft — true means public content unchanged.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"docId":{"type":"string"},"routedToDraft":{"type":"boolean"}},"required":["success","docId","routedToDraft"]}}}},"400":{"description":"Placeholder content or no updatable fields","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"status":{"type":"number"},"existingDocId":{"type":"string"},"hint":{"type":"string"}},"required":["code","message","status"]}},"required":["error"]}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"status":{"type":"number"},"existingDocId":{"type":"string"},"hint":{"type":"string"}},"required":["code","message","status"]}},"required":["error"]}}}}}},"patch":{"summary":"Save a legal doc draft (does NOT publish)","description":"Same semantics as PUT /drafts/:id. A 200 means the write was accepted — not that the public page changed. When `routedToDraft` is true, only POST /drafts/:id/publish promotes the pending draft to live.","tags":["Legal Documents"],"security":[{"apiKey":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":200},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"type":"string","minLength":1,"maxLength":200},"content":{"type":"string","minLength":1,"maxLength":200000},"properties":{"type":"array","items":{"type":"string","maxLength":200},"maxItems":20},"tags":{"type":"array","items":{"type":"string","maxLength":50},"maxItems":20},"jurisdiction":{"type":"string","maxLength":50}}}}}},"responses":{"200":{"description":"Saved (not published). Check routedToDraft — true means public content unchanged.","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"docId":{"type":"string"},"routedToDraft":{"type":"boolean"}},"required":["success","docId","routedToDraft"]}}}},"400":{"description":"Placeholder content or no updatable fields","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"status":{"type":"number"},"existingDocId":{"type":"string"},"hint":{"type":"string"}},"required":["code","message","status"]}},"required":["error"]}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"status":{"type":"number"},"existingDocId":{"type":"string"},"hint":{"type":"string"}},"required":["code","message","status"]}},"required":["error"]}}}}}},"delete":{"summary":"Discard a pending draft, or hard-delete an unpublished draft","description":"ZD-660: behavior depends on the record's status. On a PUBLISHED doc with a pending in-record draft, this DISCARDS the pending revision (clears `draft.*`) and leaves the live published version untouched. On an unpublished (status: 'draft') record — which has no published lineage to protect — this hard-deletes the record and its version snapshots. A PUBLISHED doc with NO pending draft cannot be deleted here: use DELETE /published/:docType to unpublish (status → archived).","tags":["Legal Documents"],"security":[{"apiKey":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":200},"required":true,"name":"id","in":"path"}],"responses":{"200":{"description":"Draft discarded or record deleted","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"docId":{"type":"string"}},"required":["success","docId"]}}}},"400":{"description":"Published doc with no pending draft — use unpublish instead","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"status":{"type":"number"},"existingDocId":{"type":"string"},"hint":{"type":"string"}},"required":["code","message","status"]}},"required":["error"]}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"status":{"type":"number"},"existingDocId":{"type":"string"},"hint":{"type":"string"}},"required":["code","message","status"]}},"required":["error"]}}}}}}},"/api/v1/legal/drafts/{id}/publish":{"post":{"summary":"Publish a legal document draft (ONLY path that makes content live)","description":"The sole API call that promotes draft content to publicly served legal text. PUT/PATCH /drafts/:id only save — they never publish. Requires both `effectiveDate` AND `confirmEffectiveDate` matching ISO date strings (defense against fat-fingered automation). Effective date must be today-or-later (24h UTC grace) and must not back-date relative to the currently published doc (ZD-692). Automation must call this endpoint and read the 200 before reporting a document as published.","tags":["Legal Documents"],"security":[{"apiKey":[]}],"parameters":[{"schema":{"type":"string","minLength":1,"maxLength":200},"required":true,"name":"id","in":"path"}],"requestBody":{"content":{"application/json":{"schema":{"type":"object","properties":{"effectiveDate":{"type":"string","minLength":8,"maxLength":32},"confirmEffectiveDate":{"type":"string","minLength":8,"maxLength":32}},"required":["effectiveDate","confirmEffectiveDate"]}}}},"responses":{"200":{"description":"Published live","content":{"application/json":{"schema":{"type":"object","properties":{"success":{"type":"boolean"},"docId":{"type":"string"},"effectiveDate":{"type":"string"}},"required":["success","docId","effectiveDate"]}}}},"400":{"description":"Validation error (date mismatch, regression, etc.)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"status":{"type":"number"},"existingDocId":{"type":"string"},"hint":{"type":"string"}},"required":["code","message","status"]}},"required":["error"]}}}},"404":{"description":"Draft not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"status":{"type":"number"},"existingDocId":{"type":"string"},"hint":{"type":"string"}},"required":["code","message","status"]}},"required":["error"]}}}},"429":{"description":"Published-doc limit reached","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"status":{"type":"number"},"existingDocId":{"type":"string"},"hint":{"type":"string"}},"required":["code","message","status"]}},"required":["error"]}}}}}}}},"components":{"securitySchemes":{"apiKey":{"type":"apiKey","in":"header","name":"X-CT-API-Key","description":"Your Codex Titan API key"}},"schemas":{"CreateTicketRequest":{"type":"object","required":["category","message"],"properties":{"category":{"type":"string","maxLength":100},"subject":{"type":"string","maxLength":200},"message":{"type":"string","maxLength":10000},"priority":{"type":"string","enum":["low","medium","high","urgent"],"default":"medium"},"mode":{"type":"string","enum":["support","feature_request"],"default":"support"},"externalUserId":{"type":"string","maxLength":200},"externalUserEmail":{"type":"string","maxLength":320},"isAnonymous":{"type":"boolean"},"disableAiTriage":{"type":"boolean","description":"Skip async create-time AI triage for this ticket"},"context":{"type":"object","description":"Device, user, and app context"}}},"CreateTicketResponse":{"type":"object","properties":{"ticket":{"type":"object","properties":{"id":{"type":"string"},"status":{"type":"string"},"createdAt":{"type":"string","format":"date-time"}}},"message":{"type":"object","properties":{"id":{"type":"string"},"timestamp":{"type":"string","format":"date-time"}}},"aiCreditCost":{"type":"object","nullable":true,"description":"Point-of-action disclosure (ZD-688-F): the AI credit cost this ticket may consume server-side (auto-triage). Null when the tenant has AI disabled. Live effective cost; capability-tier terminology (never model names). Widget/API hosts should surface this to end-users where appropriate.","properties":{"operation":{"type":"string","example":"ticket_triage"},"credits":{"type":"number"},"tier":{"type":"string","enum":["basic","standard","advanced"]}}}}},"UpdateTicketRequest":{"type":"object","properties":{"status":{"type":"string","enum":["open","ai_responding","waiting_on_user","waiting_on_admin","escalated","resolved","closed"]},"priority":{"type":"string","enum":["low","medium","high","urgent"]},"assignedTo":{"type":"string","nullable":true},"tags":{"type":"array","items":{"type":"string"}},"escalationReason":{"type":"string"},"resolutionSource":{"type":"string","enum":["ai","human","self_service"]},"resolutionSummary":{"type":"string"},"csat":{"type":"object","properties":{"score":{"type":"number","minimum":0,"maximum":10},"comment":{"type":"string"}}}}},"CreateMessageRequest":{"type":"object","required":["content"],"properties":{"content":{"type":"string","maxLength":10000,"description":"Message text. Role is always 'user' for API requests."},"senderId":{"type":"string","maxLength":200},"senderName":{"type":"string","maxLength":100}}},"CreateFeedbackRequest":{"type":"object","required":["type","comment"],"properties":{"type":{"type":"string","enum":["nps","csat","general"]},"score":{"type":"number","minimum":0,"maximum":10},"comment":{"type":"string","maxLength":5000},"externalUserId":{"type":"string"},"ticketId":{"type":"string"},"context":{"type":"object"}}},"CreateBugReportRequest":{"type":"object","required":["title","description"],"properties":{"title":{"type":"string","maxLength":200},"description":{"type":"string","maxLength":5000},"stepsToReproduce":{"type":"string","maxLength":5000},"severity":{"type":"string","enum":["low","medium","high","critical"],"default":"medium"},"externalUserId":{"type":"string"},"context":{"type":"object"}}},"CreateFeatureRequest":{"type":"object","required":["title","description"],"properties":{"title":{"type":"string","maxLength":200},"description":{"type":"string","maxLength":5000},"externalUserId":{"type":"string"},"context":{"type":"object"}}},"UpsertUserRequest":{"type":"object","properties":{"email":{"type":"string","format":"email"},"displayName":{"type":"string"},"photoUrl":{"type":"string","format":"uri"},"metadata":{"type":"object","description":"Arbitrary key-value metadata (subscriptionTier, appVersion, platform, etc.)"}}},"ApiError":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string"},"message":{"type":"string"},"status":{"type":"integer"}}}}},"CreateDSARRequest":{"type":"object","required":["type","subjectEmail"],"properties":{"type":{"type":"string","enum":["access","deletion","portability","rectification"]},"subjectEmail":{"type":"string","format":"email","description":"Email of the data subject"},"subjectName":{"type":"string","maxLength":200},"jurisdiction":{"type":"string","maxLength":100,"description":"e.g. 'california' for CCPA 45-day deadline, otherwise 30-day GDPR default"},"description":{"type":"string","maxLength":5000}}},"CreatePublicDSARRequest":{"type":"object","required":["apiKey","type","subjectEmail"],"description":"Public browser/embed body. Prefer a publishable key with permissions: []. Do not put dsar:create on browser keys.","properties":{"apiKey":{"type":"string","description":"Publishable API key (body auth — not X-CT-API-Key). Prefer permissions: []."},"type":{"type":"string","enum":["access","deletion","portability","rectification"]},"subjectEmail":{"type":"string","format":"email"},"subjectName":{"type":"string","maxLength":200},"description":{"type":"string","maxLength":5000},"jurisdiction":{"type":"string","description":"NXD-007 jurisdiction enum (strict allowlist on public intake)"},"honeypot":{"type":"string","description":"Bot trap — non-empty values return fake success and send no email"}}},"DSARRequest":{"type":"object","properties":{"id":{"type":"string"},"type":{"type":"string","enum":["access","deletion","portability","rectification"]},"subjectEmail":{"type":"string"},"subjectName":{"type":"string"},"status":{"type":"string","enum":["pending","verified","assigned","in_progress","data_gathered","review","processor_erased","completed","denied","closed"]},"source":{"type":"string","enum":["dashboard","public_form","api"]},"jurisdiction":{"type":"string"},"description":{"type":"string"},"deadline":{"type":"string","format":"date-time"},"requestedAt":{"type":"string","format":"date-time"},"completedAt":{"type":"string","format":"date-time","nullable":true}}},"CreateConsentLogRequest":{"type":"object","required":["subjectId","action","categories","property","bannerRevision"],"properties":{"subjectId":{"type":"string","maxLength":128,"description":"Pseudonymous visitor/user ID"},"action":{"type":"string","enum":["grant","deny","revoke"]},"categories":{"type":"object","description":"Map of category name to boolean (1-20 entries)","additionalProperties":{"type":"boolean"}},"property":{"type":"string","maxLength":253,"description":"Domain or property identifier"},"bannerRevision":{"type":"integer","minimum":0,"description":"Consent banner revision. NOTE: the field is `bannerRevision` — a body sending `revision` is rejected with a 400."},"gpcDetected":{"type":"boolean","description":"Global Privacy Control as observed by page JavaScript (navigator.globalPrivacyControl). Client-asserted, therefore forgeable — send `secGpcDetected` too where you can."},"secGpcDetected":{"type":"boolean","description":"GPC as read SERVER-SIDE from the `Sec-GPC` request header. Unforgeable by page JS and preferred over `gpcDetected` when both are present. If you post to CT directly from the browser, CT reads the header itself and this field is unnecessary."},"region":{"type":"string","maxLength":64,"description":"Coarse consent region the banner applied, e.g. \"EU\" or \"US-CA\"."},"consentRegime":{"type":"string","maxLength":32,"description":"Which consent regime the visitor was treated under — \"opt-in\" (EEA/UK) or \"opt-out\" (US/other). Not an enum: unlisted values are accepted."},"gcmSignals":{"type":"object","additionalProperties":{"type":"string","maxLength":32},"description":"Google Consent Mode v2 signal map, e.g. { analytics_storage: \"denied\" }. Max 20 entries."},"consentMode":{"deprecated":true,"description":"DEPRECATED, ambiguous — two concepts collided on this name. A STRING is treated as `consentRegime`; an OBJECT is treated as `gcmSignals`. Both are accepted and routed to the canonical field, with a `warnings.deprecations` entry in the response. Send `consentRegime` or `gcmSignals` instead.","oneOf":[{"type":"string","maxLength":32},{"type":"object","additionalProperties":{"type":"string","maxLength":32}}]},"source":{"type":"string","maxLength":64,"pattern":"^[a-zA-Z0-9][a-zA-Z0-9._-]*$","description":"Which writer produced this record, e.g. \"km-mobile\", \"kmw-web\". Optional. Attributes an entry to a caller when one property has several writers; a property name alone cannot distinguish them. Alphanumeric start, then alphanumerics, `.`, `_`, `-`."}}},"UpsertConsentRequest":{"type":"object","required":["userId","configId","categories","revision"],"properties":{"userId":{"type":"string","description":"External user/visitor ID"},"configId":{"type":"string","description":"Consent configuration ID"},"categories":{"type":"object","properties":{"necessary":{"type":"boolean"},"analytics":{"type":"boolean"},"marketing":{"type":"boolean"},"functional":{"type":"boolean"}}},"revision":{"type":"integer","minimum":0},"gpcDetected":{"type":"boolean"},"source":{"type":"string","description":"e.g. 'banner', 'settings', 'api'"},"userAgent":{"type":"string"},"ipCountry":{"type":"string"}}}},"parameters":{}},"x-tenant-context":{"tenantId":"public","plan":"enterprise","rateLimitPerMinute":0,"quotas":{"ticketsPerMonth":"unlimited","bugReportsPerMonth":"unlimited","featureRequestsPerMonth":"unlimited","feedbackPerMonth":"unlimited","kbArticles":"unlimited","faqs":"unlimited"},"features":{"aiTriage":true,"emailNotifications":true,"customBranding":true,"prioritySupport":true,"multipleApiKeys":true,"aiTierSelection":true,"webhookSubscriptions":true,"domainVerification":true,"emailDomainRestriction":true,"supportPortal":true,"portalCustomDomain":false,"portalAnalytics":true,"portalAdvancedAnalytics":true,"hideWidgetBranding":true,"contentExport":true,"widgetAbTesting":true,"widgetTargeting":true,"widgetAdvancedTargeting":true,"complianceDsar":true,"complianceConsentConfig":true,"complianceConsentLog":true,"complianceExport":true,"compliance":true,"aiAgents":true}},"webhooks":{},"tags":[{"name":"Account"},{"name":"Bug Reports"},{"name":"Canned Responses"},{"name":"Consent"},{"name":"Credits"},{"name":"Data Requests"},{"name":"FAQs"},{"name":"Feature Requests"},{"name":"Feedback"},{"name":"Knowledge Base"},{"name":"Legal Documents"},{"name":"Messages"},{"name":"Sub-Processors"},{"name":"Support Documents"},{"name":"Tickets"},{"name":"Users"},{"name":"Webhooks"},{"name":"Widgets"}]}