openapi: 3.0.3 info: title: 'Subsig API Documentation' description: '' version: 1.0.0 servers: - url: 'https://backend-staging.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: 'AI Traffic' description: "\nThe AI Traffics dashboard's GA4-sourced overview -- see GoogleAnalyticsTrafficReportService\nfor which fields are real (GA4 rollups) vs placeholders pending the bot-crawler pipeline." - 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's billing group. A key is\nalways generated from (and owned by) the group's billing-anchor organisation, but each key is\nindependently scoped to whichever subset of the group's organisations (workspaces) was chosen\nwhen it was created -- a billing group can hold several keys, each covering a different subset,\nfor different integrations." - 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: 'Google Analytics Integration' description: "\nOrg-scoped via the `X-Organisation-Id` header (see ResolveOrganisation middleware), same\npattern as the Slack integration -- no organisation_uuid path segment, $request->organisation\nis already resolved and access-checked by the time these methods run." - name: Home description: "\nFeeds the standalone Recommendations page, AI-Visibility-only. All the actual signal-gathering\nand recommendation logic lives in AiVisibilityRecommendationService, shared by nothing else, so\nthis stays a thin request/response adapter." - name: Invites description: "\nAPIs for managing invites" - name: Links description: "\nAPIs for managing project links" - name: Onboarding description: "\nAPIs for user onboarding flow" - name: Organisations description: "\nAPIs for managing organisations" - name: 'Ownership Transfer' description: "\nHands an entire organisation (and every other organisation its current owner also owns --\nthey move as one bundle) over to a new owner. Modeled as a third InviteType on the existing\nInvite table rather than a parallel system, since it already has everything a token-based\noffer/accept flow needs." - 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: 'Slack Integration' description: "\nAPIs for connecting and managing Slack workspace integration" - name: Subscriptions description: "\nAPIs for managing subscriptions" - 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 device_name: type: string description: 'Must not be greater than 255 characters.' example: 'n' 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 device_name: type: string description: 'Must not be greater than 255 characters.' example: 'n' 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 patch: summary: 'Update Profile' operationId: updateProfile description: "Update the authenticated user's own profile fields. `name` is kept in sync as\n\"{first_name} {last_name}\" for every other place in the app that still reads it." parameters: [] responses: { } tags: - Authentication requestBody: required: true content: application/json: schema: type: object properties: first_name: type: string description: '' example: Jane last_name: type: string description: '' example: Doe phone: type: string description: '' example: '+1 555 123 4567' nullable: true job_role: type: string description: '' example: Marketing company_name: type: string description: '' example: 'Acme Inc' required: - first_name - last_name - job_role - company_name delete: summary: 'Delete Account' operationId: deleteAccount description: "Permanently delete the authenticated user's own account. Any organisation this user is\nthe sole member of is deleted along with it (see UserDeletionService); shared organisations\nare left intact for the remaining members." parameters: [] responses: { } 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}/ai-visibility/traffic': post: summary: 'Traffic Overview' operationId: trafficOverview description: '' parameters: [] responses: { } tags: - 'AI Traffic' requestBody: required: false content: application/json: schema: type: object properties: date_from: type: string description: 'Optional start of the window (Y-m-d). Default: 30 days ago.' example: architecto nullable: true date_to: type: string description: 'Optional end of the window (Y-m-d). Default: today.' example: architecto nullable: true granularity: type: string description: 'Optional trend bucketing: daily, weekly, or monthly. Default: daily.' example: architecto nullable: true 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/traffic/pages': post: summary: 'Traffic Pages' operationId: trafficPages description: '' parameters: [] responses: { } tags: - 'AI Traffic' requestBody: required: false content: application/json: schema: type: object properties: date_from: type: string description: 'Optional start of the window (Y-m-d). Default: 30 days ago.' example: architecto nullable: true date_to: type: string description: 'Optional end of the window (Y-m-d). Default: today.' example: architecto nullable: true providers: type: array description: 'Optional provider ids to filter to.' example: - architecto items: type: string sort_by: type: string description: 'Optional: botVisits, retrievals, citationRate, topics, events, or sessionStarts. Default: sessionStarts.' example: architecto nullable: true sort_direction: type: string description: 'Optional: asc or desc. Default: desc.' example: architecto nullable: true per_page: type: integer description: 'Optional page size. Default: 20.' example: 16 nullable: true page: type: integer description: 'Optional page number. Default: 1.' example: 16 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': 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: "Flat, one-row-per-citation-occurrence export matching the format competitor tools like\nOtterly.ai use -- not the on-screen URL rollup, which aggregates many occurrences of the\nsame URL into one summary row and can't carry per-occurrence fields like Prompt/Date." 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: "Flat, one-row-per-citation-occurrence export matching the format competitor tools like\nOtterly.ai use -- not the on-screen domain rollup, which aggregates many occurrences of the\nsame domain into one summary row and can't carry per-occurrence fields like Prompt/Date." 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: "One row per citation occurrence (a citation URL from a specific prompt/service/date),\nmatching the flat format competitor tools like Otterly.ai export -- not one row per\nresponse, since a single response can cite several URLs." 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: 205ae76c-bd73-426b-a36a-8338353b9542 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. Full detail (including inline citation segments) is available regardless of whether\nraw_response has since been pruned by retention (Ticket 8) — citation_segments is derived\nonce at normalization time (see NormalizeAiVisibilityExecutionJob) and persisted alongside\ngenerated_answer, neither of which the prune command ever touches." 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: true 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: false 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/regions': get: summary: 'Regions In Use' operationId: regionsInUse description: "Distinct region/country codes actually configured on this organisation's own prompts --\nNOT the full curated config('ai_visibility.supported_regions') list that\nOrganisationController::supportedRegions() returns (that one backs region *pickers* on\ncreation forms, where every supported region is a valid choice). This backs the AI\nVisibility dashboard's region *filter* dropdowns (Platforms/Visibility/Sentiment/\nCitations), which should only ever offer a region that at least one of this org's prompts\ncould actually produce data for -- offering the full global list there let an org filter by\na region with zero prompts and see an always-empty result with no indication why.\n\nScoped to active/paused prompts only, same default scope as index() -- an archived prompt's\nregion isn't a live filtering concern." parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: - value: US label: 'United States' - value: GB label: 'United Kingdom' properties: data: type: array example: - value: US label: 'United States' - value: GB label: 'United Kingdom' items: type: object properties: value: type: string example: US label: type: string example: 'United States' 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: true 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-keys: get: summary: 'List every production API key generated for this billing group.' operationId: listEveryProductionAPIKeyGeneratedForThisBillingGroup description: "Returns every non-revoked key generated from the billing-anchor organisation of whichever\norganisation was selected via X-Organisation-Id, each with the workspaces it's scoped to.\nPlaintext 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: 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 organisations: - uuid: 660e8400-e29b-41d4-a716-446655440000 name: 'Acme Inc' is_primary: true created_at: '2026-06-05T10:00:00.000000Z' updated_at: '2026-06-05T10:00:00.000000Z' properties: data: type: array example: - 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 organisations: - uuid: 660e8400-e29b-41d4-a716-446655440000 name: 'Acme Inc' is_primary: true created_at: '2026-06-05T10:00:00.000000Z' updated_at: '2026-06-05T10:00:00.000000Z' items: 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 organisations: type: array example: - uuid: 660e8400-e29b-41d4-a716-446655440000 name: 'Acme Inc' is_primary: true items: type: object properties: uuid: type: string example: 660e8400-e29b-41d4-a716-446655440000 name: type: string example: 'Acme Inc' is_primary: type: boolean example: true created_at: type: string example: '2026-06-05T10:00:00.000000Z' updated_at: type: string example: '2026-06-05T10:00:00.000000Z' tags: - 'API Keys' /api/api-keys/generate: post: summary: "Generate a production API key scoped to a chosen subset of this billing group's\norganisations. Plaintext is returned only on first creation." operationId: generateAProductionAPIKeyScopedToAChosenSubsetOfThisBillingGroupsOrganisationsPlaintextIsReturnedOnlyOnFirstCreation description: '' 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: 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 organisations: - uuid: 660e8400-e29b-41d4-a716-446655440000 name: 'Acme Inc' is_primary: true 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 organisations: type: array example: - uuid: 660e8400-e29b-41d4-a716-446655440000 name: 'Acme Inc' is_primary: true items: type: object properties: uuid: type: string example: 660e8400-e29b-41d4-a716-446655440000 name: type: string example: 'Acme Inc' is_primary: type: boolean example: true created_at: type: string example: '2026-06-05T10:00:00.000000Z' updated_at: type: string example: '2026-06-05T10:00:00.000000Z' 422: description: 'Workspace outside this billing group' content: application/json: schema: type: object example: message: "One or more selected workspaces are not part of this account's billing group." error: code: organisation_not_in_billing_scope properties: message: type: string example: "One or more selected workspaces are not part of this account's billing group." error: type: object properties: code: type: string example: organisation_not_in_billing_scope tags: - 'API Keys' requestBody: required: true content: application/json: schema: type: object properties: name: type: string description: "optional A label for this key, e.g. \"Claude Desktop MCP\". If omitted,\n a placeholder name derived from the key itself (e.g. \"sk_live_eb0da992\") is generated\n automatically." example: 'Claude Desktop MCP' nullable: true confirmed: type: boolean description: 'Must be true before generating the key.' example: true organisation_uuids: type: array description: "At least one workspace UUID (from this\n billing group) this key should be able to access. The billing-anchor organisation is\n selectable like any other workspace but is never added automatically -- the key only gets\n access to the workspace(s) explicitly listed here, and at least one is required." example: - architecto items: type: string required: - confirmed - organisation_uuids '/api/api-keys/{apiKey_uuid}/rotate': post: summary: "Rotate one production API key. Plaintext is returned only on first rotation. The new key\nkeeps the exact same workspace scope as the key it replaces." operationId: rotateOneProductionAPIKeyPlaintextIsReturnedOnlyOnFirstRotationTheNewKeyKeepsTheExactSameWorkspaceScopeAsTheKeyItReplaces 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' parameters: - in: path name: apiKey_uuid description: '' example: bb5acf44-73b3-4cf7-a3d8-9eeed414dc5a required: true schema: type: string '/api/api-keys/{apiKey_uuid}/revoke': post: summary: 'Revoke one production API key immediately (and its still-rotating predecessor, if any).' operationId: revokeOneProductionAPIKeyImmediatelyandItsStillRotatingPredecessorIfAny description: 'The request must 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 parameters: - in: path name: apiKey_uuid description: '' example: bb5acf44-73b3-4cf7-a3d8-9eeed414dc5a required: true schema: type: string '/api/api-keys/{apiKey_uuid}/workspaces': patch: summary: "Replace one production API key's workspace scope entirely -- adds any newly-listed\nworkspace and drops any workspace no longer listed, in one call. Does not affect the key\nitself (its value, status, or expiry)." operationId: replaceOneProductionAPIKeysWorkspaceScopeEntirelyAddsAnyNewlyListedWorkspaceAndDropsAnyWorkspaceNoLongerListedInOneCallDoesNotAffectTheKeyItselfitsValueStatusOrExpiry description: '' 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 workspaces updated successfully.' data: uuid: 550e8400-e29b-41d4-a716-446655440000 organisations: - uuid: 660e8400-e29b-41d4-a716-446655440000 name: 'Acme Inc' is_primary: true - uuid: 770e8400-e29b-41d4-a716-446655440000 name: 'Acme Support' is_primary: false properties: message: type: string example: 'API key workspaces updated successfully.' data: type: object properties: uuid: type: string example: 550e8400-e29b-41d4-a716-446655440000 organisations: type: array example: - uuid: 660e8400-e29b-41d4-a716-446655440000 name: 'Acme Inc' is_primary: true - uuid: 770e8400-e29b-41d4-a716-446655440000 name: 'Acme Support' is_primary: false items: type: object properties: uuid: type: string example: 660e8400-e29b-41d4-a716-446655440000 name: type: string example: 'Acme Inc' is_primary: type: boolean example: true 422: description: 'Workspace outside this billing group' content: application/json: schema: type: object example: message: "One or more selected workspaces are not part of this account's billing group." error: code: organisation_not_in_billing_scope properties: message: type: string example: "One or more selected workspaces are not part of this account's billing group." error: type: object properties: code: type: string example: organisation_not_in_billing_scope tags: - 'API Keys' requestBody: required: true content: application/json: schema: type: object properties: organisation_uuids: type: array description: "At least one workspace UUID (from this\n billing group) this key should be scoped to. The billing-anchor organisation is\n selectable like any other workspace but is never added automatically, and the scope can\n never be emptied out entirely." example: - architecto items: type: string required: - organisation_uuids parameters: - in: path name: apiKey_uuid description: '' example: bb5acf44-73b3-4cf7-a3d8-9eeed414dc5a required: true schema: type: string /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/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: true nullable: true security: [] '/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: none 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: none 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: false 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/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: 205ae76c-bd73-426b-a36a-8338353b9542 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: 90d enum: - 7d - 30d - 90d nullable: true data_source: type: string description: "Shared across every agent's suggested-prompts endpoint -- the AI platform values\n(root_cause_analysis/prompts_discovery/ai_visibility_roadmap) plus the\nai_visibility default, since neither generator below is told which agent's\ndata_source shape to expect ahead of time." example: openai enum: - openai - perplexity - gemini - ai_visibility nullable: true required: - brand_id security: [] parameters: - in: path name: organisation_uuid description: '' example: 205ae76c-bd73-426b-a36a-8338353b9542 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: 205ae76c-bd73-426b-a36a-8338353b9542 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: 205ae76c-bd73-426b-a36a-8338353b9542 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: 205ae76c-bd73-426b-a36a-8338353b9542 required: true schema: type: string - in: path name: run_id description: 'The ID of the run.' example: 6 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: 205ae76c-bd73-426b-a36a-8338353b9542 required: true schema: type: string - in: path name: run_id description: 'The ID of the run.' example: 6 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/workspaces: get: summary: '' operationId: getApiV1Workspaces 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: 230a42d1-273f-49db-9e2b-7bc30ab9c025 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: 230a42d1-273f-49db-9e2b-7bc30ab9c025 tags: - Endpoints security: [] /api/v1/ai-visibility/topics: get: summary: '' operationId: getApiV1AiVisibilityTopics 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: 0e8d9117-2fd9-45a1-a933-469f26608487 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: 0e8d9117-2fd9-45a1-a933-469f26608487 tags: - Endpoints security: [] /api/v1/ai-visibility/prompts: get: summary: '' operationId: getApiV1AiVisibilityPrompts 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: bad3a0be-5a89-43aa-bf4d-67742229db96 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: bad3a0be-5a89-43aa-bf4d-67742229db96 tags: - Endpoints security: [] '/api/v1/ai-visibility/prompts/{prompt}': get: summary: '' operationId: getApiV1AiVisibilityPromptsPrompt 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: 8611e2c8-ae7b-46cc-82fa-e3f3d54ab07d 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: 8611e2c8-ae7b-46cc-82fa-e3f3d54ab07d tags: - Endpoints security: [] parameters: - in: path name: prompt description: 'The prompt.' example: '564' required: true schema: type: string /api/v1/ai-visibility/brands: get: summary: "Every brand tracked by AI Visibility (own brand + registered competitors), ranked, with\nvisibility %/share-of-voice %/average position for each — the discovery step before digging\ninto a specific brand via show() or into sentiment/citations for it. Built on the same\nAiVisibilityBrandRankService::brandMetrics()/AiVisibilityRanker::rankComposite() combination\nthe session-auth AiVisibilityCompetitorController::index() (Competitor tab) already uses." operationId: everyBrandTrackedByAIVisibilityownBrand+RegisteredCompetitorsRankedWithVisibilityshareOfVoiceaveragePositionForEachTheDiscoveryStepBeforeDiggingIntoASpecificBrandViaShowOrIntoSentimentcitationsForItBuiltOnTheSameAiVisibilityBrandRankServicebrandMetricsAiVisibilityRankerrankCompositeCombinationTheSessionAuthAiVisibilityCompetitorControllerindexCompetitorTabAlreadyUses 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: 40edd226-b7ff-4aed-a7d1-1a7507403c9f 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: 40edd226-b7ff-4aed-a7d1-1a7507403c9f tags: - Endpoints security: [] /api/v1/ai-visibility/overview: get: summary: '' operationId: getApiV1AiVisibilityOverview 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: ab3d98b7-cdef-4320-9659-7430f33fdfd7 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: ab3d98b7-cdef-4320-9659-7430f33fdfd7 tags: - Endpoints security: [] /api/v1/ai-visibility/brand: get: summary: "Detail for a single brand tracked by AI Visibility: own brand or a specific competitor,\nresolved by name against the SAME ai_visibility_brands-backed set index() draws its\ncompetitors list from. The 404 below says so explicitly rather than leaving the caller to\nguess why a name wasn't found." operationId: detailForASingleBrandTrackedByAIVisibilityOwnBrandOrASpecificCompetitorResolvedByNameAgainstTheSAMEAiVisibilityBrandsBackedSetIndexDrawsItsCompetitorsListFromThe404BelowSaysSoExplicitlyRatherThanLeavingTheCallerToGuessWhyANameWasntFound 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: 096ad0b6-fccd-4dbf-b4a4-d1de13b84f20 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: 096ad0b6-fccd-4dbf-b4a4-d1de13b84f20 tags: - Endpoints security: [] /api/v1/ai-visibility/platforms: get: summary: '' operationId: getApiV1AiVisibilityPlatforms 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: cd19ac92-ee92-4a8e-8c50-0d9a091a3ca1 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: cd19ac92-ee92-4a8e-8c50-0d9a091a3ca1 tags: - Endpoints security: [] /api/v1/ai-visibility/sentiment: get: summary: '' operationId: getApiV1AiVisibilitySentiment 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: 27c8835c-12da-440d-a236-7fb3bb4009b1 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: 27c8835c-12da-440d-a236-7fb3bb4009b1 tags: - Endpoints security: [] /api/v1/ai-visibility/citations: get: summary: '' operationId: getApiV1AiVisibilityCitations 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: 61a46285-b4b6-44a2-9978-d0229d14eb3c 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: 61a46285-b4b6-44a2-9978-d0229d14eb3c tags: - Endpoints security: [] /api/v1/ai-visibility/status: get: summary: '' operationId: getApiV1AiVisibilityStatus 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: 505e799b-99dd-444b-a3c2-9bc91b3836e3 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: 505e799b-99dd-444b-a3c2-9bc91b3836e3 tags: - Endpoints security: [] /api/v1/agent-reports: get: summary: '' operationId: getApiV1AgentReports 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: 9301ec08-32e8-4e59-b401-551c6f40f15e 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: 9301ec08-32e8-4e59-b401-551c6f40f15e tags: - Endpoints security: [] '/api/v1/agent-reports/{run_id}': get: summary: '' operationId: getApiV1AgentReportsRun_id 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: 0a83ae38-983c-476a-8c89-2966e88aeb62 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: 0a83ae38-983c-476a-8c89-2966e88aeb62 tags: - Endpoints security: [] parameters: - in: path name: run_id description: 'The ID of the run.' example: 6 required: true schema: type: integer /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 required: - name - email - organisation_name - product_name - product_website security: [] /api/integrations/google-analytics/connect: post: summary: 'Connect Google Analytics Account' operationId: connectGoogleAnalyticsAccount description: "Exchanges an authorization code (obtained by the frontend's own OAuth flow) for tokens and\ncreates or updates the connection. Supports multiple Google accounts per organisation --\na second call with a different account creates a second connection, not a collision." parameters: [] responses: 201: description: '' content: application/json: schema: type: object example: data: id: ... account_email: user@example.com status: connected connected_at: ... selected_property: null properties: data: type: object properties: id: type: string example: ... account_email: type: string example: user@example.com status: type: string example: connected connected_at: type: string example: ... selected_property: type: string example: null tags: - 'Google Analytics Integration' requestBody: required: true content: application/json: schema: type: object properties: code: type: string description: 'The authorization code from Google.' example: 4/0Ab_... redirect_uri: type: string description: 'Must exactly match the URI used to obtain the code.' example: architecto required: - code - redirect_uri security: [] /api/integrations/google-analytics/connections: get: summary: 'List Google Analytics Connections' operationId: listGoogleAnalyticsConnections description: 'No properties included -- lazy-loaded separately per connection when its row is expanded.' parameters: [] responses: 401: description: '' content: application/json: schema: type: object example: message: Unauthenticated. properties: message: type: string example: Unauthenticated. tags: - 'Google Analytics Integration' security: [] '/api/integrations/google-analytics/connections/{connection}/properties': get: summary: 'List GA4 Properties For A Connection' operationId: listGA4PropertiesForAConnection description: "Fetched live from Google on first row-expand, briefly cached after (see\nGoogleAnalyticsService::listProperties()). Not eagerly included in the connections list." parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: - id: properties/123 name: 'Subsig - GA4' domain: subscribed.fyi sessions_per_month: 12400 matches_site: true properties: data: type: array example: - id: properties/123 name: 'Subsig - GA4' domain: subscribed.fyi sessions_per_month: 12400 matches_site: true items: type: object properties: id: type: string example: properties/123 name: type: string example: 'Subsig - GA4' domain: type: string example: subscribed.fyi sessions_per_month: type: integer example: 12400 matches_site: type: boolean example: true tags: - 'Google Analytics Integration' security: [] parameters: - in: path name: connection description: "The connection's uuid." example: architecto required: true schema: type: string '/api/integrations/google-analytics/connections/{connection}/property': patch: summary: 'Select GA4 Property For A Connection' operationId: selectGA4PropertyForAConnection description: "Validated against the connection's own real property list -- a customer can only select a\nproperty their own connected account actually has. Historical rows already collected under\na previously selected property are never reassigned by changing this." parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: id: ... account_email: ... status: connected connected_at: ... selected_property: id: properties/123 name: 'Subsig - GA4' properties: data: type: object properties: id: type: string example: ... account_email: type: string example: ... status: type: string example: connected connected_at: type: string example: ... selected_property: type: object properties: id: type: string example: properties/123 name: type: string example: 'Subsig - GA4' tags: - 'Google Analytics Integration' requestBody: required: true content: application/json: schema: type: object properties: property_id: type: string description: 'One of the ids returned by the properties endpoint.' example: architecto required: - property_id security: [] parameters: - in: path name: connection description: "The connection's uuid." example: architecto required: true schema: type: string '/api/integrations/google-analytics/connections/{connection}': delete: summary: 'Disconnect Google Analytics Account' operationId: disconnectGoogleAnalyticsAccount description: "Best-effort revoke on Google's side, then removes the connection regardless of whether the\nrevoke call succeeds." parameters: [] responses: 204: description: '' content: application/json: schema: type: object example: { } properties: { } tags: - 'Google Analytics Integration' security: [] parameters: - in: path name: connection description: "The connection's uuid." example: architecto required: true schema: type: string '/api/organisations/{organisation_uuid}/agents/recommendations': get: summary: '' operationId: getApiOrganisationsOrganisation_uuidAgentsRecommendations 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: 205ae76c-bd73-426b-a36a-8338353b9542 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/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' 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' 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' 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' 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' 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' 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/organisations/supported-regions: get: summary: 'Supported Regions' operationId: supportedRegions description: "Curated list of region/country codes usable for an organisation's `region` (and, by\nfallback, a prompt's `country` -- see AiVisibilityPromptController::store()). Backs the\nfrontend's region/country picker so it never offers a value the backend would then reject.\nSee config('ai_visibility.supported_regions')'s docblock for why this list isn't simply\n\"every ISO 3166-1 alpha-2 code\" or restricted to any one provider's own narrower support." parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: - value: US label: 'United States' - value: GB label: 'United Kingdom' properties: data: type: array example: - value: US label: 'United States' - value: GB label: 'United Kingdom' items: type: object properties: value: type: string example: US label: type: string example: 'United States' tags: - Organisations security: [] /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 region: type: string description: '' example: architecto nullable: true language: type: string description: 'Must not be greater than 10 characters.' example: ngzmiy 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 region: type: string description: '' example: architecto nullable: true language: type: string description: 'Must not be greater than 10 characters.' example: ngzmiy 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: 205ae76c-bd73-426b-a36a-8338353b9542 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: 205ae76c-bd73-426b-a36a-8338353b9542 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/ownership-transfers/accept: post: summary: 'Accept Ownership Transfer' operationId: acceptOwnershipTransfer description: '' parameters: [] responses: { } tags: - 'Ownership Transfer' requestBody: required: true content: application/json: schema: type: object properties: token: type: string description: 'The ownership transfer token.' example: architecto required: - token security: [] /api/ownership-transfers: post: summary: 'Initiate Ownership Transfer' operationId: initiateOwnershipTransfer description: '' parameters: [] responses: { } tags: - 'Ownership Transfer' requestBody: required: true content: application/json: schema: type: object properties: email: type: string description: "The new owner's email address." example: gbailey@example.net organisation_id: type: string description: "The organisation UUID to transfer (its whole\n owned bundle moves together)." example: architecto required: - email - organisation_id security: [] /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' 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' 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' 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' 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' 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' required: - product_name parameters: - in: path name: organisation_uuid description: '' example: 205ae76c-bd73-426b-a36a-8338353b9542 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' 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' 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' 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' 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' 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: 205ae76c-bd73-426b-a36a-8338353b9542 required: true schema: type: string - in: path name: uuid description: '' example: cbe3b865-507c-442a-aeab-0e6b948c10e0 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/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://subsig-frontend.vercel.app/dashboard/notification/overview?slack_error=architecto\n \n \n Redirecting to https://subsig-frontend.vercel.app/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` and `limits.*_soft`.\nUsage is all-time across organisations owned by the billing user.\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: competitor_brands_limit: 3 history_months: 6 refresh_interval: daily users_limit: null workspaces_limit: null data_export_type: null ai_visibility_prompts_limit: 20 limits: ai_visibility_prompts_soft: 20 usage: mentions: 1000 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: competitor_brands_limit: type: integer example: 3 history_months: type: integer example: 6 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: ai_visibility_prompts_soft: type: integer example: 20 usage: type: object properties: mentions: type: integer example: 1000 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: 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: 23 quantity: type: integer description: 'This field is required when addons.ai_visibility_prompts is present. Must be at least 1.' example: 64 required: - plan_id - success_url - cancel_url security: [] /api/subscriptions/addons: post: summary: 'Update Subscription Add-ons' operationId: updateSubscriptionAddOns description: "Update 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_ai_visibility_prompts_123 stripe_price_id: price_inline_ai_visibility_prompts_1 price: 2000 ai_visibility_prompts_quantity: 1 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_ai_visibility_prompts_123 stripe_price_id: price_inline_ai_visibility_prompts_1 price: 2000 ai_visibility_prompts_quantity: 1 status: active items: type: object properties: id: type: integer example: 10 product_id: type: string example: prod_ai_visibility_prompts_123 stripe_price_id: type: string example: price_inline_ai_visibility_prompts_1 price: type: integer example: 2000 ai_visibility_prompts_quantity: type: integer example: 1 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: ai_visibility_prompts: type: object description: 'Optional AI Visibility Prompts add-on payload.' example: [] properties: price: type: integer description: 'Required with addons.ai_visibility_prompts. Total amount in cents.' example: 14000 quantity: type: integer description: 'Required with addons.ai_visibility_prompts. Quantity to provision.' example: 1000 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 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: ai_visibility_prompts: type: object description: 'Optional AI Visibility Prompts add-on payload.' example: [] properties: price: type: integer description: 'Required with addons.ai_visibility_prompts. Total amount in cents.' example: 14000 quantity: type: integer description: 'Required with addons.ai_visibility_prompts. Quantity to provision.' example: 1000 proration_date: type: integer description: 'Optional Unix timestamp used by Stripe for proration preview.' example: 1715000000 required: - addons /api/subscriptions/ai-platforms/pricing: get: summary: 'AI Platforms Pricing' operationId: aIPlatformsPricing description: "Cents-per-prompt rate for each AI platform add-on, keyed in the frontend's own provider\nvocabulary (GetMoreAiPlatformsModal.tsx multiplies this by the org's current AI Visibility\nPrompts quantity to render its preview) -- this is now also the exact table\nrepriceAiPlatformsAddons() uses server-side to compute the real charge in updateAddons(),\nso the preview and the actual charge can never drift out of sync." parameters: [] responses: 200: description: '' content: application/json: schema: type: object example: data: openai: 0 perplexity: 0 google_ai_overviews: 0 bing_copilot: 0 google_ai_mode: 43 gemini: 161 claude: 142 deepseek: 12 grok: 120 properties: data: type: object properties: openai: type: integer example: 0 perplexity: type: integer example: 0 google_ai_overviews: type: integer example: 0 bing_copilot: type: integer example: 0 google_ai_mode: type: integer example: 43 gemini: type: integer example: 161 claude: type: integer example: 142 deepseek: type: integer example: 12 grok: type: integer example: 120 tags: - Subscriptions /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/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