openapi: 3.0.3 info: title: 'Subsig API Documentation' description: '' version: 1.0.0 servers: - url: 'https://backend-api.subsig.com' tags: - name: Registration description: 'Create a new user account to access the application.' - name: Authentication description: "\nAPIs for user authentication" - name: 'Password Reset' description: 'Recover access to your account if you forgot your password.' - name: Reviews description: "\nAPIs for fetching aggregated reviews from multiple platforms, scoped to organizations." - name: 'Social Posts' description: "\nAPIs for fetching and filtering social media mentions from multiple platforms, scoped to organizations." - name: 'AI Visibility - Competitor' description: "\nCompetitor tab — brand list with visibility/SOV/position metrics and CRUD." - name: 'AI Visibility - Dashboard' description: "\nTicket 7 — the Visibility tab. Reads exclusively from `ai_visibility_daily_metrics` (Ticket 5),\nnever live-joins execution tables — see AiVisibilityMetricsAggregateQuery." - name: 'AI Visibility - Prompt Detail' description: "\nTicket 7 — the prompt-detail scrollable page (overview/citations/responses) and the top-level\nexecution detail view. Kept separate from AiVisibilityPromptController (already ~1700 lines,\nand owns a different, older payload shape via show()/executionShow()) — these are new endpoints\nwith their own contract, built on the same Ticket 5 aggregate-table services as the rest of\nTicket 7 rather than that controller's live-join helpers." - name: 'AI Visibility - Prompts' description: "\nAPIs for managing AI Visibility prompts, their scheduled executions, and execution detail." - name: 'AI Visibility - Suggestions' description: "\nPrompt and topic suggestions generated by GenerateAiVisibilitySuggestionsJob. Suggestions start\nas `completed` and move to `accepted` or `dismissed` when the user acts on them." - name: 'AI Visibility - Topics' description: "\nAPIs for managing topics used to group AI Visibility prompts within an organisation." - name: 'API Keys' description: "\nAPIs for managing production API keys for the current organisation." - name: AppSumo description: "\nAPIs for AppSumo lifetime-deal tier comparison and license redemption" - name: Endpoints description: '' - name: External description: "\nTrusted external onboarding (API key): create a verified user and complete onboarding in one request." - name: Home description: "\nFeeds the dashboard \"Home\" overview page (KPI cards, visibility trend/ranking,\nrecommendations, module summary). reviews_count/mentions_count in the module cards reuse the\nsame ReviewAnalyticsService/SocialAnalyticsService aggregation queries as the Review Monitoring\nand Brand Mentions pages, scoped to the organisation's own (non-competitor) project, so the\nfigures shown here always match what those pages report. Reuses the exact same\nAiVisibilityBrandRankService calls as the Visibility tab (AiVisibilityVisibilityController)\nrather than re-deriving metrics." - name: Invites description: "\nAPIs for managing invites" - name: Links description: "\nAPIs for managing project links" - name: Notifications description: "\nAPIs for managing notification rules" - name: Onboarding description: "\nAPIs for user onboarding flow" - name: Organisations description: "\nAPIs for managing organisations" - name: Platforms description: "\nAPIs for managing platforms" - name: Profiles description: "\nAPIs for managing product claims and product profile data.\n\nThe Profiles API group covers two related workflows:\n\n1. **Claim Profiles** - Submit and manage ownership claims for products\n2. **Product Profiles** - Edit and sync detailed product information" - name: Projects description: "\nAPIs for managing projects within organisations" - name: 'Read status (reviews and mentions)' description: "\nMark and check read status per project using scraper identity (platform + scraper id).\nFrontend uses scraper API directly; these endpoints store and return read state by project.\nIdentifiers match the scraper API: use `data[].id` as scraper_review_id/scraper_post_id and `data[].platform` as platform." - name: 'Review Views' description: "\nAPIs for managing saved review filter views (organization-scoped)." - name: 'Slack Integration' description: "\nAPIs for connecting and managing Slack workspace integration" - name: Subscriptions description: "\nAPIs for managing subscriptions" - name: 'Universal Keywords' description: "\nBrowse endpoint for individual universal-keyword mentions. Pulls live from the scraper's own\nfiltered/paginated /v1/universal-keyword-mentions endpoint (ScraperService::\nfetchUniversalKeywordMentions()) on every request -- the same pull-based pattern Reviews/Mentions\nalready use -- rather than the interim local `universal_keyword_mentions` table this previously\nread from while that scraper endpoint didn't exist yet." - name: Utilities description: '' - name: 'Webhook Tools' description: "\nUtility endpoints for webhook signature testing." components: securitySchemes: default: type: http scheme: bearer description: 'You can retrieve your token by visiting your dashboard and clicking Generate API token.' security: - default: [] paths: /register: post: summary: 'Create Account' operationId: createAccount description: 'Register a new user account. After successful registration, the user will be automatically logged in and redirected to the dashboard.' parameters: [] responses: 201: description: 'Account created. User logged in and redirected.' content: text/plain: schema: type: string example: '' 422: description: 'Validation error.' content: application/json: schema: type: object example: message: 'The email has already been taken.' errors: email: - 'The email has already been taken.' properties: message: type: string example: 'The email has already been taken.' errors: type: object properties: email: type: array example: - 'The email has already been taken.' items: type: string tags: - Registration requestBody: required: true content: application/json: schema: type: object properties: name: type: string description: 'Full name of the user.' example: 'John Doe' email: type: string description: 'Valid email address. Must be unique.' example: john@example.com password: type: string description: 'Password (min 8 characters, at least one uppercase letter, one lowercase letter, one number and one special character).' example: SecurePass123! password_confirmation: type: string description: 'Must match password exactly.' example: SecurePass123! required: - name - email - password - password_confirmation security: [] /api/sanctum/token: post: summary: 'Create API Token' operationId: createAPIToken description: "Generate an API token for authenticated requests.\nRequires a verified email address." parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: token: 1|abc123... properties: token: type: string example: 1|abc123... 201: description: 'Email not verified' content: application/json: schema: type: object example: message: 'Registration successful. Please check your email to verify your account.' redirect_link: 'https://frontend.example.com/register?email=john%40example.com' needsEmailVerification: true properties: message: type: string example: 'Registration successful. Please check your email to verify your account.' redirect_link: type: string example: 'https://frontend.example.com/register?email=john%40example.com' needsEmailVerification: type: boolean example: true 422: description: 'Invalid credentials' content: application/json: schema: type: object example: message: 'The provided credentials are incorrect.' errors: email: - 'The provided credentials are incorrect.' properties: message: type: string example: 'The provided credentials are incorrect.' errors: type: object properties: email: type: array example: - 'The provided credentials are incorrect.' items: type: string tags: - Authentication requestBody: required: true content: application/json: schema: type: object properties: email: type: string description: "The user's email address." example: john@example.com password: type: string description: "The user's password." example: SecurePass123! required: - email - password security: [] /api/email/verify: post: summary: 'Verify Email' operationId: verifyEmail description: "Verify user's email address using the 4-digit code sent via email.\nReturns an API token on successful verification.\nVerification link is sent via email. /verify-email?code=1234&email=john@example.com" parameters: [] responses: 200: description: '' content: application/json: schema: oneOf: - description: '' type: object example: message: 'Email verified successfully.' token: 1|abc123... properties: message: type: string example: 'Email verified successfully.' token: type: string example: 1|abc123... - description: 'Already verified' type: object example: message: 'Email already verified.' token: 1|abc123... properties: message: type: string example: 'Email already verified.' token: type: string example: 1|abc123... 422: description: '' content: application/json: schema: oneOf: - description: 'Invalid code' type: object example: message: 'Invalid verification code.' errors: code: - 'Invalid verification code.' properties: message: type: string example: 'Invalid verification code.' errors: type: object properties: code: type: array example: - 'Invalid verification code.' items: type: string - description: 'Expired code' type: object example: message: 'Verification code has expired. Please request a new one.' errors: code: - 'Verification code has expired. Please request a new one.' properties: message: type: string example: 'Verification code has expired. Please request a new one.' errors: type: object properties: code: type: array example: - 'Verification code has expired. Please request a new one.' items: type: string tags: - Authentication requestBody: required: true content: application/json: schema: type: object properties: email: type: string description: "The user's email address." example: john@example.com code: type: string description: 'The 4-digit verification code.' example: '1234' required: - email - code security: [] /api/email/resend: post: summary: 'Resend Verification Code' operationId: resendVerificationCode description: "Send a new 4-digit verification code to the user's email.\nCode expires in 60 minutes." parameters: [] responses: 200: description: '' content: application/json: schema: oneOf: - description: '' type: object example: message: 'Verification code sent.' properties: message: type: string example: 'Verification code sent.' - description: 'Already verified' type: object example: message: 'Email already verified.' properties: message: type: string example: 'Email already verified.' 422: description: 'User not found' content: application/json: schema: type: object example: message: 'No account found with this email.' errors: email: - 'No account found with this email.' properties: message: type: string example: 'No account found with this email.' errors: type: object properties: email: type: array example: - 'No account found with this email.' items: type: string tags: - Authentication requestBody: required: true content: application/json: schema: type: object properties: email: type: string description: "The user's email address." example: john@example.com required: - email security: [] /api/auth/google: post: summary: 'Authenticate with Google' operationId: authenticateWithGoogle description: 'Verify a Google ID token and log in or register based on intent.' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: token: 1|abc123... user: name: 'John Doe' email: john@example.com avatar: 'https://lh3.googleusercontent.com/...' properties: token: type: string example: 1|abc123... user: type: object properties: name: type: string example: 'John Doe' email: type: string example: john@example.com avatar: type: string example: 'https://lh3.googleusercontent.com/...' 401: description: '' content: application/json: schema: oneOf: - description: 'Invalid token' type: object example: message: 'Invalid Google token.' properties: message: type: string example: 'Invalid Google token.' - description: 'Google account conflict' type: object example: message: 'This account is linked to a different Google account.' properties: message: type: string example: 'This account is linked to a different Google account.' 404: description: 'Account not found' content: application/json: schema: type: object example: code: ACCOUNT_NOT_FOUND message: 'Account not found.' errors: email: - 'No account exists for this Google email.' properties: code: type: string example: ACCOUNT_NOT_FOUND message: type: string example: 'Account not found.' errors: type: object properties: email: type: array example: - 'No account exists for this Google email.' items: type: string 422: description: '' content: application/json: schema: oneOf: - description: 'Account already exists' type: object example: code: ACCOUNT_ALREADY_EXISTS message: 'An account with this email already exists. Please log in.' errors: email: - 'An account with this email already exists. Please log in.' properties: code: type: string example: ACCOUNT_ALREADY_EXISTS message: type: string example: 'An account with this email already exists. Please log in.' errors: type: object properties: email: type: array example: - 'An account with this email already exists. Please log in.' items: type: string - description: 'Validation error' type: object example: message: 'The token field is required.' errors: token: - 'The token field is required.' properties: message: type: string example: 'The token field is required.' errors: type: object properties: token: type: array example: - 'The token field is required.' items: type: string tags: - Authentication requestBody: required: true content: application/json: schema: type: object properties: token: type: string description: 'The Google ID token from the frontend.' example: eyJhbGciOiJSUzI1NiIs... intent: type: string description: 'The auth flow: login (existing users only) or register (new users only).' example: login appsumo_registration_token: type: string description: '' example: architecto nullable: true required: - token - intent security: [] /api/auth/linkedin: post: summary: 'Authenticate with LinkedIn' operationId: authenticateWithLinkedIn description: 'Verify a LinkedIn access token and log in or register based on intent.' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: token: 1|abc123... user: name: 'John Doe' email: john@example.com avatar: 'https://media.licdn.com/...' properties: token: type: string example: 1|abc123... user: type: object properties: name: type: string example: 'John Doe' email: type: string example: john@example.com avatar: type: string example: 'https://media.licdn.com/...' 401: description: '' content: application/json: schema: oneOf: - description: 'Invalid token' type: object example: message: 'Invalid LinkedIn token.' properties: message: type: string example: 'Invalid LinkedIn token.' - description: 'LinkedIn account conflict' type: object example: message: 'This account is linked to a different LinkedIn account.' properties: message: type: string example: 'This account is linked to a different LinkedIn account.' 404: description: 'Account not found' content: application/json: schema: type: object example: code: ACCOUNT_NOT_FOUND message: 'Account not found.' errors: email: - 'No account exists for this LinkedIn email.' properties: code: type: string example: ACCOUNT_NOT_FOUND message: type: string example: 'Account not found.' errors: type: object properties: email: type: array example: - 'No account exists for this LinkedIn email.' items: type: string 422: description: '' content: application/json: schema: oneOf: - description: 'Account already exists' type: object example: code: ACCOUNT_ALREADY_EXISTS message: 'An account with this email already exists. Please log in.' errors: email: - 'An account with this email already exists. Please log in.' properties: code: type: string example: ACCOUNT_ALREADY_EXISTS message: type: string example: 'An account with this email already exists. Please log in.' errors: type: object properties: email: type: array example: - 'An account with this email already exists. Please log in.' items: type: string - description: 'Validation error' type: object example: message: 'The token field is required.' errors: token: - 'The token field is required.' properties: message: type: string example: 'The token field is required.' errors: type: object properties: token: type: array example: - 'The token field is required.' items: type: string tags: - Authentication requestBody: required: true content: application/json: schema: type: object properties: token: type: string description: 'The LinkedIn access token from the frontend.' example: AQV... intent: type: string description: 'The auth flow: login (existing users only) or register (new users only).' example: login appsumo_registration_token: type: string description: '' example: architecto nullable: true required: - token - intent security: [] /api/auth/change-password: post: summary: 'Change Password' operationId: changePassword description: 'Change password for an authenticated API user and return a fresh token.' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: message: 'Password updated successfully.' token: 1|abc123... properties: message: type: string example: 'Password updated successfully.' token: type: string example: 1|abc123... 401: description: Unauthenticated content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. 422: description: 'Current password is incorrect' content: application/json: schema: type: object example: message: 'The given data was invalid.' errors: current_password: - 'The provided password does not match your current password.' properties: message: type: string example: 'The given data was invalid.' errors: type: object properties: current_password: type: array example: - 'The provided password does not match your current password.' items: type: string tags: - Authentication requestBody: required: true content: application/json: schema: type: object properties: current_password: type: string description: "User's current password." example: SecurePass123! password: type: string description: 'New password (min 8 characters).' example: NewSecurePass123! required: - current_password - password /api/auth/set-password: post: summary: 'Set Password' operationId: setPassword description: "Set a password for an authenticated user who signed up via Google or LinkedIn.\nPlaintext is never required for the previous random placeholder password." parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: message: 'Password set successfully.' token: 1|abc123... properties: message: type: string example: 'Password set successfully.' token: type: string example: 1|abc123... 422: description: 'Password already set' content: application/json: schema: type: object example: message: 'A password is already set. Use change-password instead.' error: code: password_already_set properties: message: type: string example: 'A password is already set. Use change-password instead.' error: type: object properties: code: type: string example: password_already_set tags: - Authentication requestBody: required: true content: application/json: schema: type: object properties: password: type: string description: 'New password (min 8 characters).' example: NewSecurePass123! password_confirmation: type: string description: 'Must match password.' example: NewSecurePass123! required: - password - password_confirmation /api/auth/complete-profile: post: summary: 'Complete Profile' operationId: completeProfile description: "Fill in the profile details collected on the \"About you\" step right after email-only\nregistration (see CreateNewUser::create()). Password is only required the first time --\nonce hasUsablePassword() is true, omit it and use change-password instead." parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: message: 'Profile completed successfully.' token: 1|abc123... user: { } properties: message: type: string example: 'Profile completed successfully.' token: type: string example: 1|abc123... user: type: object properties: { } 422: description: 'Password already set' content: application/json: schema: type: object example: message: 'A password is already set. Use change-password instead.' error: code: password_already_set properties: message: type: string example: 'A password is already set. Use change-password instead.' error: type: object properties: code: type: string example: password_already_set tags: - Authentication requestBody: required: true content: application/json: schema: type: object properties: name: type: string description: 'Full name.' example: 'Jane Doe' password: type: string description: 'Required on first call only.' example: NewSecurePass123! job_role: type: string description: 'What best describes the user.' example: founder_ceo company_name: type: string description: '' example: 'Acme Corporation' phone: type: string description: '' example: '+14155552671' nullable: true agree_to_terms: type: boolean description: 'Must be true.' example: true agree_to_marketing: type: boolean description: '' example: false nullable: true password_confirmation: type: string description: 'Required with password.' example: NewSecurePass123! required: - name - job_role - company_name - agree_to_terms /api/invites/accept: post: summary: 'Register with Invite' operationId: registerWithInvite description: "Accept an invitation and create a new user account. The email address must match\nthe email address on the invite. After successful registration, the user will be\nadded to the organisation or project and will receive an email verification code.\n\nNote: This endpoint bypasses the business email requirement since the invitation\nitself validates the user's legitimacy." parameters: [] responses: 201: description: '' content: application/json: schema: type: object example: message: 'Registration successful. Please check your email to verify your account.' properties: message: type: string example: 'Registration successful. Please check your email to verify your account.' 404: description: 'Token not found' content: application/json: schema: type: object example: message: 'Invite not found.' errors: token: - 'The invite token is invalid or does not exist.' properties: message: type: string example: 'Invite not found.' errors: type: object properties: token: type: array example: - 'The invite token is invalid or does not exist.' items: type: string 410: description: '' content: application/json: schema: oneOf: - description: 'Invite expired' type: object example: message: 'Invite has expired.' errors: token: - 'This invite has expired. Please request a new invitation.' properties: message: type: string example: 'Invite has expired.' errors: type: object properties: token: type: array example: - 'This invite has expired. Please request a new invitation.' items: type: string - description: 'Invite already accepted' type: object example: message: 'Invite has already been accepted.' errors: token: - 'This invite has already been accepted.' properties: message: type: string example: 'Invite has already been accepted.' errors: type: object properties: token: type: array example: - 'This invite has already been accepted.' items: type: string 422: description: '' content: application/json: schema: oneOf: - description: 'Email mismatch' type: object example: message: 'The email address does not match the invitation.' errors: email: - 'The email address must match the email on the invitation.' properties: message: type: string example: 'The email address does not match the invitation.' errors: type: object properties: email: type: array example: - 'The email address must match the email on the invitation.' items: type: string - description: 'Validation error' type: object example: message: 'The name field is required.' errors: name: - 'The name field is required.' properties: message: type: string example: 'The name field is required.' errors: type: object properties: name: type: array example: - 'The name field is required.' items: type: string - description: 'Email already registered' type: object example: message: 'The email has already been taken.' errors: email: - 'The email has already been taken.' properties: message: type: string example: 'The email has already been taken.' errors: type: object properties: email: type: array example: - 'The email has already been taken.' items: type: string tags: - Authentication requestBody: required: true content: application/json: schema: type: object properties: token: type: string description: 'The invite token from the invitation link.' example: eEtgjrcdtubjCu4817MfGiimvC2DQLBgaI7LpY1g5kdDMK5wJlQank7ZJ6PWurmb name: type: string description: "The user's full name." example: 'John Doe' email: type: string description: "The user's email address (must match the invite email)." example: user@example.com password: type: string description: "The user's password (min 8 characters)." example: SecurePass123! password_confirmation: type: string description: 'Password confirmation.' example: SecurePass123! required: - token - name - email - password - password_confirmation security: [] /api/user: get: summary: 'Get Current User' operationId: getCurrentUser description: "Get the authenticated user's details including organisation and subscription information." parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: name: 'John Doe' email: john@example.com email_verified_at: '2025-12-04T12:00:00.000000Z' created_at: '2025-12-04T10:00:00.000000Z' organisation: uuid: 550e8400-e29b-41d4-a716-446655440000 name: 'Acme Inc' website: 'https://acme.com' product_logo: 'https://example.com/logo.png' role: organisation_owner subscription: id: 1 stripe_price_id: price_1234567890 name: 'Pro Plan' status: active expiration_date: '2025-12-31T23:59:59.000000Z' trial_end_date: '2025-12-11T23:59:59.000000Z' properties: name: type: string example: 'John Doe' email: type: string example: john@example.com email_verified_at: type: string example: '2025-12-04T12:00:00.000000Z' created_at: type: string example: '2025-12-04T10:00:00.000000Z' organisation: type: object properties: uuid: type: string example: 550e8400-e29b-41d4-a716-446655440000 name: type: string example: 'Acme Inc' website: type: string example: 'https://acme.com' product_logo: type: string example: 'https://example.com/logo.png' role: type: string example: organisation_owner subscription: type: object properties: id: type: integer example: 1 stripe_price_id: type: string example: price_1234567890 name: type: string example: 'Pro Plan' status: type: string example: active expiration_date: type: string example: '2025-12-31T23:59:59.000000Z' trial_end_date: type: string example: '2025-12-11T23:59:59.000000Z' 401: description: Unauthenticated content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - Authentication /login: post: summary: 'Log In' operationId: logIn description: 'Authenticate with your email and password to start a session. On success, you receive a token for subsequent requests.' parameters: [] responses: 200: description: 'Login successful. Session started.' content: text/plain: schema: type: string example: '' 422: description: 'Invalid credentials.' content: application/json: schema: type: object example: message: 'These credentials do not match our records.' errors: email: - 'These credentials do not match our records.' properties: message: type: string example: 'These credentials do not match our records.' errors: type: object properties: email: type: array example: - 'These credentials do not match our records.' items: type: string tags: - Authentication requestBody: required: true content: application/json: schema: type: object properties: email: type: string description: 'Your registered email address.' example: john@example.com password: type: string description: 'Your account password.' example: SecurePass123! remember: type: boolean description: 'Stay logged in for extended period.' example: true required: - email - password security: [] /logout: post: summary: 'Log Out' operationId: logOut description: 'End your current session. You will need to log in again to access protected resources.' parameters: [] responses: 200: description: 'Logged out successfully.' content: text/plain: schema: type: string example: '' 401: description: 'Not logged in.' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - Authentication /forgot-password: post: summary: 'Request Password Reset' operationId: requestPasswordReset description: 'Send a password reset link to your email. The link expires after 60 minutes. Same response for security even if email not found.' parameters: [] responses: 200: description: 'Reset link sent.' content: application/json: schema: type: object example: status: 'We have emailed your password reset link.' properties: status: type: string example: 'We have emailed your password reset link.' tags: - 'Password Reset' requestBody: required: true content: application/json: schema: type: object properties: email: type: string description: 'Email address associated with your account.' example: john@example.com required: - email security: [] /reset-password: post: summary: 'Reset Password' operationId: resetPassword description: 'Set a new password using the token from your email. Token is valid for 60 minutes.' parameters: [] responses: 200: description: 'Password reset successful.' content: application/json: schema: type: object example: status: 'Your password has been reset.' properties: status: type: string example: 'Your password has been reset.' 422: description: 'Invalid or expired token.' content: application/json: schema: type: object example: message: 'This password reset token is invalid.' errors: email: - 'This password reset token is invalid.' properties: message: type: string example: 'This password reset token is invalid.' errors: type: object properties: email: type: array example: - 'This password reset token is invalid.' items: type: string tags: - 'Password Reset' requestBody: required: true content: application/json: schema: type: object properties: token: type: string description: 'Reset token from the email link.' example: a1b2c3d4e5f6g7h8i9j0 email: type: string description: 'Your account email address.' example: john@example.com password: type: string description: 'Password (min 8 characters, at least one letter and one number).' example: NewSecurePass123! password_confirmation: type: string description: 'Must match new password exactly.' example: NewSecurePass123! required: - token - email - password - password_confirmation security: [] '/api/organisations/{organisation_uuid}/reviews': post: summary: 'Get Organization Reviews' operationId: getOrganizationReviews description: "Fetch and aggregate reviews from multiple projects/brands within an organization.\nReviews are fetched from external API, persisted to database, and returned with filters applied.\nSupports filtering by multiple brands (projects), platforms, ratings, languages, and more." parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: organisation_id: 660e8400-e29b-41d4-a716-446655440001 organisation_name: 'Acme Corp' reviews: current_page: 1 data: - id: 1 scraper_review_id: 10452 organisation_id: 5 project_id: 30 platform: Capterra platform_icon: null rating: 5.0 content: 'Great product with excellent features...' author: 'John Smith' job_role: 'Product Manager' language: en date: '2026-01-13' link_url: 'https://www.capterra.com/reviews/' direct_review_url: 'https://scraper.example.com/reviews' created_at: '2026-01-15T09:30:21.000000Z' per_page: 10 total: 150 properties: data: type: object properties: organisation_id: type: string example: 660e8400-e29b-41d4-a716-446655440001 organisation_name: type: string example: 'Acme Corp' reviews: type: object properties: current_page: type: integer example: 1 data: type: array example: - id: 1 scraper_review_id: 10452 organisation_id: 5 project_id: 30 platform: Capterra platform_icon: null rating: 5 content: 'Great product with excellent features...' author: 'John Smith' job_role: 'Product Manager' language: en date: '2026-01-13' link_url: 'https://www.capterra.com/reviews/' direct_review_url: 'https://scraper.example.com/reviews' created_at: '2026-01-15T09:30:21.000000Z' items: type: object properties: id: type: integer example: 1 scraper_review_id: type: integer example: 10452 organisation_id: type: integer example: 5 project_id: type: integer example: 30 platform: type: string example: Capterra platform_icon: type: string example: null rating: type: number example: 5.0 content: type: string example: 'Great product with excellent features...' author: type: string example: 'John Smith' job_role: type: string example: 'Product Manager' language: type: string example: en date: type: string example: '2026-01-13' link_url: type: string example: 'https://www.capterra.com/reviews/' direct_review_url: type: string example: 'https://scraper.example.com/reviews' created_at: type: string example: '2026-01-15T09:30:21.000000Z' per_page: type: integer example: 10 total: type: integer example: 150 403: description: '' content: application/json: schema: type: object example: message: 'You do not have access to this organisation.' properties: message: type: string example: 'You do not have access to this organisation.' 404: description: '' content: application/json: schema: type: object example: message: 'Organisation not found.' properties: message: type: string example: 'Organisation not found.' tags: - Reviews requestBody: required: false content: application/json: schema: type: object properties: brands: type: array description: 'Optional - Filter by brand/project UUIDs. Empty/omitted = all brands.' example: - uuid-1 - uuid-2 items: type: string view_uuid: type: string description: 'Optional - Apply filters from a saved view. When provided, all other filters (except search and page) are ignored.' example: a1b2c3d4-e5f6-g7h8-i9j0 nullable: true platforms: type: array description: 'Optional - Filter by platforms. Ignored if view_uuid is provided.' example: - G2 - Capterra items: type: string date_range: type: object description: '' example: null properties: type: type: string description: 'This field is required when date_range is present.' example: preset enum: - preset - custom - all_time value: type: string description: 'This field is required when date_range.type is preset.' example: all_time enum: - last_14_days - last_3_months - last_6_months - last_12_months - last_2_years - last_3_years - all_time nullable: true date_range_custom: type: object description: '' example: null properties: start: type: string description: 'This field is required when date_range_custom is present. Must be a valid date.' example: '2026-08-27T06:47:11' end: type: string description: 'This field is required when date_range_custom is present. Must be a valid date. Must be a date after or equal to date_range_custom.start.' example: '2052-09-19' nullable: true date_from: type: string description: 'Optional - Start date. Ignored if view_uuid is provided.' example: '2026-01-01' nullable: true date_to: type: string description: 'Optional - End date. Ignored if view_uuid is provided.' example: '2026-01-24' nullable: true rating_buckets: type: array description: 'Optional - Filter by rating buckets. Ignored if view_uuid is provided.' example: - '5.0' - 4.0-4.9 items: type: string languages: type: array description: 'Optional - Filter by languages (ISO 639-1 codes). Ignored if view_uuid is provided.' example: - en - de items: type: string read_status: type: string description: 'Optional - Filter by read status. Ignored if view_uuid is provided.' example: unread nullable: true search: type: string description: 'Optional - Keyword search. Can be used with view_uuid to search within saved view filters.' example: 'customer support' nullable: true sort_by: type: string description: 'Optional - Sort field. Use creation_date (maps to review_date), review_date, rating, platform, or created_at. Ignored if view_uuid is provided.' example: creation_date nullable: true sort_direction: type: string description: 'Optional - Sort direction. Ignored if view_uuid is provided.' example: "desc\n\nNote: Results are paginated with 10 items per page (fixed, not configurable)." nullable: true max_period: type: integer description: 'Must be at least 1.' example: 67 max_results: type: integer description: 'Must be at least 1.' example: 66 period_in_days: type: integer description: 'Optional - Number of days to fetch reviews for. Default: 30. Ignored if view_uuid is provided.' example: 30 nullable: true page: type: integer description: 'Optional - Page number for pagination. Default: 1.' example: 1 nullable: true parameters: - in: path name: organisation_uuid description: 'The organization UUID.' example: 660e8400-e29b-41d4-a716-446655440001 required: true schema: type: string '/api/organisations/{organisation_uuid}/reviews/aggregation/sync': post: summary: 'Sync review aggregation tables from scraper historical data.' operationId: syncReviewAggregationTablesFromScraperHistoricalData description: "Fetches enabled link URLs from the links table (via link_project), calls scraper\n`/v1/reviews` for all pages, aggregates data, and upserts into:\n- review_aggregation_daily\n- review_aggregation_rating_daily\n\nHistorical flow only (no process_id and no webhook deliveries checks)." parameters: [] responses: 202: description: '' content: application/json: schema: type: object example: data: organisation_id: 660e8400-e29b-41d4-a716-446655440001 organisation_name: 'Acme Corp' run_id: 0f57a8e6-89e6-43d8-8d5d-176c52747467 period_in_days: 365 projects_total: 1 links_queued: 2 status: queued properties: data: type: object properties: organisation_id: type: string example: 660e8400-e29b-41d4-a716-446655440001 organisation_name: type: string example: 'Acme Corp' run_id: type: string example: 0f57a8e6-89e6-43d8-8d5d-176c52747467 period_in_days: type: integer example: 365 projects_total: type: integer example: 1 links_queued: type: integer example: 2 status: type: string example: queued 403: description: '' content: application/json: schema: type: object example: message: 'You do not have access to this organisation.' properties: message: type: string example: 'You do not have access to this organisation.' 404: description: '' content: application/json: schema: type: object example: message: 'Organisation not found.' properties: message: type: string example: 'Organisation not found.' 422: description: '' content: application/json: schema: type: object example: message: 'Review aggregation sync is unavailable. Scraper service is not configured.' properties: message: type: string example: 'Review aggregation sync is unavailable. Scraper service is not configured.' tags: - Reviews requestBody: required: false content: application/json: schema: type: object properties: all_organisations: type: boolean description: 'Optional - When true, queue sync for all organisations in the system (admin only; brands must be omitted).' example: true nullable: true brands: type: array description: 'Optional - Filter by brand/project UUIDs. Empty/omitted = all brands.' example: - uuid-1 - uuid-2 items: type: string period_in_days: type: integer description: 'Optional - Scraper history window in days. Default: 365.' example: 365 nullable: true parameters: - in: path name: organisation_uuid description: 'The organization UUID.' example: 660e8400-e29b-41d4-a716-446655440001 required: true schema: type: string '/api/organisations/{organisation_uuid}/reviews/aggregation/runs/{run_id}': get: summary: 'Get aggregation run status using run_id.' operationId: getAggregationRunStatusUsingRunId description: '' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: run_id: 0f57a8e6-89e6-43d8-8d5d-176c52747467 status: running period_in_days: 365 projects_total: 1 total_links: 8 processed_links: 5 success_links: 4 failed_links: 1 no_rows_links: 1 queued_at: '2026-04-22T06:31:06.000000Z' started_at: '2026-04-22T06:31:10.000000Z' completed_at: null properties: data: type: object properties: run_id: type: string example: 0f57a8e6-89e6-43d8-8d5d-176c52747467 status: type: string example: running period_in_days: type: integer example: 365 projects_total: type: integer example: 1 total_links: type: integer example: 8 processed_links: type: integer example: 5 success_links: type: integer example: 4 failed_links: type: integer example: 1 no_rows_links: type: integer example: 1 queued_at: type: string example: '2026-04-22T06:31:06.000000Z' started_at: type: string example: '2026-04-22T06:31:10.000000Z' completed_at: type: string example: null 403: description: '' content: application/json: schema: type: object example: message: 'You do not have access to this organisation.' properties: message: type: string example: 'You do not have access to this organisation.' 404: description: '' content: application/json: schema: type: object example: message: 'Aggregation run not found.' properties: message: type: string example: 'Aggregation run not found.' tags: - Reviews parameters: - in: path name: organisation_uuid description: '' example: 2958b10e-6a80-4903-868f-79b048b5e0c4 required: true schema: type: string - in: path name: run_id description: 'Aggregation run UUID returned by sync endpoint.' example: architecto required: true schema: type: string '/api/organisations/{organisation_uuid}/reviews/analytics': post: summary: 'Get Review Analytics' operationId: getReviewAnalytics description: "Calculate analytics metrics for reviews including new reviews count, average rating,\nreviews per month, with comparison to previous period. Also includes rating distribution\nand platform breakdown. Each product in data includes is_scraping_reviews (boolean): true\nif a review scrape is currently in progress for that product; use it to show a loading state in the UI.\nis_data (boolean): true if any aggregation record exists for the same scope (project_ids, platforms, date range) as the metrics; false otherwise." parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: - product_uuid: 660e8400-e29b-41d4-a716-446655440001 product_name: 'Acme Product' product_logo: 'https://...' period: start: '2025-10-28' end: '2026-01-27' duration: 3 metrics: new_reviews: current: 2 previous: 10 change_percentage: -80.0 trend: down average_rating: current: 4.1 previous: 4.4 change_percentage: -6.8 trend: down reviews_per_month: current: 2 previous: 6 change_percentage: -68.3 trend: down review_velocity: default_interval: week allowed_intervals: - week - month by_interval: week: data: - group: 2025-W48 count: 1 cumulative_before: 0 rating: 4.2 month: data: - group: 2025-11 count: 1 cumulative_before: 0 rating: 4.2 interval: week data: - group: 2025-W48 count: 1 cumulative_before: 0 rating: 4.2 breakdown: ratings: '5.0': 0 '4.0': 2 '3.0': 0 '2.0': 0 '1.0': 0 platforms: - name: G2 has_data: 1 - name: Capterra has_data: 2 is_scraping_reviews: false competitors: - project_uuid: 660e8400-e29b-41d4-a716-446655440001 product_name: 'Base Product' product_logo: 'https://...' is_competitor: false total_reviews: current: 10 previous: 5 change_percentage: 100.0 trend: up average_rating: current: 4.3 previous: 4.1 change_percentage: 4.9 trend: up rank: 1 applied_filters: brands: - uuid-1 platforms: - G2 - Capterra date_range: type: preset value: last_3_months organisation_id: 660e8400-e29b-41d4-a716-446655440001 organisation_name: 'Acme Corp' is_data: true properties: data: type: array example: - product_uuid: 660e8400-e29b-41d4-a716-446655440001 product_name: 'Acme Product' product_logo: 'https://...' period: start: '2025-10-28' end: '2026-01-27' duration: 3 metrics: new_reviews: current: 2 previous: 10 change_percentage: -80 trend: down average_rating: current: 4.1 previous: 4.4 change_percentage: -6.8 trend: down reviews_per_month: current: 2 previous: 6 change_percentage: -68.3 trend: down review_velocity: default_interval: week allowed_intervals: - week - month by_interval: week: data: - group: 2025-W48 count: 1 cumulative_before: 0 rating: 4.2 month: data: - group: 2025-11 count: 1 cumulative_before: 0 rating: 4.2 interval: week data: - group: 2025-W48 count: 1 cumulative_before: 0 rating: 4.2 breakdown: ratings: '5.0': 0 '4.0': 2 '3.0': 0 '2.0': 0 '1.0': 0 platforms: - name: G2 has_data: 1 - name: Capterra has_data: 2 is_scraping_reviews: false items: type: object properties: product_uuid: type: string example: 660e8400-e29b-41d4-a716-446655440001 product_name: type: string example: 'Acme Product' product_logo: type: string example: 'https://...' period: type: object properties: start: type: string example: '2025-10-28' end: type: string example: '2026-01-27' duration: type: integer example: 3 metrics: type: object properties: new_reviews: type: object properties: current: type: integer example: 2 previous: type: integer example: 10 change_percentage: type: number example: -80.0 trend: type: string example: down average_rating: type: object properties: current: type: number example: 4.1 previous: type: number example: 4.4 change_percentage: type: number example: -6.8 trend: type: string example: down reviews_per_month: type: object properties: current: type: integer example: 2 previous: type: integer example: 6 change_percentage: type: number example: -68.3 trend: type: string example: down review_velocity: type: object properties: default_interval: type: string example: week allowed_intervals: type: array example: - week - month items: type: string by_interval: type: object properties: week: type: object properties: { data: { type: array, example: [{ group: 2025-W48, count: 1, cumulative_before: 0, rating: 4.2 }], items: { type: object, properties: { group: { type: string, example: 2025-W48 }, count: { type: integer, example: 1 }, cumulative_before: { type: integer, example: 0 }, rating: { type: number, example: 4.2 } } } } } month: type: object properties: { data: { type: array, example: [{ group: 2025-11, count: 1, cumulative_before: 0, rating: 4.2 }], items: { type: object, properties: { group: { type: string, example: 2025-11 }, count: { type: integer, example: 1 }, cumulative_before: { type: integer, example: 0 }, rating: { type: number, example: 4.2 } } } } } interval: type: string example: week data: type: array example: - group: 2025-W48 count: 1 cumulative_before: 0 rating: 4.2 items: type: object properties: group: { type: string, example: 2025-W48 } count: { type: integer, example: 1 } cumulative_before: { type: integer, example: 0 } rating: { type: number, example: 4.2 } breakdown: type: object properties: ratings: type: object properties: '5.0': type: integer example: 0 '4.0': type: integer example: 2 '3.0': type: integer example: 0 '2.0': type: integer example: 0 '1.0': type: integer example: 0 platforms: type: array example: - name: G2 has_data: 1 - name: Capterra has_data: 2 items: type: object properties: name: type: string example: G2 has_data: type: integer example: 1 is_scraping_reviews: type: boolean example: false competitors: type: array example: - project_uuid: 660e8400-e29b-41d4-a716-446655440001 product_name: 'Base Product' product_logo: 'https://...' is_competitor: false total_reviews: current: 10 previous: 5 change_percentage: 100 trend: up average_rating: current: 4.3 previous: 4.1 change_percentage: 4.9 trend: up rank: 1 items: type: object properties: project_uuid: type: string example: 660e8400-e29b-41d4-a716-446655440001 product_name: type: string example: 'Base Product' product_logo: type: string example: 'https://...' is_competitor: type: boolean example: false total_reviews: type: object properties: current: type: integer example: 10 previous: type: integer example: 5 change_percentage: type: number example: 100.0 trend: type: string example: up average_rating: type: object properties: current: type: number example: 4.3 previous: type: number example: 4.1 change_percentage: type: number example: 4.9 trend: type: string example: up rank: type: integer example: 1 applied_filters: type: object properties: brands: type: array example: - uuid-1 items: type: string platforms: type: array example: - G2 - Capterra items: type: string date_range: type: object properties: type: type: string example: preset value: type: string example: last_3_months organisation_id: type: string example: 660e8400-e29b-41d4-a716-446655440001 organisation_name: type: string example: 'Acme Corp' is_data: type: boolean example: true 403: description: '' content: application/json: schema: type: object example: message: 'You do not have access to this organisation.' properties: message: type: string example: 'You do not have access to this organisation.' 404: description: '' content: application/json: schema: type: object example: message: 'Organisation not found.' properties: message: type: string example: 'Organisation not found.' tags: - Reviews requestBody: required: false content: application/json: schema: type: object properties: brands: type: array description: 'Optional - Filter by brand/project UUIDs. Empty/omitted = all brands. Ignored if compare_products is provided.' example: - uuid-1 - uuid-2 items: type: string compare_products: type: array description: 'Optional - Compare up to 2 products. When provided, returns array of analytics per product. Takes precedence over brands.' example: - uuid-1 - uuid-2 items: type: string platforms: type: array description: 'Optional - Filter by platforms.' example: - G2 - Capterra items: type: string date_range: type: object description: 'Optional - Preset date range.' example: type: preset value: last_3_months properties: type: type: string description: 'This field is required when date_range is present.' example: preset enum: - preset - all_time value: type: string description: 'This field is required when date_range.type is preset.' example: last_2_years enum: - last_14_days - last_3_months - last_6_months - last_12_months - last_2_years - last_3_years - all_time nullable: true date_range_custom: type: object description: 'Optional - Custom date range.' example: start: '2025-10-01' end: '2026-01-27' properties: start: type: string description: 'This field is required when date_range_custom is present. Must be a valid date.' example: '2026-08-27T06:47:12' end: type: string description: 'This field is required when date_range_custom is present. Must be a valid date. Must be a date after or equal to date_range_custom.start.' example: '2052-09-19' nullable: true max_results: type: integer description: 'Must be at least 1.' example: 22 nullable: true max_period: type: integer description: 'Must be at least 1.' example: 67 nullable: true parameters: - in: path name: organisation_uuid description: 'The organization UUID.' example: 660e8400-e29b-41d4-a716-446655440001 required: true schema: type: string '/api/organisations/{organisation_uuid}/reviews/access-window': get: summary: "Reviews entitlement window -- mirrors SocialAnalyticsController::mentionsAccessWindow(),\nthe boundary /v1/bulk-reviews (no plan gating of its own) should clamp its own request to." operationId: reviewsEntitlementWindowMirrorsSocialAnalyticsControllermentionsAccessWindowTheBoundaryv1bulkReviewsnoPlanGatingOfItsOwnShouldClampItsOwnRequestTo description: '' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: earliest_allowed_date: '2026-05-21' reviews_limit: 1000 properties: data: type: object properties: earliest_allowed_date: type: string example: '2026-05-21' reviews_limit: type: integer example: 1000 403: description: '' content: application/json: schema: type: object example: message: 'You do not have access to this organisation.' properties: message: type: string example: 'You do not have access to this organisation.' 404: description: '' content: application/json: schema: type: object example: message: 'Organisation not found.' properties: message: type: string example: 'Organisation not found.' tags: - Reviews parameters: - in: path name: organisation_uuid description: 'The organisation UUID.' example: 660e8400-e29b-41d4-a716-446655440001 required: true schema: type: string '/api/organisations/{organisation_uuid}/reviews/cap-thresholds': post: summary: 'Get Review Cap Thresholds' operationId: getReviewCapThresholds description: "Returns per-platform cap thresholds for each project in the organisation.\nUse projectIds to narrow the response to specific projects; omit (or pass []) for all." parameters: [] responses: { } tags: - Reviews requestBody: required: true content: application/json: schema: type: object properties: projectIds: type: array description: 'nullable UUIDs of projects to scope the response. Empty = all projects.' example: - architecto items: type: string url: type: string description: 'Must be "organisation-level".' example: 'http://www.bailey.biz/quos-velit-et-fugiat-sunt-nihil-accusantium-harum.html' required: - url parameters: - in: path name: organisation_uuid description: 'The organization UUID.' example: 6ff8f7f6-1eb3-3525-be4a-3932c805afed required: true schema: type: string '/api/organisations/{organisation_uuid}/reviews/report': post: summary: 'Generate AEO/GEO Report' operationId: generateAEOGEOReport description: "Generate a one-page AEO (Answer Engine Optimization) and GEO (Generative Engine Optimization)\nreport from the organisation's filtered reviews. Reuses the same filter shape as the reviews\nindex/export endpoints, samples the top-N most recent matching reviews, and sends them to\nOpenAI for analysis. Returns a structured JSON report ready to render as a one-pager." parameters: [] responses: { } tags: - Reviews requestBody: required: true content: application/json: schema: type: object properties: brands: type: array description: 'Must be a valid UUID. The uuid of an existing record in the projects table. The uuid of an existing record in the projects table.' example: - 6ff8f7f6-1eb3-3525-be4a-3932c805afed items: type: string view_uuid: type: string description: 'Must be a valid UUID. The uuid of an existing record in the review_views table.' example: 6b72fe4a-5b40-307c-bc24-f79acf9a1bb9 nullable: true platforms: type: array description: '' example: - software_advice items: type: string enum: - g2 - capterra - software_advice - trustpilot - omr_reviews - clutch - sourceforge - product_hunt - hubspot_directory - goodfirms - google_workspace_marketplace - play_store - app_store - subscribed_fyi - chrome_web_store - google_reviews date_range: type: object description: '' example: null properties: type: type: string description: 'This field is required when date_range is present.' example: custom enum: - preset - custom - all_time value: type: string description: 'This field is required when date_range.type is preset.' example: last_14_days enum: - last_14_days - last_3_months - last_6_months - last_12_months - last_2_years - last_3_years - all_time nullable: true date_range_custom: type: object description: '' example: null properties: start: type: string description: 'This field is required when date_range_custom is present. Must be a valid date.' example: '2026-08-27T06:47:12' end: type: string description: 'This field is required when date_range_custom is present. Must be a valid date. Must be a date after or equal to date_range_custom.start.' example: '2052-09-19' nullable: true date_from: type: string description: 'Must be a valid date.' example: '2026-08-27T06:47:12' nullable: true date_to: type: string description: 'Must be a valid date. Must be a date after or equal to date_from.' example: '2052-09-19' nullable: true rating_buckets: type: array description: '' example: - 3.0-3.9 items: type: string enum: - '5.0' - 4.0-4.9 - 3.0-3.9 - 2.0-2.9 - 1.0-1.9 languages: type: array description: '' example: - architecto items: type: string read_status: type: string description: '' example: read enum: - all - read - unread nullable: true search: type: string description: 'Must not be greater than 500 characters.' example: 'n' nullable: true sort_by: type: string description: '' example: platform enum: - creation_date - review_date - rating - platform - created_at nullable: true sort_direction: type: string description: '' example: asc enum: - asc - desc nullable: true max_period: type: integer description: 'Must be at least 1.' example: 67 max_results: type: integer description: 'Must be at least 1.' example: 66 period_in_days: type: integer description: 'Must be at least 1. Must not be greater than 365.' example: 17 nullable: true page: type: integer description: 'Must be at least 1.' example: 35 nullable: true required: - platforms parameters: - in: path name: organisation_uuid description: 'The organization UUID.' example: 6ff8f7f6-1eb3-3525-be4a-3932c805afed required: true schema: type: string '/api/reviews/aggregation/runs/{run_id}': get: summary: 'Get aggregation run status using run_id without organisation in URL.' operationId: getAggregationRunStatusUsingRunIdWithoutOrganisationInURL description: '' parameters: [] responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - Reviews parameters: - in: path name: run_id description: 'Aggregation run UUID returned by sync endpoint.' example: architecto required: true schema: type: string '/api/organisations/{organisation_uuid}/social': post: summary: 'Get Organization Social Posts' operationId: getOrganizationSocialPosts description: "Fetch and aggregate social posts from multiple projects/brands within an organization.\nPosts are fetched from external Scraper API, persisted to database, and returned with filters applied.\nSupports filtering by brands, platforms, types, sentiments, keywords, and more." parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: posts: - uuid: 770e8400-e29b-41d4-a716-446655440002 platform: reddit community_name: saas content_type: post brand_keyword: Acme title: 'Great product discussion' text: "We've been using this for months..." post_date: '2026-01-20T10:00:00.000000Z' url: 'https://reddit.com/r/saas/comments/123' sentiment: positive intensity: 5 score_0_100: 85 user_name: johndoe upvotes_count: 42 comments_count: 10 project: uuid: 660e8400-e29b-41d4-a716-446655440001 product_name: 'Acme CRM' product_logo: 'https://...' created_at: '2026-01-20T09:30:21.000000Z' updated_at: '2026-01-20T09:30:21.000000Z' meta: current_page: 1 per_page: 10 total: 838 last_page: 17 applied_filters: brands: - uuid1 - uuid2 platforms: - reddit - github types: - post sentiments: - positive - negative keywords: - Loom - Figma properties: data: type: object properties: posts: type: array example: - uuid: 770e8400-e29b-41d4-a716-446655440002 platform: reddit community_name: saas content_type: post brand_keyword: Acme title: 'Great product discussion' text: "We've been using this for months..." post_date: '2026-01-20T10:00:00.000000Z' url: 'https://reddit.com/r/saas/comments/123' sentiment: positive intensity: 5 score_0_100: 85 user_name: johndoe upvotes_count: 42 comments_count: 10 project: uuid: 660e8400-e29b-41d4-a716-446655440001 product_name: 'Acme CRM' product_logo: 'https://...' created_at: '2026-01-20T09:30:21.000000Z' updated_at: '2026-01-20T09:30:21.000000Z' items: type: object properties: uuid: type: string example: 770e8400-e29b-41d4-a716-446655440002 platform: type: string example: reddit community_name: type: string example: saas content_type: type: string example: post brand_keyword: type: string example: Acme title: type: string example: 'Great product discussion' text: type: string example: "We've been using this for months..." post_date: type: string example: '2026-01-20T10:00:00.000000Z' url: type: string example: 'https://reddit.com/r/saas/comments/123' sentiment: type: string example: positive intensity: type: integer example: 5 score_0_100: type: integer example: 85 user_name: type: string example: johndoe upvotes_count: type: integer example: 42 comments_count: type: integer example: 10 project: type: object properties: uuid: type: string example: 660e8400-e29b-41d4-a716-446655440001 product_name: type: string example: 'Acme CRM' product_logo: type: string example: 'https://...' created_at: type: string example: '2026-01-20T09:30:21.000000Z' updated_at: type: string example: '2026-01-20T09:30:21.000000Z' meta: type: object properties: current_page: type: integer example: 1 per_page: type: integer example: 10 total: type: integer example: 838 last_page: type: integer example: 17 applied_filters: type: object properties: brands: type: array example: - uuid1 - uuid2 items: type: string platforms: type: array example: - reddit - github items: type: string types: type: array example: - post items: type: string sentiments: type: array example: - positive - negative items: type: string keywords: type: array example: - Loom - Figma items: type: string 403: description: '' content: application/json: schema: type: object example: message: 'You do not have access to this organisation.' properties: message: type: string example: 'You do not have access to this organisation.' 404: description: '' content: application/json: schema: type: object example: message: 'Organisation not found.' properties: message: type: string example: 'Organisation not found.' tags: - 'Social Posts' requestBody: required: false content: application/json: schema: type: object properties: brands: type: array description: 'Optional - Filter by brand/project UUIDs. Empty/omitted = all brands.' example: - uuid-1 - uuid-2 items: type: string view_uuid: type: string description: 'Optional - Apply filters from a saved view. When provided, all other filters (except search, page, and per_page) are ignored.' example: a1b2c3d4-e5f6-g7h8-i9j0 nullable: true platforms: type: array description: 'Optional - Filter by platforms. Ignored if view_uuid is provided.' example: - reddit - github items: type: string types: type: array description: 'Optional - Filter by content type. Ignored if view_uuid is provided.' example: - post - comment items: type: string sentiments: type: array description: 'Optional - Filter by sentiment. Ignored if view_uuid is provided.' example: - positive - negative items: type: string keywords: type: array description: 'Optional - Filter by keywords. Ignored if view_uuid is provided.' example: - Loom - Figma items: type: string date_range: type: object description: 'Optional - Preset date range. Ignored if view_uuid is provided.' example: type: preset value: last_3_months properties: type: type: string description: 'This field is required when date_range is present.' example: custom enum: - preset - custom - all_time value: type: string description: 'This field is required when date_range.type is preset.' example: all_time enum: - last_14_days - last_3_months - last_6_months - last_12_months - last_2_years - last_3_years - all_time nullable: true date_range_custom: type: object description: 'Optional - Custom date range. Ignored if view_uuid is provided.' example: start: '2026-01-01' end: '2026-01-24' properties: start: type: string description: 'This field is required when date_range_custom is present. Must be a valid date.' example: '2026-08-27T06:47:12' end: type: string description: 'This field is required when date_range_custom is present. Must be a valid date. Must be a date after or equal to date_range_custom.start.' example: '2052-09-19' nullable: true read_status: type: string description: 'Optional - Filter by read status. Ignored if view_uuid is provided.' example: unread nullable: true search: type: string description: 'Optional - Keyword search. Can be used with view_uuid to search within saved view filters.' example: 'customer support' nullable: true sort_by: type: string description: 'Optional - Sort field. Ignored if view_uuid is provided.' example: post_date nullable: true sort_direction: type: string description: 'Optional - Sort direction. Ignored if view_uuid is provided.' example: desc nullable: true relevant: type: integer description: '' example: '0' enum: - '0' - '1' quality_score_threshold: type: number description: '' example: 4326.41688 max_period: type: integer description: 'Must be at least 1.' example: 27 max_results: type: integer description: 'Must be at least 1.' example: 35 page: type: integer description: 'Optional - Page number for pagination. Default: 1.' example: 1 nullable: true per_page: type: integer description: 'Optional - Items per page. Default: 10. Maximum: 100.' example: 10 nullable: true parameters: - in: path name: organisation_uuid description: 'The organization UUID.' example: 660e8400-e29b-41d4-a716-446655440001 required: true schema: type: string '/api/organisations/{organisation_uuid}/social/aggregation/sync': post: summary: 'Sync social aggregation tables from mentions API historical data.' operationId: syncSocialAggregationTablesFromMentionsAPIHistoricalData description: "Uses projects.social_platform.brand_id and fetches both /v1/mentions/posts and\n/v1/mentions/comments for all supported platforms:\nreddit, github, stackoverflow, hackernews, x, youtube, linkedin." parameters: [] responses: 202: description: '' content: application/json: schema: type: object example: data: organisation_id: 660e8400-e29b-41d4-a716-446655440001 organisation_name: 'Acme Corp' run_id: 0f57a8e6-89e6-43d8-8d5d-176c52747467 period_in_days: 365 projects_total: 1 tasks_queued: 7 supported_platforms: - reddit - github - stackoverflow - hackernews - x - youtube - linkedin status: queued properties: data: type: object properties: organisation_id: type: string example: 660e8400-e29b-41d4-a716-446655440001 organisation_name: type: string example: 'Acme Corp' run_id: type: string example: 0f57a8e6-89e6-43d8-8d5d-176c52747467 period_in_days: type: integer example: 365 projects_total: type: integer example: 1 tasks_queued: type: integer example: 7 supported_platforms: type: array example: - reddit - github - stackoverflow - hackernews - x - youtube - linkedin items: type: string status: type: string example: queued 403: description: '' content: application/json: schema: type: object example: message: 'You do not have access to this organisation.' properties: message: type: string example: 'You do not have access to this organisation.' 404: description: '' content: application/json: schema: type: object example: message: 'Organisation not found.' properties: message: type: string example: 'Organisation not found.' 422: description: '' content: application/json: schema: type: object example: message: 'Social aggregation sync is unavailable. Mentions service is not configured.' properties: message: type: string example: 'Social aggregation sync is unavailable. Mentions service is not configured.' tags: - 'Social Posts' requestBody: required: false content: application/json: schema: type: object properties: all_organisations: type: boolean description: 'Optional - When true, queue sync for all organisations in the system (admin only; brands must be omitted).' example: true nullable: true brands: type: array description: 'Optional - Filter by brand/project UUIDs. Empty/omitted = all brands.' example: - architecto items: type: string period_in_days: type: integer description: 'Optional - Mentions history window in days. Default: 365.' example: 16 nullable: true parameters: - in: path name: organisation_uuid description: 'The organization UUID.' example: 6ff8f7f6-1eb3-3525-be4a-3932c805afed required: true schema: type: string '/api/organisations/{organisation_uuid}/social/aggregation/runs/{run_id}': get: summary: 'Get social aggregation run status for an organisation-scoped run.' operationId: getSocialAggregationRunStatusForAnOrganisationScopedRun description: '' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: run_id: 0f57a8e6-89e6-43d8-8d5d-176c52747467 status: running period_in_days: 365 projects_total: 3 tasks_total: 21 processed_tasks: 9 success_tasks: 8 failed_tasks: 1 no_rows_tasks: 2 properties: data: type: object properties: run_id: type: string example: 0f57a8e6-89e6-43d8-8d5d-176c52747467 status: type: string example: running period_in_days: type: integer example: 365 projects_total: type: integer example: 3 tasks_total: type: integer example: 21 processed_tasks: type: integer example: 9 success_tasks: type: integer example: 8 failed_tasks: type: integer example: 1 no_rows_tasks: type: integer example: 2 403: description: '' content: application/json: schema: type: object example: message: 'You do not have access to this organisation.' properties: message: type: string example: 'You do not have access to this organisation.' 404: description: '' content: application/json: schema: type: object example: message: 'Aggregation run not found.' properties: message: type: string example: 'Aggregation run not found.' tags: - 'Social Posts' parameters: - in: path name: organisation_uuid description: 'The organization UUID.' example: 6ff8f7f6-1eb3-3525-be4a-3932c805afed required: true schema: type: string - in: path name: run_id description: 'Aggregation run UUID returned by social aggregation sync endpoint.' example: architecto required: true schema: type: string '/api/organisations/{organisation_uuid}/social/analytics': post: summary: 'Organisation-level social analytics.' operationId: organisationLevelSocialAnalytics description: "Returns mention count and average sentiment with period-over-period comparison.\nFilters: brands (brand_id), platforms, date_range, date_range_custom (start/end), or custom_date_range (from/to).\ndata.is_scraping_mentions (boolean): true if a mentions scrape is currently in progress for any project in scope; use it to show a loading state in the UI.\nis_data (boolean): true if any aggregation record exists for the same scope (organisation, brands, platforms, date range) as the metrics; false otherwise." parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: mentions: current: 42 previous: 30 percentage_change: 40.0 average_sentiment: current: 0.65 previous: 0.58 percentage_change: 12.1 estimated_reach_per_month: current: 1200 previous: 800 percentage_change: 50.0 mentions_and_reach_velocity: default_interval: week allowed_intervals: - week - month by_interval: week: data: - group: 2025-W48 mentions: 0 reach: 0 by_project: [] month: data: - group: 2025-11 mentions: 0 reach: 0 by_project: [] interval: week data: - group: 2025-W48 mentions: 0 reach: 0 by_project: [] breakdown: platforms: - name: reddit has_data: 1 count: 20 sentiments: positive: 12 neutral: 5 negative: 3 sentiment_score: 0.58 - name: github has_data: 2 count: 22 sentiments: positive: 0 neutral: 0 negative: 0 sentiment_score: 0 sentiments: positive: 25 neutral: 12 negative: 5 competitors: [] is_scraping_mentions: 2 is_data: true properties: data: type: object properties: mentions: type: object properties: current: type: integer example: 42 previous: type: integer example: 30 percentage_change: type: number example: 40.0 average_sentiment: type: object properties: current: type: number example: 0.65 previous: type: number example: 0.58 percentage_change: type: number example: 12.1 estimated_reach_per_month: type: object properties: current: type: integer example: 1200 previous: type: integer example: 800 percentage_change: type: number example: 50.0 mentions_and_reach_velocity: type: object properties: default_interval: type: string example: week allowed_intervals: type: array example: - week - month items: type: string by_interval: type: object properties: week: type: object properties: data: type: array example: - { group: 2025-W48, mentions: 0, reach: 0, by_project: [] } items: type: object properties: { group: { type: string, example: 2025-W48 }, mentions: { type: integer, example: 0 }, reach: { type: integer, example: 0 }, by_project: { type: array, example: [] } } month: type: object properties: data: type: array example: - { group: 2025-11, mentions: 0, reach: 0, by_project: [] } items: type: object properties: { group: { type: string, example: 2025-11 }, mentions: { type: integer, example: 0 }, reach: { type: integer, example: 0 }, by_project: { type: array, example: [] } } interval: type: string example: week data: type: array example: - group: 2025-W48 mentions: 0 reach: 0 by_project: [] items: type: object properties: group: type: string example: 2025-W48 mentions: type: integer example: 0 reach: type: integer example: 0 by_project: type: array example: [] breakdown: type: object properties: platforms: type: array example: - name: reddit has_data: 1 count: 20 sentiments: positive: 12 neutral: 5 negative: 3 sentiment_score: 0.58 - name: github has_data: 2 count: 22 sentiments: positive: 0 neutral: 0 negative: 0 sentiment_score: 0 items: type: object properties: name: type: string example: reddit has_data: type: integer example: 1 count: type: integer example: 20 sentiments: type: object properties: positive: type: integer example: 12 neutral: type: integer example: 5 negative: type: integer example: 3 sentiment_score: type: number example: 0.58 sentiments: type: object properties: positive: type: integer example: 25 neutral: type: integer example: 12 negative: type: integer example: 5 competitors: type: array example: [] is_scraping_mentions: type: integer example: 2 is_data: type: boolean example: true 403: description: '' content: application/json: schema: type: object example: message: 'You do not have access to this organisation.' properties: message: type: string example: 'You do not have access to this organisation.' 404: description: '' content: application/json: schema: type: object example: message: 'Organisation not found.' properties: message: type: string example: 'Organisation not found.' tags: - 'Social Posts' requestBody: required: false content: application/json: schema: type: object properties: brands: type: array description: 'Optional - Filter by brand IDs (project social_platform.brand_id).' example: - '123' - '456' items: type: string platforms: type: array description: 'Optional - Filter by platforms.' example: - reddit - github items: type: string exclude_brands: type: array description: 'Optional - Exclude aggregation rows for specific brand_id + platform pairs (only organisation-owned brand_ids apply; unknown ids are ignored).' example: - brand_id: 123 platforms: - youtube - x - brand_id: 124 platforms: - youtube items: type: object nullable: true properties: brand_id: type: integer description: '' example: 16 platforms: type: array description: '' example: - youtube items: type: string enum: - reddit - github - stackoverflow - hackernews - youtube - linkedin - x '*': type: object description: '' example: brand_id: 123 properties: brand_id: type: integer description: 'required-with:exclude_brands Brand id (project social_platform.brand_id).' example: 123 platforms: type: array description: 'required-with:exclude_brands Platforms to exclude for this brand.' example: - youtube - x items: type: string required: - brand_id - platforms date_range: type: string description: 'Optional - Preset date range.' example: last_3_months nullable: true custom_date_range: type: object description: 'Optional - Custom date range (from/to).' example: from: '2025-10-01' to: '2026-01-27' properties: from: type: string description: 'This field is required when custom_date_range is present. Must be a valid date.' example: '2026-08-27T06:47:12' to: type: string description: 'This field is required when custom_date_range is present. Must be a valid date. Must be a date after or equal to custom_date_range.from.' example: '2052-09-19' nullable: true date_range_custom: type: object description: 'Optional - Custom date range.' example: start: '2025-10-01' end: '2026-01-27' properties: start: type: string description: 'This field is required when date_range_custom is present. Must be a valid date.' example: '2026-08-27T06:47:12' end: type: string description: 'This field is required when date_range_custom is present. Must be a valid date. Must be a date after or equal to date_range_custom.start.' example: '2052-09-19' nullable: true max_results: type: integer description: 'Must be at least 1.' example: 22 nullable: true max_period: type: integer description: 'Must be at least 1.' example: 67 nullable: true parameters: - in: path name: organisation_uuid description: 'The organisation UUID.' example: 660e8400-e29b-41d4-a716-446655440001 required: true schema: type: string '/api/organisations/{organisation_uuid}/social/mentions-access-window': get: summary: "Mentions entitlement window for this organisation's plan -- the boundary any *other*\nmentions read path (in particular the live scraper-backed /v1/bulk-mentions list, which\nnever itself applies plan-based date/quota gating the way analytics() above does via\nOverviewPlanFilterService/resolveOrgMentionsLimit) should clamp its own request to, so an\norg can't retrieve more history/volume than its plan actually allows just by using a\ndifferent read path than the one this gating already covers." operationId: mentionsEntitlementWindowForThisOrganisationsPlanTheBoundaryAnyotherMentionsReadPathinParticularTheLiveScraperBackedv1bulkMentionsListWhichNeverItselfAppliesPlanBasedDatequotaGatingTheWayAnalyticsAboveDoesViaOverviewPlanFilterServiceresolveOrgMentionsLimitShouldClampItsOwnRequestToSoAnOrgCantRetrieveMoreHistoryvolumeThanItsPlanActuallyAllowsJustByUsingADifferentReadPathThanTheOneThisGatingAlreadyCovers description: '' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: earliest_allowed_date: '2026-05-21' mentions_limit: 21 properties: data: type: object properties: earliest_allowed_date: type: string example: '2026-05-21' mentions_limit: type: integer example: 21 403: description: '' content: application/json: schema: type: object example: message: 'You do not have access to this organisation.' properties: message: type: string example: 'You do not have access to this organisation.' 404: description: '' content: application/json: schema: type: object example: message: 'Organisation not found.' properties: message: type: string example: 'Organisation not found.' tags: - 'Social Posts' parameters: - in: path name: organisation_uuid description: 'The organisation UUID.' example: 660e8400-e29b-41d4-a716-446655440001 required: true schema: type: string '/api/organisations/{organisation_uuid}/social/views': get: summary: 'List Social Post Views' operationId: listSocialPostViews description: 'Get all saved filter views for the current user in this organization.' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: - uuid: a1b2c3d4-e5f6-g7h8-i9j0 name: 'Critical Alerts' filters: platforms: - reddit sentiments: - negative created_at: '2026-01-26T10:00:00.000000Z' properties: data: type: array example: - uuid: a1b2c3d4-e5f6-g7h8-i9j0 name: 'Critical Alerts' filters: platforms: - reddit sentiments: - negative created_at: '2026-01-26T10:00:00.000000Z' items: type: object properties: uuid: type: string example: a1b2c3d4-e5f6-g7h8-i9j0 name: type: string example: 'Critical Alerts' filters: type: object properties: platforms: type: array example: - reddit items: type: string sentiments: type: array example: - negative items: type: string created_at: type: string example: '2026-01-26T10:00:00.000000Z' tags: - 'Social Posts' post: summary: 'Store Social Post View' operationId: storeSocialPostView description: 'Create a new saved filter view for social posts.' parameters: [] responses: 201: description: '' content: application/json: schema: type: object example: data: uuid: a1b2c3d4-e5f6-g7h8-i9j0 name: 'Critical Alerts' filters: platforms: - reddit sentiments: - negative created_at: '2026-01-26T10:00:00.000000Z' properties: data: type: object properties: uuid: type: string example: a1b2c3d4-e5f6-g7h8-i9j0 name: type: string example: 'Critical Alerts' filters: type: object properties: platforms: type: array example: - reddit items: type: string sentiments: type: array example: - negative items: type: string created_at: type: string example: '2026-01-26T10:00:00.000000Z' tags: - 'Social Posts' requestBody: required: true content: application/json: schema: type: object properties: name: type: string description: 'The name of the view.' example: 'Critical Alerts' filters: type: object description: 'Filter configuration.' example: platforms: - reddit sentiments: - negative properties: brands: type: array description: 'Must be a valid UUID. The uuid of an existing record in the projects table. The uuid of an existing record in the projects table.' example: - 6ff8f7f6-1eb3-3525-be4a-3932c805afed items: type: string platforms: type: array description: '' example: - reddit items: type: string enum: - reddit - github - stackoverflow - hackernews - youtube - linkedin - x types: type: array description: '' example: - comment items: type: string enum: - post - comment sentiments: type: array description: '' example: - neutral items: type: string enum: - positive - neutral - negative keywords: type: array description: 'Must not be greater than 255 characters.' example: - g items: type: string date_range: type: object description: '' example: null properties: type: type: string description: 'This field is required when filters.date_range is present.' example: custom enum: - preset - custom - all_time value: type: string description: 'This field is required when filters.date_range.type is preset.' example: last_6_months enum: - last_14_days - last_3_months - last_6_months - last_12_months - last_2_years - last_3_years - all_time nullable: true date_range_custom: type: object description: '' example: null properties: start: type: string description: 'This field is required when filters.date_range_custom is present. Must be a valid date.' example: '2026-08-27T06:47:12' end: type: string description: 'This field is required when filters.date_range_custom is present. Must be a valid date. Must be a date after or equal to filters.date_range_custom.start.' example: '2052-09-19' nullable: true read_status: type: string description: '' example: all enum: - all - read - unread nullable: true search: type: string description: 'Must not be greater than 500 characters.' example: 'n' nullable: true sort_by: type: string description: '' example: post_date enum: - date - post_date - sentiment - platform - upvotes_count - comments_count - created_at nullable: true sort_direction: type: string description: '' example: asc enum: - asc - desc nullable: true required: - name - filters parameters: - in: path name: organisation_uuid description: 'The organization UUID.' example: 6ff8f7f6-1eb3-3525-be4a-3932c805afed required: true schema: type: string '/api/organisations/{organisation_uuid}/social/views/{view_uuid}': get: summary: 'Show Social Post View' operationId: showSocialPostView description: 'Get a specific saved filter view.' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: uuid: a1b2c3d4-e5f6-g7h8-i9j0 name: 'Critical Alerts' filters: platforms: - reddit sentiments: - negative created_at: '2026-01-26T10:00:00.000000Z' properties: data: type: object properties: uuid: type: string example: a1b2c3d4-e5f6-g7h8-i9j0 name: type: string example: 'Critical Alerts' filters: type: object properties: platforms: type: array example: - reddit items: type: string sentiments: type: array example: - negative items: type: string created_at: type: string example: '2026-01-26T10:00:00.000000Z' tags: - 'Social Posts' put: summary: 'Update Social Post View' operationId: updateSocialPostView description: 'Update a saved filter view.' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: uuid: a1b2c3d4-e5f6-g7h8-i9j0 name: 'Critical Alerts Updated' filters: platforms: - reddit sentiments: - negative updated_at: '2026-01-26T11:00:00.000000Z' properties: data: type: object properties: uuid: type: string example: a1b2c3d4-e5f6-g7h8-i9j0 name: type: string example: 'Critical Alerts Updated' filters: type: object properties: platforms: type: array example: - reddit items: type: string sentiments: type: array example: - negative items: type: string updated_at: type: string example: '2026-01-26T11:00:00.000000Z' tags: - 'Social Posts' requestBody: required: false content: application/json: schema: type: object properties: sort_order: type: integer description: 'Must be at least 0.' example: 27 name: type: string description: 'optional The name of the view.' example: architecto filters: type: object description: 'optional Filter configuration.' example: [] properties: brands: type: array description: 'Must be a valid UUID. The uuid of an existing record in the projects table. The uuid of an existing record in the projects table.' example: - 6b72fe4a-5b40-307c-bc24-f79acf9a1bb9 items: type: string platforms: type: array description: '' example: - x items: type: string enum: - reddit - github - stackoverflow - hackernews - youtube - linkedin - x types: type: array description: '' example: - post items: type: string enum: - post - comment sentiments: type: array description: '' example: - negative items: type: string enum: - positive - neutral - negative keywords: type: array description: 'Must not be greater than 255 characters.' example: - m items: type: string date_range: type: object description: '' example: null properties: type: type: string description: 'This field is required when filters.date_range is present.' example: custom enum: - preset - custom - all_time value: type: string description: 'This field is required when filters.date_range.type is preset.' example: all_time enum: - last_14_days - last_3_months - last_6_months - last_12_months - last_2_years - last_3_years - all_time nullable: true date_range_custom: type: object description: '' example: null properties: start: type: string description: 'This field is required when filters.date_range_custom is present. Must be a valid date.' example: '2026-08-27T06:47:12' end: type: string description: 'This field is required when filters.date_range_custom is present. Must be a valid date. Must be a date after or equal to filters.date_range_custom.start.' example: '2052-09-19' nullable: true read_status: type: string description: '' example: unread enum: - all - read - unread nullable: true search: type: string description: 'Must not be greater than 500 characters.' example: 'n' nullable: true sort_by: type: string description: '' example: post_date enum: - date - post_date - sentiment - platform - upvotes_count - comments_count - created_at nullable: true sort_direction: type: string description: '' example: desc enum: - asc - desc nullable: true delete: summary: 'Delete Social Post View' operationId: deleteSocialPostView description: 'Delete a saved filter view.' parameters: [] responses: 204: description: '' content: application/json: schema: type: object example: { } properties: { } tags: - 'Social Posts' parameters: - in: path name: organisation_uuid description: 'The organization UUID.' example: 6ff8f7f6-1eb3-3525-be4a-3932c805afed required: true schema: type: string - in: path name: view_uuid description: 'The view UUID.' example: 6ff8f7f6-1eb3-3525-be4a-3932c805afed required: true schema: type: string '/api/organisations/{organisation_uuid}/ai-visibility/competitors': post: summary: 'Competitor overview' operationId: competitorOverview description: "Returns all tracked brands (own + competitors) with visibility, share-of-voice, and\naverage-position metrics for the requested date window, plus change deltas vs the\npreceding window of equal length." parameters: [] responses: { } tags: - 'AI Visibility - Competitor' requestBody: required: false content: application/json: schema: type: object properties: date_from: type: string description: 'Optional start date (Y-m-d). Default: 30 days ago.' example: architecto date_to: type: string description: 'Optional end date (Y-m-d).' example: architecto provider: type: string description: 'Optional filter by provider (openai, perplexity, gemini, google_ai_overviews).' example: architecto page: type: integer description: 'Optional page number. Default: 1.' example: 16 per_page: type: integer description: 'Optional items per page. Default: 10.' example: 16 search: type: string description: 'Optional name search filter.' example: architecto parameters: - in: path name: organisation_uuid description: 'The organisation UUID.' example: 660e8400-e29b-41d4-a716-446655440001 required: true schema: type: string '/api/organisations/{organisation_uuid}/ai-visibility/competitor-brands': post: summary: 'Add competitor' operationId: addCompetitor description: '' parameters: [] responses: { } tags: - 'AI Visibility - Competitor' requestBody: required: true content: application/json: schema: type: object properties: name: type: string description: 'Competitor name.' example: 'Acme Corp' website: type: string description: 'Competitor domain.' example: acme.com required: - name - website parameters: - in: path name: organisation_uuid description: '' example: 6ff8f7f6-1eb3-3525-be4a-3932c805afed required: true schema: type: string '/api/organisations/{organisation_uuid}/ai-visibility/competitor-brands/{brand}': patch: summary: 'Update competitor' operationId: updateCompetitor description: '' parameters: [] responses: { } tags: - 'AI Visibility - Competitor' requestBody: required: true content: application/json: schema: type: object properties: name: type: string description: '' example: architecto website: type: string description: '' example: architecto required: - name - website delete: summary: 'Delete competitor' operationId: deleteCompetitor description: '' parameters: [] responses: { } tags: - 'AI Visibility - Competitor' parameters: - in: path name: organisation_uuid description: '' example: 6ff8f7f6-1eb3-3525-be4a-3932c805afed required: true schema: type: string - in: path name: brand description: 'The brand ID.' example: 16 required: true schema: type: integer '/api/organisations/{organisation_uuid}/ai-visibility/visibility': post: summary: 'Visibility Tab' operationId: visibilityTab description: '' parameters: [] responses: { } tags: - 'AI Visibility - Dashboard' requestBody: required: false content: application/json: schema: type: object properties: date_from: type: string description: 'Optional start of the current window (Y-m-d). Default: 30 days ago.' example: '2026-06-01' date_to: type: string description: 'Optional end of the current window (Y-m-d).' example: '2026-06-30' compare_from: type: string description: 'Optional start of the comparison window. Default: the preceding window of equal length.' example: architecto compare_to: type: string description: 'Optional end of the comparison window.' example: architecto provider: type: string description: 'Optional - filter by provider.' example: openai model: type: string description: 'Optional - filter by model.' example: architecto prompt_id: type: integer description: 'Optional - filter by prompt.' example: 16 country: type: string description: 'Optional - filter by country.' example: architecto language: type: string description: 'Optional - filter by language.' example: architecto source: type: string description: 'Optional - active (default), brand_radar, or all.' example: architecto parameters: - in: path name: organisation_uuid description: 'The organisation UUID.' example: 660e8400-e29b-41d4-a716-446655440001 required: true schema: type: string '/api/organisations/{organisation_uuid}/ai-visibility/platforms': post: summary: 'Platforms Tab' operationId: platformsTab description: '' parameters: [] responses: { } tags: - 'AI Visibility - Dashboard' requestBody: required: false content: application/json: schema: type: object properties: date_from: type: string description: 'Optional start of the current window (Y-m-d).' example: architecto date_to: type: string description: 'Optional end of the current window (Y-m-d).' example: architecto parameters: - in: path name: organisation_uuid description: 'The organisation UUID.' example: 660e8400-e29b-41d4-a716-446655440001 required: true schema: type: string '/api/organisations/{organisation_uuid}/ai-visibility/sentiment': post: summary: 'Sentiment Tab' operationId: sentimentTab description: '' parameters: [] responses: { } tags: - 'AI Visibility - Dashboard' requestBody: required: false content: application/json: schema: type: object properties: brand: type: integer description: "Optional - brand_id to scope to. Default: the organisation's own brand." example: 16 per_page: type: integer description: 'Optional - sentiment_sources page size. Default: 20.' example: 16 parameters: - in: path name: organisation_uuid description: 'The organisation UUID.' example: 660e8400-e29b-41d4-a716-446655440001 required: true schema: type: string '/api/organisations/{organisation_uuid}/ai-visibility/sentiment/sources/export': post: summary: 'Export Sentiment Sources' operationId: exportSentimentSources description: '' parameters: [] responses: { } tags: - 'AI Visibility - Dashboard' parameters: - in: path name: organisation_uuid description: 'The organisation UUID.' example: 660e8400-e29b-41d4-a716-446655440001 required: true schema: type: string '/api/organisations/{organisation_uuid}/ai-visibility/citations': post: summary: 'Citations Tab' operationId: citationsTab description: '' parameters: [] responses: { } tags: - 'AI Visibility - Dashboard' parameters: - in: path name: organisation_uuid description: 'The organisation UUID.' example: 660e8400-e29b-41d4-a716-446655440001 required: true schema: type: string '/api/organisations/{organisation_uuid}/ai-visibility/citations/domains': post: summary: 'Citations — Domains' operationId: citationsDomains description: '' parameters: [] responses: { } tags: - 'AI Visibility - Dashboard' requestBody: required: false content: application/json: schema: type: object properties: per_page: type: integer description: 'Optional - results per page. Default: 20.' example: 16 parameters: - in: path name: organisation_uuid description: 'The organisation UUID.' example: 660e8400-e29b-41d4-a716-446655440001 required: true schema: type: string '/api/organisations/{organisation_uuid}/ai-visibility/citations/urls': post: summary: 'Citations — URLs' operationId: citationsURLs description: '' parameters: [] responses: { } tags: - 'AI Visibility - Dashboard' requestBody: required: false content: application/json: schema: type: object properties: per_page: type: integer description: 'Optional - results per page. Default: 20.' example: 16 parameters: - in: path name: organisation_uuid description: 'The organisation UUID.' example: 660e8400-e29b-41d4-a716-446655440001 required: true schema: type: string '/api/organisations/{organisation_uuid}/ai-visibility/citations/urls/export': post: summary: 'Export Citation URLs' operationId: exportCitationURLs description: '' parameters: [] responses: { } tags: - 'AI Visibility - Dashboard' requestBody: required: false content: application/json: schema: type: object properties: domain: type: string description: 'Optional - filter to a specific domain.' example: architecto parameters: - in: path name: organisation_uuid description: 'The organisation UUID.' example: 660e8400-e29b-41d4-a716-446655440001 required: true schema: type: string '/api/organisations/{organisation_uuid}/ai-visibility/citations/domains/export': post: summary: 'Export Citation Domains' operationId: exportCitationDomains description: '' parameters: [] responses: { } tags: - 'AI Visibility - Dashboard' parameters: - in: path name: organisation_uuid description: 'The organisation UUID.' example: 660e8400-e29b-41d4-a716-446655440001 required: true schema: type: string '/api/organisations/{organisation_uuid}/ai-visibility/citations/domain-detail': post: summary: 'Citations — Domain Detail' operationId: citationsDomainDetail description: '' parameters: [] responses: { } tags: - 'AI Visibility - Dashboard' requestBody: required: true content: application/json: schema: type: object properties: domain: type: string description: 'The domain to retrieve detail for.' example: reddit.com required: - domain parameters: - in: path name: organisation_uuid description: 'The organisation UUID.' example: 6ff8f7f6-1eb3-3525-be4a-3932c805afed required: true schema: type: string '/api/organisations/{organisation_uuid}/ai-visibility/citations/domain-urls': post: summary: 'Citations — URLs for a Domain (paginated)' operationId: citationsURLsForADomainpaginated description: '' parameters: [] responses: { } tags: - 'AI Visibility - Dashboard' requestBody: required: true content: application/json: schema: type: object properties: domain: type: string description: 'The domain to list URLs for.' example: architecto required: - domain parameters: - in: path name: organisation_uuid description: 'The organisation UUID.' example: 6ff8f7f6-1eb3-3525-be4a-3932c805afed required: true schema: type: string '/api/organisations/{organisation_uuid}/ai-visibility/citations/url-detail': post: summary: 'Citations — URL Detail' operationId: citationsURLDetail description: '' parameters: [] responses: { } tags: - 'AI Visibility - Dashboard' requestBody: required: true content: application/json: schema: type: object properties: url: type: string description: 'The URL to retrieve detail for.' example: 'http://www.bailey.biz/quos-velit-et-fugiat-sunt-nihil-accusantium-harum.html' required: - url parameters: - in: path name: organisation_uuid description: 'The organisation UUID.' example: 6ff8f7f6-1eb3-3525-be4a-3932c805afed required: true schema: type: string '/api/organisations/{organisation_uuid}/ai-visibility/citations/url-responses': post: summary: 'Citations — AI Responses that cited a URL (paginated)' operationId: citationsAIResponsesThatCitedAURLpaginated description: '' parameters: [] responses: { } tags: - 'AI Visibility - Dashboard' requestBody: required: true content: application/json: schema: type: object properties: url: type: string description: 'The URL whose citing executions to list.' example: 'http://www.bailey.biz/quos-velit-et-fugiat-sunt-nihil-accusantium-harum.html' required: - url parameters: - in: path name: organisation_uuid description: 'The organisation UUID.' example: 6ff8f7f6-1eb3-3525-be4a-3932c805afed required: true schema: type: string '/api/organisations/{organisation_uuid}/ai-visibility/citations/url-responses/export': post: summary: 'Export AI Responses that cited a URL' operationId: exportAIResponsesThatCitedAURL description: '' parameters: [] responses: { } tags: - 'AI Visibility - Dashboard' requestBody: required: true content: application/json: schema: type: object properties: url: type: string description: 'The URL to export responses for.' example: 'http://www.bailey.biz/quos-velit-et-fugiat-sunt-nihil-accusantium-harum.html' required: - url parameters: - in: path name: organisation_uuid description: 'The organisation UUID.' example: 6ff8f7f6-1eb3-3525-be4a-3932c805afed required: true schema: type: string '/api/organisations/{organisation_uuid}/ai-visibility/prompts/{prompt}/overview': get: summary: 'Prompt Overview' operationId: promptOverview description: '' parameters: [] responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - 'AI Visibility - Prompt Detail' parameters: - in: path name: organisation_uuid description: 'The organisation UUID.' example: 660e8400-e29b-41d4-a716-446655440001 required: true schema: type: string - in: path name: prompt description: 'The prompt ID.' example: 1 required: true schema: type: integer '/api/organisations/{organisation_uuid}/ai-visibility/prompts/{prompt}/filters': get: summary: 'Prompt Filter Options' operationId: promptFilterOptions description: "Distinct providers, models, countries and languages that have actually run for this prompt.\nUsed to populate the filter dropdowns on the prompt detail page." parameters: [] responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - 'AI Visibility - Prompt Detail' parameters: - in: path name: organisation_uuid description: 'The organisation UUID.' example: 660e8400-e29b-41d4-a716-446655440001 required: true schema: type: string - in: path name: prompt description: 'The prompt ID.' example: 1 required: true schema: type: integer '/api/organisations/{organisation_uuid}/ai-visibility/prompts/{prompt}/citations': post: summary: 'Prompt Citations' operationId: promptCitations description: 'Same shape as `POST /ai-visibility/citations`, scoped to this prompt.' parameters: [] responses: { } tags: - 'AI Visibility - Prompt Detail' parameters: - in: path name: organisation_uuid description: 'The organisation UUID.' example: 660e8400-e29b-41d4-a716-446655440001 required: true schema: type: string - in: path name: prompt description: 'The prompt ID.' example: 1 required: true schema: type: integer '/api/organisations/{organisation_uuid}/ai-visibility/prompts/{prompt}/citations/domains': post: summary: 'Prompt Citation Domains (paginated)' operationId: promptCitationDomainspaginated description: 'Same shape as `POST /ai-visibility/citations/domains`, scoped to this prompt.' parameters: [] responses: { } tags: - 'AI Visibility - Prompt Detail' requestBody: required: false content: application/json: schema: type: object properties: per_page: type: integer description: 'Optional - results per page. Default: 20.' example: 16 page: type: integer description: 'Optional - page number. Default: 1.' example: 16 parameters: - in: path name: organisation_uuid description: 'The organisation UUID.' example: 660e8400-e29b-41d4-a716-446655440001 required: true schema: type: string - in: path name: prompt description: 'The prompt ID.' example: 1 required: true schema: type: integer '/api/organisations/{organisation_uuid}/ai-visibility/prompts/{prompt}/citations/urls': post: summary: 'Prompt Citation URLs (paginated)' operationId: promptCitationURLspaginated description: 'Same shape as `POST /ai-visibility/citations/urls`, scoped to this prompt.' parameters: [] responses: { } tags: - 'AI Visibility - Prompt Detail' requestBody: required: false content: application/json: schema: type: object properties: per_page: type: integer description: 'Optional - results per page. Default: 20.' example: 16 page: type: integer description: 'Optional - page number. Default: 1.' example: 16 parameters: - in: path name: organisation_uuid description: 'The organisation UUID.' example: 660e8400-e29b-41d4-a716-446655440001 required: true schema: type: string - in: path name: prompt description: 'The prompt ID.' example: 1 required: true schema: type: integer '/api/organisations/{organisation_uuid}/ai-visibility/prompts/{prompt}/citations/export': post: summary: 'Export Prompt Citation URLs' operationId: exportPromptCitationURLs description: '' parameters: [] responses: { } tags: - 'AI Visibility - Prompt Detail' parameters: - in: path name: organisation_uuid description: 'The organisation UUID.' example: 660e8400-e29b-41d4-a716-446655440001 required: true schema: type: string - in: path name: prompt description: 'The prompt ID.' example: 1 required: true schema: type: integer '/api/organisations/{organisation_uuid}/ai-visibility/prompts/{prompt}/responses': get: summary: '' operationId: getApiOrganisationsOrganisation_uuidAiVisibilityPromptsPromptResponses description: '' parameters: [] responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - 'AI Visibility - Prompt Detail' parameters: - in: path name: organisation_uuid description: '' example: 2958b10e-6a80-4903-868f-79b048b5e0c4 required: true schema: type: string - in: path name: prompt description: 'The prompt.' example: architecto required: true schema: type: string '/api/organisations/{organisation_uuid}/ai-visibility/prompts/{prompt}/responses/{execution}': get: summary: 'Prompt Response Detail' operationId: promptResponseDetail description: 'Full detail for a single execution — backs the "AI Response" modal on the prompt responses tab.' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: execution_id: 42 prompt_title: 'How does Notion help teams?' provider: openai model: gpt-4o date: '2026-06-25 09:00:00' detected_brands: - name: Notion website: notion.com sentiment: positive is_brand_mentioned: true response_content: 'Notion helps teams...' citations: - rank: 1 url: 'https://notion.so/blog' title: 'Notion Blog' domain: notion.so page_type: references properties: data: type: object properties: execution_id: type: integer example: 42 prompt_title: type: string example: 'How does Notion help teams?' provider: type: string example: openai model: type: string example: gpt-4o date: type: string example: '2026-06-25 09:00:00' detected_brands: type: array example: - name: Notion website: notion.com items: type: object properties: name: type: string example: Notion website: type: string example: notion.com sentiment: type: string example: positive is_brand_mentioned: type: boolean example: true response_content: type: string example: 'Notion helps teams...' citations: type: array example: - rank: 1 url: 'https://notion.so/blog' title: 'Notion Blog' domain: notion.so page_type: references items: type: object properties: rank: type: integer example: 1 url: type: string example: 'https://notion.so/blog' title: type: string example: 'Notion Blog' domain: type: string example: notion.so page_type: type: string example: references tags: - 'AI Visibility - Prompt Detail' parameters: - in: path name: organisation_uuid description: 'The organisation UUID.' example: 660e8400-e29b-41d4-a716-446655440001 required: true schema: type: string - in: path name: prompt description: 'The prompt ID.' example: 1 required: true schema: type: integer - in: path name: execution description: 'The execution ID.' example: 42 required: true schema: type: integer '/api/organisations/{organisation_uuid}/ai-visibility/prompts/{prompt}/radar-responses/{radarResponse}': get: summary: 'Full detail for a single radar (Google AI Overview) response.' operationId: fullDetailForASingleRadarGoogleAIOverviewResponse description: '' parameters: [] responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - 'AI Visibility - Prompt Detail' parameters: - in: path name: organisation_uuid description: '' example: 6ff8f7f6-1eb3-3525-be4a-3932c805afed required: true schema: type: string - in: path name: prompt description: '' example: 16 required: true schema: type: integer - in: path name: radarResponse description: 'The radar_response id.' example: 16 required: true schema: type: integer '/api/organisations/{organisation_uuid}/ai-visibility/prompts/{prompt}/responses/export': post: summary: 'Export Prompt Responses' operationId: exportPromptResponses description: '' parameters: [] responses: { } tags: - 'AI Visibility - Prompt Detail' parameters: - in: path name: organisation_uuid description: 'The organisation UUID.' example: 660e8400-e29b-41d4-a716-446655440001 required: true schema: type: string - in: path name: prompt description: 'The prompt ID.' example: 1 required: true schema: type: integer '/api/organisations/{organisation_uuid}/ai-visibility/executions/{execution}': get: summary: 'Execution Detail' operationId: executionDetail description: "Not scoped to a prompt and exempt from the dashboard cache — reads live, per Ticket 7's\nspec. Returns `{status: \"detail_expired\"}` once raw_response has been pruned by retention\n(Ticket 8), since the full chat detail this view exists to show is gone at that point." parameters: [] responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - 'AI Visibility - Prompt Detail' parameters: - in: path name: organisation_uuid description: 'The organisation UUID.' example: 660e8400-e29b-41d4-a716-446655440001 required: true schema: type: string - in: path name: execution description: 'The execution ID.' example: 501 required: true schema: type: integer '/api/organisations/{organisation_uuid}/ai-visibility/prompts': get: summary: 'List Prompts' operationId: listPrompts description: '' parameters: - in: query name: topic_id description: 'Optional - filter by topic.' example: 16 required: false schema: type: integer description: 'Optional - filter by topic.' example: 16 - in: query name: status description: 'Optional - filter by status (active, paused, archived).' example: active required: false schema: type: string description: 'Optional - filter by status (active, paused, archived).' example: active - in: query name: search description: 'Optional - keyword search on prompt text.' example: pricing required: false schema: type: string description: 'Optional - keyword search on prompt text.' example: pricing - in: query name: per_page description: 'Optional - results per page. Default: 20.' example: 20 required: false schema: type: integer description: 'Optional - results per page. Default: 20.' example: 20 responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - 'AI Visibility - Prompts' post: summary: 'Create Prompt' operationId: createPrompt description: '' parameters: [] responses: 201: description: '' content: application/json: schema: type: object example: data: id: 1 text: 'How does our product compare to competitors?' topic_id: 16 country: NG language: en frequency: weekly status: active providers: - openai - perplexity properties: data: type: object properties: id: type: integer example: 1 text: type: string example: 'How does our product compare to competitors?' topic_id: type: integer example: 16 country: type: string example: NG language: type: string example: en frequency: type: string example: weekly status: type: string example: active providers: type: array example: - openai - perplexity items: type: string 403: description: '' content: application/json: schema: type: object example: message: 'AI Visibility is not enabled for this organisation.' properties: message: type: string example: 'AI Visibility is not enabled for this organisation.' 422: description: '' content: application/json: schema: type: object example: message: 'Active prompt limit reached for this plan.' properties: message: type: string example: 'Active prompt limit reached for this plan.' tags: - 'AI Visibility - Prompts' requestBody: required: true content: application/json: schema: type: object properties: text: type: string description: 'Prompt text. Max 700 characters.' example: 'How does our product compare to competitors?' topic_id: type: integer description: 'Topic ID. Every prompt must belong to a topic.' example: 16 nullable: true country: type: string description: 'Optional 2-letter country code. Default: US.' example: NG language: type: string description: 'Optional language code, max 10 chars. Default: en.' example: en frequency: type: string description: 'One of daily, weekly, monthly.' example: weekly providers: type: array description: 'Provider names, e.g. openai, perplexity.' example: - openai - perplexity items: type: string skip_first_run: type: boolean description: '' example: false required: - text - topic_id - frequency - providers parameters: - in: path name: organisation_uuid description: 'The organisation UUID.' example: 660e8400-e29b-41d4-a716-446655440001 required: true schema: type: string '/api/organisations/{organisation_uuid}/ai-visibility/prompts/bulk-import': post: summary: 'Bulk Import Prompts' operationId: bulkImportPrompts description: "Accepts a flat list of prompt texts assigned to a single topic, split against the plan\nactive-prompt limit: prompts that fit are created as active and dispatched on the priority\nqueue; any overflow is created as paused. Case-insensitive exact duplicates are skipped." parameters: [] responses: { } tags: - 'AI Visibility - Prompts' requestBody: required: true content: application/json: schema: type: object properties: topic_id: type: integer description: 'Topic ID to assign all imported prompts to.' example: 16 nullable: true prompts: type: array description: 'Array of prompt text strings. Max 500 items, each max 700 characters.' example: - architecto items: type: string activate: type: boolean description: "Onboarding's CSV import uses activate=false to persist the overflow beyond the\nuser's chosen top-3 as paused, without running them or eating into the plan's\nactive-prompt limit. Defaults to true to preserve the existing dashboard behavior." example: true required: - topic_id - prompts parameters: - in: path name: organisation_uuid description: 'The organisation UUID.' example: 6ff8f7f6-1eb3-3525-be4a-3932c805afed required: true schema: type: string '/api/organisations/{organisation_uuid}/ai-visibility/prompts/execution-status': get: summary: 'Get Organisation-Level Execution Status' operationId: getOrganisationLevelExecutionStatus description: "Polling endpoint for the frontend to show a banner (\"results are being recalculated\")\nwhile any AI Visibility execution across the organisation is still in progress. Bounded by\nupdated_at (not scheduled_at -- recurring prompts get scheduled_at pinned to midnight, so\nthat column can't tell \"just went pending/running\" apart from \"queued 10 hours ago\") to a\nrecent window rather than all-time, since any legitimate in-flight execution resolves\n(completes, fails, or gets reaped by ai-visibility:reap-stale-executions) well within it.\nAlso counts radar (Google AI Overview) rows still in 'pending' status.\n\nin_progress_count is the number of distinct prompts still in flight, not the number of\nrows -- a single prompt fans out to one execution row per provider (openai, perplexity,\n...) plus its own radar row, so counting rows would overstate how many prompts are\nactually still running.\n\nAlways queried fresh (no caching layer, no-store response header) — every poll reflects\nthe current DB state exactly." parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: in_progress: true in_progress_count: 2 properties: data: type: object properties: in_progress: type: boolean example: true in_progress_count: type: integer example: 2 tags: - 'AI Visibility - Prompts' parameters: - in: path name: organisation_uuid description: 'The organisation UUID.' example: 660e8400-e29b-41d4-a716-446655440001 required: true schema: type: string '/api/organisations/{organisation_uuid}/ai-visibility/prompts/{prompt}': get: summary: 'Get Prompt' operationId: getPrompt description: '' parameters: [] responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. 404: description: '' content: application/json: schema: type: object example: message: 'Prompt not found.' properties: message: type: string example: 'Prompt not found.' tags: - 'AI Visibility - Prompts' patch: summary: 'Update Prompt' operationId: updatePrompt description: '' parameters: [] responses: 404: description: '' content: application/json: schema: type: object example: message: 'Prompt not found.' properties: message: type: string example: 'Prompt not found.' 422: description: '' content: application/json: schema: type: object example: message: 'This frequency is not allowed on the current plan.' properties: message: type: string example: 'This frequency is not allowed on the current plan.' tags: - 'AI Visibility - Prompts' requestBody: required: false content: application/json: schema: type: object properties: text: type: string description: 'Optional prompt text. Max 700 characters.' example: 'How does our product compare to competitors?' topic_id: type: integer description: 'Optional topic ID. If provided, cannot be null - prompts must always belong to a topic.' example: 16 country: type: string description: 'Optional 2-letter country code.' example: NG language: type: string description: 'Optional language code, max 10 chars.' example: en frequency: type: string description: 'Optional - one of daily, weekly, monthly.' example: weekly status: type: string description: 'Optional - one of active, paused, archived.' example: active providers: type: array description: 'Optional provider names, e.g. openai, perplexity.' example: - openai - perplexity items: type: string delete: summary: 'Delete Prompt' operationId: deletePrompt description: '' parameters: [] responses: 204: description: '' content: application/json: schema: type: object example: { } properties: { } 404: description: '' content: application/json: schema: type: object example: message: 'Prompt not found.' properties: message: type: string example: 'Prompt not found.' tags: - 'AI Visibility - Prompts' parameters: - in: path name: organisation_uuid description: 'The organisation UUID.' example: 660e8400-e29b-41d4-a716-446655440001 required: true schema: type: string - in: path name: prompt description: 'The prompt ID.' example: 1 required: true schema: type: integer '/api/organisations/{organisation_uuid}/ai-visibility/prompts/{prompt}/duplicate': post: summary: 'Duplicate Prompt' operationId: duplicatePrompt description: "Creates a paused copy of the prompt with the same topic, text, country, language,\nfrequency and providers." parameters: [] responses: 404: description: '' content: application/json: schema: type: object example: message: 'Prompt not found.' properties: message: type: string example: 'Prompt not found.' 422: description: '' content: application/json: schema: type: object example: message: 'Active prompt limit reached for this plan.' properties: message: type: string example: 'Active prompt limit reached for this plan.' tags: - 'AI Visibility - Prompts' parameters: - in: path name: organisation_uuid description: 'The organisation UUID.' example: 660e8400-e29b-41d4-a716-446655440001 required: true schema: type: string - in: path name: prompt description: 'The prompt ID to duplicate.' example: 1 required: true schema: type: integer '/api/organisations/{organisation_uuid}/ai-visibility/prompts/{prompt}/run': post: summary: 'Run Prompt Now' operationId: runPromptNow description: "Manually dispatches a fresh execution for this prompt across all of its allowed providers,\nindependent of the scheduler." parameters: [] responses: 202: description: '' content: application/json: schema: type: object example: data: prompt_id: 1 execution_ids: - 501 - 502 properties: data: type: object properties: prompt_id: type: integer example: 1 execution_ids: type: array example: - 501 - 502 items: type: integer 404: description: '' content: application/json: schema: type: object example: message: 'Prompt not found.' properties: message: type: string example: 'Prompt not found.' 422: description: '' content: application/json: schema: type: object example: message: 'No allowed and available provider could run this prompt.' properties: message: type: string example: 'No allowed and available provider could run this prompt.' tags: - 'AI Visibility - Prompts' parameters: - in: path name: organisation_uuid description: 'The organisation UUID.' example: 660e8400-e29b-41d4-a716-446655440001 required: true schema: type: string - in: path name: prompt description: 'The prompt ID.' example: 1 required: true schema: type: integer '/api/organisations/{organisation_uuid}/ai-visibility/prompts/{prompt}/executions': get: summary: 'List Prompt Executions' operationId: listPromptExecutions description: '"All Responses" list — one row per execution ("chat") run for this prompt, newest first.' parameters: - in: query name: provider description: 'Optional - filter by provider.' example: openai required: false schema: type: string description: 'Optional - filter by provider.' example: openai - in: query name: status description: 'Optional - filter by execution status.' example: completed required: false schema: type: string description: 'Optional - filter by execution status.' example: completed - in: query name: per_page description: 'Optional - results per page. Default: 20.' example: 20 required: false schema: type: integer description: 'Optional - results per page. Default: 20.' example: 20 responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. 404: description: '' content: application/json: schema: type: object example: message: 'Prompt not found.' properties: message: type: string example: 'Prompt not found.' tags: - 'AI Visibility - Prompts' parameters: - in: path name: organisation_uuid description: 'The organisation UUID.' example: 660e8400-e29b-41d4-a716-446655440001 required: true schema: type: string - in: path name: prompt description: 'The prompt ID.' example: 1 required: true schema: type: integer '/api/organisations/{organisation_uuid}/ai-visibility/prompts/{prompt}/executions/{execution}': get: summary: 'Get Prompt Execution Detail' operationId: getPromptExecutionDetail description: "\"Edit Responses\" chat detail — full generated answer, citations, and per-brand mentions\nfor a single execution." parameters: [] responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. 404: description: '' content: application/json: schema: type: object example: message: 'Execution not found.' properties: message: type: string example: 'Execution not found.' tags: - 'AI Visibility - Prompts' parameters: - in: path name: organisation_uuid description: 'The organisation UUID.' example: 660e8400-e29b-41d4-a716-446655440001 required: true schema: type: string - in: path name: prompt description: 'The prompt ID.' example: 1 required: true schema: type: integer - in: path name: execution description: 'The execution ID.' example: 501 required: true schema: type: integer '/api/organisations/{organisation_uuid}/ai-visibility/prompts/{prompt}/execution-status': get: summary: 'Get Prompt Execution Status' operationId: getPromptExecutionStatus description: "Polling endpoint for the frontend to show a loading skeleton while a prompt's latest run\nis still in progress. \"In progress\" covers both an execution that hasn't finished calling\nits provider yet, and one that has (status=completed) but whose citations/brand-mentions\nhaven't been written yet by the normalization job that runs right after — status alone\nflips to completed before that data exists, so this checks processing_version too rather\nthan reporting \"ready\" before there's anything real to show. Also folds in the org's latest\nradar (Google AI Overview) pull for this prompt, if radar is enabled on its plan — radar\nrows live in a separate table (ai_visibility_radar_responses) with their own\npending/normalised/failed lifecycle, but ai_visibility_radar_responses.prompt_id is a real\nFK to this same prompt, so it belongs in this prompt's status too.\n\nAlways queried fresh (no caching layer, no-store response header) — every poll reflects\nthe current DB state exactly." parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: status: in_progress scheduled_at: '2026-08-14 12:23:00' providers: - provider: openai status: in_progress - provider: perplexity status: ready - provider: google_ai_overviews status: ready properties: data: type: object properties: status: type: string example: in_progress scheduled_at: type: string example: '2026-08-14 12:23:00' providers: type: array example: - provider: openai status: in_progress - provider: perplexity status: ready - provider: google_ai_overviews status: ready items: type: object properties: provider: type: string example: openai status: type: string example: in_progress 404: description: '' content: application/json: schema: type: object example: message: 'Prompt not found.' properties: message: type: string example: 'Prompt not found.' tags: - 'AI Visibility - Prompts' parameters: - in: path name: organisation_uuid description: 'The organisation UUID.' example: 660e8400-e29b-41d4-a716-446655440001 required: true schema: type: string - in: path name: prompt description: 'The prompt ID.' example: 1 required: true schema: type: integer '/api/organisations/{organisation_uuid}/ai-visibility/prompts/suggested': get: summary: 'List prompt suggestions' operationId: listPromptSuggestions description: "Returns up to 15 prompt suggestions with status `completed` for the organisation,\nordered by source (radar first), then volume descending, then most recent." parameters: - in: query name: exclude_categories description: 'Optional categories to exclude.' example: - branded required: false schema: type: array description: 'Optional categories to exclude.' example: - branded items: type: string responses: 200: description: '' content: application/json: schema: type: object example: data: - id: 1 text: 'What is the best tool for notes and docs?' category: awareness source: radar volume: 12000 status: completed generated_at: '2026-06-01 00:00:00' properties: data: type: array example: - id: 1 text: 'What is the best tool for notes and docs?' category: awareness source: radar volume: 12000 status: completed generated_at: '2026-06-01 00:00:00' items: type: object properties: id: type: integer example: 1 text: type: string example: 'What is the best tool for notes and docs?' category: type: string example: awareness source: type: string example: radar volume: type: integer example: 12000 status: type: string example: completed generated_at: type: string example: '2026-06-01 00:00:00' tags: - 'AI Visibility - Suggestions' parameters: - in: path name: organisation_uuid description: 'The organisation UUID.' example: 660e8400-e29b-41d4-a716-446655440001 required: true schema: type: string '/api/organisations/{organisation_uuid}/ai-visibility/prompts/suggested/refetch': post: summary: 'Refetch suggestions' operationId: refetchSuggestions description: "A separate, standalone generation path from GenerateAiVisibilitySuggestionsJob (the\nonboarding job) — deliberately not shared code, so this can never change that job's\nbehavior. Two differences from onboarding generation: (1) previously dismissed suggestions\nare deleted first so a fresh batch can take their place — an upsert alone would otherwise\nleave a dismissed row's regenerated duplicate stuck as dismissed forever, since the upsert\nnever touches `status`; `completed`/`accepted` suggestions are left untouched. (2) an LLM\nfailure here is a real error, not silently swallowed into the generic fallback set — a user\nwho clicks \"refetch\" wants real, freshly-generated suggestions, and substituting the generic\nfallback would look like a genuine refresh when it isn't one.\n\nRate-limited to once per 10 minutes per org: a successful call holds its lock for the full\ncooldown (not released in a finally) so a second click can't trigger another LLM call too\nsoon; a failed call releases immediately since it never produced anything worth protecting.\nA request that lands inside an active cooldown isn't rejected — it gets back the same\nexisting (still-valid) suggestions with `cached: true`, rather than a bare error." parameters: [] responses: 200: description: '' content: application/json: schema: oneOf: - description: '' type: object example: data: topics: - id: 1 name: 'Feature Discovery' description: ... source: llm status: completed generated_at: '2026-06-01 00:00:00' prompts: - id: 1 text: 'What is the best tool for notes and docs?' category: awareness source: llm status: completed generated_at: '2026-06-01 00:00:00' properties: data: type: object properties: topics: type: array example: - id: 1 name: 'Feature Discovery' description: ... source: llm status: completed generated_at: '2026-06-01 00:00:00' items: type: object properties: id: type: integer example: 1 name: type: string example: 'Feature Discovery' description: type: string example: ... source: type: string example: llm status: type: string example: completed generated_at: type: string example: '2026-06-01 00:00:00' prompts: type: array example: - id: 1 text: 'What is the best tool for notes and docs?' category: awareness source: llm status: completed generated_at: '2026-06-01 00:00:00' items: type: object properties: id: type: integer example: 1 text: type: string example: 'What is the best tool for notes and docs?' category: type: string example: awareness source: type: string example: llm status: type: string example: completed generated_at: type: string example: '2026-06-01 00:00:00' - description: '' type: object example: data: topics: [] prompts: [] cached: true properties: data: type: object properties: topics: type: array example: [] prompts: type: array example: [] cached: type: boolean example: true 422: description: '' content: application/json: schema: type: object example: message: 'No own brand found for this organisation.' properties: message: type: string example: 'No own brand found for this organisation.' 502: description: '' content: application/json: schema: type: object example: message: 'Failed to generate suggestions. Please try again.' properties: message: type: string example: 'Failed to generate suggestions. Please try again.' tags: - 'AI Visibility - Suggestions' parameters: - in: path name: organisation_uuid description: 'The organisation UUID.' example: 660e8400-e29b-41d4-a716-446655440001 required: true schema: type: string '/api/organisations/{organisation_uuid}/ai-visibility/prompts/suggested/{suggestion}/accept': post: summary: 'Accept a prompt suggestion' operationId: acceptAPromptSuggestion description: "Creates a live prompt from the suggestion, dispatches an immediate first run, and marks the\nsuggestion as `accepted`. Returns the new prompt's ID and settings." parameters: [] responses: 201: description: '' content: application/json: schema: type: object example: data: prompt_id: 42 text: 'What is the best tool for notes and docs?' topic_id: 3 frequency: daily providers: - openai country: US language: en properties: data: type: object properties: prompt_id: type: integer example: 42 text: type: string example: 'What is the best tool for notes and docs?' topic_id: type: integer example: 3 frequency: type: string example: daily providers: type: array example: - openai items: type: string country: type: string example: US language: type: string example: en 422: description: '' content: application/json: schema: type: object example: message: 'Active prompt limit reached for this plan.' properties: message: type: string example: 'Active prompt limit reached for this plan.' tags: - 'AI Visibility - Suggestions' requestBody: required: true content: application/json: schema: type: object properties: frequency: type: string description: 'Execution frequency.' example: daily providers: type: array description: 'AI providers to enable.' example: - openai items: type: string topic_id: type: integer description: 'Optional topic to attach the prompt to.' example: 3 nullable: true country: type: string description: 'Optional 2-letter country code.' example: US language: type: string description: 'Optional language code.' example: en skip_first_run: type: boolean description: '' example: false required: - frequency - providers parameters: - in: path name: organisation_uuid description: 'The organisation UUID.' example: 660e8400-e29b-41d4-a716-446655440001 required: true schema: type: string - in: path name: suggestion description: 'The suggestion ID.' example: 1 required: true schema: type: integer '/api/organisations/{organisation_uuid}/ai-visibility/prompts/suggested/{suggestion}': delete: summary: 'Dismiss a prompt suggestion' operationId: dismissAPromptSuggestion description: 'Marks the suggestion as `dismissed`. It will no longer appear in the suggestions list.' parameters: [] responses: 204: description: '' content: application/json: schema: type: object example: { } properties: { } tags: - 'AI Visibility - Suggestions' parameters: - in: path name: organisation_uuid description: 'The organisation UUID.' example: 660e8400-e29b-41d4-a716-446655440001 required: true schema: type: string - in: path name: suggestion description: 'The suggestion ID.' example: 1 required: true schema: type: integer '/api/organisations/{organisation_uuid}/ai-visibility/onboarding/suggestions': get: summary: 'Onboarding suggestions' operationId: onboardingSuggestions description: "Returns both topic and prompt suggestions for the onboarding wizard in a single call.\nDispatch `GenerateAiVisibilitySuggestionsJob` for the org before calling this endpoint to\nensure suggestions are populated." parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: topics: - id: 1 name: 'Feature Discovery' description: ... source: llm status: completed generated_at: '2026-06-01 00:00:00' prompts: - id: 1 text: 'What is the best tool for notes?' category: awareness source: radar volume: 12000 status: completed generated_at: '2026-06-01 00:00:00' properties: data: type: object properties: topics: type: array example: - id: 1 name: 'Feature Discovery' description: ... source: llm status: completed generated_at: '2026-06-01 00:00:00' items: type: object properties: id: type: integer example: 1 name: type: string example: 'Feature Discovery' description: type: string example: ... source: type: string example: llm status: type: string example: completed generated_at: type: string example: '2026-06-01 00:00:00' prompts: type: array example: - id: 1 text: 'What is the best tool for notes?' category: awareness source: radar volume: 12000 status: completed generated_at: '2026-06-01 00:00:00' items: type: object properties: id: type: integer example: 1 text: type: string example: 'What is the best tool for notes?' category: type: string example: awareness source: type: string example: radar volume: type: integer example: 12000 status: type: string example: completed generated_at: type: string example: '2026-06-01 00:00:00' tags: - 'AI Visibility - Suggestions' parameters: - in: path name: organisation_uuid description: 'The organisation UUID.' example: 660e8400-e29b-41d4-a716-446655440001 required: true schema: type: string '/api/organisations/{organisation_uuid}/ai-visibility/onboarding/kickoff': post: summary: 'Onboarding kickoff' operationId: onboardingKickoff description: "Dispatches first-run executions on the priority queue for all active prompts the org\naccepted during onboarding (those created with skip_first_run=true). Call this from the\nBrandAnalysisProgress loading screen, after the user has committed to onboarding (step 4),\nso we don't waste compute on users who abandon after the suggestion step." parameters: [] responses: 202: description: '' content: application/json: schema: type: object example: data: dispatched: 5 properties: data: type: object properties: dispatched: type: integer example: 5 tags: - 'AI Visibility - Suggestions' parameters: - in: path name: organisation_uuid description: 'The organisation UUID.' example: 6ff8f7f6-1eb3-3525-be4a-3932c805afed required: true schema: type: string '/api/organisations/{organisation_uuid}/ai-visibility/onboarding/status': get: summary: 'Onboarding analysis status' operationId: onboardingAnalysisStatus description: "Returns how many of the org's active prompt executions have completed and a live\nvisibility percentage computed directly from execution_brands rows (no daily_metrics\naggregation needed — available as soon as the first execution is normalised).\n\nPoll this from BrandAnalysisProgress until `ready` is true." parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: ready: true executions_total: 10 executions_completed: 8 visibility_pct: 45.0 properties: data: type: object properties: ready: type: boolean example: true executions_total: type: integer example: 10 executions_completed: type: integer example: 8 visibility_pct: type: number example: 45.0 tags: - 'AI Visibility - Suggestions' parameters: - in: path name: organisation_uuid description: 'The organisation UUID.' example: 6ff8f7f6-1eb3-3525-be4a-3932c805afed required: true schema: type: string '/api/organisations/{organisation_uuid}/ai-visibility/onboarding/improvement': post: summary: 'Onboarding improvement estimate' operationId: onboardingImprovementEstimate description: "One-shot, synchronous LLM call — no queue, no polling. Call this once from the onboarding\nscreen right when onboardingStatus() first reports has_own_brand_data (i.e. the real,\naggregated visibility_pct is already showing), so the estimate stays grounded in the exact\nsame number the dashboard will show after login. Cached for 30 days, so a repeat call\n(re-entering onboarding, a second tab) returns instantly without re-running the LLM. Returns\npotential_visibility_pct: null if there isn't enough own-brand data yet, or if the estimate\ngenuinely can't be computed (LLM call failed, no headroom found)." parameters: [] responses: 200: description: '' content: application/json: schema: oneOf: - description: '' type: object example: data: potential_visibility_pct: 45.0 properties: data: type: object properties: potential_visibility_pct: type: number example: 45.0 - description: '' type: object example: data: potential_visibility_pct: null properties: data: type: object properties: potential_visibility_pct: type: string example: null tags: - 'AI Visibility - Suggestions' parameters: - in: path name: organisation_uuid description: 'The organisation UUID.' example: 6ff8f7f6-1eb3-3525-be4a-3932c805afed required: true schema: type: string '/api/organisations/{organisation_uuid}/ai-visibility/competitors/suggested': get: summary: 'List suggested competitors' operationId: listSuggestedCompetitors description: "Returns up to 10 pending competitor suggestions for the organisation, most recent first.\nPopulated by SuggestAiVisibilityCompetitorsJob (dispatched at onboarding and daily\nthereafter via ai-visibility:suggest-competitors)." parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: - id: 1 name: 'Acme Corp' website: acme.com reason: 'Direct competitor in the same market.' generated_at: '2026-07-09 00:00:00' properties: data: type: array example: - id: 1 name: 'Acme Corp' website: acme.com reason: 'Direct competitor in the same market.' generated_at: '2026-07-09 00:00:00' items: type: object properties: id: type: integer example: 1 name: type: string example: 'Acme Corp' website: type: string example: acme.com reason: type: string example: 'Direct competitor in the same market.' generated_at: type: string example: '2026-07-09 00:00:00' tags: - 'AI Visibility - Suggestions' parameters: - in: path name: organisation_uuid description: 'The organisation UUID.' example: 660e8400-e29b-41d4-a716-446655440001 required: true schema: type: string '/api/organisations/{organisation_uuid}/ai-visibility/competitors/suggested/{suggestion}/accept': post: summary: 'Accept a suggested competitor' operationId: acceptASuggestedCompetitor description: "Creates a real, tracked competitor brand from the suggestion (source=manual, same as one\nadded directly via the Competitor tab), backfills its historical mention data across every\ncompleted execution so far, and marks the suggestion accepted." parameters: [] responses: 201: description: '' content: application/json: schema: type: object example: data: brand_id: 42 name: 'Acme Corp' website: acme.com properties: data: type: object properties: brand_id: type: integer example: 42 name: type: string example: 'Acme Corp' website: type: string example: acme.com 422: description: '' content: application/json: schema: type: object example: message: 'This suggestion has already been accepted.' properties: message: type: string example: 'This suggestion has already been accepted.' tags: - 'AI Visibility - Suggestions' parameters: - in: path name: organisation_uuid description: 'The organisation UUID.' example: 660e8400-e29b-41d4-a716-446655440001 required: true schema: type: string - in: path name: suggestion description: 'The competitor suggestion ID.' example: 1 required: true schema: type: integer '/api/organisations/{organisation_uuid}/ai-visibility/competitors/suggested/{suggestion}': delete: summary: 'Dismiss a suggested competitor' operationId: dismissASuggestedCompetitor description: "Marks the competitor suggestion as `rejected`. It will no longer appear in the suggestions\nlist, and the name will never be suggested again for this organisation." parameters: [] responses: 204: description: '' content: application/json: schema: type: object example: { } properties: { } tags: - 'AI Visibility - Suggestions' parameters: - in: path name: organisation_uuid description: 'The organisation UUID.' example: 660e8400-e29b-41d4-a716-446655440001 required: true schema: type: string - in: path name: suggestion description: 'The competitor suggestion ID.' example: 1 required: true schema: type: integer '/api/organisations/{organisation_uuid}/ai-visibility/topics/suggested': get: summary: 'List topic suggestions' operationId: listTopicSuggestions description: "Returns up to 10 topic suggestions with status `completed` for the organisation,\nordered by most recent first." parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: - id: 1 name: 'Feature Discovery' description: 'Questions about discovering features in productivity tools' source: llm status: completed generated_at: '2026-06-01 00:00:00' properties: data: type: array example: - id: 1 name: 'Feature Discovery' description: 'Questions about discovering features in productivity tools' source: llm status: completed generated_at: '2026-06-01 00:00:00' items: type: object properties: id: type: integer example: 1 name: type: string example: 'Feature Discovery' description: type: string example: 'Questions about discovering features in productivity tools' source: type: string example: llm status: type: string example: completed generated_at: type: string example: '2026-06-01 00:00:00' tags: - 'AI Visibility - Suggestions' parameters: - in: path name: organisation_uuid description: 'The organisation UUID.' example: 660e8400-e29b-41d4-a716-446655440001 required: true schema: type: string '/api/organisations/{organisation_uuid}/ai-visibility/topics/suggested/{suggestion}/accept': post: summary: 'Accept a topic suggestion' operationId: acceptATopicSuggestion description: "Creates a real topic from the suggestion and marks it as `accepted`.\nReturns the new topic's ID and details." parameters: [] responses: 201: description: '' content: application/json: schema: type: object example: data: topic_id: 5 name: 'Feature Discovery' description: 'Questions about discovering features in productivity tools' properties: data: type: object properties: topic_id: type: integer example: 5 name: type: string example: 'Feature Discovery' description: type: string example: 'Questions about discovering features in productivity tools' 422: description: '' content: application/json: schema: type: object example: message: 'This suggestion has already been accepted.' properties: message: type: string example: 'This suggestion has already been accepted.' tags: - 'AI Visibility - Suggestions' parameters: - in: path name: organisation_uuid description: 'The organisation UUID.' example: 660e8400-e29b-41d4-a716-446655440001 required: true schema: type: string - in: path name: suggestion description: 'The topic suggestion ID.' example: 1 required: true schema: type: integer '/api/organisations/{organisation_uuid}/ai-visibility/topics/suggested/{suggestion}': delete: summary: 'Dismiss a topic suggestion' operationId: dismissATopicSuggestion description: 'Marks the topic suggestion as `dismissed`. It will no longer appear in the suggestions list.' parameters: [] responses: 204: description: '' content: application/json: schema: type: object example: { } properties: { } tags: - 'AI Visibility - Suggestions' parameters: - in: path name: organisation_uuid description: 'The organisation UUID.' example: 660e8400-e29b-41d4-a716-446655440001 required: true schema: type: string - in: path name: suggestion description: 'The topic suggestion ID.' example: 1 required: true schema: type: integer '/api/organisations/{organisation_uuid}/ai-visibility/topics': get: summary: 'List Topics' operationId: listTopics description: '' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: - id: 1 name: 'Competitor comparisons' description: 'Prompts comparing us against competitors' created_at: '2026-06-05T10:00:00.000000Z' updated_at: '2026-06-05T10:00:00.000000Z' active_count: 3 inactive_count: 1 suggested_count: 0 properties: data: type: array example: - id: 1 name: 'Competitor comparisons' description: 'Prompts comparing us against competitors' created_at: '2026-06-05T10:00:00.000000Z' updated_at: '2026-06-05T10:00:00.000000Z' active_count: 3 inactive_count: 1 suggested_count: 0 items: type: object properties: id: type: integer example: 1 name: type: string example: 'Competitor comparisons' description: type: string example: 'Prompts comparing us against competitors' created_at: type: string example: '2026-06-05T10:00:00.000000Z' updated_at: type: string example: '2026-06-05T10:00:00.000000Z' active_count: type: integer example: 3 inactive_count: type: integer example: 1 suggested_count: type: integer example: 0 tags: - 'AI Visibility - Topics' post: summary: 'Create Topic' operationId: createTopic description: '' parameters: [] responses: 201: description: '' content: application/json: schema: type: object example: data: id: 1 name: 'Competitor comparisons' description: 'Prompts comparing us against competitors' created_at: '2026-06-05T10:00:00.000000Z' updated_at: '2026-06-05T10:00:00.000000Z' properties: data: type: object properties: id: type: integer example: 1 name: type: string example: 'Competitor comparisons' description: type: string example: 'Prompts comparing us against competitors' created_at: type: string example: '2026-06-05T10:00:00.000000Z' updated_at: type: string example: '2026-06-05T10:00:00.000000Z' tags: - 'AI Visibility - Topics' requestBody: required: true content: application/json: schema: type: object properties: name: type: string description: 'Topic name.' example: 'Competitor comparisons' description: type: string description: 'Optional description.' example: 'Prompts comparing us against competitors' nullable: true required: - name parameters: - in: path name: organisation_uuid description: 'The organisation UUID.' example: 660e8400-e29b-41d4-a716-446655440001 required: true schema: type: string '/api/organisations/{organisation_uuid}/ai-visibility/topics/{topic}': patch: summary: 'Update Topic' operationId: updateTopic description: '' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: id: 1 name: 'Competitor comparisons' description: 'Prompts comparing us against competitors' created_at: '2026-06-05T10:00:00.000000Z' updated_at: '2026-06-05T10:00:00.000000Z' properties: data: type: object properties: id: type: integer example: 1 name: type: string example: 'Competitor comparisons' description: type: string example: 'Prompts comparing us against competitors' created_at: type: string example: '2026-06-05T10:00:00.000000Z' updated_at: type: string example: '2026-06-05T10:00:00.000000Z' 404: description: '' content: application/json: schema: type: object example: message: 'Topic not found.' properties: message: type: string example: 'Topic not found.' tags: - 'AI Visibility - Topics' requestBody: required: false content: application/json: schema: type: object properties: name: type: string description: 'Optional topic name.' example: 'Competitor comparisons' description: type: string description: 'Optional description.' example: 'Prompts comparing us against competitors' nullable: true delete: summary: 'Delete Topic' operationId: deleteTopic description: '' parameters: [] responses: 204: description: '' content: application/json: schema: type: object example: { } properties: { } 404: description: '' content: application/json: schema: type: object example: message: 'Topic not found.' properties: message: type: string example: 'Topic not found.' 422: description: '' content: application/json: schema: type: object example: message: 'Cannot delete a topic with active prompts assigned to it.' properties: message: type: string example: 'Cannot delete a topic with active prompts assigned to it.' tags: - 'AI Visibility - Topics' parameters: - in: path name: organisation_uuid description: 'The organisation UUID.' example: 660e8400-e29b-41d4-a716-446655440001 required: true schema: type: string - in: path name: topic description: 'The topic ID.' example: 1 required: true schema: type: integer /api/api-key: get: summary: 'Show current production API key metadata for the selected organisation.' operationId: showCurrentProductionAPIKeyMetadataForTheSelectedOrganisation description: "Returns the current masked production API key for the organisation selected by\nthe X-Organisation-Id header. Plaintext API keys are never returned from this endpoint." parameters: - in: header name: X-Organisation-Id description: '' example: 'string required The organisation UUID.' schema: type: string responses: 200: description: '' content: application/json: schema: oneOf: - description: '' type: object example: data: uuid: 550e8400-e29b-41d4-a716-446655440000 environment: live status: active key: null masked_key: 'sk_live_****************************Ab12' last_four: Ab12 plain_text_available: false idempotent_replay: false rotation_expires_at: null expires_at: null revoked_at: null created_at: '2026-06-05T10:00:00.000000Z' updated_at: '2026-06-05T10:00:00.000000Z' properties: data: type: object properties: uuid: type: string example: 550e8400-e29b-41d4-a716-446655440000 environment: type: string example: live status: type: string example: active key: type: string example: null masked_key: type: string example: 'sk_live_****************************Ab12' last_four: type: string example: Ab12 plain_text_available: type: boolean example: false idempotent_replay: type: boolean example: false rotation_expires_at: type: string example: null expires_at: type: string example: null revoked_at: type: string example: null created_at: type: string example: '2026-06-05T10:00:00.000000Z' updated_at: type: string example: '2026-06-05T10:00:00.000000Z' - description: 'No API key exists' type: object example: data: null properties: data: type: string example: null tags: - 'API Keys' /api/api-key/generate: post: summary: 'Generate a production API key. Plaintext is returned only on first creation.' operationId: generateAProductionAPIKeyPlaintextIsReturnedOnlyOnFirstCreation description: "Creates one production API key for the current organisation. The plaintext key\nis shown only in the first successful response and is never stored." parameters: - in: header name: X-Organisation-Id description: '' example: 'string required The organisation UUID.' schema: type: string - in: header name: Idempotency-Key description: '' example: 'string optional Safe retry key for this generation request.' schema: type: string responses: 200: description: 'Idempotent replay' content: application/json: schema: type: object example: message: 'API key generation request replayed.' data: uuid: 550e8400-e29b-41d4-a716-446655440000 environment: live status: active key: null masked_key: 'sk_live_****************************Ab12' last_four: Ab12 plain_text_available: false idempotent_replay: true properties: message: type: string example: 'API key generation request replayed.' data: type: object properties: uuid: type: string example: 550e8400-e29b-41d4-a716-446655440000 environment: type: string example: live status: type: string example: active key: type: string example: null masked_key: type: string example: 'sk_live_****************************Ab12' last_four: type: string example: Ab12 plain_text_available: type: boolean example: false idempotent_replay: type: boolean example: true 201: description: '' content: application/json: schema: type: object example: message: 'API key generated successfully.' data: uuid: 550e8400-e29b-41d4-a716-446655440000 environment: live status: active key: sk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx masked_key: 'sk_live_****************************Ab12' last_four: Ab12 plain_text_available: true idempotent_replay: false rotation_expires_at: null expires_at: null revoked_at: null created_at: '2026-06-05T10:00:00.000000Z' updated_at: '2026-06-05T10:00:00.000000Z' properties: message: type: string example: 'API key generated successfully.' data: type: object properties: uuid: type: string example: 550e8400-e29b-41d4-a716-446655440000 environment: type: string example: live status: type: string example: active key: type: string example: sk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx masked_key: type: string example: 'sk_live_****************************Ab12' last_four: type: string example: Ab12 plain_text_available: type: boolean example: true idempotent_replay: type: boolean example: false rotation_expires_at: type: string example: null expires_at: type: string example: null revoked_at: type: string example: null created_at: type: string example: '2026-06-05T10:00:00.000000Z' updated_at: type: string example: '2026-06-05T10:00:00.000000Z' 409: description: 'Active key already exists' content: application/json: schema: type: object example: message: 'An active production API key already exists for this account.' error: code: active_api_key_exists properties: message: type: string example: 'An active production API key already exists for this account.' error: type: object properties: code: type: string example: active_api_key_exists tags: - 'API Keys' requestBody: required: true content: application/json: schema: type: object properties: confirmed: type: boolean description: 'Must be true before generating the key.' example: true required: - confirmed /api/api-key/rotate: post: summary: 'Rotate the current production API key. Plaintext is returned only on first rotation.' operationId: rotateTheCurrentProductionAPIKeyPlaintextIsReturnedOnlyOnFirstRotation description: "Creates a new active production key immediately and keeps the previous key valid\nfor 24 hours before it is auto-revoked." parameters: - in: header name: X-Organisation-Id description: '' example: 'string required The organisation UUID.' schema: type: string - in: header name: Idempotency-Key description: '' example: 'string optional Safe retry key for this rotation request.' schema: type: string responses: 201: description: '' content: application/json: schema: type: object example: message: 'API key rotated successfully.' data: current_key: uuid: 660e8400-e29b-41d4-a716-446655440000 environment: live status: active key: sk_live_yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy masked_key: 'sk_live_****************************Cd34' last_four: Cd34 plain_text_available: true idempotent_replay: false previous_key: uuid: 550e8400-e29b-41d4-a716-446655440000 environment: live status: rotating key: null masked_key: 'sk_live_****************************Ab12' last_four: Ab12 plain_text_available: false idempotent_replay: false rotation_expires_at: '2026-06-06T10:00:00.000000Z' properties: message: type: string example: 'API key rotated successfully.' data: type: object properties: current_key: type: object properties: uuid: type: string example: 660e8400-e29b-41d4-a716-446655440000 environment: type: string example: live status: type: string example: active key: type: string example: sk_live_yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy masked_key: type: string example: 'sk_live_****************************Cd34' last_four: type: string example: Cd34 plain_text_available: type: boolean example: true idempotent_replay: type: boolean example: false previous_key: type: object properties: uuid: type: string example: 550e8400-e29b-41d4-a716-446655440000 environment: type: string example: live status: type: string example: rotating key: type: string example: null masked_key: type: string example: 'sk_live_****************************Ab12' last_four: type: string example: Ab12 plain_text_available: type: boolean example: false idempotent_replay: type: boolean example: false rotation_expires_at: type: string example: '2026-06-06T10:00:00.000000Z' 409: description: 'Idempotency key conflict' content: application/json: schema: type: object example: message: 'This idempotency key was already used for a different API key action.' error: code: idempotency_key_conflict properties: message: type: string example: 'This idempotency key was already used for a different API key action.' error: type: object properties: code: type: string example: idempotency_key_conflict tags: - 'API Keys' /api/api-key/revoke: post: summary: 'Revoke all currently usable production API keys for the selected organisation.' operationId: revokeAllCurrentlyUsableProductionAPIKeysForTheSelectedOrganisation description: "Revokes both the active key and any old rotating key immediately. The request\nmust include the exact confirmation text REVOKE." parameters: - in: header name: X-Organisation-Id description: '' example: 'string required The organisation UUID.' schema: type: string responses: 200: description: '' content: application/json: schema: type: object example: message: 'API key revoked successfully.' data: revoked_count: 1 properties: message: type: string example: 'API key revoked successfully.' data: type: object properties: revoked_count: type: integer example: 1 422: description: 'Missing confirmation' content: application/json: schema: type: object example: message: 'The selected confirmation is invalid.' errors: confirmation: - 'The selected confirmation is invalid.' properties: message: type: string example: 'The selected confirmation is invalid.' errors: type: object properties: confirmation: type: array example: - 'The selected confirmation is invalid.' items: type: string tags: - 'API Keys' requestBody: required: true content: application/json: schema: type: object properties: confirmation: type: string description: 'Must be exactly REVOKE.' example: REVOKE required: - confirmation /api/appsumo/licenses/validate: post: summary: "Read-only preview so the FE can show \"you're redeeming Tier 2 -- here's what you get\"\nbefore the user commits. No writes." operationId: readOnlyPreviewSoTheFECanShowyoureRedeemingTier2HeresWhatYouGetBeforeTheUserCommitsNoWrites description: '' parameters: [] responses: { } tags: - AppSumo requestBody: required: true content: application/json: schema: type: object properties: license_key: type: string description: '' example: architecto required: - license_key security: [] /api/appsumo/licenses/redeem: post: summary: '' operationId: postApiAppsumoLicensesRedeem description: '' parameters: [] responses: { } tags: - AppSumo requestBody: required: true content: application/json: schema: type: object properties: license_key: type: string description: '' example: architecto required: - license_key security: [] /api/appsumo/licenses/current: get: summary: '' operationId: getApiAppsumoLicensesCurrent description: '' parameters: [] responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - AppSumo security: [] /api/appsumo/plans: get: summary: 'List AppSumo tiers' operationId: listAppSumoTiers description: "Unauthenticated tier comparison listing (name/price/reviews/mentions/credits per tier) for\na marketing/comparison page -- unlike GET /subscription-plans, deliberately does not filter\non is_active, since every AppSumo tier is seeded with is_active=false (see\nAppSumoPlanSeeder) precisely so it's excluded from that general, checkout-oriented listing.\nOrdered tier 1 -> 4 by config('billing.appsumo_tier_price_ids') key order, not by id." parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: - tier: 1 stripe_price_id: price_internal_appsumo_tier1 name: 'AppSumo Lifetime - Tier 1' description: 'AppSumo lifetime deal, Tier 1 - 1,200 reviews, 1,000 mentions, 10 tracked prompts, 240 lifetime AI credits' amount: 4900 currency: usd plan_threshold: { } properties: data: type: array example: - tier: 1 stripe_price_id: price_internal_appsumo_tier1 name: 'AppSumo Lifetime - Tier 1' description: 'AppSumo lifetime deal, Tier 1 - 1,200 reviews, 1,000 mentions, 10 tracked prompts, 240 lifetime AI credits' amount: 4900 currency: usd plan_threshold: [] items: type: object properties: tier: type: integer example: 1 stripe_price_id: type: string example: price_internal_appsumo_tier1 name: type: string example: 'AppSumo Lifetime - Tier 1' description: type: string example: 'AppSumo lifetime deal, Tier 1 - 1,200 reviews, 1,000 mentions, 10 tracked prompts, 240 lifetime AI credits' amount: type: integer example: 4900 currency: type: string example: usd plan_threshold: type: object properties: { } tags: - AppSumo security: [] /api/auth/forgot-password: post: summary: 'Send a reset link to the given user.' operationId: sendAResetLinkToTheGivenUser description: '' parameters: [] responses: { } tags: - Endpoints security: [] /api/auth/reset-password: post: summary: "Reset the user's password." operationId: resetTheUsersPassword description: '' parameters: [] responses: { } tags: - Endpoints requestBody: required: true content: application/json: schema: type: object properties: token: type: string description: '' example: architecto password: type: string description: '' example: null required: - token security: [] '/api/organisations/{organisation_uuid}/reviews/export': post: summary: '' operationId: postApiOrganisationsOrganisation_uuidReviewsExport description: '' parameters: [] responses: { } tags: - Endpoints requestBody: required: true content: application/json: schema: type: object properties: brands: type: array description: 'Must be a valid UUID. The uuid of an existing record in the projects table. The uuid of an existing record in the projects table.' example: - 6ff8f7f6-1eb3-3525-be4a-3932c805afed items: type: string view_uuid: type: string description: 'Must be a valid UUID. The uuid of an existing record in the review_views table.' example: 6b72fe4a-5b40-307c-bc24-f79acf9a1bb9 nullable: true platforms: type: array description: '' example: - sourceforge items: type: string enum: - g2 - capterra - software_advice - trustpilot - omr_reviews - clutch - sourceforge - product_hunt - hubspot_directory - goodfirms - google_workspace_marketplace - play_store - app_store - subscribed_fyi - chrome_web_store - google_reviews date_range: type: object description: '' example: null properties: type: type: string description: 'This field is required when date_range is present.' example: all_time enum: - preset - custom - all_time value: type: string description: 'This field is required when date_range.type is preset.' example: last_12_months enum: - last_14_days - last_3_months - last_6_months - last_12_months - last_2_years - last_3_years - all_time nullable: true date_range_custom: type: object description: '' example: null properties: start: type: string description: 'This field is required when date_range_custom is present. Must be a valid date.' example: '2026-08-27T06:47:10' end: type: string description: 'This field is required when date_range_custom is present. Must be a valid date. Must be a date after or equal to date_range_custom.start.' example: '2052-09-19' nullable: true date_from: type: string description: 'Must be a valid date.' example: '2026-08-27T06:47:10' nullable: true date_to: type: string description: 'Must be a valid date. Must be a date after or equal to date_from.' example: '2052-09-19' nullable: true rating_buckets: type: array description: '' example: - 1.0-1.9 items: type: string enum: - '5.0' - 4.0-4.9 - 3.0-3.9 - 2.0-2.9 - 1.0-1.9 languages: type: array description: '' example: - architecto items: type: string read_status: type: string description: '' example: all enum: - all - read - unread nullable: true search: type: string description: 'Must not be greater than 500 characters.' example: 'n' nullable: true sort_by: type: string description: '' example: created_at enum: - creation_date - review_date - rating - platform - created_at nullable: true sort_direction: type: string description: '' example: asc enum: - asc - desc nullable: true max_period: type: integer description: 'Must be at least 1.' example: 67 max_results: type: integer description: 'Must be at least 1.' example: 66 period_in_days: type: integer description: 'Must be at least 1. Must not be greater than 365.' example: 17 nullable: true page: type: integer description: 'Must be at least 1.' example: 35 nullable: true required: - platforms security: [] parameters: - in: path name: organisation_uuid description: '' example: 6ff8f7f6-1eb3-3525-be4a-3932c805afed required: true schema: type: string '/api/organisations/{organisation_uuid}/social/export': post: summary: '' operationId: postApiOrganisationsOrganisation_uuidSocialExport description: '' parameters: [] responses: { } tags: - Endpoints requestBody: required: false content: application/json: schema: type: object properties: brands: type: array description: 'Must be a valid UUID. The uuid of an existing record in the projects table. The uuid of an existing record in the projects table.' example: - 6ff8f7f6-1eb3-3525-be4a-3932c805afed items: type: string view_uuid: type: string description: 'Must be a valid UUID. The uuid of an existing record in the social_post_views table.' example: 6b72fe4a-5b40-307c-bc24-f79acf9a1bb9 nullable: true platforms: type: array description: '' example: - x items: type: string enum: - reddit - github - stackoverflow - hackernews - youtube - linkedin - x types: type: array description: '' example: - comment items: type: string enum: - post - comment sentiments: type: array description: '' example: - negative items: type: string enum: - positive - neutral - negative keywords: type: array description: 'Must not be greater than 255 characters.' example: - m items: type: string date_range: type: object description: '' example: null properties: type: type: string description: 'This field is required when date_range is present.' example: custom enum: - preset - custom - all_time value: type: string description: 'This field is required when date_range.type is preset.' example: last_3_years enum: - last_14_days - last_3_months - last_6_months - last_12_months - last_2_years - last_3_years - all_time nullable: true date_range_custom: type: object description: '' example: null properties: start: type: string description: 'This field is required when date_range_custom is present. Must be a valid date.' example: '2026-08-27T06:47:10' end: type: string description: 'This field is required when date_range_custom is present. Must be a valid date. Must be a date after or equal to date_range_custom.start.' example: '2052-09-19' nullable: true read_status: type: string description: '' example: read enum: - all - read - unread nullable: true search: type: string description: 'Must not be greater than 500 characters.' example: 'n' nullable: true sort_by: type: string description: '' example: upvotes_count enum: - date - post_date - sentiment - platform - upvotes_count - comments_count - created_at nullable: true sort_direction: type: string description: '' example: desc enum: - asc - desc nullable: true relevant: type: integer description: '' example: '0' enum: - '0' - '1' quality_score_threshold: type: number description: '' example: 4326.41688 max_period: type: integer description: 'Must be at least 1.' example: 27 max_results: type: integer description: 'Must be at least 1.' example: 35 page: type: integer description: 'Must be at least 1.' example: 43 nullable: true per_page: type: integer description: 'Must be at least 1. Must not be greater than 100.' example: 1 nullable: true security: [] parameters: - in: path name: organisation_uuid description: '' example: 2958b10e-6a80-4903-868f-79b048b5e0c4 required: true schema: type: string /api/links/deactivate-bulk-temporary: post: summary: 'Bulk Deactivate Links (Temporary)' operationId: bulkDeactivateLinksTemporary description: "Disabled unless TEMP_BULK_REVIEW_SCRAPE_ENABLED is set. When TEMP_BULK_REVIEW_SCRAPE_TOKEN\nis set, the request must include a matching X-Temporary-Bulk-Token header." parameters: - in: header name: X-Temporary-Bulk-Token description: '' example: '{token}' schema: type: string responses: { } tags: - Endpoints requestBody: required: false content: application/json: schema: type: object properties: platform: type: string description: 'Must not be greater than 100 characters.' example: b nullable: true chunk_size: type: integer description: 'Must be at least 50. Must not be greater than 500.' example: 22 nullable: true dry_run: type: boolean description: '' example: true nullable: true security: [] /api/brands/deactivate-bulk-temporary: post: summary: 'Bulk Deactivate Brands (Temporary)' operationId: bulkDeactivateBrandsTemporary description: "Disabled unless TEMP_BULK_BRAND_DEACTIVATE_ENABLED is set. When TEMP_BULK_BRAND_DEACTIVATE_TOKEN\nis set, the request must include a matching X-Temporary-Bulk-Token header." parameters: - in: header name: X-Temporary-Bulk-Token description: '' example: '{token}' schema: type: string responses: { } tags: - Endpoints requestBody: required: false content: application/json: schema: type: object properties: chunk_size: type: integer description: 'Must be at least 50. Must not be greater than 500.' example: 1 nullable: true dry_run: type: boolean description: '' example: false nullable: true security: [] '/api/organisations/{organisation_uuid}/universal-keywords/analytics': post: summary: '' operationId: postApiOrganisationsOrganisation_uuidUniversalKeywordsAnalytics description: '' parameters: [] responses: { } tags: - Endpoints requestBody: required: true content: application/json: schema: type: object properties: keyword_uuids: type: array description: 'Must be a valid UUID. The uuid of an existing record in the universal_keywords table.' example: - 6ff8f7f6-1eb3-3525-be4a-3932c805afed items: type: string platforms: type: array description: '' example: - github items: type: string enum: - reddit - github - stackoverflow - hackernews - youtube - linkedin - x date_range: type: object description: '' example: null properties: type: type: string description: 'This field is required when date_range is present.' example: preset enum: - preset - all_time value: type: string description: 'This field is required when date_range.type is preset.' example: last_6_months enum: - last_14_days - last_3_months - last_6_months - last_12_months - last_2_years - last_3_years - all_time nullable: true date_range_custom: type: object description: '' example: null properties: start: type: string description: 'This field is required when date_range_custom is present. Must be a valid date.' example: '2026-08-27T06:47:10' end: type: string description: 'This field is required when date_range_custom is present. Must be a valid date. Must be a date after or equal to date_range_custom.start.' example: '2052-09-19' nullable: true max_results: type: integer description: 'Must be at least 1.' example: 22 nullable: true max_period: type: integer description: 'Must be at least 1.' example: 67 nullable: true required: - platforms security: [] parameters: - in: path name: organisation_uuid description: '' example: 2958b10e-6a80-4903-868f-79b048b5e0c4 required: true schema: type: string '/api/outbound-webhooks/{webhook}/test': post: summary: 'Send a test webhook (manual trigger).' operationId: sendATestWebhookmanualTrigger description: '' parameters: [] responses: { } tags: - Endpoints security: [] parameters: - in: path name: webhook description: '' example: architecto required: true schema: type: string /api/outbound-webhooks: get: summary: 'List outbound webhooks for the current organisation.' operationId: listOutboundWebhooksForTheCurrentOrganisation description: '' parameters: [] responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - Endpoints security: [] post: summary: 'Create an outbound webhook.' operationId: createAnOutboundWebhook description: '' parameters: [] responses: { } tags: - Endpoints requestBody: required: true content: application/json: schema: type: object properties: url: type: string description: 'Must be a valid URL. Must not be greater than 2048 characters.' example: 'http://www.bailey.biz/quos-velit-et-fugiat-sunt-nihil-accusantium-harum.html' headers: type: array description: 'Must not be greater than 1024 characters.' example: - i items: type: string auth_type: type: string description: '' example: bearer enum: - none - bearer - custom auth_token: type: string description: 'Must not be greater than 2048 characters.' example: k nullable: true secret: type: string description: 'Must not be greater than 2048 characters.' example: h nullable: true retry_config: type: object description: '' example: null properties: { } nullable: true is_active: type: boolean description: '' example: true required: - url - auth_type security: [] '/api/outbound-webhooks/{id}': get: summary: 'Show a single outbound webhook.' operationId: showASingleOutboundWebhook description: '' parameters: [] responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - Endpoints security: [] put: summary: 'Update an outbound webhook.' operationId: updateAnOutboundWebhook description: '' parameters: [] responses: { } tags: - Endpoints requestBody: required: false content: application/json: schema: type: object properties: url: type: string description: 'Must be a valid URL. Must not be greater than 2048 characters.' example: 'http://www.bailey.biz/quos-velit-et-fugiat-sunt-nihil-accusantium-harum.html' headers: type: array description: 'Must not be greater than 1024 characters.' example: - i items: type: string auth_type: type: string description: '' example: custom enum: - none - bearer - custom auth_token: type: string description: 'Must not be greater than 2048 characters.' example: k nullable: true secret: type: string description: 'Must not be greater than 2048 characters.' example: h nullable: true retry_config: type: object description: '' example: null properties: { } nullable: true is_active: type: boolean description: '' example: true security: [] delete: summary: 'Delete an outbound webhook.' operationId: deleteAnOutboundWebhook description: '' parameters: [] responses: { } tags: - Endpoints security: [] parameters: - in: path name: id description: 'The ID of the outbound webhook.' example: architecto required: true schema: type: string /api/notifications/email: post: summary: '' operationId: postApiNotificationsEmail description: '' parameters: [] responses: { } tags: - Endpoints requestBody: required: true content: application/json: schema: type: object properties: email: type: string description: 'Must be a valid email address.' example: gbailey@example.net template: type: string description: '' example: architecto data: type: object description: '' example: null properties: { } nullable: true required: - email - template security: [] '/api/organisations/{organisation_uuid}/agents/own-brand': get: summary: "Lets the frontend resolve the org's own tracked brand_id on its own -- every agent\nendpoint requires it (see AgentRegistry::baseInputSchema()), but there's exactly one\nis_own_brand row per org and nothing today returns just that id without also computing\na full competitor/metrics payload (AiVisibilityCompetitorController::index)." operationId: letsTheFrontendResolveTheOrgsOwnTrackedBrandIdOnItsOwnEveryAgentEndpointRequiresItseeAgentRegistrybaseInputSchemaButTheresExactlyOneIsOwnBrandRowPerOrgAndNothingTodayReturnsJustThatIdWithoutAlsoComputingAFullCompetitormetricsPayloadAiVisibilityCompetitorControllerindex description: '' parameters: [] responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - Endpoints security: [] parameters: - in: path name: organisation_uuid description: '' example: 2958b10e-6a80-4903-868f-79b048b5e0c4 required: true schema: type: string '/api/organisations/{organisation_uuid}/agents/{key}/suggested-prompts': get: summary: "Populates the run form's \"Try our AI Recommendations\" chips with real, org-specific\nobservations (see AgentSuggestedPromptGenerator) instead of the agent's static\nsuggestedPrompts() copy. Meaningful for root_cause_analysis and competitive_benchmark --\nother agents fall back to their registry-declared suggestedPrompts() unchanged since\nnothing generates dynamic ones for them yet." operationId: populatesTheRunFormsTryOurAIRecommendationsChipsWithRealOrgSpecificObservationsseeAgentSuggestedPromptGeneratorInsteadOfTheAgentsStaticSuggestedPromptsCopyMeaningfulForRootCauseAnalysisAndCompetitiveBenchmarkOtherAgentsFallBackToTheirRegistryDeclaredSuggestedPromptsUnchangedSinceNothingGeneratesDynamicOnesForThemYet description: '' parameters: [] responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - Endpoints requestBody: required: true content: application/json: schema: type: object properties: brand_id: type: integer description: '' example: 16 timeframe: type: string description: '' example: 30d enum: - 7d - 30d - 90d nullable: true data_source: type: string description: "Shared across every agent's suggested-prompts endpoint -- accepts both the AI\nplatform values (root_cause_analysis/prompts_discovery/ai_visibility_roadmap) and\nthe shared reviews/mentions/keywords data_source values (see\nAgentRegistry::reviewsMentionsKeywordsDataSourceField()), since neither generator\nbelow is told which agent's data_source shape to expect ahead of time." example: openai enum: - openai - perplexity - gemini - ai_visibility - reviews - mentions - keywords nullable: true required: - brand_id security: [] parameters: - in: path name: organisation_uuid description: '' example: 2958b10e-6a80-4903-868f-79b048b5e0c4 required: true schema: type: string - in: path name: key description: '' example: architecto required: true schema: type: string '/api/organisations/{organisation_uuid}/agents/{key}/runs': post: summary: '' operationId: postApiOrganisationsOrganisation_uuidAgentsKeyRuns description: '' parameters: [] responses: { } tags: - Endpoints security: [] parameters: - in: path name: organisation_uuid description: '' example: 2958b10e-6a80-4903-868f-79b048b5e0c4 required: true schema: type: string - in: path name: key description: '' example: architecto required: true schema: type: string '/api/organisations/{organisation_uuid}/agent-runs': get: summary: "Ticket 5's Recent Outputs listing endpoint -- never built alongside show()/download(),\nso that page has been stuck on hardcoded demo rows. Matches the mockup's columns exactly:\nFocus Topics, Agents, Status, Date created. Filterable by agent_type/status per Ticket 5's\ndefinition of done; paginated the same way AiVisibilityCompetitorController::index() is." operationId: ticket5sRecentOutputsListingEndpointNeverBuiltAlongsideShowdownloadSoThatPageHasBeenStuckOnHardcodedDemoRowsMatchesTheMockupsColumnsExactlyFocusTopicsAgentsStatusDateCreatedFilterableByAgentTypestatusPerTicket5sDefinitionOfDonePaginatedTheSameWayAiVisibilityCompetitorControllerindexIs description: '' parameters: [] responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - Endpoints security: [] parameters: - in: path name: organisation_uuid description: '' example: 2958b10e-6a80-4903-868f-79b048b5e0c4 required: true schema: type: string '/api/organisations/{organisation_uuid}/agent-runs/{run_id}': get: summary: "Pulled forward from Ticket 5's full listing/polling scope -- the Root Cause Analysis run\npage needs to poll run_plan step-by-step progress and read result once completed, and\nnothing else in Ticket 5 (agent listing, Recent Outputs table) is needed for that yet." operationId: pulledForwardFromTicket5sFullListingpollingScopeTheRootCauseAnalysisRunPageNeedsToPollRunPlanStepByStepProgressAndReadResultOnceCompletedAndNothingElseInTicket5agentListingRecentOutputsTableIsNeededForThatYet description: '' parameters: [] responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - Endpoints security: [] parameters: - in: path name: organisation_uuid description: '' example: 2958b10e-6a80-4903-868f-79b048b5e0c4 required: true schema: type: string - in: path name: run_id description: 'The ID of the run.' example: 17 required: true schema: type: integer '/api/organisations/{organisation_uuid}/agent-runs/{run_id}/download': get: summary: "Ticket 4/5's own definition of done requires this endpoint, but it was never actually\nbuilt alongside RenderAgentReportPdfJob -- the PDF existed with nothing to fetch it\nthrough. Never returns a permanent URL, only a signed one generated on demand." operationId: ticket45sOwnDefinitionOfDoneRequiresThisEndpointButItWasNeverActuallyBuiltAlongsideRenderAgentReportPdfJobThePDFExistedWithNothingToFetchItThroughNeverReturnsAPermanentURLOnlyASignedOneGeneratedOnDemand description: "Backs both the frontend's Download button (open-and-forget) and Share button (copy the\nlink to hand to someone without a Subsig login) -- one URL serves both, so the expiry has\nto be long enough for a genuinely shared link to still work hours later, not just a quick\nclick. 24h, matching this feature's other \"good enough, not permanent\" TTLs (see\nAgentSuggestedPromptGenerator's suggestion cache)." parameters: - in: query name: format description: "Optional - 'pdf' (default) or 'docx'." example: docx required: false schema: type: string description: "Optional - 'pdf' (default) or 'docx'." example: docx responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - Endpoints security: [] parameters: - in: path name: organisation_uuid description: '' example: 2958b10e-6a80-4903-868f-79b048b5e0c4 required: true schema: type: string - in: path name: run_id description: 'The ID of the run.' example: 17 required: true schema: type: integer '/api/organisations/{organisation_uuid}/ai-visibility/radar/usage': get: summary: 'Radar Usage' operationId: radarUsage description: "Returns this month's SerpApi spend for the organisation's passive brand radar,\nbroken down against the plan budget cap (null when uncapped)." parameters: [] responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - Endpoints security: [] parameters: - in: path name: organisation_uuid description: 'The organisation UUID.' example: 6ff8f7f6-1eb3-3525-be4a-3932c805afed required: true schema: type: string /api/appsumo/oauth/exchange: post: summary: "Exchange the single-use `code` AppSumo redirects buyers with (after \"Activate now\") for a\nlicense_key + status. Unauthenticated -- a brand-new buyer has no subsig session yet." operationId: exchangeTheSingleUsecodeAppSumoRedirectsBuyersWithafterActivateNowForALicenseKey+StatusUnauthenticatedABrandNewBuyerHasNoSubsigSessionYet description: "AppSumo's own `status` field flips to \"active\" as soon as our webhook responds 200 to their\n`activate` event -- which happens independently of whether this specific person has ever\nbeen through our OAuth flow (AppSumoWebhookController always 200s that event). So it does\nNOT mean \"this browser's user already has a subsig account\" -- every first-time buyer would\nincorrectly see \"active\" before ever signing up. `already_redeemed` is the real signal:\nwhether *our own* AppSumoLicense record has an organisation attached yet.\n\nFrontend branches on `already_redeemed` for new-vs-returning, and on `status === \"deactivated\"`\n(still AppSumo's own billing-state field, legitimately authoritative for this one case) to\nblock a cancelled/refunded license outright regardless of redemption state." parameters: [] responses: { } tags: - Endpoints requestBody: required: true content: application/json: schema: type: object properties: code: type: string description: '' example: architecto required: - code security: [] /api/webhook: post: summary: '' operationId: postApiWebhook description: '' parameters: [] responses: { } tags: - Endpoints security: [] /api/v1/brands: get: summary: '' operationId: getApiV1Brands description: '' parameters: [] responses: 401: description: '' content: application/json: schema: type: object example: error: code: unauthorized message: Unauthorized. docs_url: 'https://docs.subsig.com/errors/unauthorized' request_id: 8a1d9823-1db0-489b-9b2f-a657e9db22fd properties: error: type: object properties: code: type: string example: unauthorized message: type: string example: Unauthorized. docs_url: type: string example: 'https://docs.subsig.com/errors/unauthorized' request_id: type: string example: 8a1d9823-1db0-489b-9b2f-a657e9db22fd tags: - Endpoints requestBody: required: true content: application/json: schema: type: object properties: query: type: string description: 'Must not be greater than 255 characters.' example: b limit: type: integer description: 'Must be at least 1.' example: 22 cursor: type: string description: 'Must not be greater than 2048 characters.' example: g required: - query security: [] /api/v1/mentions: get: summary: '' operationId: getApiV1Mentions description: '' parameters: [] responses: 401: description: '' content: application/json: schema: type: object example: error: code: unauthorized message: Unauthorized. docs_url: 'https://docs.subsig.com/errors/unauthorized' request_id: ec425689-1b1a-4251-8868-71faf05c56f7 properties: error: type: object properties: code: type: string example: unauthorized message: type: string example: Unauthorized. docs_url: type: string example: 'https://docs.subsig.com/errors/unauthorized' request_id: type: string example: ec425689-1b1a-4251-8868-71faf05c56f7 tags: - Endpoints requestBody: required: true content: application/json: schema: type: object properties: brand: type: string description: 'Must not be greater than 255 characters.' example: b start_date: type: string description: 'Must be a valid date in the format Y-m-d. Must be a date before or equal to today.' example: '2022-09-20' end_date: type: string description: 'Must be a valid date in the format Y-m-d. Must be a date after or equal to start_date. Must be a date before or equal to today.' example: '2022-09-20' platform: type: string description: 'Must not be greater than 100 characters.' example: 'n' sentiment: type: string description: '' example: positive enum: - positive - neutral - negative limit: type: integer description: 'Must be at least 1.' example: 67 cursor: type: string description: 'Must not be greater than 2048 characters.' example: z required: - brand security: [] /api/v1/reviews: get: summary: '' operationId: getApiV1Reviews description: '' parameters: [] responses: 401: description: '' content: application/json: schema: type: object example: error: code: unauthorized message: Unauthorized. docs_url: 'https://docs.subsig.com/errors/unauthorized' request_id: f46c2f33-e26c-47ae-ae10-0b89a28d88af properties: error: type: object properties: code: type: string example: unauthorized message: type: string example: Unauthorized. docs_url: type: string example: 'https://docs.subsig.com/errors/unauthorized' request_id: type: string example: f46c2f33-e26c-47ae-ae10-0b89a28d88af tags: - Endpoints requestBody: required: true content: application/json: schema: type: object properties: brand: type: string description: 'Must not be greater than 255 characters.' example: b start_date: type: string description: 'Must be a valid date in the format Y-m-d. Must be a date before or equal to today.' example: '2022-09-20' end_date: type: string description: 'Must be a valid date in the format Y-m-d. Must be a date after or equal to start_date. Must be a date before or equal to today.' example: '2022-09-20' platform: type: string description: 'Must not be greater than 100 characters.' example: 'n' min_rating: type: number description: 'Must be between 1 and 5.' example: 1 max_rating: type: number description: 'Must be between 1 and 5.' example: 2 limit: type: integer description: 'Must be at least 1.' example: 27 cursor: type: string description: 'Must not be greater than 2048 characters.' example: i required: - brand security: [] /api/external/claim-onboarding: post: summary: 'Create User And Complete Claim Onboarding' operationId: createUserAndCompleteClaimOnboarding description: "Creates a user (or reuses an existing one), then either:\n- completes onboarding, or\n- if a matching non-competitor project already exists under the user account, skips onboarding\n and returns a magic link for direct sign-in flow." parameters: [] responses: 201: description: '' content: application/json: schema: type: object example: message: 'Onboarding completed successfully.' token: 1|abc123... magic_link: 'https://subsig-frontend.vercel.app/login?qid=1%7Cabc123...&is_claim_profile=1&org_id=550e8400-e29b-41d4-a716-446655440000' data: organisation_uuid: 550e8400-e29b-41d4-a716-446655440000 product_uuid: 660e8400-e29b-41d4-a716-446655440001 properties: message: type: string example: 'Onboarding completed successfully.' token: type: string example: 1|abc123... magic_link: type: string example: 'https://subsig-frontend.vercel.app/login?qid=1%7Cabc123...&is_claim_profile=1&org_id=550e8400-e29b-41d4-a716-446655440000' data: type: object properties: organisation_uuid: type: string example: 550e8400-e29b-41d4-a716-446655440000 product_uuid: type: string example: 660e8400-e29b-41d4-a716-446655440001 401: description: '' content: application/json: schema: type: object example: message: Unauthorized. properties: message: type: string example: Unauthorized. 422: description: '' content: application/json: schema: type: object example: message: 'Provided organisation does not match the existing claimed profile context.' errors: organisation_name: - 'Organisation does not match existing profile project.' properties: message: type: string example: 'Provided organisation does not match the existing claimed profile context.' errors: type: object properties: organisation_name: type: array example: - 'Organisation does not match existing profile project.' items: type: string tags: - External requestBody: required: true content: application/json: schema: type: object properties: name: type: string description: 'User full name.' example: 'John Doe' email: type: string description: 'User email address.' example: john@example.com organisation_name: type: string description: 'Organisation name.' example: 'Acme Inc' product_name: type: string description: 'Product name.' example: 'Acme CRM' product_website: type: string description: 'Product website URL.' example: 'https://acme.com' product_logo: type: string description: 'optional Product logo URL.' example: 'https://cdn.example.com/logo.png' nullable: true platforms: type: array description: 'optional Social listening platforms for brand creation (non-empty). Defaults to all supported platforms when omitted.' example: - reddit - github - x items: type: string required: - name - email - organisation_name - product_name - product_website security: [] '/api/organisations/{organisation_uuid}/agents/recommendations': get: summary: "The standalone Recommendations page's data source -- same real recommendation logic\noverview() already computes for the Home dashboard's top-3 widget (buildRecommendations()),\njust with a higher limit since a dedicated page can reasonably show more than a 3-card\npreview. Deliberately NOT extracted into a separate service: every input buildRecommendations()\nneeds (brandMetrics, pendingSuggestionsCount) is already a method on this controller with its\nown real DB/service dependencies already wired via the constructor -- re-running that same\ngathering for a second, independently-timed page request is consistent with how overview()\nitself already works (nothing here is cached except the LLM-backed content-gap\nrecommendation, which buildRecommendations() already handles internally)." operationId: theStandaloneRecommendationsPagesDataSourceSameRealRecommendationLogicOverviewAlreadyComputesForTheHomeDashboardsTop3WidgetbuildRecommendationsJustWithAHigherLimitSinceADedicatedPageCanReasonablyShowMoreThanA3CardPreviewDeliberatelyNOTExtractedIntoASeparateServiceEveryInputBuildRecommendationsNeedsbrandMetricsPendingSuggestionsCountIsAlreadyAMethodOnThisControllerWithItsOwnRealDBserviceDependenciesAlreadyWiredViaTheConstructorReRunningThatSameGatheringForASecondIndependentlyTimedPageRequestIsConsistentWithHowOverviewItselfAlreadyWorksnothingHereIsCachedExceptTheLLMBackedContentGapRecommendationWhichBuildRecommendationsAlreadyHandlesInternally description: '' parameters: [] responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - Home security: [] parameters: - in: path name: organisation_uuid description: '' example: 2958b10e-6a80-4903-868f-79b048b5e0c4 required: true schema: type: string '/api/organisations/{organisation_uuid}/home/overview': get: summary: 'Home Overview' operationId: homeOverview description: '' parameters: [] responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - Home security: [] parameters: - in: path name: organisation_uuid description: 'The organisation UUID.' example: 6ff8f7f6-1eb3-3525-be4a-3932c805afed required: true schema: type: string '/api/invites/validate/{token}': get: summary: 'Validate Invite' operationId: validateInvite description: "Validate an invite token and return the associated email if the invite is valid.\nAlso returns whether a user account already exists for the invite email,\nso the frontend can route to login vs signup.\nThis endpoint is public and does not require authentication." parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: email: user@example.com properties: email: type: string example: user@example.com 404: description: 'Invite not found' content: application/json: schema: type: object example: message: 'Invite not found.' properties: message: type: string example: 'Invite not found.' 410: description: '' content: application/json: schema: oneOf: - description: 'Invite expired' type: object example: message: 'This invite has expired.' properties: message: type: string example: 'This invite has expired.' - description: 'Invite no longer valid' type: object example: message: 'This invite is no longer valid.' properties: message: type: string example: 'This invite is no longer valid.' tags: - Invites security: [] parameters: - in: path name: token description: 'The invite token.' example: abc123def456... required: true schema: type: string /api/invites/accept-invite: post: summary: 'Accept Invite (Existing User)' operationId: acceptInviteExistingUser description: "Accept an invitation as an already-registered, authenticated user.\nThe authenticated user's email must match the invite email.\nThe user will be added to the organisation or project specified in the invite." parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: message: 'Invite accepted successfully.' data: organisation: uuid: 550e8400-e29b-41d4-a716-446655440000 name: 'Acme Inc' properties: message: type: string example: 'Invite accepted successfully.' data: type: object properties: organisation: type: object properties: uuid: type: string example: 550e8400-e29b-41d4-a716-446655440000 name: type: string example: 'Acme Inc' 403: description: 'Email mismatch' content: application/json: schema: type: object example: message: 'This invite was sent to a different email address.' errors: email: - 'The invite email does not match your account email.' properties: message: type: string example: 'This invite was sent to a different email address.' errors: type: object properties: email: type: array example: - 'The invite email does not match your account email.' items: type: string 404: description: 'Token not found' content: application/json: schema: type: object example: message: 'Invite not found.' errors: token: - 'The invite token is invalid or does not exist.' properties: message: type: string example: 'Invite not found.' errors: type: object properties: token: type: array example: - 'The invite token is invalid or does not exist.' items: type: string 410: description: '' content: application/json: schema: oneOf: - description: 'Invite expired' type: object example: message: 'Invite has expired.' errors: token: - 'This invite has expired. Please request a new invitation.' properties: message: type: string example: 'Invite has expired.' errors: type: object properties: token: type: array example: - 'This invite has expired. Please request a new invitation.' items: type: string - description: 'Invite already accepted' type: object example: message: 'Invite has already been accepted.' errors: token: - 'This invite has already been accepted.' properties: message: type: string example: 'Invite has already been accepted.' errors: type: object properties: token: type: array example: - 'This invite has already been accepted.' items: type: string 422: description: 'Already a member' content: application/json: schema: type: object example: message: 'You are already a member of this organisation.' errors: organisation: - 'You are already a member of this organisation.' properties: message: type: string example: 'You are already a member of this organisation.' errors: type: object properties: organisation: type: array example: - 'You are already a member of this organisation.' items: type: string tags: - Invites requestBody: required: true content: application/json: schema: type: object properties: token: type: string description: 'The invite token from the invitation link.' example: eEtgjrcdtubjCu4817MfGiimvC2DQLBgaI7LpY1g5kdDMK5wJlQank7ZJ6PWurmb required: - token /api/invites: get: summary: 'List Invites' operationId: listInvites description: "Get invites based on context:\n- Organisation level (no project_id): Returns all organisation invites + all project invites for the organisation\n- Project level (with project_id): Returns only invites for the specified project" parameters: - in: query name: organisation_id description: "Optional. Organisation UUID to list invites for. If omitted, organisation is taken from X-Organisation-Id header or the user's current organisation." example: 550e8400-e29b-41d4-a716-446655440000 required: false schema: type: string description: "Optional. Organisation UUID to list invites for. If omitted, organisation is taken from X-Organisation-Id header or the user's current organisation." example: 550e8400-e29b-41d4-a716-446655440000 - in: query name: project_id description: 'Optional. Filter invites by project UUID.' example: 660e8400-e29b-41d4-a716-446655440001 required: false schema: type: string description: 'Optional. Filter invites by project UUID.' example: 660e8400-e29b-41d4-a716-446655440001 responses: 200: description: '' content: application/json: schema: type: array items: type: object properties: id: type: integer example: 1 email: type: string example: user@example.com type: type: string example: organisation status: type: string example: pending expires_at: type: string example: '2025-12-31T10:00:00.000000Z' organisation_id: type: string example: 550e8400-e29b-41d4-a716-446655440000 organisation: type: object properties: uuid: type: string example: 550e8400-e29b-41d4-a716-446655440000 name: type: string example: 'Acme Corp' project_id: type: string example: null project: type: string example: null inviter: type: object properties: name: type: string example: 'John Admin' email: type: string example: admin@acme.com created_at: type: string example: '2025-12-24T10:00:00.000000Z' updated_at: type: string example: '2025-12-24T10:00:00.000000Z' example: - id: 1 email: user@example.com type: organisation status: pending expires_at: '2025-12-31T10:00:00.000000Z' organisation_id: 550e8400-e29b-41d4-a716-446655440000 organisation: uuid: 550e8400-e29b-41d4-a716-446655440000 name: 'Acme Corp' project_id: null project: null inviter: name: 'John Admin' email: admin@acme.com created_at: '2025-12-24T10:00:00.000000Z' updated_at: '2025-12-24T10:00:00.000000Z' - id: 2 email: developer@example.com type: project status: pending expires_at: '2025-12-31T10:00:00.000000Z' organisation_id: null organisation: null project_id: 660e8400-e29b-41d4-a716-446655440001 project: uuid: 660e8400-e29b-41d4-a716-446655440001 name: 'My Product' inviter: name: 'John Admin' email: admin@acme.com created_at: '2025-12-24T10:00:00.000000Z' updated_at: '2025-12-24T10:00:00.000000Z' 403: description: '' content: application/json: schema: oneOf: - description: 'No access to project' type: object example: message: 'You do not have access to this project.' properties: message: type: string example: 'You do not have access to this project.' - description: 'No access to organisation' type: object example: message: 'You do not have access to this organisation.' properties: message: type: string example: 'You do not have access to this organisation.' 404: description: '' content: application/json: schema: oneOf: - description: 'Project not found' type: object example: message: 'Project not found.' properties: message: type: string example: 'Project not found.' - description: 'Organisation not found' type: object example: message: 'Organisation not found.' properties: message: type: string example: 'Organisation not found.' 422: description: 'No organisation context' content: application/json: schema: type: object example: message: 'No organisation context found.' errors: organisation: - 'Please select an organisation or set current organisation.' properties: message: type: string example: 'No organisation context found.' errors: type: object properties: organisation: type: array example: - 'Please select an organisation or set current organisation.' items: type: string tags: - Invites post: summary: 'Create Invite' operationId: createInvite description: "Create a new invite for a user to join an organisation or project.\nFor organisation invites, the organisation is determined from the provided organisation UUID." parameters: [] responses: 201: description: '' content: application/json: schema: type: object example: message: 'Invite created successfully.' data: id: 1 email: user@example.com type: organisation status: pending token: abc123... expires_at: '2025-12-31T10:00:00.000000Z' organisation_id: 550e8400-e29b-41d4-a716-446655440000 project_id: null created_at: '2025-12-24T10:00:00.000000Z' updated_at: '2025-12-24T10:00:00.000000Z' properties: message: type: string example: 'Invite created successfully.' data: type: object properties: id: type: integer example: 1 email: type: string example: user@example.com type: type: string example: organisation status: type: string example: pending token: type: string example: abc123... expires_at: type: string example: '2025-12-31T10:00:00.000000Z' organisation_id: type: string example: 550e8400-e29b-41d4-a716-446655440000 project_id: type: string example: null created_at: type: string example: '2025-12-24T10:00:00.000000Z' updated_at: type: string example: '2025-12-24T10:00:00.000000Z' 403: description: 'Inviter not whitelisted' content: application/json: schema: type: object example: message: 'You are not permitted to send invites at this time.' properties: message: type: string example: 'You are not permitted to send invites at this time.' 422: description: 'No organisation context' content: application/json: schema: type: object example: message: 'No organisation context found.' errors: organisation: - 'Please select an organisation or set current organisation.' properties: message: type: string example: 'No organisation context found.' errors: type: object properties: organisation: type: array example: - 'Please select an organisation or set current organisation.' items: type: string tags: - Invites requestBody: required: true content: application/json: schema: type: object properties: email: type: string description: 'The email address to send the invite to.' example: user@example.com type: type: string description: 'The type of invite (organisation or project).' example: organisation organisation_id: type: string description: 'The organisation UUID this invite belongs to.' example: 550e8400-e29b-41d4-a716-446655440000 project_id: type: string description: 'The project UUID (required when type is `project`).' example: 660e8400-e29b-41d4-a716-446655440001 nullable: true required: - email - type - organisation_id '/api/invites/{invite_id}': delete: summary: 'Delete Invite' operationId: deleteInvite description: 'Delete an invite. Only the inviter or organisation owners can delete invites.' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: message: 'Invite deleted successfully.' properties: message: type: string example: 'Invite deleted successfully.' 403: description: 'Not authorized' content: application/json: schema: type: object example: message: 'You are not authorized to delete this invite.' properties: message: type: string example: 'You are not authorized to delete this invite.' 404: description: 'Invite not found' content: application/json: schema: type: object example: message: 'Invite not found.' properties: message: type: string example: 'Invite not found.' 422: description: 'No organisation context' content: application/json: schema: type: object example: message: 'No organisation context found.' errors: organisation: - 'Please select an organisation or set current organisation.' properties: message: type: string example: 'No organisation context found.' errors: type: object properties: organisation: type: array example: - 'Please select an organisation or set current organisation.' items: type: string tags: - Invites parameters: - in: path name: invite_id description: 'The invite ID.' example: 1 required: true schema: type: integer '/api/projects/{project_uuid}/links': get: summary: 'List Project Links' operationId: listProjectLinks description: 'Get all links associated with a specific project.' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: - uuid: 550e8400-e29b-41d4-a716-446655440000 platform: g2 url: 'https://g2.com/products/acme' enabled: true created_at: '2026-01-22T10:00:00.000000Z' updated_at: '2026-01-22T10:00:00.000000Z' properties: data: type: array example: - uuid: 550e8400-e29b-41d4-a716-446655440000 platform: g2 url: 'https://g2.com/products/acme' enabled: true created_at: '2026-01-22T10:00:00.000000Z' updated_at: '2026-01-22T10:00:00.000000Z' items: type: object properties: uuid: type: string example: 550e8400-e29b-41d4-a716-446655440000 platform: type: string example: g2 url: type: string example: 'https://g2.com/products/acme' enabled: type: boolean example: true created_at: type: string example: '2026-01-22T10:00:00.000000Z' updated_at: type: string example: '2026-01-22T10:00:00.000000Z' 403: description: 'No Access' content: application/json: schema: type: object example: message: 'You do not have access to this project.' properties: message: type: string example: 'You do not have access to this project.' 404: description: 'Project Not Found' content: application/json: schema: type: object example: message: 'Project not found.' properties: message: type: string example: 'Project not found.' tags: - Links parameters: - in: path name: project_uuid description: 'The project UUID.' example: 660e8400-e29b-41d4-a716-446655440001 required: true schema: type: string /api/notifications: get: summary: 'List Notifications' operationId: listNotifications description: 'Get all notification rules for the current organisation.' parameters: [] responses: 200: description: '' content: application/json: schema: type: array items: type: object properties: uuid: type: string example: 770e8400-e29b-41d4-a716-446655440000 name: type: string example: 'Daily Review Alerts' trigger_type: type: string example: new_review languages: type: array example: - en - es items: type: string auto_translate: type: boolean example: true rating_filters: type: array example: - 1 - 2 - 3 items: type: integer sentiment_filters: type: array example: - negative items: type: string condition_filters: type: object properties: logic: type: string example: and rules: type: array example: - field: platforms operator: contains value: - g2 items: type: object properties: field: type: string example: platforms operator: type: string example: contains value: type: array example: - g2 items: type: string notification_frequency: type: string example: instant read_status: type: string example: all is_active: type: boolean example: true channels: type: array example: [] projects: type: array example: [] created_at: type: string example: '2025-12-31T12:00:00.000000Z' updated_at: type: string example: '2025-12-31T12:00:00.000000Z' example: - uuid: 770e8400-e29b-41d4-a716-446655440000 name: 'Daily Review Alerts' trigger_type: new_review languages: - en - es auto_translate: true rating_filters: - 1 - 2 - 3 sentiment_filters: - negative condition_filters: logic: and rules: - field: platforms operator: contains value: - g2 notification_frequency: instant read_status: all is_active: true channels: [] projects: [] created_at: '2025-12-31T12:00:00.000000Z' updated_at: '2025-12-31T12:00:00.000000Z' tags: - Notifications post: summary: 'Create Notification' operationId: createNotification description: 'Create a new notification rule with channels and project mappings.' parameters: [] responses: 201: description: Success content: application/json: schema: type: object example: message: 'Notification created successfully.' data: uuid: 770e8400-e29b-41d4-a716-446655440000 name: 'Daily Review Alerts' trigger_type: new_review languages: - en - es auto_translate: true rating_filters: - 1 - 2 - 3 - 4 - 5 sentiment_filters: - positive - neutral - negative condition_filters: logic: and rules: - field: platforms operator: contains value: - g2 - capterra - field: sentiment operator: contains value: - negative notification_frequency: instant read_status: unread keyword: null content_type: post is_active: true channels: - uuid: 880e8400-e29b-41d4-a716-446655440001 channel_type: slack is_active: true config: channel_names: - alerts - incidents channel_url: 'https://hooks.slack.com/services/xxx' recipients: - alerts@company.com url: 'https://your-server.com/webhook/notifications' projects: - uuid: 990e8400-e29b-41d4-a716-446655440002 project_uuid: 5f1812b1-15f7-432d-996a-9ab5cfbe01d6 project_name: 'Acme App' is_active: true product_logo: 'https://cdn.example.com/logo.png' platforms: review: - g2 - capterra social: - reddit created_at: '2025-12-31T12:00:00.000000Z' updated_at: '2025-12-31T12:00:00.000000Z' properties: message: type: string example: 'Notification created successfully.' data: type: object properties: uuid: type: string example: 770e8400-e29b-41d4-a716-446655440000 name: type: string example: 'Daily Review Alerts' trigger_type: type: string example: new_review languages: type: array example: - en - es items: type: string auto_translate: type: boolean example: true rating_filters: type: array example: - 1 - 2 - 3 - 4 - 5 items: type: integer sentiment_filters: type: array example: - positive - neutral - negative items: type: string condition_filters: type: object properties: logic: type: string example: and rules: type: array example: - field: platforms operator: contains value: - g2 - capterra - field: sentiment operator: contains value: - negative items: type: object properties: field: type: string example: platforms operator: type: string example: contains value: type: array example: - g2 - capterra items: type: string notification_frequency: type: string example: instant read_status: type: string example: unread keyword: type: string example: null content_type: type: string example: post is_active: type: boolean example: true channels: type: array example: - uuid: 880e8400-e29b-41d4-a716-446655440001 channel_type: slack is_active: true config: channel_names: - alerts - incidents channel_url: 'https://hooks.slack.com/services/xxx' recipients: - alerts@company.com url: 'https://your-server.com/webhook/notifications' items: type: object properties: uuid: type: string example: 880e8400-e29b-41d4-a716-446655440001 channel_type: type: string example: slack is_active: type: boolean example: true config: type: object properties: channel_names: type: array example: - alerts - incidents items: type: string channel_url: type: string example: 'https://hooks.slack.com/services/xxx' recipients: type: array example: - alerts@company.com items: type: string url: type: string example: 'https://your-server.com/webhook/notifications' projects: type: array example: - uuid: 990e8400-e29b-41d4-a716-446655440002 project_uuid: 5f1812b1-15f7-432d-996a-9ab5cfbe01d6 project_name: 'Acme App' is_active: true product_logo: 'https://cdn.example.com/logo.png' platforms: review: - g2 - capterra social: - reddit items: type: object properties: uuid: type: string example: 990e8400-e29b-41d4-a716-446655440002 project_uuid: type: string example: 5f1812b1-15f7-432d-996a-9ab5cfbe01d6 project_name: type: string example: 'Acme App' is_active: type: boolean example: true product_logo: type: string example: 'https://cdn.example.com/logo.png' platforms: type: object properties: review: type: array example: - g2 - capterra items: type: string social: type: array example: - reddit items: type: string created_at: type: string example: '2025-12-31T12:00:00.000000Z' updated_at: type: string example: '2025-12-31T12:00:00.000000Z' 422: description: 'Validation Error' content: application/json: schema: type: object example: message: 'The name field is required. (and 2 more errors)' errors: name: - 'The name field is required.' channels: - 'At least one channel is required.' projects: - 'Select at least one brand.' properties: message: type: string example: 'The name field is required. (and 2 more errors)' errors: type: object properties: name: type: array example: - 'The name field is required.' items: type: string channels: type: array example: - 'At least one channel is required.' items: type: string projects: type: array example: - 'Select at least one brand.' items: type: string tags: - Notifications requestBody: required: true content: application/json: schema: type: object properties: name: type: string description: 'The notification name.' example: 'Daily Review Alerts' trigger_type: type: string description: 'The trigger type. Defaults to new_review.' example: new_review is_active: type: boolean description: 'Whether the notification is active. Defaults to true.' example: true languages: type: array description: 'Language filters (empty = all languages).' example: - en - es - fr - de items: type: string auto_translate: type: boolean description: 'Auto-translate to English. Defaults to false.' example: true rating_filters: type: array description: 'Rating filters 1-5 (empty = all stars).' example: - 1 - 2 - 3 - 4 - 5 items: type: integer sentiment_filters: type: array description: 'Sentiment filters (empty = all).' example: - positive - neutral - negative items: type: string keyword: type: array description: 'Keyword filters (empty or omitted = all).' example: - ai - support items: type: string content_type: type: string description: 'Content type: post, comment, all. Defaults to all.' example: post notification_frequency: type: string description: 'Notification delivery frequency: instant, daily. Defaults to instant.' example: instant read_status: type: string description: 'Filter by read status: all, read, unread. Defaults to all.' example: all condition_filters: type: object description: 'Advanced condition filters with AND/OR logic.' example: [] properties: logic: type: string description: 'Logic operator for combining rules: and, or.' example: and rules: type: array description: 'Array of filter rules or nested groups.' example: - [] items: type: object properties: field: type: string description: 'Filter field: brands, platforms, read_status, keywords, type, sentiment, content, user.' example: platforms operator: type: string description: 'Filter operator: contains, not_contains.' example: contains value: type: mixed description: 'Filter value (string or array depending on field).' example: '["g2", "capterra"]' logic: type: string description: 'For nested groups: and, or.' example: or rules: type: array description: 'Nested rules array for grouped conditions.' example: - [] items: type: object required: - logic - rules nullable: true channels: type: array description: 'Delivery channels configuration. At least one required.' example: - [] items: type: object properties: channel_type: type: string description: 'Channel type: slack, email, ms_teams, lark, webhook.' example: slack is_active: type: boolean description: 'Whether channel is active.' example: true config: type: object description: 'Channel-specific configuration.' example: [] properties: channel_names: type: array description: 'Slack channel names.' example: - alerts - incidents items: type: string channel_url: type: string description: 'Slack channel webhook URL.' example: 'https://hooks.slack.com/services/xxx' nullable: true recipients: type: array description: 'Email recipients (required if email channel is active).' example: - alerts@company.com items: type: string template: type: string description: '' example: review_alert enum: - generic - review_alert - mention - review nullable: true url: type: string description: 'Custom webhook URL (required if webhook channel is active).' example: 'https://your-server.com/webhook/notifications' nullable: true webhook_url: type: string description: '' example: 'http://www.bailey.biz/quos-velit-et-fugiat-sunt-nihil-accusantium-harum.html' nullable: true nullable: true required: - channel_type projects: type: array description: 'Project/brand mappings. At least one required.' example: - [] items: type: object properties: project_uuid: type: string description: 'The project UUID.' example: 5f1812b1-15f7-432d-996a-9ab5cfbe01d6 is_active: type: boolean description: 'Whether project mapping is active.' example: true platforms: type: object description: 'Platform selections for this project.' example: [] properties: review: type: array description: 'Review platforms to monitor. Supported: g2, capterra, software_advice, trustpilot, omr_reviews, clutch, sourceforge, product_hunt, hubspot_directory, goodfirms, chrome_web_store, google_workspace_marketplace, play_store, app_store, subscribed_fyi, google_reviews, getapp, yotpo, trustradius, gartner, product_review.' example: - g2 - capterra - product_hunt - play_store items: type: string social: type: array description: 'Social platforms to monitor.' example: - reddit - hackernews - linkedin items: type: string nullable: true product_logo: type: string description: 'Logo URL for the project (optional).' example: 'https://cdn.example.com/logo.png' nullable: true required: - project_uuid required: - name - channels - projects '/api/notifications/{uuid}': get: summary: 'Get Notification' operationId: getNotification description: 'Get details of a specific notification.' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: uuid: 770e8400-e29b-41d4-a716-446655440000 name: 'Daily Review Alerts' trigger_type: new_review languages: - en - es auto_translate: true rating_filters: - 1 - 2 - 3 sentiment_filters: - negative condition_filters: logic: and rules: - field: platforms operator: contains value: - g2 notification_frequency: instant read_status: all keyword: null is_active: true channels: [] projects: [] created_at: '2025-12-31T12:00:00.000000Z' updated_at: '2025-12-31T12:00:00.000000Z' properties: uuid: type: string example: 770e8400-e29b-41d4-a716-446655440000 name: type: string example: 'Daily Review Alerts' trigger_type: type: string example: new_review languages: type: array example: - en - es items: type: string auto_translate: type: boolean example: true rating_filters: type: array example: - 1 - 2 - 3 items: type: integer sentiment_filters: type: array example: - negative items: type: string condition_filters: type: object properties: logic: type: string example: and rules: type: array example: - field: platforms operator: contains value: - g2 items: type: object properties: field: type: string example: platforms operator: type: string example: contains value: type: array example: - g2 items: type: string notification_frequency: type: string example: instant read_status: type: string example: all keyword: type: string example: null is_active: type: boolean example: true channels: type: array example: [] projects: type: array example: [] created_at: type: string example: '2025-12-31T12:00:00.000000Z' updated_at: type: string example: '2025-12-31T12:00:00.000000Z' tags: - Notifications put: summary: 'Update Notification' operationId: updateNotification description: 'Update an existing notification rule with channels and project mappings.' parameters: [] responses: 200: description: Success content: application/json: schema: type: object example: message: 'Notification updated successfully.' data: uuid: 770e8400-e29b-41d4-a716-446655440000 name: 'Updated Review Alerts' trigger_type: new_review languages: - en auto_translate: false rating_filters: - 1 - 2 sentiment_filters: - negative condition_filters: logic: and rules: - field: sentiment operator: contains value: - negative notification_frequency: daily read_status: unread keyword: null content_type: comment is_active: true channels: [] projects: [] created_at: '2025-12-31T12:00:00.000000Z' updated_at: '2025-12-31T13:00:00.000000Z' properties: message: type: string example: 'Notification updated successfully.' data: type: object properties: uuid: type: string example: 770e8400-e29b-41d4-a716-446655440000 name: type: string example: 'Updated Review Alerts' trigger_type: type: string example: new_review languages: type: array example: - en items: type: string auto_translate: type: boolean example: false rating_filters: type: array example: - 1 - 2 items: type: integer sentiment_filters: type: array example: - negative items: type: string condition_filters: type: object properties: logic: type: string example: and rules: type: array example: - field: sentiment operator: contains value: - negative items: type: object properties: field: type: string example: sentiment operator: type: string example: contains value: type: array example: - negative items: type: string notification_frequency: type: string example: daily read_status: type: string example: unread keyword: type: string example: null content_type: type: string example: comment is_active: type: boolean example: true channels: type: array example: [] projects: type: array example: [] created_at: type: string example: '2025-12-31T12:00:00.000000Z' updated_at: type: string example: '2025-12-31T13:00:00.000000Z' 404: description: 'Not Found' content: application/json: schema: type: object example: message: 'Notification not found.' properties: message: type: string example: 'Notification not found.' tags: - Notifications requestBody: required: true content: application/json: schema: type: object properties: name: type: string description: 'The notification name.' example: 'Daily Review Alerts' trigger_type: type: string description: 'The trigger type.' example: new_review is_active: type: boolean description: 'Whether the notification is active.' example: true languages: type: array description: 'Language filters (empty = all languages).' example: - en - es - fr - de items: type: string auto_translate: type: boolean description: 'Auto-translate to English.' example: true rating_filters: type: array description: 'Rating filters 1-5.' example: - 1 - 2 - 3 - 4 - 5 items: type: integer sentiment_filters: type: array description: 'Sentiment filters.' example: - positive - neutral - negative items: type: string keyword: type: array description: 'Keyword filters (empty or omitted = all).' example: - ai - support items: type: string content_type: type: string description: 'Content type: post, comment, all. Defaults to all.' example: post notification_frequency: type: string description: 'Notification delivery frequency: instant, daily.' example: instant read_status: type: string description: 'Filter by read status: all, read, unread.' example: all condition_filters: type: object description: 'Advanced condition filters with AND/OR logic.' example: [] properties: logic: type: string description: 'Logic operator for combining rules: and, or.' example: and rules: type: array description: 'Array of filter rules or nested groups.' example: - [] items: type: object properties: field: type: string description: 'Filter field: brands, platforms, read_status, keywords, type, sentiment, content, user.' example: platforms operator: type: string description: 'Filter operator: contains, not_contains.' example: contains value: type: mixed description: 'Filter value (string or array depending on field).' example: '["g2", "capterra"]' required: - logic - rules nullable: true channels: type: array description: 'Delivery channels configuration.' example: - [] items: type: object properties: channel_type: type: string description: 'Channel type: slack, email, ms_teams, lark, webhook.' example: slack is_active: type: boolean description: 'Whether channel is active.' example: true config: type: object description: 'Channel-specific configuration.' example: [] properties: channel_names: type: array description: 'Slack channel names.' example: - alerts - incidents items: type: string channel_url: type: string description: 'Slack channel webhook URL.' example: 'https://hooks.slack.com/services/xxx' nullable: true recipients: type: array description: 'Email recipients (required if email channel is active).' example: - alerts@company.com items: type: string template: type: string description: '' example: generic enum: - generic - review_alert - mention - review nullable: true url: type: string description: 'Custom webhook URL (required if webhook channel is active).' example: 'https://your-server.com/webhook/notifications' nullable: true webhook_url: type: string description: '' example: 'http://www.bailey.biz/quos-velit-et-fugiat-sunt-nihil-accusantium-harum.html' nullable: true nullable: true required: - channel_type projects: type: array description: 'Project/brand mappings.' example: - [] items: type: object properties: project_uuid: type: string description: 'The project UUID.' example: 5f1812b1-15f7-432d-996a-9ab5cfbe01d6 is_active: type: boolean description: 'Whether project mapping is active.' example: true platforms: type: object description: 'Platform selections for this project.' example: [] properties: review: type: array description: '' example: - clutch items: type: string enum: - g2 - capterra - trustpilot - trustradius - product_hunt - gartner - getapp - yotpo - software_advice - omr_reviews - product_review - subscribed_fyi - app_store - play_store - google_reviews - clutch - sourceforge - hubspot_directory - goodfirms - chrome_web_store - google_workspace_marketplace - google_maps - google_play - apple_app_store social: type: array description: '' example: - reddit items: type: string enum: - reddit - github - stackoverflow - hackernews - youtube - linkedin - x nullable: true product_logo: type: string description: 'Must not be greater than 2048 characters.' example: g nullable: true required: - project_uuid required: - name - channels - projects delete: summary: 'Delete Notification' operationId: deleteNotification description: 'Delete a notification and all associated channels and project mappings.' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: message: 'Notification deleted successfully.' properties: message: type: string example: 'Notification deleted successfully.' 404: description: '' content: application/json: schema: type: object example: message: 'Notification not found.' properties: message: type: string example: 'Notification not found.' tags: - Notifications parameters: - in: path name: uuid description: 'The notification UUID.' example: 770e8400-e29b-41d4-a716-446655440000 required: true schema: type: string /api/onboarding: post: summary: 'Complete Onboarding' operationId: completeOnboarding description: "Create an organisation and project in a single step during onboarding.\nThe authenticated user becomes the organisation owner." parameters: [] responses: 201: description: '' content: application/json: schema: type: object example: message: 'Onboarding completed successfully.' data: organisation_uuid: 550e8400-e29b-41d4-a716-446655440000 product_uuid: 660e8400-e29b-41d4-a716-446655440001 organisation: uuid: 550e8400-e29b-41d4-a716-446655440000 name: 'Acme Inc' website: 'https://acme.com' threshold_consumption_date: '2026-05-11' project: uuid: 660e8400-e29b-41d4-a716-446655440001 product_name: 'Acme CRM' product_website: 'https://acme.com' product_logo: 'https://cdn.brandfetch.io/acme.com/fallback/lettermark/icon?c=BRANDFETCH_CLIENT_ID' review_platforms: { } reddit_keywords: - acme - 'acme crm' negative_keywords: [] reddit_brand_name: Acme platforms: - reddit - github social_platform: { } scraper_sync: success: true synced_platforms: - g2 - capterra errors: [] brand_sync: success: true brand_id: '12345' error: null properties: message: type: string example: 'Onboarding completed successfully.' data: type: object properties: organisation_uuid: type: string example: 550e8400-e29b-41d4-a716-446655440000 product_uuid: type: string example: 660e8400-e29b-41d4-a716-446655440001 organisation: type: object properties: uuid: type: string example: 550e8400-e29b-41d4-a716-446655440000 name: type: string example: 'Acme Inc' website: type: string example: 'https://acme.com' threshold_consumption_date: type: string example: '2026-05-11' project: type: object properties: uuid: type: string example: 660e8400-e29b-41d4-a716-446655440001 product_name: type: string example: 'Acme CRM' product_website: type: string example: 'https://acme.com' product_logo: type: string example: 'https://cdn.brandfetch.io/acme.com/fallback/lettermark/icon?c=BRANDFETCH_CLIENT_ID' review_platforms: type: object properties: { } reddit_keywords: type: array example: - acme - 'acme crm' items: type: string negative_keywords: type: array example: [] reddit_brand_name: type: string example: Acme platforms: type: array example: - reddit - github items: type: string social_platform: type: object properties: { } scraper_sync: type: object properties: success: type: boolean example: true synced_platforms: type: array example: - g2 - capterra items: type: string errors: type: array example: [] brand_sync: type: object properties: success: type: boolean example: true brand_id: type: string example: '12345' error: type: string example: null 422: description: 'Validation error' content: application/json: schema: type: object example: message: 'The organisation name field is required.' errors: organisation_name: - 'The organisation name field is required.' properties: message: type: string example: 'The organisation name field is required.' errors: type: object properties: organisation_name: type: array example: - 'The organisation name field is required.' items: type: string tags: - Onboarding requestBody: required: true content: application/json: schema: type: object properties: organisation_name: type: string description: 'The organisation/company name.' example: 'Acme Inc' product_name: type: string description: 'The product name.' example: 'Acme CRM' product_website: type: string description: 'The product website URL.' example: 'https://acme.com' product_logo: type: string description: 'The product logo URL.' example: 'https://cdn.brandfetch.io/acme.com/fallback/lettermark/icon?c=BRANDFETCH_CLIENT_ID' review_platforms: type: object description: 'Review platform settings with enabled status and URLs.' example: g2: enabled: true url: 'https://g2.com/products/acme' capterra: enabled: false url: null properties: { } reddit_keywords: type: array description: 'Reddit keyword variations to track.' example: - acme - 'acme crm' items: type: string reddit_brand_name: type: string description: 'The brand name for Reddit tracking.' example: Acme platforms: type: array description: 'optional Social listening platforms for `/v1/brand/create` (non-empty). Defaults to all supported platforms when omitted.' example: - reddit - github - x items: type: string negative_keywords: type: array description: 'optional Keywords to exclude.' example: - spam - competitor items: type: string threshold_consumption_date: type: string description: 'optional Threshold consumption preset for the organisation.' example: 'last_14_days. Allowed: last_14_days, last_3_months, last_6_months, last_12_months, last_2_years, last_3_years, all_time' required: - organisation_name - product_name put: summary: 'Update Onboarding' operationId: updateOnboarding description: 'Update the current organisation and project for the authenticated user.' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: message: 'Onboarding updated successfully.' data: organisation: uuid: 550e8400-e29b-41d4-a716-446655440000 name: 'Acme Inc' website: 'https://acme.com' threshold_consumption_date: '2026-02-25' project: uuid: 660e8400-e29b-41d4-a716-446655440001 product_name: 'Acme CRM' product_website: 'https://acme.com' product_logo: 'https://cdn.brandfetch.io/acme.com/fallback/lettermark/icon?c=BRANDFETCH_CLIENT_ID' review_platforms: { } reddit_keywords: - acme - 'acme crm' negative_keywords: [] reddit_brand_name: Acme platforms: - reddit - github social_platform: { } scraper_sync: success: true synced_platforms: - g2 - capterra errors: [] brand_sync: success: true brand_id: '12345' error: null properties: message: type: string example: 'Onboarding updated successfully.' data: type: object properties: organisation: type: object properties: uuid: type: string example: 550e8400-e29b-41d4-a716-446655440000 name: type: string example: 'Acme Inc' website: type: string example: 'https://acme.com' threshold_consumption_date: type: string example: '2026-02-25' project: type: object properties: uuid: type: string example: 660e8400-e29b-41d4-a716-446655440001 product_name: type: string example: 'Acme CRM' product_website: type: string example: 'https://acme.com' product_logo: type: string example: 'https://cdn.brandfetch.io/acme.com/fallback/lettermark/icon?c=BRANDFETCH_CLIENT_ID' review_platforms: type: object properties: { } reddit_keywords: type: array example: - acme - 'acme crm' items: type: string negative_keywords: type: array example: [] reddit_brand_name: type: string example: Acme platforms: type: array example: - reddit - github items: type: string social_platform: type: object properties: { } scraper_sync: type: object properties: success: type: boolean example: true synced_platforms: type: array example: - g2 - capterra items: type: string errors: type: array example: [] brand_sync: type: object properties: success: type: boolean example: true brand_id: type: string example: '12345' error: type: string example: null 404: description: 'No organisation or project found' content: application/json: schema: type: object example: message: 'No organisation or project found for the current user.' properties: message: type: string example: 'No organisation or project found for the current user.' 422: description: 'Validation error' content: application/json: schema: type: object example: message: 'The organisation name field is required.' errors: organisation_name: - 'The organisation name field is required.' properties: message: type: string example: 'The organisation name field is required.' errors: type: object properties: organisation_name: type: array example: - 'The organisation name field is required.' items: type: string tags: - Onboarding requestBody: required: true content: application/json: schema: type: object properties: organisation_name: type: string description: 'The organisation/company name.' example: 'Acme Inc' product_name: type: string description: 'The product name.' example: 'Acme CRM' product_website: type: string description: 'The product website URL.' example: 'https://acme.com' product_logo: type: string description: 'The product logo URL.' example: 'https://cdn.brandfetch.io/acme.com/fallback/lettermark/icon?c=BRANDFETCH_CLIENT_ID' review_platforms: type: object description: 'Review platform settings with enabled status and URLs.' example: g2: enabled: true url: 'https://g2.com/products/acme' capterra: enabled: false url: null properties: { } reddit_keywords: type: array description: 'Reddit keyword variations to track.' example: - acme - 'acme crm' items: type: string reddit_brand_name: type: string description: 'The brand name for Reddit tracking.' example: Acme platforms: type: array description: 'optional When provided, updates social listening platforms (non-empty).' example: - reddit - github - x items: type: string negative_keywords: type: array description: 'optional Keywords to exclude.' example: - spam - competitor items: type: string threshold_consumption_date: type: string description: 'optional Threshold consumption preset; recalculates stored date when provided.' example: last_6_months required: - organisation_name - product_name /api/platform-urls: post: summary: 'Search Platform URLs' operationId: searchPlatformURLs description: "Search for a product's listing pages across multiple review platforms using Perplexity AI-powered web search.\nProduct name used for lookup is derived from the product_website domain label.\nEach URL is validated and assigned a status: valid, invalid, or needs_review." parameters: [] responses: 200: description: '' content: application/json: schema: oneOf: - description: 'All platforms' type: object example: product_name: Slack product_website: 'https://slack.com' reviews: g2: url: 'https://www.g2.com/products/slack/reviews' status: valid capterra: url: 'https://www.capterra.com/p/135003/Slack/' status: valid software_advice: url: 'https://www.softwareadvice.com/team-communication/slack-profile/' status: needs_review trustpilot: url: 'https://www.trustpilot.com/review/slack.com' status: valid omr_reviews: url: 'https://omr.com/reviews/product/slack' status: valid clutch: url: null status: invalid sourceforge: url: 'https://sourceforge.net/software/product/Slack/' status: valid product_hunt: url: 'https://www.producthunt.com/products/slack' status: valid hubspot_directory: url: null status: invalid goodfirms: url: 'https://www.goodfirms.co/software/slack' status: valid chrome_web_store: url: null status: invalid google_workspace_marketplace: url: 'https://workspace.google.com/marketplace/app/slack/429783454934' status: needs_review app_store: url: null status: invalid play_store: url: null status: invalid subscribed_fyi: url: 'https://subscribed.fyi/slack/reviews/' status: valid properties: product_name: type: string example: Slack product_website: type: string example: 'https://slack.com' reviews: type: object properties: g2: type: object properties: url: type: string example: 'https://www.g2.com/products/slack/reviews' status: type: string example: valid capterra: type: object properties: url: type: string example: 'https://www.capterra.com/p/135003/Slack/' status: type: string example: valid software_advice: type: object properties: url: type: string example: 'https://www.softwareadvice.com/team-communication/slack-profile/' status: type: string example: needs_review trustpilot: type: object properties: url: type: string example: 'https://www.trustpilot.com/review/slack.com' status: type: string example: valid omr_reviews: type: object properties: url: type: string example: 'https://omr.com/reviews/product/slack' status: type: string example: valid clutch: type: object properties: url: type: string example: null status: type: string example: invalid sourceforge: type: object properties: url: type: string example: 'https://sourceforge.net/software/product/Slack/' status: type: string example: valid product_hunt: type: object properties: url: type: string example: 'https://www.producthunt.com/products/slack' status: type: string example: valid hubspot_directory: type: object properties: url: type: string example: null status: type: string example: invalid goodfirms: type: object properties: url: type: string example: 'https://www.goodfirms.co/software/slack' status: type: string example: valid chrome_web_store: type: object properties: url: type: string example: null status: type: string example: invalid google_workspace_marketplace: type: object properties: url: type: string example: 'https://workspace.google.com/marketplace/app/slack/429783454934' status: type: string example: needs_review app_store: type: object properties: url: type: string example: null status: type: string example: invalid play_store: type: object properties: url: type: string example: null status: type: string example: invalid subscribed_fyi: type: object properties: url: type: string example: 'https://subscribed.fyi/slack/reviews/' status: type: string example: valid - description: 'Filtered by platform' type: object example: product_name: Slack product_website: 'https://slack.com' reviews: g2: url: 'https://www.g2.com/products/slack/reviews' status: valid properties: product_name: type: string example: Slack product_website: type: string example: 'https://slack.com' reviews: type: object properties: g2: type: object properties: url: type: string example: 'https://www.g2.com/products/slack/reviews' status: type: string example: valid 422: description: 'Validation error' content: application/json: schema: type: object example: message: 'The product name field is required.' errors: product_name: - 'The product name field is required.' properties: message: type: string example: 'The product name field is required.' errors: type: object properties: product_name: type: array example: - 'The product name field is required.' items: type: string tags: - Onboarding requestBody: required: true content: application/json: schema: type: object properties: product_name: type: string description: 'Accepted for backward compatibility. If URL parsing fails, this value is used as fallback.' example: Slack product_website: type: string description: "The product's website URL." example: 'https://slack.com' platform: type: string description: "Optional platform filter. If provided, only returns that platform's URL. Valid values: g2, capterra, software_advice, trustpilot, omr_reviews, clutch, sourceforge, product_hunt, hubspot_directory, goodfirms, chrome_web_store, google_workspace_marketplace, play_store, app_store, subscribed_fyi, google_reviews." example: g2 nullable: true required: - product_name - product_website /api/reddit-keywords: post: summary: 'Generate Reddit Keywords' operationId: generateRedditKeywords description: 'Generate commonly used variations of a brand name for Reddit tracking, including misspellings, abbreviations, and nicknames.' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: brand_name: Salesforce top_variations: exact: Salesforce misspellings: - Salesfoce - Salseforce - Saleforce abbreviations: - SF - SFDC nicknames: - 'The Force' - 'SF CRM' - 'Sales Cloud' properties: brand_name: type: string example: Salesforce top_variations: type: object properties: exact: type: string example: Salesforce misspellings: type: array example: - Salesfoce - Salseforce - Saleforce items: type: string abbreviations: type: array example: - SF - SFDC items: type: string nicknames: type: array example: - 'The Force' - 'SF CRM' - 'Sales Cloud' items: type: string 422: description: 'Validation error' content: application/json: schema: type: object example: message: 'The brand name field is required.' errors: brand_name: - 'The brand name field is required.' properties: message: type: string example: 'The brand name field is required.' errors: type: object properties: brand_name: type: array example: - 'The brand name field is required.' items: type: string tags: - Onboarding requestBody: required: true content: application/json: schema: type: object properties: brand_name: type: string description: 'The brand name to generate variations for.' example: Salesforce required: - brand_name /api/organisations: get: summary: 'List Organisations' operationId: listOrganisations description: 'Get all organisations the authenticated user belongs to.' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: - uuid: 550e8400-e29b-41d4-a716-446655440000 name: 'Acme Inc' website: 'https://acme.com' role: organisation_owner created_at: '2025-12-10T10:00:00.000000Z' updated_at: '2025-12-10T10:00:00.000000Z' properties: data: type: array example: - uuid: 550e8400-e29b-41d4-a716-446655440000 name: 'Acme Inc' website: 'https://acme.com' role: organisation_owner created_at: '2025-12-10T10:00:00.000000Z' updated_at: '2025-12-10T10:00:00.000000Z' items: type: object properties: uuid: type: string example: 550e8400-e29b-41d4-a716-446655440000 name: type: string example: 'Acme Inc' website: type: string example: 'https://acme.com' role: type: string example: organisation_owner created_at: type: string example: '2025-12-10T10:00:00.000000Z' updated_at: type: string example: '2025-12-10T10:00:00.000000Z' tags: - Organisations post: summary: 'Create Organisation' operationId: createOrganisation description: 'Create a new organisation. The authenticated user becomes the admin.' parameters: [] responses: 201: description: '' content: application/json: schema: type: object example: message: 'Organisation created successfully.' data: uuid: 550e8400-e29b-41d4-a716-446655440000 name: 'Acme Inc' website: 'https://acme.com' role: organisation_owner created_at: '2025-12-10T10:00:00.000000Z' updated_at: '2025-12-10T10:00:00.000000Z' properties: message: type: string example: 'Organisation created successfully.' data: type: object properties: uuid: type: string example: 550e8400-e29b-41d4-a716-446655440000 name: type: string example: 'Acme Inc' website: type: string example: 'https://acme.com' role: type: string example: organisation_owner created_at: type: string example: '2025-12-10T10:00:00.000000Z' updated_at: type: string example: '2025-12-10T10:00:00.000000Z' tags: - Organisations requestBody: required: true content: application/json: schema: type: object properties: name: type: string description: 'The organisation name.' example: 'Acme Inc' website: type: string description: 'optional The organisation website URL.' example: 'https://acme.com' nullable: true required: - name '/api/organisations/{uuid}': get: summary: 'Get Organisation' operationId: getOrganisation description: 'Get details of a specific organisation.' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: uuid: 550e8400-e29b-41d4-a716-446655440000 name: 'Acme Inc' website: 'https://acme.com' role: organisation_owner created_at: '2025-12-10T10:00:00.000000Z' updated_at: '2025-12-10T10:00:00.000000Z' properties: data: type: object properties: uuid: type: string example: 550e8400-e29b-41d4-a716-446655440000 name: type: string example: 'Acme Inc' website: type: string example: 'https://acme.com' role: type: string example: organisation_owner created_at: type: string example: '2025-12-10T10:00:00.000000Z' updated_at: type: string example: '2025-12-10T10:00:00.000000Z' 403: description: 'No access' content: application/json: schema: type: object example: message: 'You do not have access to this organisation.' properties: message: type: string example: 'You do not have access to this organisation.' tags: - Organisations put: summary: 'Update Organisation' operationId: updateOrganisation description: "Update an organisation's details. Requires admin role." parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: message: 'Organisation updated successfully.' data: uuid: 550e8400-e29b-41d4-a716-446655440000 name: 'Acme Corp' website: 'https://acme.com' role: organisation_owner created_at: '2025-12-10T10:00:00.000000Z' updated_at: '2025-12-10T10:00:00.000000Z' properties: message: type: string example: 'Organisation updated successfully.' data: type: object properties: uuid: type: string example: 550e8400-e29b-41d4-a716-446655440000 name: type: string example: 'Acme Corp' website: type: string example: 'https://acme.com' role: type: string example: organisation_owner created_at: type: string example: '2025-12-10T10:00:00.000000Z' updated_at: type: string example: '2025-12-10T10:00:00.000000Z' 403: description: 'Not admin' content: application/json: schema: type: object example: message: 'You must be an organisation admin to perform this action.' properties: message: type: string example: 'You must be an organisation admin to perform this action.' tags: - Organisations requestBody: required: true content: application/json: schema: type: object properties: name: type: string description: 'The organisation name.' example: 'Acme Corp' website: type: string description: 'optional The organisation website URL.' example: 'https://acme.com' nullable: true required: - name delete: summary: 'Delete Organisation' operationId: deleteOrganisation description: "Delete an organisation and its projects (cascaded). Requires organisation owner role.\nCannot delete the authenticated user's current (primary) workspace; switch first." parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: message: 'Organisation deleted successfully.' properties: message: type: string example: 'Organisation deleted successfully.' 403: description: '' content: application/json: schema: oneOf: - description: 'No access' type: object example: message: 'You do not have access to this organisation.' properties: message: type: string example: 'You do not have access to this organisation.' - description: 'Not owner' type: object example: message: 'You must be an organisation admin to perform this action.' properties: message: type: string example: 'You must be an organisation admin to perform this action.' 422: description: 'Current workspace' content: application/json: schema: type: object example: message: 'You cannot delete your current workspace. Switch to another organisation first.' properties: message: type: string example: 'You cannot delete your current workspace. Switch to another organisation first.' tags: - Organisations parameters: - in: path name: uuid description: '' example: 2958b10e-6a80-4903-868f-79b048b5e0c4 required: true schema: type: string - in: path name: organisation description: 'The organisation UUID.' example: 550e8400-e29b-41d4-a716-446655440000 required: true schema: type: string '/api/organisations/{organisation_uuid}/switch': post: summary: 'Switch Current Organisation' operationId: switchCurrentOrganisation description: "Set the user's current organisation for subsequent requests." parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: message: 'Switched to organisation successfully.' data: uuid: 550e8400-e29b-41d4-a716-446655440000 name: 'Acme Inc' properties: message: type: string example: 'Switched to organisation successfully.' data: type: object properties: uuid: type: string example: 550e8400-e29b-41d4-a716-446655440000 name: type: string example: 'Acme Inc' 403: description: 'No access' content: application/json: schema: type: object example: message: 'You do not have access to this organisation.' properties: message: type: string example: 'You do not have access to this organisation.' tags: - Organisations parameters: - in: path name: organisation_uuid description: '' example: 2958b10e-6a80-4903-868f-79b048b5e0c4 required: true schema: type: string - in: path name: organisation description: 'The organisation UUID.' example: 550e8400-e29b-41d4-a716-446655440000 required: true schema: type: string /api/platforms: get: summary: 'List All Platforms' operationId: listAllPlatforms description: 'Get all platforms.' parameters: [] responses: 200: description: '' content: application/json: schema: type: array items: type: object properties: uuid: type: string example: 550e8400-e29b-41d4-a716-446655440000 name: type: string example: G2 created_at: type: string example: '2026-01-08T10:00:00.000000Z' updated_at: type: string example: '2026-01-08T10:00:00.000000Z' example: - uuid: 550e8400-e29b-41d4-a716-446655440000 name: G2 created_at: '2026-01-08T10:00:00.000000Z' updated_at: '2026-01-08T10:00:00.000000Z' tags: - Platforms post: summary: 'Create Platform' operationId: createPlatform description: 'Create a new platform.' parameters: [] responses: 201: description: '' content: application/json: schema: type: object example: message: 'Platform created successfully.' data: uuid: 550e8400-e29b-41d4-a716-446655440000 name: G2 created_at: '2026-01-08T10:00:00.000000Z' updated_at: '2026-01-08T10:00:00.000000Z' properties: message: type: string example: 'Platform created successfully.' data: type: object properties: uuid: type: string example: 550e8400-e29b-41d4-a716-446655440000 name: type: string example: G2 created_at: type: string example: '2026-01-08T10:00:00.000000Z' updated_at: type: string example: '2026-01-08T10:00:00.000000Z' 422: description: 'Validation Error' content: application/json: schema: type: object example: message: 'The name has already been taken.' errors: name: - 'The name has already been taken.' properties: message: type: string example: 'The name has already been taken.' errors: type: object properties: name: type: array example: - 'The name has already been taken.' items: type: string tags: - Platforms requestBody: required: true content: application/json: schema: type: object properties: name: type: string description: 'The platform name.' example: G2 required: - name '/api/platforms/{uuid}': patch: summary: 'Update Platform' operationId: updatePlatform description: "Update a platform's details." parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: message: 'Platform updated successfully.' data: uuid: 550e8400-e29b-41d4-a716-446655440000 name: Capterra created_at: '2026-01-08T10:00:00.000000Z' updated_at: '2026-01-08T10:30:00.000000Z' properties: message: type: string example: 'Platform updated successfully.' data: type: object properties: uuid: type: string example: 550e8400-e29b-41d4-a716-446655440000 name: type: string example: Capterra created_at: type: string example: '2026-01-08T10:00:00.000000Z' updated_at: type: string example: '2026-01-08T10:30:00.000000Z' 404: description: 'Not Found' content: application/json: schema: type: object example: message: 'Platform not found.' properties: message: type: string example: 'Platform not found.' 422: description: 'Validation Error' content: application/json: schema: type: object example: message: 'The name has already been taken.' errors: name: - 'The name has already been taken.' properties: message: type: string example: 'The name has already been taken.' errors: type: object properties: name: type: array example: - 'The name has already been taken.' items: type: string tags: - Platforms requestBody: required: true content: application/json: schema: type: object properties: name: type: string description: 'The platform name.' example: Capterra required: - name delete: summary: 'Delete Platform' operationId: deletePlatform description: 'Delete a platform.' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: message: 'Platform deleted successfully.' properties: message: type: string example: 'Platform deleted successfully.' 404: description: 'Not Found' content: application/json: schema: type: object example: message: 'Platform not found.' properties: message: type: string example: 'Platform not found.' tags: - Platforms parameters: - in: path name: uuid description: 'The platform UUID.' example: 550e8400-e29b-41d4-a716-446655440000 required: true schema: type: string /api/products/search: get: summary: 'Search Products' operationId: searchProducts description: 'Search for products in the Curiosity database to claim.' parameters: - in: query name: q description: 'The search query.' example: slack required: true schema: type: string description: 'The search query.' example: slack - in: query name: limit description: 'The maximum number of results (default 10, max 50).' example: 10 required: false schema: type: integer description: 'The maximum number of results (default 10, max 50).' example: 10 responses: 200: description: '' content: application/json: schema: type: object example: data: products: - id: 123 name: Slack url: 'https://slack.com' logo_path: products/slack-logo.png properties: data: type: object properties: products: type: array example: - id: 123 name: Slack url: 'https://slack.com' logo_path: products/slack-logo.png items: type: object properties: id: type: integer example: 123 name: type: string example: Slack url: type: string example: 'https://slack.com' logo_path: type: string example: products/slack-logo.png tags: - Profiles /api/claim-profiles: get: summary: 'List All Claim Profiles' operationId: listAllClaimProfiles description: 'Get all claim profiles across all organisations. Intended for admin panel usage.' parameters: [] responses: 200: description: '' content: application/json: schema: type: array items: type: object properties: uuid: type: string example: 880e8400-e29b-41d4-a716-446655440000 business_email: type: string example: john@company.com job_title: type: string example: 'Product Manager' business_phone: type: string example: '+1234567890' status: type: string example: pending project: type: object properties: uuid: type: string example: 660e8400-e29b-41d4-a716-446655440001 product_name: type: string example: 'Acme App' organisation: type: object properties: uuid: type: string example: 550e8400-e29b-41d4-a716-446655440000 name: type: string example: 'Acme Corp' status_updated_by: type: string example: null created_at: type: string example: '2026-01-03T12:00:00.000000Z' updated_at: type: string example: '2026-01-03T12:00:00.000000Z' example: - uuid: 880e8400-e29b-41d4-a716-446655440000 business_email: john@company.com job_title: 'Product Manager' business_phone: '+1234567890' status: pending project: uuid: 660e8400-e29b-41d4-a716-446655440001 product_name: 'Acme App' organisation: uuid: 550e8400-e29b-41d4-a716-446655440000 name: 'Acme Corp' status_updated_by: null created_at: '2026-01-03T12:00:00.000000Z' updated_at: '2026-01-03T12:00:00.000000Z' tags: - Profiles post: summary: 'Create Claim Profile' operationId: createClaimProfile description: "Create a new claim profile for a project. The organisation is automatically\nderived from the authenticated user's current organisation context." parameters: [] responses: 201: description: Success content: application/json: schema: type: object example: message: 'Claim profile created successfully.' data: uuid: 880e8400-e29b-41d4-a716-446655440000 scraper_product_id: 123 product_name: Slack product_url: 'https://slack.com' business_email: john@company.com job_title: 'Product Manager' business_phone: '+1234567890' status: pending created_at: '2026-01-03T12:00:00.000000Z' updated_at: '2026-01-03T12:00:00.000000Z' properties: message: type: string example: 'Claim profile created successfully.' data: type: object properties: uuid: type: string example: 880e8400-e29b-41d4-a716-446655440000 scraper_product_id: type: integer example: 123 product_name: type: string example: Slack product_url: type: string example: 'https://slack.com' business_email: type: string example: john@company.com job_title: type: string example: 'Product Manager' business_phone: type: string example: '+1234567890' status: type: string example: pending created_at: type: string example: '2026-01-03T12:00:00.000000Z' updated_at: type: string example: '2026-01-03T12:00:00.000000Z' 403: description: 'No organisation context' content: application/json: schema: type: object example: message: 'This action is unauthorized.' properties: message: type: string example: 'This action is unauthorized.' 422: description: 'Validation Error' content: application/json: schema: type: object example: message: 'The project is required. (and 1 more error)' errors: project_id: - 'The project is required.' business_email: - 'Please use a business email address.' properties: message: type: string example: 'The project is required. (and 1 more error)' errors: type: object properties: project_id: type: array example: - 'The project is required.' items: type: string business_email: type: array example: - 'Please use a business email address.' items: type: string tags: - Profiles requestBody: required: true content: application/json: schema: type: object properties: project_id: type: string description: 'The project UUID.' example: 660e8400-e29b-41d4-a716-446655440001 scraper_product_id: type: integer description: 'The ID of the product from Curiosity.' example: 123 product_name: type: string description: 'The name of the product being claimed.' example: Slack product_url: type: string description: 'The URL of the product.' example: 'https://slack.com' nullable: true business_email: type: string description: 'The business email address.' example: john@company.com job_title: type: string description: 'The job title.' example: 'Product Manager' business_phone: type: string description: 'The business phone number (optional).' example: '+1234567890' nullable: true required: - project_id - scraper_product_id - product_name - business_email - job_title '/api/projects/{project_uuid}/profile': get: summary: 'Get Product Profile' operationId: getProductProfile description: "Get the product profile for a specific project.\nReturns all profile data including content, SEO meta, categories, and sync status." parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: uuid: 770e8400-e29b-41d4-a716-446655440002 curiosity_product_id: 123 name: Slack url: 'https://slack.com' logo_path: products/slack-logo.png local_logo: null subtitle: '

Where work happens

' overview: '

Slack is a messaging platform...

' pricing: '

Free, Pro $7.25/user, Business+ $12.50/user

' verified_badge: true parent_category: id: 5 name: Communication review_platforms: g2: enabled: true url: 'https://g2.com/products/slack' score: 4.5 reviews_count: 120 capterra: enabled: true url: 'https://capterra.com/p/123/slack' score: 4.6 reviews_count: 85 trustpilot: score: 3.6 reviews_count: 11218 categories: - id: 1 name: Communication segments: - id: 1 name: Enterprise search_fields: built_for: - id: 1 name: 'Marketing Teams' platform: - id: 2 name: Web pricing_model: - id: 3 name: Subscription competitors: - id: 456 name: 'Microsoft Teams' url: 'https://teams.microsoft.com' logo_path: null videos: - 'https://www.youtube.com/watch?v=abc' awards: - id: 1 name: 'Best Communication Tool 2025' deal: 'Get 20% off annual plans' deals_meta_title: 'Best Slack Deals' deals_meta_description: null cancellation_content: '

To cancel your subscription...

' cancellation_content_summary: '

Cancel anytime from settings

' book_demo_url: 'https://slack.com/demo' pricing_url: 'https://slack.com/pricing' pros_cons: "Pros: Easy to use\nCons: Can be expensive" analysis: '

Detailed analysis of Slack...

' faq: "Q: How much does it cost?\nA: Free tier available" alternatives_text: '

Consider Microsoft Teams or Discord...

' pricing_range: $0-$15/user/mo is_ai_powered: false meta: main_page: title: 'Slack - Where Work Happens' description: 'Team messaging platform' deals: title: 'Slack Deals' description: 'Best Slack discounts' cancellation: title: 'Cancel Slack' description: 'How to cancel' sync_status: synced synced_at: '2026-01-06T10:00:00.000000Z' local_changes_at: null last_change_request: uuid: 880e8400-e29b-41d4-a716-446655440003 status: pending requested_at: '2026-01-06T11:00:00.000000Z' reviewed_at: null created_at: '2026-01-06T09:00:00.000000Z' updated_at: '2026-01-06T10:00:00.000000Z' properties: data: type: object properties: uuid: type: string example: 770e8400-e29b-41d4-a716-446655440002 curiosity_product_id: type: integer example: 123 name: type: string example: Slack url: type: string example: 'https://slack.com' logo_path: type: string example: products/slack-logo.png local_logo: type: string example: null subtitle: type: string example: '

Where work happens

' overview: type: string example: '

Slack is a messaging platform...

' pricing: type: string example: '

Free, Pro $7.25/user, Business+ $12.50/user

' verified_badge: type: boolean example: true parent_category: type: object properties: id: type: integer example: 5 name: type: string example: Communication review_platforms: type: object properties: g2: type: object properties: enabled: type: boolean example: true url: type: string example: 'https://g2.com/products/slack' score: type: number example: 4.5 reviews_count: type: integer example: 120 capterra: type: object properties: enabled: type: boolean example: true url: type: string example: 'https://capterra.com/p/123/slack' score: type: number example: 4.6 reviews_count: type: integer example: 85 trustpilot: type: object properties: score: type: number example: 3.6 reviews_count: type: integer example: 11218 categories: type: array example: - id: 1 name: Communication items: type: object properties: id: type: integer example: 1 name: type: string example: Communication segments: type: array example: - id: 1 name: Enterprise items: type: object properties: id: type: integer example: 1 name: type: string example: Enterprise search_fields: type: object properties: built_for: type: array example: - id: 1 name: 'Marketing Teams' items: type: object properties: id: type: integer example: 1 name: type: string example: 'Marketing Teams' platform: type: array example: - id: 2 name: Web items: type: object properties: id: type: integer example: 2 name: type: string example: Web pricing_model: type: array example: - id: 3 name: Subscription items: type: object properties: id: type: integer example: 3 name: type: string example: Subscription competitors: type: array example: - id: 456 name: 'Microsoft Teams' url: 'https://teams.microsoft.com' logo_path: null items: type: object properties: id: type: integer example: 456 name: type: string example: 'Microsoft Teams' url: type: string example: 'https://teams.microsoft.com' logo_path: type: string example: null videos: type: array example: - 'https://www.youtube.com/watch?v=abc' items: type: string awards: type: array example: - id: 1 name: 'Best Communication Tool 2025' items: type: object properties: id: type: integer example: 1 name: type: string example: 'Best Communication Tool 2025' deal: type: string example: 'Get 20% off annual plans' deals_meta_title: type: string example: 'Best Slack Deals' deals_meta_description: type: string example: null cancellation_content: type: string example: '

To cancel your subscription...

' cancellation_content_summary: type: string example: '

Cancel anytime from settings

' book_demo_url: type: string example: 'https://slack.com/demo' pricing_url: type: string example: 'https://slack.com/pricing' pros_cons: type: string example: "Pros: Easy to use\nCons: Can be expensive" analysis: type: string example: '

Detailed analysis of Slack...

' faq: type: string example: "Q: How much does it cost?\nA: Free tier available" alternatives_text: type: string example: '

Consider Microsoft Teams or Discord...

' pricing_range: type: string example: $0-$15/user/mo is_ai_powered: type: boolean example: false meta: type: object properties: main_page: type: object properties: title: type: string example: 'Slack - Where Work Happens' description: type: string example: 'Team messaging platform' deals: type: object properties: title: type: string example: 'Slack Deals' description: type: string example: 'Best Slack discounts' cancellation: type: object properties: title: type: string example: 'Cancel Slack' description: type: string example: 'How to cancel' sync_status: type: string example: synced synced_at: type: string example: '2026-01-06T10:00:00.000000Z' local_changes_at: type: string example: null last_change_request: type: object properties: uuid: type: string example: 880e8400-e29b-41d4-a716-446655440003 status: type: string example: pending requested_at: type: string example: '2026-01-06T11:00:00.000000Z' reviewed_at: type: string example: null created_at: type: string example: '2026-01-06T09:00:00.000000Z' updated_at: type: string example: '2026-01-06T10:00:00.000000Z' 404: description: '' content: application/json: schema: oneOf: - description: '' type: object example: message: 'Project not found.' properties: message: type: string example: 'Project not found.' - description: '' type: object example: message: 'This project does not have a product profile yet.' properties: message: type: string example: 'This project does not have a product profile yet.' tags: - Profiles put: summary: 'Update Product Profile' operationId: updateProductProfile description: "Submit a change request for the product profile. Changes are stored as a pending\nrequest for admin review. Once approved, changes will be applied and synced to Curiosity.\nFields like subtitle, overview, pricing, analysis, and alternatives_text support HTML/richtext." parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: message: 'Change request submitted for review.' data: change_request_uuid: 880e8400-e29b-41d4-a716-446655440003 status: pending properties: message: type: string example: 'Change request submitted for review.' data: type: object properties: change_request_uuid: type: string example: 880e8400-e29b-41d4-a716-446655440003 status: type: string example: pending 404: description: '' content: application/json: schema: type: object example: message: 'This project does not have a product profile yet.' properties: message: type: string example: 'This project does not have a product profile yet.' tags: - Profiles requestBody: required: false content: application/json: schema: type: object properties: subtitle: type: string description: 'The product tagline (HTML supported).' example: '

Where work happens

' overview: type: string description: 'The product description (HTML supported).' example: '

Slack is a messaging platform...

' pricing: type: string description: 'Pricing information (HTML supported).' example: '

Free, Pro $7.25/user

' parent_category_id: type: integer description: 'The parent category ID from Curiosity.' example: 5 review_platforms: type: object description: 'Review platform URLs and data.' example: [] properties: g2: type: object description: '' example: url: 'https://g2.com/products/slack' properties: url: type: string description: 'G2 review page URL.' example: 'https://g2.com/products/slack' enabled: type: boolean description: 'Whether G2 is enabled.' example: true capterra: type: object description: '' example: url: 'https://capterra.com/p/123/slack' properties: url: type: string description: 'Capterra review page URL.' example: 'https://capterra.com/p/123/slack' enabled: type: boolean description: 'Whether Capterra is enabled.' example: true videos: type: array description: 'List of video URLs.' example: - architecto items: type: string categories: type: array description: 'List of category objects with id and name.' example: - [] items: type: object segments: type: array description: 'List of segment objects with id and name.' example: - [] items: type: object search_fields: type: object description: 'Search field categorization by type (built_for, platform, pricing_model arrays).' example: [] properties: { } competitors: type: array description: 'List of competitor products with id, name, url, logo_path.' example: - [] items: type: object deal: type: string description: 'Deal content/description.' example: architecto deals_meta_title: type: string description: 'SEO meta title for deals page.' example: 'Best Slack Deals 2026' deals_meta_description: type: string description: 'SEO meta description for deals page.' example: architecto cancellation_content: type: string description: 'How to cancel subscription content (HTML supported).' example: architecto cancellation_content_summary: type: string description: 'Summary of cancellation info (HTML supported).' example: architecto book_demo_url: type: string description: 'URL to book a demo.' example: 'https://slack.com/demo' pricing_url: type: string description: 'URL to pricing page.' example: 'https://slack.com/pricing' pros_cons: type: string description: 'Product pros and cons.' example: architecto analysis: type: string description: 'Detailed product analysis (HTML supported).' example: architecto faq: type: string description: 'Frequently asked questions.' example: architecto alternatives_text: type: string description: 'Description of alternatives (HTML supported).' example: architecto pricing_range: type: string description: 'Price tier display.' example: $99-$999/mo is_ai_powered: type: boolean description: 'Whether product uses AI technology.' example: true meta: type: object description: 'SEO meta tags for various pages.' example: [] properties: main_page: type: object description: '' example: title: 'Slack - Where Work Happens' properties: title: type: string description: 'Main page meta title.' example: 'Slack - Where Work Happens' description: type: string description: 'Main page meta description.' example: 'Eius et animi quos velit et.' deals: type: object description: '' example: title: architecto properties: title: type: string description: 'Deals page meta title.' example: architecto description: type: string description: 'Deals page meta description.' example: 'Eius et animi quos velit et.' cancellation: type: object description: '' example: title: architecto properties: title: type: string description: 'Cancellation page meta title.' example: architecto description: type: string description: 'Cancellation page meta description.' example: 'Eius et animi quos velit et.' parameters: - in: path name: project_uuid description: 'The project UUID.' example: 660e8400-e29b-41d4-a716-446655440001 required: true schema: type: string '/api/projects/{project_uuid}/profile/logo': post: summary: 'Upload Product Logo' operationId: uploadProductLogo description: "Upload a new logo image for the product profile. The logo is stored locally\nand will be synced to Curiosity when you push changes." parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: message: 'Logo uploaded successfully.' data: local_logo: product-logos/abc123.png sync_status: local_changes local_changes_at: '2026-01-06T11:00:00.000000Z' properties: message: type: string example: 'Logo uploaded successfully.' data: type: object properties: local_logo: type: string example: product-logos/abc123.png sync_status: type: string example: local_changes local_changes_at: type: string example: '2026-01-06T11:00:00.000000Z' 404: description: '' content: application/json: schema: type: object example: message: 'This project does not have a product profile yet.' properties: message: type: string example: 'This project does not have a product profile yet.' 422: description: '' content: application/json: schema: type: object example: message: 'The logo field is required.' errors: logo: - 'The logo field is required.' properties: message: type: string example: 'The logo field is required.' errors: type: object properties: logo: type: array example: - 'The logo field is required.' items: type: string tags: - Profiles requestBody: required: true content: multipart/form-data: schema: type: object properties: logo: type: string format: binary description: 'The logo image file (max 5MB, must be an image).' required: - logo parameters: - in: path name: project_uuid description: 'The project UUID.' example: 660e8400-e29b-41d4-a716-446655440001 required: true schema: type: string '/api/projects/{project_uuid}/profile/status': get: summary: 'Get Sync Status' operationId: getSyncStatus description: "Get the current sync status of a project's product profile." parameters: [] responses: 200: description: '' content: application/json: schema: oneOf: - description: '' type: object example: data: has_profile: true has_curiosity_link: true sync_status: local_changes synced_at: '2026-01-06T10:00:00.000000Z' local_changes_at: '2026-01-06T11:00:00.000000Z' last_change_request: uuid: 880e8400-e29b-41d4-a716-446655440003 status: pending requested_at: '2026-01-06T11:00:00.000000Z' reviewed_at: null properties: data: type: object properties: has_profile: type: boolean example: true has_curiosity_link: type: boolean example: true sync_status: type: string example: local_changes synced_at: type: string example: '2026-01-06T10:00:00.000000Z' local_changes_at: type: string example: '2026-01-06T11:00:00.000000Z' last_change_request: type: object properties: uuid: type: string example: 880e8400-e29b-41d4-a716-446655440003 status: type: string example: pending requested_at: type: string example: '2026-01-06T11:00:00.000000Z' reviewed_at: type: string example: null - description: '' type: object example: data: has_profile: false has_curiosity_link: true sync_status: null synced_at: null local_changes_at: null last_change_request: null properties: data: type: object properties: has_profile: type: boolean example: false has_curiosity_link: type: boolean example: true sync_status: type: string example: null synced_at: type: string example: null local_changes_at: type: string example: null last_change_request: type: string example: null tags: - Profiles parameters: - in: path name: project_uuid description: 'The project UUID.' example: 660e8400-e29b-41d4-a716-446655440001 required: true schema: type: string /api/catalog/parent-categories: get: summary: 'List Parent Categories' operationId: listParentCategories description: "Get all top-level parent categories for product classification.\nParent categories represent broad product domains." parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: - id: 1 name: Analytics - id: 2 name: Communication - id: 3 name: Marketing - id: 4 name: 'Project Management' - id: 5 name: Sales properties: data: type: array example: - id: 1 name: Analytics - id: 2 name: Communication - id: 3 name: Marketing - id: 4 name: 'Project Management' - id: 5 name: Sales items: type: object properties: id: type: integer example: 1 name: type: string example: Analytics tags: - Profiles /api/catalog/categories: get: summary: 'List Categories' operationId: listCategories description: "Get all detailed categories for product classification.\nCategories are more specific than parent categories and can be assigned to products." parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: - id: 1 name: 'Video Conferencing' - id: 2 name: 'Team Chat' - id: 3 name: 'Email Marketing' - id: 4 name: CRM - id: 5 name: 'Task Management' properties: data: type: array example: - id: 1 name: 'Video Conferencing' - id: 2 name: 'Team Chat' - id: 3 name: 'Email Marketing' - id: 4 name: CRM - id: 5 name: 'Task Management' items: type: object properties: id: type: integer example: 1 name: type: string example: 'Video Conferencing' tags: - Profiles /api/catalog/segments: get: summary: 'List Segments' operationId: listSegments description: "Get all market segments for product targeting.\nSegments define the target audience or market size for products." parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: - id: 1 name: Enterprise - id: 2 name: Mid-Market - id: 3 name: SMB - id: 4 name: Startup - id: 5 name: Freelancer properties: data: type: array example: - id: 1 name: Enterprise - id: 2 name: Mid-Market - id: 3 name: SMB - id: 4 name: Startup - id: 5 name: Freelancer items: type: object properties: id: type: integer example: 1 name: type: string example: Enterprise tags: - Profiles /api/catalog/search-fields: get: summary: 'List Search Fields' operationId: listSearchFields description: "Get all search field options grouped by type.\nSearch fields are structured attributes used for filtering and discovery.\n\n**Field Types:**\n- `built_for` - Target user roles or teams (e.g., \"Marketing Teams\", \"Developers\")\n- `platform` - Deployment platforms (e.g., \"Web\", \"iOS\", \"Android\", \"Desktop\")\n- `pricing_model` - Business models (e.g., \"Subscription\", \"One-time\", \"Freemium\")" parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: built_for: - id: 1 name: 'Marketing Teams' - id: 2 name: 'Sales Teams' - id: 3 name: Developers - id: 4 name: 'HR Teams' platform: - id: 1 name: Web - id: 2 name: iOS - id: 3 name: Android - id: 4 name: Desktop pricing_model: - id: 1 name: Subscription - id: 2 name: 'One-time Purchase' - id: 3 name: Freemium - id: 4 name: Usage-based properties: data: type: object properties: built_for: type: array example: - id: 1 name: 'Marketing Teams' - id: 2 name: 'Sales Teams' - id: 3 name: Developers - id: 4 name: 'HR Teams' items: type: object properties: id: type: integer example: 1 name: type: string example: 'Marketing Teams' platform: type: array example: - id: 1 name: Web - id: 2 name: iOS - id: 3 name: Android - id: 4 name: Desktop items: type: object properties: id: type: integer example: 1 name: type: string example: Web pricing_model: type: array example: - id: 1 name: Subscription - id: 2 name: 'One-time Purchase' - id: 3 name: Freemium - id: 4 name: Usage-based items: type: object properties: id: type: integer example: 1 name: type: string example: Subscription tags: - Profiles '/api/organisations/{organisation_uuid}/projects': get: summary: 'List Projects' operationId: listProjects description: 'Get all projects in an organisation that the user has access to.' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: - uuid: 660e8400-e29b-41d4-a716-446655440001 product_name: 'Acme App' product_website: 'https://acme.com' review_platforms: g2: enabled: true url: 'https://g2.com/products/acme' reddit_keywords: - acme - 'acme app' negative_keywords: [] created_at: '2025-12-10T10:00:00.000000Z' updated_at: '2025-12-10T10:00:00.000000Z' properties: data: type: array example: - uuid: 660e8400-e29b-41d4-a716-446655440001 product_name: 'Acme App' product_website: 'https://acme.com' review_platforms: g2: enabled: true url: 'https://g2.com/products/acme' reddit_keywords: - acme - 'acme app' negative_keywords: [] created_at: '2025-12-10T10:00:00.000000Z' updated_at: '2025-12-10T10:00:00.000000Z' items: type: object properties: uuid: type: string example: 660e8400-e29b-41d4-a716-446655440001 product_name: type: string example: 'Acme App' product_website: type: string example: 'https://acme.com' review_platforms: type: object properties: g2: type: object properties: enabled: type: boolean example: true url: type: string example: 'https://g2.com/products/acme' reddit_keywords: type: array example: - acme - 'acme app' items: type: string negative_keywords: type: array example: [] created_at: type: string example: '2025-12-10T10:00:00.000000Z' updated_at: type: string example: '2025-12-10T10:00:00.000000Z' tags: - Projects post: summary: 'Create Project' operationId: createProject description: 'Create a new project in an organisation. Requires organisation admin role.' parameters: [] responses: 201: description: '' content: application/json: schema: type: object example: message: 'Project created successfully.' data: uuid: 660e8400-e29b-41d4-a716-446655440001 product_name: 'Acme App' product_website: 'https://acme.com' product_logo: 'https://cdn.brandfetch.io/acme.com/fallback/lettermark/icon?c=BRANDFETCH_CLIENT_ID' review_platforms: { } reddit_keywords: - acme negative_keywords: [] reddit_brand_name: Acme platforms: - reddit - github is_competitor: false created_at: '2025-12-10T10:00:00.000000Z' updated_at: '2025-12-10T10:00:00.000000Z' properties: message: type: string example: 'Project created successfully.' data: type: object properties: uuid: type: string example: 660e8400-e29b-41d4-a716-446655440001 product_name: type: string example: 'Acme App' product_website: type: string example: 'https://acme.com' product_logo: type: string example: 'https://cdn.brandfetch.io/acme.com/fallback/lettermark/icon?c=BRANDFETCH_CLIENT_ID' review_platforms: type: object properties: { } reddit_keywords: type: array example: - acme items: type: string negative_keywords: type: array example: [] reddit_brand_name: type: string example: Acme platforms: type: array example: - reddit - github items: type: string is_competitor: type: boolean example: false created_at: type: string example: '2025-12-10T10:00:00.000000Z' updated_at: type: string example: '2025-12-10T10:00:00.000000Z' tags: - Projects requestBody: required: true content: application/json: schema: type: object properties: product_name: type: string description: 'The product name.' example: 'Acme App' product_website: type: string description: 'The product website URL.' example: 'https://acme.com' product_logo: type: string description: 'The product logo URL.' example: 'https://cdn.brandfetch.io/acme.com/fallback/lettermark/icon?c=BRANDFETCH_CLIENT_ID' review_platforms: type: object description: 'Review platform settings.' example: g2: enabled: true url: 'https://g2.com/products/acme' capterra: enabled: false url: null properties: { } reddit_keywords: type: array description: 'Reddit keyword variations.' example: - acme - 'acme app' items: type: string reddit_brand_name: type: string description: 'The brand name for Reddit tracking.' example: Acme platforms: type: array description: 'optional Social listening platforms for `/v1/brand/create` (non-empty). Defaults to all supported platforms when omitted.' example: - reddit - github - x items: type: string social_platform: type: object description: 'Social platform configuration.' example: twitter: enabled: true url: 'https://twitter.com/acme' properties: { } required: - product_name parameters: - in: path name: organisation_uuid description: '' example: 2958b10e-6a80-4903-868f-79b048b5e0c4 required: true schema: type: string - in: path name: organisation description: 'The organisation UUID.' example: 550e8400-e29b-41d4-a716-446655440000 required: true schema: type: string '/api/organisations/{organisation_uuid}/projects/{uuid}': get: summary: 'Get Project' operationId: getProject description: 'Get details of a specific project.' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: uuid: 660e8400-e29b-41d4-a716-446655440001 product_name: 'Acme App' product_website: 'https://acme.com' review_platforms: g2: enabled: true url: 'https://g2.com/products/acme' reddit_keywords: - acme - 'acme app' negative_keywords: [] created_at: '2025-12-10T10:00:00.000000Z' updated_at: '2025-12-10T10:00:00.000000Z' properties: data: type: object properties: uuid: type: string example: 660e8400-e29b-41d4-a716-446655440001 product_name: type: string example: 'Acme App' product_website: type: string example: 'https://acme.com' review_platforms: type: object properties: g2: type: object properties: enabled: type: boolean example: true url: type: string example: 'https://g2.com/products/acme' reddit_keywords: type: array example: - acme - 'acme app' items: type: string negative_keywords: type: array example: [] created_at: type: string example: '2025-12-10T10:00:00.000000Z' updated_at: type: string example: '2025-12-10T10:00:00.000000Z' tags: - Projects put: summary: 'Update Project' operationId: updateProject description: "Update a project's details. Requires organisation admin role." parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: message: 'Project updated successfully.' data: uuid: 660e8400-e29b-41d4-a716-446655440001 product_name: 'Acme App Pro' product_website: 'https://acme.com' product_logo: 'https://cdn.brandfetch.io/acme.com/fallback/lettermark/icon?c=BRANDFETCH_CLIENT_ID' review_platforms: { } reddit_keywords: - acme - 'acme pro' negative_keywords: [] reddit_brand_name: Acme platforms: - reddit - github is_competitor: false created_at: '2025-12-10T10:00:00.000000Z' updated_at: '2025-12-10T10:00:00.000000Z' properties: message: type: string example: 'Project updated successfully.' data: type: object properties: uuid: type: string example: 660e8400-e29b-41d4-a716-446655440001 product_name: type: string example: 'Acme App Pro' product_website: type: string example: 'https://acme.com' product_logo: type: string example: 'https://cdn.brandfetch.io/acme.com/fallback/lettermark/icon?c=BRANDFETCH_CLIENT_ID' review_platforms: type: object properties: { } reddit_keywords: type: array example: - acme - 'acme pro' items: type: string negative_keywords: type: array example: [] reddit_brand_name: type: string example: Acme platforms: type: array example: - reddit - github items: type: string is_competitor: type: boolean example: false created_at: type: string example: '2025-12-10T10:00:00.000000Z' updated_at: type: string example: '2025-12-10T10:00:00.000000Z' tags: - Projects requestBody: required: true content: application/json: schema: type: object properties: product_name: type: string description: 'The product name.' example: 'Acme App Pro' product_website: type: string description: 'The product website URL.' example: 'https://acme.com' product_logo: type: string description: 'The product logo URL.' example: 'https://cdn.brandfetch.io/acme.com/fallback/lettermark/icon?c=BRANDFETCH_CLIENT_ID' review_platforms: type: object description: 'Review platform settings.' example: g2: enabled: true url: 'https://g2.com/products/acme' capterra: enabled: false url: null properties: { } reddit_keywords: type: array description: 'Reddit keyword variations.' example: - acme - 'acme pro' items: type: string reddit_brand_name: type: string description: 'The brand name for Reddit tracking.' example: Acme platforms: type: array description: 'optional When provided, updates social listening platforms (non-empty).' example: - reddit - github - x items: type: string social_platform: type: object description: 'Social platform configuration.' example: twitter: enabled: true url: 'https://twitter.com/acme' properties: { } required: - product_name delete: summary: 'Delete Project' operationId: deleteProject description: 'Delete a project. Requires organisation admin role.' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: message: 'Project deleted successfully.' properties: message: type: string example: 'Project deleted successfully.' tags: - Projects parameters: - in: path name: organisation_uuid description: '' example: 2958b10e-6a80-4903-868f-79b048b5e0c4 required: true schema: type: string - in: path name: uuid description: '' example: b2b0131c-f2cb-42fa-ad1e-00605ce7c898 required: true schema: type: string - in: path name: organisation description: 'The organisation UUID.' example: 550e8400-e29b-41d4-a716-446655440000 required: true schema: type: string - in: path name: project description: 'The project UUID.' example: 660e8400-e29b-41d4-a716-446655440001 required: true schema: type: string '/api/organisations/{organisation_uuid}/reviews/read': post: summary: 'Mark a review as read for a project' operationId: markAReviewAsReadForAProject description: 'Store that a review (identified by platform + scraper_review_id from the scraper API) is read for the given project.' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: message: 'Review marked as read.' properties: message: type: string example: 'Review marked as read.' 403: description: '' content: application/json: schema: oneOf: - description: '' type: object example: message: 'You do not have access to this organisation.' properties: message: type: string example: 'You do not have access to this organisation.' - description: '' type: object example: message: 'You do not have access to this project.' properties: message: type: string example: 'You do not have access to this project.' 404: description: '' content: application/json: schema: type: object example: message: 'Organisation not found.' properties: message: type: string example: 'Organisation not found.' tags: - 'Read status (reviews and mentions)' requestBody: required: true content: application/json: schema: type: object properties: project_uuid: type: string description: 'The project UUID (must belong to the organisation).' example: 550e8400-e29b-41d4-a716-446655440002 platform: type: string description: 'Platform from scraper response (e.g. G2, Capterra).' example: G2 scraper_review_id: type: integer description: 'The review id from scraper API data[].id.' example: 987654 required: - project_uuid - platform - scraper_review_id parameters: - in: path name: organisation_uuid description: 'The organisation UUID.' example: 660e8400-e29b-41d4-a716-446655440001 required: true schema: type: string '/api/organisations/{organisation_uuid}/reviews/read-status': get: summary: 'Get read status for a review' operationId: getReadStatusForAReview description: 'Check whether a review (identified by platform + scraper_review_id) is marked as read for the given project.' parameters: - in: query name: project_uuid description: 'The project UUID.' example: 550e8400-e29b-41d4-a716-446655440002 required: true schema: type: string description: 'The project UUID.' example: 550e8400-e29b-41d4-a716-446655440002 - in: query name: platform description: 'Platform from scraper (e.g. G2).' example: G2 required: true schema: type: string description: 'Platform from scraper (e.g. G2).' example: G2 - in: query name: scraper_review_id description: 'The review id from scraper API data[].id.' example: 987654 required: true schema: type: integer description: 'The review id from scraper API data[].id.' example: 987654 responses: 200: description: '' content: application/json: schema: oneOf: - description: '' type: object example: read: true properties: read: type: boolean example: true - description: '' type: object example: read: false properties: read: type: boolean example: false 403: description: '' content: application/json: schema: oneOf: - description: '' type: object example: message: 'You do not have access to this organisation.' properties: message: type: string example: 'You do not have access to this organisation.' - description: '' type: object example: message: 'You do not have access to this project.' properties: message: type: string example: 'You do not have access to this project.' 404: description: '' content: application/json: schema: oneOf: - description: '' type: object example: message: 'Organisation not found.' properties: message: type: string example: 'Organisation not found.' - description: '' type: object example: message: 'Project not found.' properties: message: type: string example: 'Project not found.' tags: - 'Read status (reviews and mentions)' requestBody: required: true content: application/json: schema: type: object properties: project_uuid: type: string description: 'Must be a valid UUID. The uuid of an existing record in the projects table.' example: 6ff8f7f6-1eb3-3525-be4a-3932c805afed platform: type: string description: 'Must not be greater than 255 characters.' example: g scraper_review_id: type: integer description: 'Must be at least 1.' example: 66 required: - project_uuid - platform - scraper_review_id parameters: - in: path name: organisation_uuid description: 'The organisation UUID.' example: 660e8400-e29b-41d4-a716-446655440001 required: true schema: type: string '/api/organisations/{organisation_uuid}/reviews/read-ids': get: summary: 'Get list of read review IDs for multiple projects' operationId: getListOfReadReviewIDsForMultipleProjects description: "Returns scraper review IDs (data[].id) per project. Use with optional platform filter.\nFrontend can map by project_uuid to show read/unread state per brand." parameters: - in: query name: project_uuids description: 'Array of project UUIDs.' example: - 550e8400-e29b-41d4-a716-446655440002 - 660e8400-e29b-41d4-a716-446655440003 required: true schema: type: array description: 'Array of project UUIDs.' example: - 550e8400-e29b-41d4-a716-446655440002 - 660e8400-e29b-41d4-a716-446655440003 items: type: string - in: query name: 'project_uuids[]' description: 'A project UUID (must belong to the organisation).' example: 550e8400-e29b-41d4-a716-446655440002 required: true schema: type: string description: 'A project UUID (must belong to the organisation).' example: 550e8400-e29b-41d4-a716-446655440002 - in: query name: platform description: 'optional Filter by platform. If omitted, returns all read review IDs per project.' example: G2 required: false schema: type: string description: 'optional Filter by platform. If omitted, returns all read review IDs per project.' example: G2 responses: 200: description: '' content: application/json: schema: type: object example: data: - project_uuid: 550e8400-e29b-41d4-a716-446655440002 scraper_review_ids: - 987654 - 987655 - project_uuid: 660e8400-e29b-41d4-a716-446655440003 scraper_review_ids: [] properties: data: type: array example: - project_uuid: 550e8400-e29b-41d4-a716-446655440002 scraper_review_ids: - 987654 - 987655 - project_uuid: 660e8400-e29b-41d4-a716-446655440003 scraper_review_ids: [] items: type: object properties: project_uuid: type: string example: 550e8400-e29b-41d4-a716-446655440002 scraper_review_ids: type: array example: - 987654 - 987655 items: type: integer 403: description: '' content: application/json: schema: oneOf: - description: '' type: object example: message: 'You do not have access to this organisation.' properties: message: type: string example: 'You do not have access to this organisation.' - description: '' type: object example: message: 'You do not have access to one or more projects.' properties: message: type: string example: 'You do not have access to one or more projects.' 404: description: '' content: application/json: schema: oneOf: - description: '' type: object example: message: 'Organisation not found.' properties: message: type: string example: 'Organisation not found.' - description: '' type: object example: message: 'One or more projects not found.' properties: message: type: string example: 'One or more projects not found.' tags: - 'Read status (reviews and mentions)' requestBody: required: false content: application/json: schema: type: object properties: project_uuids: type: array description: 'Must be a valid UUID. The uuid of an existing record in the projects table.' example: - 6ff8f7f6-1eb3-3525-be4a-3932c805afed items: type: string platform: type: string description: 'Must not be greater than 255 characters.' example: g nullable: true parameters: - in: path name: organisation_uuid description: 'The organisation UUID.' example: 660e8400-e29b-41d4-a716-446655440001 required: true schema: type: string '/api/organisations/{organisation_uuid}/social/read': post: summary: 'Mark a mention as read for a project' operationId: markAMentionAsReadForAProject description: 'Store that a mention/social post (identified by platform + scraper_post_id from the scraper API) is read for the given project.' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: message: 'Mention marked as read.' properties: message: type: string example: 'Mention marked as read.' 403: description: '' content: application/json: schema: oneOf: - description: '' type: object example: message: 'You do not have access to this organisation.' properties: message: type: string example: 'You do not have access to this organisation.' - description: '' type: object example: message: 'You do not have access to this project.' properties: message: type: string example: 'You do not have access to this project.' 404: description: '' content: application/json: schema: type: object example: message: 'Organisation not found.' properties: message: type: string example: 'Organisation not found.' tags: - 'Read status (reviews and mentions)' requestBody: required: true content: application/json: schema: type: object properties: project_uuid: type: string description: 'The project UUID (must belong to the organisation).' example: 550e8400-e29b-41d4-a716-446655440002 platform: type: string description: 'Platform from scraper response (e.g. reddit, github).' example: reddit scraper_post_id: type: integer description: 'The post id from scraper API data[].id.' example: 98765 required: - project_uuid - platform - scraper_post_id parameters: - in: path name: organisation_uuid description: 'The organisation UUID.' example: 660e8400-e29b-41d4-a716-446655440001 required: true schema: type: string '/api/organisations/{organisation_uuid}/social/read-status': get: summary: 'Get read status for a mention' operationId: getReadStatusForAMention description: 'Check whether a mention (identified by platform + scraper_post_id) is marked as read for the given project.' parameters: - in: query name: project_uuid description: 'The project UUID.' example: 550e8400-e29b-41d4-a716-446655440002 required: true schema: type: string description: 'The project UUID.' example: 550e8400-e29b-41d4-a716-446655440002 - in: query name: platform description: 'Platform from scraper (e.g. reddit).' example: reddit required: true schema: type: string description: 'Platform from scraper (e.g. reddit).' example: reddit - in: query name: scraper_post_id description: 'The post id from scraper API data[].id.' example: 98765 required: true schema: type: integer description: 'The post id from scraper API data[].id.' example: 98765 responses: 200: description: '' content: application/json: schema: oneOf: - description: '' type: object example: read: true properties: read: type: boolean example: true - description: '' type: object example: read: false properties: read: type: boolean example: false 403: description: '' content: application/json: schema: oneOf: - description: '' type: object example: message: 'You do not have access to this organisation.' properties: message: type: string example: 'You do not have access to this organisation.' - description: '' type: object example: message: 'You do not have access to this project.' properties: message: type: string example: 'You do not have access to this project.' 404: description: '' content: application/json: schema: oneOf: - description: '' type: object example: message: 'Organisation not found.' properties: message: type: string example: 'Organisation not found.' - description: '' type: object example: message: 'Project not found.' properties: message: type: string example: 'Project not found.' tags: - 'Read status (reviews and mentions)' requestBody: required: true content: application/json: schema: type: object properties: project_uuid: type: string description: 'Must be a valid UUID. The uuid of an existing record in the projects table.' example: 6ff8f7f6-1eb3-3525-be4a-3932c805afed platform: type: string description: 'Must not be greater than 255 characters.' example: g scraper_post_id: type: integer description: 'Must be at least 1.' example: 66 required: - project_uuid - platform - scraper_post_id parameters: - in: path name: organisation_uuid description: 'The organisation UUID.' example: 660e8400-e29b-41d4-a716-446655440001 required: true schema: type: string '/api/organisations/{organisation_uuid}/social/read-ids': get: summary: 'Get list of read mention IDs for multiple projects' operationId: getListOfReadMentionIDsForMultipleProjects description: "Returns scraper post IDs (data[].id) per project. Use with optional platform filter.\nFrontend can map by project_uuid to show read/unread state per brand." parameters: - in: query name: project_uuids description: 'Array of project UUIDs.' example: - 550e8400-e29b-41d4-a716-446655440002 - 660e8400-e29b-41d4-a716-446655440003 required: true schema: type: array description: 'Array of project UUIDs.' example: - 550e8400-e29b-41d4-a716-446655440002 - 660e8400-e29b-41d4-a716-446655440003 items: type: string - in: query name: 'project_uuids[]' description: 'A project UUID (must belong to the organisation).' example: 550e8400-e29b-41d4-a716-446655440002 required: true schema: type: string description: 'A project UUID (must belong to the organisation).' example: 550e8400-e29b-41d4-a716-446655440002 - in: query name: platform description: 'optional Filter by platform. If omitted, returns all read post IDs per project.' example: reddit required: false schema: type: string description: 'optional Filter by platform. If omitted, returns all read post IDs per project.' example: reddit responses: 200: description: '' content: application/json: schema: type: object example: data: - project_uuid: 550e8400-e29b-41d4-a716-446655440002 scraper_post_ids: - 98765 - 98766 - project_uuid: 660e8400-e29b-41d4-a716-446655440003 scraper_post_ids: [] properties: data: type: array example: - project_uuid: 550e8400-e29b-41d4-a716-446655440002 scraper_post_ids: - 98765 - 98766 - project_uuid: 660e8400-e29b-41d4-a716-446655440003 scraper_post_ids: [] items: type: object properties: project_uuid: type: string example: 550e8400-e29b-41d4-a716-446655440002 scraper_post_ids: type: array example: - 98765 - 98766 items: type: integer 403: description: '' content: application/json: schema: oneOf: - description: '' type: object example: message: 'You do not have access to this organisation.' properties: message: type: string example: 'You do not have access to this organisation.' - description: '' type: object example: message: 'You do not have access to one or more projects.' properties: message: type: string example: 'You do not have access to one or more projects.' 404: description: '' content: application/json: schema: oneOf: - description: '' type: object example: message: 'Organisation not found.' properties: message: type: string example: 'Organisation not found.' - description: '' type: object example: message: 'One or more projects not found.' properties: message: type: string example: 'One or more projects not found.' tags: - 'Read status (reviews and mentions)' requestBody: required: false content: application/json: schema: type: object properties: project_uuids: type: array description: 'Must be a valid UUID. The uuid of an existing record in the projects table.' example: - 6ff8f7f6-1eb3-3525-be4a-3932c805afed items: type: string platform: type: string description: 'Must not be greater than 255 characters.' example: g nullable: true parameters: - in: path name: organisation_uuid description: 'The organisation UUID.' example: 660e8400-e29b-41d4-a716-446655440001 required: true schema: type: string '/api/organisations/{organisation_uuid}/review-views': get: summary: 'Get All Review Views' operationId: getAllReviewViews description: "List all saved review views for the authenticated user in a specific organisation.\nViews are ordered by sort_order and creation date." parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: - uuid: a1b2c3d4-... name: 'Critical Reviews' sort_order: 0 filters: brands: - uuid-1 - uuid-2 platforms: - G2 - Capterra rating_buckets: - 1.0-1.9 - 2.0-2.9 date_range: type: preset value: last_3_months created_at: '2026-01-15T10:30:00.000Z' updated_at: '2026-01-20T14:22:00.000Z' properties: data: type: array example: - uuid: a1b2c3d4-... name: 'Critical Reviews' sort_order: 0 filters: brands: - uuid-1 - uuid-2 platforms: - G2 - Capterra rating_buckets: - 1.0-1.9 - 2.0-2.9 date_range: type: preset value: last_3_months created_at: '2026-01-15T10:30:00.000Z' updated_at: '2026-01-20T14:22:00.000Z' items: type: object properties: uuid: type: string example: a1b2c3d4-... name: type: string example: 'Critical Reviews' sort_order: type: integer example: 0 filters: type: object properties: brands: type: array example: - uuid-1 - uuid-2 items: type: string platforms: type: array example: - G2 - Capterra items: type: string rating_buckets: type: array example: - 1.0-1.9 - 2.0-2.9 items: type: string date_range: type: object properties: type: type: string example: preset value: type: string example: last_3_months created_at: type: string example: '2026-01-15T10:30:00.000Z' updated_at: type: string example: '2026-01-20T14:22:00.000Z' tags: - 'Review Views' post: summary: 'Create Review View' operationId: createReviewView description: 'Create a new saved review view for the authenticated user.' parameters: [] responses: 201: description: '' content: text/plain: schema: type: string example: "{\n \"data\": {\n \"uuid\": \"e5f6g7h8-...\",\n \"name\": \"High Priority Reviews\",\n \"sort_order\": 1,\n \"filters\": {...},\n \"created_at\": \"2026-01-24T10:00:00.000Z\",\n \"updated_at\": \"2026-01-24T10:00:00.000Z\"\n }\n}" tags: - 'Review Views' requestBody: required: true content: application/json: schema: type: object properties: name: type: string description: 'The name of the view. Must be unique per user/organisation.' example: 'High Priority Reviews' filters: type: object description: 'Filter configuration.' example: platforms: - G2 rating_buckets: - 1.0-1.9 properties: brands: type: array description: 'Filter by brands (project UUIDs).' example: - uuid-1 - uuid-2 items: type: string platforms: type: array description: 'Platforms to filter by.' example: - G2 - Capterra items: type: string date_range: type: object description: 'Date range preset.' example: type: preset value: last_3_months properties: type: type: string description: 'This field is required when filters.date_range is present.' example: custom enum: - preset - custom - all_time value: type: string description: 'This field is required when filters.date_range.type is preset.' example: all_time enum: - last_14_days - last_3_months - last_6_months - last_12_months - last_2_years - last_3_years - all_time nullable: true date_range_custom: type: object description: 'Custom date range.' example: start: '2026-01-01' end: '2026-01-24' properties: start: type: string description: 'This field is required when filters.date_range_custom is present. Must be a valid date.' example: '2026-08-27T06:47:12' end: type: string description: 'This field is required when filters.date_range_custom is present. Must be a valid date. Must be a date after or equal to filters.date_range_custom.start.' example: '2052-09-19' nullable: true rating_buckets: type: array description: 'Rating buckets.' example: - '5.0' - 4.0-4.9 items: type: string languages: type: array description: 'Language filters (ISO 639-1 codes).' example: - en - de - es items: type: string read_status: type: string description: 'Read status filter.' example: unread nullable: true search: type: string description: 'Keyword search.' example: 'customer support' nullable: true sort_by: type: string description: 'Sort field. Use creation_date (maps to review_date), review_date, rating, platform, or created_at.' example: creation_date nullable: true sort_direction: type: string description: 'Sort direction.' example: desc nullable: true required: - name - filters parameters: - in: path name: organisation_uuid description: 'The organisation UUID.' example: 660e8400-e29b-41d4-a716-446655440001 required: true schema: type: string '/api/organisations/{organisation_uuid}/review-views/{view_uuid}': get: summary: 'Get Review View' operationId: getReviewView description: 'Get a specific saved review view.' parameters: [] responses: 200: description: '' content: text/plain: schema: type: string example: "{\n \"data\": {\n \"uuid\": \"a1b2c3d4-...\",\n \"name\": \"Critical Reviews\",\n \"sort_order\": 0,\n \"filters\": {...},\n \"created_at\": \"2026-01-15T10:30:00.000Z\",\n \"updated_at\": \"2026-01-20T14:22:00.000Z\"\n }\n}" tags: - 'Review Views' put: summary: 'Update Review View' operationId: updateReviewView description: 'Update an existing saved review view. Can update name and/or filters.' parameters: [] responses: 200: description: '' content: text/plain: schema: type: string example: "{\n \"data\": {\n \"uuid\": \"a1b2c3d4-...\",\n \"name\": \"Critical Reviews - Updated\",\n \"sort_order\": 0,\n \"filters\": {...},\n \"created_at\": \"2026-01-15T10:30:00.000Z\",\n \"updated_at\": \"2026-01-24T11:15:00.000Z\"\n }\n}" tags: - 'Review Views' requestBody: required: false content: application/json: schema: type: object properties: name: type: string description: 'The name of the view.' example: 'Critical Reviews - Updated' filters: type: object description: 'Filter configuration.' example: platforms: - G2 properties: brands: type: array description: 'Must be a valid UUID. The uuid of an existing record in the projects table. The uuid of an existing record in the projects table.' example: - a4855dc5-0acb-33c3-b921-f4291f719ca0 items: type: string platforms: type: array description: '' example: - hubspot_directory items: type: string enum: - g2 - capterra - software_advice - trustpilot - omr_reviews - clutch - sourceforge - product_hunt - hubspot_directory - goodfirms - chrome_web_store - google_workspace_marketplace - play_store - app_store - subscribed_fyi - google_reviews - G2 - Capterra - 'Software Advice' - Trustpilot - 'OMR Reviews' - Clutch - SourceForge - 'Product Hunt' - 'HubSpot Directory' - GoodFirms - 'Chrome Web Store' - 'Google Workspace Marketplace' - 'Play Store' - 'App Store' - Subscribed.FYI - 'Google Reviews' date_range: type: object description: '' example: null properties: type: type: string description: 'This field is required when filters.date_range is present.' example: custom enum: - preset - custom - all_time value: type: string description: 'This field is required when filters.date_range.type is preset.' example: last_3_years enum: - last_14_days - last_3_months - last_6_months - last_12_months - last_2_years - last_3_years - all_time nullable: true date_range_custom: type: object description: '' example: null properties: start: type: string description: 'This field is required when filters.date_range_custom is present. Must be a valid date.' example: '2026-08-27T06:47:12' end: type: string description: 'This field is required when filters.date_range_custom is present. Must be a valid date. Must be a date after or equal to filters.date_range_custom.start.' example: '2052-09-19' nullable: true rating_buckets: type: array description: '' example: - 2.0-2.9 items: type: string enum: - '5.0' - 4.0-4.9 - 3.0-3.9 - 2.0-2.9 - 1.0-1.9 languages: type: array description: '' example: - architecto items: type: string read_status: type: string description: '' example: read enum: - all - read - unread nullable: true search: type: string description: 'Must not be greater than 500 characters.' example: 'n' nullable: true sort_by: type: string description: 'Sort field. Use creation_date (maps to review_date), review_date, rating, platform, or created_at.' example: creation_date nullable: true sort_direction: type: string description: 'Sort direction.' example: desc nullable: true delete: summary: 'Delete Review View' operationId: deleteReviewView description: 'Delete a saved review view.' parameters: [] responses: 204: description: '' content: application/json: schema: type: object example: { } properties: { } tags: - 'Review Views' parameters: - in: path name: organisation_uuid description: 'The organisation UUID.' example: 660e8400-e29b-41d4-a716-446655440001 required: true schema: type: string - in: path name: view_uuid description: 'The view UUID.' example: a1b2c3d4-e5f6-g7h8-i9j0-k1l2m3n4o5p6 required: true schema: type: string /api/integrations/slack/callback: get: summary: 'Slack OAuth Callback' operationId: slackOAuthCallback description: "Handles the OAuth callback from Slack after user authorization.\nExchanges the authorization code for an access token.\nThis endpoint is unauthenticated - uses cached state for auth context." parameters: - in: query name: code description: 'The authorization code from Slack.' example: 123456789.abcdef required: false schema: type: string description: 'The authorization code from Slack.' example: 123456789.abcdef - in: query name: state description: 'The state parameter for CSRF protection.' example: abc123... required: false schema: type: string description: 'The state parameter for CSRF protection.' example: abc123... - in: query name: error description: 'OAuth error if user denied access.' example: access_denied required: false schema: type: string description: 'OAuth error if user denied access.' example: access_denied responses: 302: description: '' content: text/plain: schema: oneOf: - description: Success type: string example: 'Redirects to /dashboard/notification/overview?slack_connected=true&workspace={name}' - description: Error type: string example: 'Redirects to /dashboard/notification/overview?slack_error={error_code}' - description: '' type: string example: "\n\n \n \n \n\n Redirecting to https://app.subsig.com/dashboard/notification/overview?slack_error=architecto\n \n \n Redirecting to https://app.subsig.com/dashboard/notification/overview?slack_error=architecto.\n \n" tags: - 'Slack Integration' requestBody: required: true content: application/json: schema: type: object properties: code: type: string description: 'This field is required when error is not present.' example: architecto state: type: string description: 'Must be 64 characters.' example: ngzmiyvdljnikhwaykcmyuwpwlvqwrsitcpscqldzsnrwtujwvlxjklqppwqbewt error: type: string description: '' example: architecto required: - state security: [] /api/integrations/slack/connect: get: summary: 'Initiate Slack OAuth' operationId: initiateSlackOAuth description: "Starts the OAuth flow to connect a Slack workspace.\nRedirects to Slack's authorization page." parameters: [] responses: 302: description: 'Redirect to Slack' content: text/plain: schema: type: string example: 'Redirects to Slack OAuth page' 400: description: 'Not configured' content: application/json: schema: type: object example: error: configuration message: 'Slack integration is not configured.' properties: error: type: string example: configuration message: type: string example: 'Slack integration is not configured.' 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. 403: description: 'No organisation' content: application/json: schema: type: object example: message: 'Organisation context required.' properties: message: type: string example: 'Organisation context required.' tags: - 'Slack Integration' /api/integrations/slack/status: get: summary: 'Get Slack Connection Status' operationId: getSlackConnectionStatus description: 'Returns the current Slack connection status for the organisation.' parameters: [] responses: 200: description: '' content: application/json: schema: oneOf: - description: Connected type: object example: connected: true team_id: T123456789 team_name: 'My Workspace' scopes: 'chat:write,channels:read' connected_at: '2025-01-01T12:00:00Z' properties: connected: type: boolean example: true team_id: type: string example: T123456789 team_name: type: string example: 'My Workspace' scopes: type: string example: 'chat:write,channels:read' connected_at: type: string example: '2025-01-01T12:00:00Z' - description: 'Not connected' type: object example: connected: false properties: connected: type: boolean example: false - description: 'Invalid token' type: object example: connected: false error: token_invalid message: 'Slack connection needs to be re-authorized.' properties: connected: type: boolean example: false error: type: string example: token_invalid message: type: string example: 'Slack connection needs to be re-authorized.' 403: description: 'No organisation' content: application/json: schema: type: object example: message: 'Organisation context required.' properties: message: type: string example: 'Organisation context required.' tags: - 'Slack Integration' /api/integrations/slack/disconnect: post: summary: 'Disconnect Slack' operationId: disconnectSlack description: 'Removes the Slack workspace connection for the organisation.' parameters: [] responses: 200: description: '' content: application/json: schema: oneOf: - description: Success type: object example: success: true message: 'Slack connection removed.' properties: success: type: boolean example: true message: type: string example: 'Slack connection removed.' - description: 'Not connected' type: object example: success: true message: 'No Slack connection found.' properties: success: type: boolean example: true message: type: string example: 'No Slack connection found.' 403: description: 'No organisation' content: application/json: schema: type: object example: message: 'Organisation context required.' properties: message: type: string example: 'Organisation context required.' tags: - 'Slack Integration' /api/integrations/slack/channels: get: summary: 'List Slack Channels' operationId: listSlackChannels description: 'Fetches the list of channels from the connected Slack workspace.' parameters: [] responses: 200: description: Success content: application/json: schema: type: object example: ok: true channels: - id: C123456789 name: general is_member: true - id: C987654321 name: random is_member: false properties: ok: type: boolean example: true channels: type: array example: - id: C123456789 name: general is_member: true - id: C987654321 name: random is_member: false items: type: object properties: id: type: string example: C123456789 name: type: string example: general is_member: type: boolean example: true 401: description: '' content: application/json: schema: oneOf: - description: 'Not connected' type: object example: ok: false error: not_connected message: 'Not connected to Slack.' properties: ok: type: boolean example: false error: type: string example: not_connected message: type: string example: 'Not connected to Slack.' - description: 'Invalid token' type: object example: ok: false error: token_invalid message: 'Slack connection needs to be re-authorized.' properties: ok: type: boolean example: false error: type: string example: token_invalid message: type: string example: 'Slack connection needs to be re-authorized.' 403: description: 'No organisation' content: application/json: schema: type: object example: message: 'Organisation context required.' properties: message: type: string example: 'Organisation context required.' tags: - 'Slack Integration' /api/integrations/slack/send: post: summary: 'Send a message to Slack' operationId: sendAMessageToSlack description: "Accepts a payload in either Reviews format (data = array of reviews) or\nMentions format (data = object with posts and/or comments) and posts\na Block Kit message to the given Slack channel via the notification dispatcher." parameters: [] responses: 200: description: Success content: application/json: schema: type: object example: ok: true message: 'Message sent.' properties: ok: type: boolean example: true message: type: string example: 'Message sent.' 400: description: 'Slack API error' content: application/json: schema: type: object example: ok: false error: ... message: ... properties: ok: type: boolean example: false error: type: string example: ... message: type: string example: ... 401: description: 'Not connected' content: application/json: schema: type: object example: ok: false error: not_connected message: 'Not connected to Slack.' properties: ok: type: boolean example: false error: type: string example: not_connected message: type: string example: 'Not connected to Slack.' 422: description: 'Invalid payload' content: application/json: schema: type: object example: message: 'Invalid payload: data must be reviews array or mentions object.' properties: message: type: string example: 'Invalid payload: data must be reviews array or mentions object.' 500: description: 'Server error' content: application/json: schema: type: object example: message: 'Failed to send message.' properties: message: type: string example: 'Failed to send message.' tags: - 'Slack Integration' requestBody: required: true content: application/json: schema: type: object properties: channel_id: type: string description: 'Must not be greater than 32 characters.' example: b status: type: string description: '' example: architecto process_id: type: string description: '' example: architecto created_at: type: string description: '' example: architecto data: type: object description: '' example: [] properties: { } required: - channel_id - status - process_id - created_at - data /api/subscription-plans: get: summary: 'List Subscription Plans' operationId: listSubscriptionPlans description: 'Get all available subscription plans.' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: - id: 1 stripe_price_id: price_internal_free name: Free description: 'Post-trial free tier' amount: 0 currency: usd interval: month plan_threshold: { } properties: data: type: array example: - id: 1 stripe_price_id: price_internal_free name: Free description: 'Post-trial free tier' amount: 0 currency: usd interval: month plan_threshold: [] items: type: object properties: id: type: integer example: 1 stripe_price_id: type: string example: price_internal_free name: type: string example: Free description: type: string example: 'Post-trial free tier' amount: type: integer example: 0 currency: type: string example: usd interval: type: string example: month plan_threshold: type: object properties: { } tags: - Subscriptions /api/subscriptions/threshold-usage: get: summary: 'Get Threshold Usage' operationId: getThresholdUsage description: "Returns plan soft limits in `threshold` (display denominators) and global enforcement caps in `limits.*_hard`.\nUsage is all-time across organisations owned by the billing user (same scope as hard-limit enforcement).\n`usage.reviews` and `usage.mentions` are capped at soft+hard for display; `reviews_raw` and `mentions_raw` are uncapped totals.\nPer-platform breakdowns are uncapped; they may sum above the top-level totals." parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: threshold: reviews_limit: 3000 mentions_limit: 4000 competitor_brands_limit: 3 history_months: 6 mentions_history_months: 3 refresh_interval: daily users_limit: null workspaces_limit: null data_export_type: null ai_visibility_prompts_limit: 20 limits: reviews_soft: 3000 mentions_soft: 4000 reviews_hard: 1000 mentions_hard: 500 usage: reviews: 900 mentions: 1000 reviews_raw: 900 mentions_raw: 1200 reviews_per_platform: G2: 50 Capterra: 50 mentions_per_platform: reddit: 200 competitor_brands: 0 ai_visibility_prompts: 7 workspaces: 1 users: 1 usage_period: scope: all_time start: '2025-01-01' end: '2026-04-02' trial_days_left: 3 properties: data: type: object properties: threshold: type: object properties: reviews_limit: type: integer example: 3000 mentions_limit: type: integer example: 4000 competitor_brands_limit: type: integer example: 3 history_months: type: integer example: 6 mentions_history_months: type: integer example: 3 refresh_interval: type: string example: daily users_limit: type: string example: null workspaces_limit: type: string example: null data_export_type: type: string example: null ai_visibility_prompts_limit: type: integer example: 20 limits: type: object properties: reviews_soft: type: integer example: 3000 mentions_soft: type: integer example: 4000 reviews_hard: type: integer example: 1000 mentions_hard: type: integer example: 500 usage: type: object properties: reviews: type: integer example: 900 mentions: type: integer example: 1000 reviews_raw: type: integer example: 900 mentions_raw: type: integer example: 1200 reviews_per_platform: type: object properties: G2: type: integer example: 50 Capterra: type: integer example: 50 mentions_per_platform: type: object properties: reddit: type: integer example: 200 competitor_brands: type: integer example: 0 ai_visibility_prompts: type: integer example: 7 workspaces: type: integer example: 1 users: type: integer example: 1 usage_period: type: object properties: scope: type: string example: all_time start: type: string example: '2025-01-01' end: type: string example: '2026-04-02' trial_days_left: type: integer example: 3 403: description: 'No organisation access' content: application/json: schema: type: object example: message: 'You do not have access to this organisation.' properties: message: type: string example: 'You do not have access to this organisation.' tags: - Subscriptions /api/subscriptions/checkout: post: summary: '' operationId: postApiSubscriptionsCheckout description: '' parameters: [] responses: { } tags: - Subscriptions requestBody: required: true content: application/json: schema: type: object properties: plan_id: type: integer description: 'The id of an existing record in the subscription_plans table.' example: 16 success_url: type: string description: 'Must be a valid URL.' example: 'http://bailey.com/' cancel_url: type: string description: 'Must be a valid URL.' example: 'http://rempel.com/sunt-nihil-accusantium-harum-mollitia' addons: type: object description: '' example: null properties: mentions: type: object description: '' example: null properties: price: type: integer description: 'This field is required when addons.mentions is present. Must be at least 1.' example: 23 quantity: type: integer description: 'This field is required when addons.mentions is present. Must be at least 1.' example: 64 reviews: type: object description: '' example: null properties: price: type: integer description: 'This field is required when addons.reviews is present. Must be at least 1.' example: 43 quantity: type: integer description: 'This field is required when addons.reviews is present. Must be at least 1.' example: 7 ai_visibility_prompts: type: object description: '' example: null properties: price: type: integer description: 'This field is required when addons.ai_visibility_prompts is present. Must be at least 1.' example: 79 quantity: type: integer description: 'This field is required when addons.ai_visibility_prompts is present. Must be at least 1.' example: 73 keywords: type: object description: '' example: null properties: price: type: integer description: 'This field is required when addons.keywords is present. Must be at least 1.' example: 13 quantity: type: integer description: 'This field is required when addons.keywords is present. Must be at least 1.' example: 49 review_platforms: type: object description: '' example: null properties: price: type: integer description: 'This field is required when addons.review_platforms is present. Must be at least 1.' example: 43 quantity: type: integer description: 'This field is required when addons.review_platforms is present. Must be at least 1.' example: 61 required: - plan_id - success_url - cancel_url security: [] /api/subscriptions/addons: post: summary: 'Update Subscription Add-ons' operationId: updateSubscriptionAddOns description: "Update Mentions/Reviews add-ons on the organisation's current active subscription.\nSupports optional `proration_date` to control Stripe proration timing." parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: id: 1 stripe_subscription_id: sub_1234567890 status: active addons: - id: 10 product_id: prod_mentions_123 stripe_price_id: price_inline_mentions_1 price: 2000 mentions_quantity: 50 reviews_quantity: 0 status: active properties: data: type: object properties: id: type: integer example: 1 stripe_subscription_id: type: string example: sub_1234567890 status: type: string example: active addons: type: array example: - id: 10 product_id: prod_mentions_123 stripe_price_id: price_inline_mentions_1 price: 2000 mentions_quantity: 50 reviews_quantity: 0 status: active items: type: object properties: id: type: integer example: 10 product_id: type: string example: prod_mentions_123 stripe_price_id: type: string example: price_inline_mentions_1 price: type: integer example: 2000 mentions_quantity: type: integer example: 50 reviews_quantity: type: integer example: 0 status: type: string example: active 403: description: 'Not owner or no organisation access' content: application/json: schema: type: object example: message: 'You must be an organisation owner to manage subscriptions.' properties: message: type: string example: 'You must be an organisation owner to manage subscriptions.' 404: description: 'No active subscription' content: application/json: schema: type: object example: message: 'No active subscription found for this organisation.' properties: message: type: string example: 'No active subscription found for this organisation.' 422: description: 'Validation error' content: application/json: schema: type: object example: message: 'The given data was invalid.' properties: message: type: string example: 'The given data was invalid.' tags: - Subscriptions requestBody: required: true content: application/json: schema: type: object properties: addons: type: object description: 'Add-ons payload to update.' example: [] properties: mentions: type: object description: 'Optional Mentions add-on payload.' example: [] properties: price: type: integer description: 'Required with addons.mentions. Total amount in cents.' example: 14000 quantity: type: integer description: 'Required with addons.mentions. Mentions quantity to provision.' example: 7000 reviews: type: object description: 'Optional Reviews add-on payload.' example: [] properties: price: type: integer description: 'Required with addons.reviews. Total amount in cents.' example: 13000 quantity: type: integer description: 'Required with addons.reviews. Reviews quantity to provision.' example: 11000 proration_date: type: integer description: 'Optional Unix timestamp used by Stripe for proration.' example: 1715000000 required: - addons /api/subscriptions/addons/preview-proration: post: summary: 'Preview Add-on Proration' operationId: previewAddOnProration description: "Preview Stripe prorated invoice impact for Mentions/Reviews add-on changes\nwithout applying the subscription update." parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: amount_due: 2400 subtotal: 2400 total: 2400 currency: usd proration_lines: - id: il_proration_123 amount: 2400 currency: usd description: 'Proration adjustment' properties: data: type: object properties: amount_due: type: integer example: 2400 subtotal: type: integer example: 2400 total: type: integer example: 2400 currency: type: string example: usd proration_lines: type: array example: - id: il_proration_123 amount: 2400 currency: usd description: 'Proration adjustment' items: type: object properties: id: type: string example: il_proration_123 amount: type: integer example: 2400 currency: type: string example: usd description: type: string example: 'Proration adjustment' 403: description: 'Not owner or no organisation access' content: application/json: schema: type: object example: message: 'You must be an organisation owner to manage subscriptions.' properties: message: type: string example: 'You must be an organisation owner to manage subscriptions.' 404: description: 'No active subscription' content: application/json: schema: type: object example: message: 'No active subscription found for this organisation.' properties: message: type: string example: 'No active subscription found for this organisation.' 422: description: 'Validation error' content: application/json: schema: type: object example: message: 'The given data was invalid.' properties: message: type: string example: 'The given data was invalid.' tags: - Subscriptions requestBody: required: true content: application/json: schema: type: object properties: addons: type: object description: 'Add-ons payload to preview.' example: [] properties: mentions: type: object description: 'Optional Mentions add-on payload.' example: [] properties: price: type: integer description: 'Required with addons.mentions. Total amount in cents.' example: 14000 quantity: type: integer description: 'Required with addons.mentions. Mentions quantity to provision.' example: 7000 reviews: type: object description: 'Optional Reviews add-on payload.' example: [] properties: price: type: integer description: 'Required with addons.reviews. Total amount in cents.' example: 13000 quantity: type: integer description: 'Required with addons.reviews. Reviews quantity to provision.' example: 11000 proration_date: type: integer description: 'Optional Unix timestamp used by Stripe for proration preview.' example: 1715000000 required: - addons /api/subscriptions/current: get: summary: 'Get Current Subscription' operationId: getCurrentSubscription description: 'Get the current subscription for the organisation.' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: id: 1 stripe_subscription_id: sub_1234567890 status: active current_period_start: '2025-12-01T00:00:00.000000Z' current_period_end: '2026-01-01T00:00:00.000000Z' plan: id: 1 stripe_price_id: price_1234567890 description: 'Professional features' name: 'Pro Plan' amount: 2999 currency: usd interval: month features: - 'Feature 1' - 'Feature 2' properties: data: type: object properties: id: type: integer example: 1 stripe_subscription_id: type: string example: sub_1234567890 status: type: string example: active current_period_start: type: string example: '2025-12-01T00:00:00.000000Z' current_period_end: type: string example: '2026-01-01T00:00:00.000000Z' plan: type: object properties: id: type: integer example: 1 stripe_price_id: type: string example: price_1234567890 description: type: string example: 'Professional features' name: type: string example: 'Pro Plan' amount: type: integer example: 2999 currency: type: string example: usd interval: type: string example: month features: type: array example: - 'Feature 1' - 'Feature 2' items: type: string 403: description: 'No organisation access' content: application/json: schema: type: object example: message: 'You do not have access to this organisation.' properties: message: type: string example: 'You do not have access to this organisation.' 404: description: 'No subscription' content: application/json: schema: type: object example: message: 'No subscription found for this organisation.' properties: message: type: string example: 'No subscription found for this organisation.' tags: - Subscriptions /api/subscriptions/refresh: post: summary: 'Refresh subscription from Stripe' operationId: refreshSubscriptionFromStripe description: "Fetch the organisation's current subscription from Stripe and sync to the database.\nUse after the user returns from the billing portal so subscription and threshold data are up to date." parameters: [] responses: 200: description: '' content: text/plain: schema: type: string example: "{\n \"data\": {\n \"id\": 1,\n \"stripe_subscription_id\": \"sub_xxx\",\n \"status\": \"active\",\n \"current_period_start\": \"2025-12-01T00:00:00.000000Z\",\n \"current_period_end\": \"2026-01-01T00:00:00.000000Z\",\n \"canceled_at\": null,\n \"ends_at\": null,\n \"plan\": { \"id\": 1, \"stripe_price_id\": \"price_xxx\", \"name\": \"Pro - Monthly\", ... }\n }\n}" 403: description: 'Not owner' content: application/json: schema: type: object example: message: 'You must be an organisation owner to manage subscriptions.' properties: message: type: string example: 'You must be an organisation owner to manage subscriptions.' 404: description: 'No subscription' content: application/json: schema: type: object example: message: 'No active subscription found for this organisation.' properties: message: type: string example: 'No active subscription found for this organisation.' tags: - Subscriptions /api/subscriptions/billing-portal: post: summary: 'Get Billing Portal URL' operationId: getBillingPortalURL description: "Create a Stripe Billing Portal session for the organisation's customer.\nThe portal allows customers to manage their subscription, update payment methods,\nview invoices, and cancel their subscription." parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: portal_url: 'https://billing.stripe.com/p/session/...' properties: portal_url: type: string example: 'https://billing.stripe.com/p/session/...' 403: description: 'Not owner' content: application/json: schema: type: object example: message: 'You must be an organisation owner to manage subscriptions.' properties: message: type: string example: 'You must be an organisation owner to manage subscriptions.' 404: description: 'No subscription' content: application/json: schema: type: object example: message: 'No active subscription found for this organisation.' properties: message: type: string example: 'No active subscription found for this organisation.' tags: - Subscriptions requestBody: required: true content: application/json: schema: type: object properties: return_url: type: string description: 'The URL to redirect to when the customer is done.' example: 'https://app.example.com/settings/billing' required: - return_url '/api/organisations/{organisation_uuid}/platform-limits': get: summary: 'Get Platform Limits' operationId: getPlatformLimits description: "Returns the maximum allocated review and mention counts across all platforms,\nbroken down per project. Pass `project_uuids[]` to scope to specific projects;\nomit to return all projects in the organisation.\n\nAllocated values come from the most recent settled scrape job per platform." parameters: - in: query name: project_uuids description: 'optional One or more project UUIDs to scope the response.' example: null required: false schema: type: array description: 'optional One or more project UUIDs to scope the response.' example: null items: type: string responses: 200: description: '' content: application/json: schema: type: object example: data: - project_uuid: 550e8400-e29b-41d4-a716-446655440000 project_name: Slack reviews_limit: - url: 'https://www.trustpilot.com/review/slack.com' platform: trustpilot max_limit: 200 - url: 'https://www.capterra.com/p/1234567890/slack' platform: capterra max_limit: 187 mentions_limit: - brand_id: 1 platform: X max_limit: 200 - brand_id: 1 platform: Reddit max_limit: 150 properties: data: type: array example: - project_uuid: 550e8400-e29b-41d4-a716-446655440000 project_name: Slack reviews_limit: - url: 'https://www.trustpilot.com/review/slack.com' platform: trustpilot max_limit: 200 - url: 'https://www.capterra.com/p/1234567890/slack' platform: capterra max_limit: 187 mentions_limit: - brand_id: 1 platform: X max_limit: 200 - brand_id: 1 platform: Reddit max_limit: 150 items: type: object properties: project_uuid: type: string example: 550e8400-e29b-41d4-a716-446655440000 project_name: type: string example: Slack reviews_limit: type: array example: - url: 'https://www.trustpilot.com/review/slack.com' platform: trustpilot max_limit: 200 - url: 'https://www.capterra.com/p/1234567890/slack' platform: capterra max_limit: 187 items: type: object properties: url: type: string example: 'https://www.trustpilot.com/review/slack.com' platform: type: string example: trustpilot max_limit: type: integer example: 200 mentions_limit: type: array example: - brand_id: 1 platform: X max_limit: 200 - brand_id: 1 platform: Reddit max_limit: 150 items: type: object properties: brand_id: type: integer example: 1 platform: type: string example: X max_limit: type: integer example: 200 403: description: 'No organisation access' content: application/json: schema: type: object example: message: 'You do not have access to this organisation.' properties: message: type: string example: 'You do not have access to this organisation.' 404: description: 'Organisation not found' content: application/json: schema: type: object example: message: 'Organisation not found.' properties: message: type: string example: 'Organisation not found.' tags: - Subscriptions parameters: - in: path name: organisation_uuid description: 'The organisation UUID.' example: 6ff8f7f6-1eb3-3525-be4a-3932c805afed required: true schema: type: string '/api/organisations/{organisation_uuid}/universal-keywords/mentions': get: summary: '' operationId: getApiOrganisationsOrganisation_uuidUniversalKeywordsMentions description: '' parameters: [] responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - 'Universal Keywords' requestBody: required: true content: application/json: schema: type: object properties: keyword_uuids: type: array description: 'Must be a valid UUID. The uuid of an existing record in the universal_keywords table.' example: - 6ff8f7f6-1eb3-3525-be4a-3932c805afed items: type: string platforms: type: array description: '' example: - linkedin items: type: string enum: - reddit - github - stackoverflow - hackernews - youtube - linkedin - x sentiments: type: array description: '' example: - negative items: type: string enum: - positive - neutral - negative intents: type: array description: 'Must not be greater than 100 characters.' example: - g items: type: string type: type: string description: '' example: comment enum: - post - comment nullable: true search: type: string description: 'Must not be greater than 255 characters.' example: z nullable: true start_date: type: string description: 'Must be a valid date.' example: '2026-08-27T06:47:10' nullable: true end_date: type: string description: 'Must be a valid date. Must be a date after or equal to start_date.' example: '2052-09-19' nullable: true page: type: integer description: 'Must be at least 1.' example: 22 nullable: true per_page: type: integer description: 'Must be at least 1. Must not be greater than 100.' example: 7 nullable: true required: - platforms - sentiments - intents security: [] parameters: - in: path name: organisation_uuid description: '' example: 2958b10e-6a80-4903-868f-79b048b5e0c4 required: true schema: type: string '/api/organisations/{organisation_id}/universal-keywords': get: summary: 'List universal keywords.' operationId: listUniversalKeywords description: '' parameters: [] responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - 'Universal Keywords' post: summary: 'Create a universal keyword.' operationId: createAUniversalKeyword description: '' parameters: [] responses: { } tags: - 'Universal Keywords' requestBody: required: true content: application/json: schema: type: object properties: keyword: type: string description: 'Must not be greater than 255 characters.' example: b context: type: string description: 'Must not be greater than 2000 characters.' example: 'n' nullable: true include_keywords_match: type: string description: '' example: all enum: - all - any include_keywords: type: array description: 'Must not be greater than 255 characters.' example: - g items: type: string negative_terms: type: array description: 'Must not be greater than 255 characters.' example: - z items: type: string platforms: type: array description: '' example: - reddit items: type: string enum: - reddit - github - stackoverflow - hackernews - youtube - linkedin - x is_active: type: boolean description: '' example: false required: - keyword - include_keywords - negative_terms - platforms parameters: - in: path name: organisation_id description: 'The ID of the organisation.' example: 2958b10e-6a80-4903-868f-79b048b5e0c4 required: true schema: type: string '/api/organisations/{organisation_id}/universal-keywords/{keyword_uuid}': get: summary: 'Show a universal keyword.' operationId: showAUniversalKeyword description: '' parameters: [] responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - 'Universal Keywords' put: summary: 'Update a universal keyword.' operationId: updateAUniversalKeyword description: '' parameters: [] responses: { } tags: - 'Universal Keywords' requestBody: required: true content: application/json: schema: type: object properties: keyword: type: string description: 'Must not be greater than 255 characters.' example: b context: type: string description: 'Must not be greater than 2000 characters.' example: 'n' nullable: true include_keywords_match: type: string description: '' example: any enum: - all - any include_keywords: type: array description: 'Must not be greater than 255 characters.' example: - g items: type: string negative_terms: type: array description: 'Must not be greater than 255 characters.' example: - z items: type: string platforms: type: array description: '' example: - github items: type: string enum: - reddit - github - stackoverflow - hackernews - youtube - linkedin - x is_active: type: boolean description: '' example: false required: - include_keywords - negative_terms - platforms delete: summary: 'Delete a universal keyword.' operationId: deleteAUniversalKeyword description: '' parameters: [] responses: { } tags: - 'Universal Keywords' parameters: - in: path name: organisation_id description: 'The ID of the organisation.' example: 2958b10e-6a80-4903-868f-79b048b5e0c4 required: true schema: type: string - in: path name: keyword_uuid description: '' example: 6ff8f7f6-1eb3-3525-be4a-3932c805afed required: true schema: type: string /api/validate-url: post: summary: 'Validate URL Reachability' operationId: validateURLReachability description: 'Validates URL format and checks if the URL is reachable.' parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: valid: true status_code: 200 message: 'URL is valid and reachable' properties: valid: type: boolean example: true status_code: type: integer example: 200 message: type: string example: 'URL is valid and reachable' 422: description: '' content: application/json: schema: type: object example: valid: false message: 'Invalid URL format' properties: valid: type: boolean example: false message: type: string example: 'Invalid URL format' tags: - Utilities requestBody: required: true content: application/json: schema: type: object properties: url: type: string description: 'URL to validate.' example: 'https://example.com' required: - url /api/tools/webhook-signatures: post: summary: 'Generate webhook signature' operationId: generateWebhookSignature description: "Generate `X-Scraper-Signature` using HMAC SHA256 from the exact `raw_body` string you send.\nUse this for Swagger testing before calling scrape webhook endpoints." parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: signature: 8e1b1b0d7c6c1b3b8c9a... properties: signature: type: string example: 8e1b1b0d7c6c1b3b8c9a... 422: description: '' content: application/json: schema: type: object example: message: 'The given data was invalid.' errors: target: - 'The selected target is invalid.' properties: message: type: string example: 'The given data was invalid.' errors: type: object properties: target: type: array example: - 'The selected target is invalid.' items: type: string tags: - 'Webhook Tools' requestBody: required: true content: application/json: schema: type: object properties: target: type: string description: 'Which webhook secret to use. Allowed: reviews, social, universal_keywords.' example: social raw_body: type: string description: 'Exact raw JSON string to sign.' example: '{"status":"SUCCESS","process_id":76445,"created_at":"2026-01-19 14:23:11","link_url":"https://www.trustpilot.com/review/example.com","data":[]}' required: - target - raw_body