openapi: 3.0.3
info:
title: 'Subsig API Documentation'
description: ''
version: 1.0.0
servers:
-
url: 'https://backend-api.subsig.com'
tags:
-
name: Registration
description: 'Create a new user account to access the application.'
-
name: Authentication
description: "\nAPIs for user authentication"
-
name: 'Password Reset'
description: 'Recover access to your account if you forgot your password.'
-
name: '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 - Chats'
description: "\nThe org-wide \"Chats\" tab -- every AI response (execution or radar pull) across every prompt,\nwith brand-mention/citation/web-search summary stats and a filterable, paginated, exportable\nlist. Deliberately separate from AiVisibilityPromptAnalyticsController's responses()/\nexportResponses(): those are hard-scoped to one prompt and gap-fill against that single\nprompt's own check cadence (AiVisibilityResponseGapFill) -- a concept that doesn't generalize\nacross an org's many prompts with different frequencies. This controller queries the raw\nexecution/radar rows directly with no gap-fill, and duplicates the small amount of\nrow-mapping/CSV logic it needs rather than pulling in that per-prompt machinery."
-
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 - Fanout Queries'
description: "\nThe org-wide \"Fanout Query\" tab -- every sub-query an AI engine ran behind the scenes to\nanswer a tracked prompt, across both the active LLM pipeline (ai_visibility_fanout_queries)\nand the radar/google_ai_overviews pipeline (ai_visibility_radar_fanout_queries). See\nAiVisibilityFanoutQueryAggregateQuery's class doc for exactly which providers ever populate\neither table and why."
-
name: 'AI Visibility - Locations'
description: "\nSettings > Account and Workspaces \"Locations\" list -- unlike onboarding's Step 1 (where every\nlocation is collected client-side and submitted in one batch because the organisation doesn't\nexist yet), the organisation already exists here, so each location is a real, immediate call."
-
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 - Saved Views'
description: "\nNamed filter-state snapshots for the AI Visibility Overview page (date range/providers/brands/\ntopics/tags/regions/prompt_type) -- \"Weekly review\", \"US market\", etc. Shared across the\norganisation: any member can save, see, and delete a view, same as tags/topics. There is no\npersisted \"Default\" row -- that's the frontend's own reset state, never saved here."
-
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 - Tags'
description: "\nAPIs for managing tags used to group AI Visibility prompts within an organisation. Unlike\nai_visibility_topics (one nullable topic per prompt), a prompt can carry several tags --\nsee ai_visibility_prompt_tags."
-
name: 'AI Visibility - Topics'
description: "\nAPIs for managing topics used to group AI Visibility prompts within an organisation."
-
name: 'AI Visibility — Addon Access'
description: ''
-
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: Platforms
description: "\nAPIs for managing platforms"
-
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
required:
- token
- intent
security: []
/api/auth/linkedin:
post:
summary: 'Authenticate with LinkedIn'
operationId: authenticateWithLinkedIn
description: 'Verify a LinkedIn access token and log in or register based on intent.'
parameters: []
responses:
200:
description: ''
content:
application/json:
schema:
type: object
example:
token: 1|abc123...
user:
name: 'John Doe'
email: john@example.com
avatar: 'https://media.licdn.com/...'
properties:
token:
type: string
example: 1|abc123...
user:
type: object
properties:
name:
type: string
example: 'John Doe'
email:
type: string
example: john@example.com
avatar:
type: string
example: 'https://media.licdn.com/...'
401:
description: ''
content:
application/json:
schema:
oneOf:
-
description: 'Invalid token'
type: object
example:
message: 'Invalid LinkedIn token.'
properties:
message:
type: string
example: 'Invalid LinkedIn token.'
-
description: 'LinkedIn account conflict'
type: object
example:
message: 'This account is linked to a different LinkedIn account.'
properties:
message:
type: string
example: 'This account is linked to a different LinkedIn account.'
404:
description: 'Account not found'
content:
application/json:
schema:
type: object
example:
code: ACCOUNT_NOT_FOUND
message: 'Account not found.'
errors:
email:
- 'No account exists for this LinkedIn email.'
properties:
code:
type: string
example: ACCOUNT_NOT_FOUND
message:
type: string
example: 'Account not found.'
errors:
type: object
properties:
email:
type: array
example:
- 'No account exists for this LinkedIn email.'
items:
type: string
422:
description: ''
content:
application/json:
schema:
oneOf:
-
description: 'Account already exists'
type: object
example:
code: ACCOUNT_ALREADY_EXISTS
message: 'An account with this email already exists. Please log in.'
errors:
email:
- 'An account with this email already exists. Please log in.'
properties:
code:
type: string
example: ACCOUNT_ALREADY_EXISTS
message:
type: string
example: 'An account with this email already exists. Please log in.'
errors:
type: object
properties:
email:
type: array
example:
- 'An account with this email already exists. Please log in.'
items:
type: string
-
description: 'Validation error'
type: object
example:
message: 'The token field is required.'
errors:
token:
- 'The token field is required.'
properties:
message:
type: string
example: 'The token field is required.'
errors:
type: object
properties:
token:
type: array
example:
- 'The token field is required.'
items:
type: string
tags:
- Authentication
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
token:
type: string
description: 'The LinkedIn access token from the frontend.'
example: AQV...
intent:
type: string
description: 'The auth flow: login (existing users only) or register (new users only).'
example: login
appsumo_registration_token:
type: string
description: ''
example: architecto
nullable: true
required:
- token
- intent
security: []
/api/auth/change-password:
post:
summary: 'Change Password'
operationId: changePassword
description: 'Change password for an authenticated API user and return a fresh token.'
parameters: []
responses:
200:
description: ''
content:
application/json:
schema:
type: object
example:
message: 'Password updated successfully.'
token: 1|abc123...
properties:
message:
type: string
example: 'Password updated successfully.'
token:
type: string
example: 1|abc123...
401:
description: Unauthenticated
content:
application/json:
schema:
type: object
example:
message: Unauthenticated.
properties:
message:
type: string
example: Unauthenticated.
422:
description: 'Current password is incorrect'
content:
application/json:
schema:
type: object
example:
message: 'The given data was invalid.'
errors:
current_password:
- 'The provided password does not match your current password.'
properties:
message:
type: string
example: 'The given data was invalid.'
errors:
type: object
properties:
current_password:
type: array
example:
- 'The provided password does not match your current password.'
items:
type: string
tags:
- Authentication
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
current_password:
type: string
description: "User's current password."
example: SecurePass123!
password:
type: string
description: 'New password (min 8 characters).'
example: NewSecurePass123!
required:
- current_password
- password
/api/auth/set-password:
post:
summary: 'Set Password'
operationId: setPassword
description: "Set a password for an authenticated user who signed up via Google or LinkedIn.\nPlaintext is never required for the previous random placeholder password."
parameters: []
responses:
200:
description: ''
content:
application/json:
schema:
type: object
example:
message: 'Password set successfully.'
token: 1|abc123...
properties:
message:
type: string
example: 'Password set successfully.'
token:
type: string
example: 1|abc123...
422:
description: 'Password already set'
content:
application/json:
schema:
type: object
example:
message: 'A password is already set. Use change-password instead.'
error:
code: password_already_set
properties:
message:
type: string
example: 'A password is already set. Use change-password instead.'
error:
type: object
properties:
code:
type: string
example: password_already_set
tags:
- Authentication
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
password:
type: string
description: 'New password (min 8 characters).'
example: NewSecurePass123!
password_confirmation:
type: string
description: 'Must match password.'
example: NewSecurePass123!
required:
- password
- password_confirmation
/api/auth/complete-profile:
post:
summary: 'Complete Profile'
operationId: completeProfile
description: "Fill in the profile details collected on the \"About you\" step right after email-only\nregistration (see CreateNewUser::create()). Password is only required the first time --\nonce hasUsablePassword() is true, omit it and use change-password instead."
parameters: []
responses:
200:
description: ''
content:
application/json:
schema:
type: object
example:
message: 'Profile completed successfully.'
token: 1|abc123...
user: { }
properties:
message:
type: string
example: 'Profile completed successfully.'
token:
type: string
example: 1|abc123...
user:
type: object
properties: { }
422:
description: 'Password already set'
content:
application/json:
schema:
type: object
example:
message: 'A password is already set. Use change-password instead.'
error:
code: password_already_set
properties:
message:
type: string
example: 'A password is already set. Use change-password instead.'
error:
type: object
properties:
code:
type: string
example: password_already_set
tags:
- Authentication
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
name:
type: string
description: 'Full name.'
example: 'Jane Doe'
password:
type: string
description: 'Required on first call only.'
example: NewSecurePass123!
job_role:
type: string
description: 'What best describes the user.'
example: founder_ceo
company_name:
type: string
description: ''
example: 'Acme Corporation'
phone:
type: string
description: ''
example: '+14155552671'
nullable: true
agree_to_terms:
type: boolean
description: 'Must be true.'
example: true
agree_to_marketing:
type: boolean
description: ''
example: false
nullable: true
password_confirmation:
type: string
description: 'Required with password.'
example: NewSecurePass123!
required:
- name
- job_role
- company_name
- agree_to_terms
/api/invites/accept:
post:
summary: 'Register with Invite'
operationId: registerWithInvite
description: "Accept an invitation and create a new user account. The email address must match\nthe email address on the invite. After successful registration, the user will be\nadded to the organisation or project and will receive an email verification code.\n\nNote: This endpoint bypasses the business email requirement since the invitation\nitself validates the user's legitimacy."
parameters: []
responses:
201:
description: ''
content:
application/json:
schema:
type: object
example:
message: 'Registration successful. Please check your email to verify your account.'
properties:
message:
type: string
example: 'Registration successful. Please check your email to verify your account.'
404:
description: 'Token not found'
content:
application/json:
schema:
type: object
example:
message: 'Invite not found.'
errors:
token:
- 'The invite token is invalid or does not exist.'
properties:
message:
type: string
example: 'Invite not found.'
errors:
type: object
properties:
token:
type: array
example:
- 'The invite token is invalid or does not exist.'
items:
type: string
410:
description: ''
content:
application/json:
schema:
oneOf:
-
description: 'Invite expired'
type: object
example:
message: 'Invite has expired.'
errors:
token:
- 'This invite has expired. Please request a new invitation.'
properties:
message:
type: string
example: 'Invite has expired.'
errors:
type: object
properties:
token:
type: array
example:
- 'This invite has expired. Please request a new invitation.'
items:
type: string
-
description: 'Invite already accepted'
type: object
example:
message: 'Invite has already been accepted.'
errors:
token:
- 'This invite has already been accepted.'
properties:
message:
type: string
example: 'Invite has already been accepted.'
errors:
type: object
properties:
token:
type: array
example:
- 'This invite has already been accepted.'
items:
type: string
422:
description: ''
content:
application/json:
schema:
oneOf:
-
description: 'Email mismatch'
type: object
example:
message: 'The email address does not match the invitation.'
errors:
email:
- 'The email address must match the email on the invitation.'
properties:
message:
type: string
example: 'The email address does not match the invitation.'
errors:
type: object
properties:
email:
type: array
example:
- 'The email address must match the email on the invitation.'
items:
type: string
-
description: 'Validation error'
type: object
example:
message: 'The name field is required.'
errors:
name:
- 'The name field is required.'
properties:
message:
type: string
example: 'The name field is required.'
errors:
type: object
properties:
name:
type: array
example:
- 'The name field is required.'
items:
type: string
-
description: 'Email already registered'
type: object
example:
message: 'The email has already been taken.'
errors:
email:
- 'The email has already been taken.'
properties:
message:
type: string
example: 'The email has already been taken.'
errors:
type: object
properties:
email:
type: array
example:
- 'The email has already been taken.'
items:
type: string
tags:
- Authentication
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
token:
type: string
description: 'The invite token from the invitation link.'
example: eEtgjrcdtubjCu4817MfGiimvC2DQLBgaI7LpY1g5kdDMK5wJlQank7ZJ6PWurmb
name:
type: string
description: "The user's full name."
example: 'John Doe'
email:
type: string
description: "The user's email address (must match the invite email)."
example: user@example.com
password:
type: string
description: "The user's password (min 8 characters)."
example: SecurePass123!
password_confirmation:
type: string
description: 'Password confirmation.'
example: SecurePass123!
required:
- token
- name
- email
- password
- password_confirmation
security: []
/api/user:
get:
summary: 'Get Current User'
operationId: getCurrentUser
description: "Get the authenticated user's details including organisation and subscription information."
parameters: []
responses:
200:
description: ''
content:
application/json:
schema:
type: object
example:
name: 'John Doe'
email: john@example.com
email_verified_at: '2025-12-04T12:00:00.000000Z'
created_at: '2025-12-04T10:00:00.000000Z'
organisation:
uuid: 550e8400-e29b-41d4-a716-446655440000
name: 'Acme Inc'
website: 'https://acme.com'
product_logo: 'https://example.com/logo.png'
role: organisation_owner
subscription:
id: 1
stripe_price_id: price_1234567890
name: 'Pro Plan'
status: active
expiration_date: '2025-12-31T23:59:59.000000Z'
trial_end_date: '2025-12-11T23:59:59.000000Z'
properties:
name:
type: string
example: 'John Doe'
email:
type: string
example: john@example.com
email_verified_at:
type: string
example: '2025-12-04T12:00:00.000000Z'
created_at:
type: string
example: '2025-12-04T10:00:00.000000Z'
organisation:
type: object
properties:
uuid:
type: string
example: 550e8400-e29b-41d4-a716-446655440000
name:
type: string
example: 'Acme Inc'
website:
type: string
example: 'https://acme.com'
product_logo:
type: string
example: 'https://example.com/logo.png'
role:
type: string
example: organisation_owner
subscription:
type: object
properties:
id:
type: integer
example: 1
stripe_price_id:
type: string
example: price_1234567890
name:
type: string
example: 'Pro Plan'
status:
type: string
example: active
expiration_date:
type: string
example: '2025-12-31T23:59:59.000000Z'
trial_end_date:
type: string
example: '2025-12-11T23:59:59.000000Z'
401:
description: Unauthenticated
content:
application/json:
schema:
type: object
example:
message: Unauthenticated.
properties:
message:
type: string
example: Unauthenticated.
tags:
- Authentication
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/chats':
post:
summary: 'Chats — List'
operationId: chatsList
description: 'Paginated, org-wide list of every AI response (execution or radar pull), newest first.'
parameters: []
responses: { }
tags:
- 'AI Visibility - Chats'
requestBody:
required: false
content:
application/json:
schema:
type: object
properties:
providers:
type: array
description: 'Optional AI model/provider filter.'
example:
- architecto
items:
type: string
date_from:
type: string
description: 'Optional start date.'
example: architecto
date_to:
type: string
description: 'Optional end date.'
example: architecto
topic_ids:
type: array
description: 'Optional topic filter.'
example:
- 16
items:
type: integer
brand_ids:
type: array
description: 'Optional "All brands" mention filter.'
example:
- 16
items:
type: integer
sources:
type: array
description: 'Optional "All sources" citation-domain filter.'
example:
- architecto
items:
type: string
search:
type: string
description: 'Optional free-text search over the response text.'
example: architecto
page:
type: integer
description: 'Optional. Default: 1.'
example: 16
per_page:
type: integer
description: 'Optional. 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/chats/overview':
post:
summary: 'Chats — Overview'
operationId: chatsOverview
description: "The 4 header cards (Total chats, Brand mentioned, Answers with web search, Average\ncitations), each with a period-over-period change against the filters' comparison window.\nbrand_ids narrows the cards the same way it narrows the table below (top filter bar).\nDeliberately still ignores sources/search -- those remain table-only refinements."
parameters: []
responses: { }
tags:
- 'AI Visibility - Chats'
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/chats/export':
post:
summary: 'Chats — Export'
operationId: chatsExport
description: 'CSV of the full filtered set (not just the current page).'
parameters: []
responses: { }
tags:
- 'AI Visibility - Chats'
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/chats/filters':
get:
summary: 'Chats — Filters'
operationId: chatsFilters
description: "Distinct citation domains ever seen across the org's chats (execution + radar), for the\n\"All sources\" dropdown. Not date-scoped -- a filter's option list is the full universe of\nvalues, not just what the current date range happens to show."
parameters: []
responses:
401:
description: ''
content:
application/json:
schema:
type: object
example:
message: Unauthenticated.
properties:
message:
type: string
example: Unauthenticated.
tags:
- 'AI Visibility - Chats'
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/chats/{chat}':
get:
summary: 'Chats — Detail'
operationId: chatsDetail
description: "A single chat, keyed by the same composite id the list/export rows carry\n(\"execution-{id}\" or \"radar-{id}\") -- there's no single backend table a chat lives in, so\nthe id itself says which one to look up."
parameters: []
responses:
401:
description: ''
content:
application/json:
schema:
type: object
example:
message: Unauthenticated.
properties:
message:
type: string
example: Unauthenticated.
tags:
- 'AI Visibility - Chats'
parameters:
-
in: path
name: organisation_uuid
description: 'The organisation UUID.'
example: 660e8400-e29b-41d4-a716-446655440001
required: true
schema:
type: string
-
in: path
name: chat
description: 'The composite chat id from a list row.'
example: execution-42
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: "Either website or social_profiles is required (a competitor with no website can still be\ntracked via its social profiles, same as the own brand's onboarding modes) -- see\nCitationClassifier for how social_profiles feeds 'competitor' citation classification."
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
nullable: true
social_profiles:
type: array
description: ''
example:
-
platform: instagram
url: 'https://instagram.com/acme'
items:
type: object
nullable: true
properties:
platform:
type: string
description: 'This field is required when social_profiles is present. Must not be greater than 50 characters.'
example: 'n'
url:
type: string
description: 'This field is required when social_profiles is present. Must be a valid URL. Must not be greater than 500 characters.'
example: 'http://crooks.biz/et-fugiat-sunt-nihil-accusantium'
location_scope:
type: string
description: 'Optional, franchise orgs only: global (default), all_locations, or chosen. Inferred from location_ids when omitted.'
example: architecto
location_ids:
type: array
description: 'The specific locations to track at, when scope is chosen (or inferred chosen).'
example:
- 16
items:
type: integer
track_globally:
type: boolean
description: 'Independent of location_scope -- whether this competitor is compared on Global (no-city) prompts too. Default true.'
example: false
required:
- name
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: "Either website or social_profiles is required, same rule as Add Competitor -- editing a\ncompetitor from website-tracked to social-tracked (or vice versa) is a supported mode\nswitch, not just a field edit."
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: 'Required without social_profiles.'
example: architecto
nullable: true
social_profiles:
type: array
description: 'Required without website.'
example:
-
platform: instagram
url: 'https://instagram.com/acme'
items:
type: object
nullable: true
properties:
platform:
type: string
description: 'This field is required when social_profiles is present. Must not be greater than 50 characters.'
example: 'n'
url:
type: string
description: 'This field is required when social_profiles is present. Must be a valid URL. Must not be greater than 500 characters.'
example: 'http://crooks.biz/et-fugiat-sunt-nihil-accusantium'
location_scope:
type: string
description: 'Optional, franchise orgs only: global (default), all_locations, or chosen. Inferred from location_ids when omitted.'
example: architecto
location_ids:
type: array
description: 'The specific locations to track at, when scope is chosen (or inferred chosen).'
example:
- 16
items:
type: integer
track_globally:
type: boolean
description: 'Independent of location_scope -- whether this competitor is compared on Global (no-city) prompts too. Default true.'
example: false
required:
- name
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/competitor-brands/{brand}/locations/{location}/store-match':
patch:
summary: "Override a competitor's store match"
operationId: overrideACompetitorsStoreMatch
description: "Lets an org admin correct a wrong auto-match (or manually set/clear one) for one of the\norganisation's own locations. Always sets is_manual_override, so\nMatchAiVisibilityCompetitorStoreJob never revisits this (brand, location) pair again -- see\nits own docblock. Upserts, same shape as the job's own persist() method, since an admin may\nset a match before the job has ever run for this pair."
parameters: []
responses: { }
tags:
- 'AI Visibility - Competitor'
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
status:
type: string
description: 'matched or not_found.'
example: matched
matched_place_id:
type: string
description: 'Required when status is matched.'
example: architecto
nullable: true
matched_name:
type: string
description: 'Required when status is matched.'
example: architecto
nullable: true
matched_address:
type: string
description: Optional.
example: architecto
nullable: true
required:
- status
parameters:
-
in: path
name: organisation_uuid
description: ''
example: 6ff8f7f6-1eb3-3525-be4a-3932c805afed
required: true
schema:
type: string
-
in: path
name: brand
description: 'The competitor brand ID.'
example: 16
required: true
schema:
type: integer
-
in: path
name: location
description: "The organisation's own location 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
prompt_type:
type: string
description: "Optional - branded or non_branded, computed live from each\n prompt's text against the org's tracked brand names (see\n AiVisibilityPromptBrandedClassifier). Omit for no prompt-type 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/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-overview':
post:
summary: 'Citations — Domains Overview'
operationId: citationsDomainsOverview
description: "Cards, retrievals trend, top domains and the domain-type breakdown for the Domains main\nscreen — the domain-scoped counterpart of index()'s combined payload, so that screen isn't\npulling URL-scoped fields it never renders."
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/urls-overview':
post:
summary: 'Citations — URLs Overview'
operationId: citationsURLsOverview
description: 'URL-scoped counterpart of domainsOverview() above, for the URLs main screen.'
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/fanout':
post:
summary: 'Fanout Queries: List'
operationId: fanoutQueriesList
description: "Paginated, searchable, org-wide list of distinct query fanouts, most-occurring first.\nPass topic_id to page through just one topic's group of queries (the \"Group by Topics\" UI\nexpands one topic at a time and lazy-loads its own page via this same endpoint) -- the\ntopic list itself (with in-scope prompt counts) is served by topics() below."
parameters: []
responses: { }
tags:
- 'AI Visibility - Fanout Queries'
requestBody:
required: false
content:
application/json:
schema:
type: object
properties:
providers:
type: array
description: 'Optional AI model filter.'
example:
- architecto
items:
type: string
date_from:
type: string
description: 'Optional start date.'
example: architecto
date_to:
type: string
description: 'Optional end date.'
example: architecto
search:
type: string
description: 'Optional free-text search over the query text.'
example: architecto
type:
type: string
description: 'Optional "search" or "question" filter.'
example: architecto
topic_id:
type: integer
description: "Optional -- narrows to one topic's queries."
example: 16
page:
type: integer
description: 'Optional. Default: 1.'
example: 16
per_page:
type: integer
description: 'Optional. 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/fanout/overview':
post:
summary: 'Fanout Queries: Overview'
operationId: fanoutQueriesOverview
description: "The 4 header cards, each with a period-over-period change against the filters' comparison\nwindow."
parameters: []
responses: { }
tags:
- 'AI Visibility - Fanout Queries'
requestBody:
required: false
content:
application/json:
schema:
type: object
properties:
providers:
type: array
description: 'Optional AI model filter.'
example:
- architecto
items:
type: string
date_from:
type: string
description: 'Optional start date.'
example: architecto
date_to:
type: string
description: 'Optional end date.'
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/fanout/topics':
post:
summary: 'Fanout Queries: Topic Groups'
operationId: fanoutQueriesTopicGroups
description: "Powers the \"Group by Topics\" accordion -- only topics that actually have a fanout query in\nthe current filtered scope, each with its distinct-prompt count (the \"N Prompts\" badge).\nDistinct from GET /ai-visibility/topics (every topic, unscoped prompt-status counts): that\nendpoint doesn't know about date/provider filters or fanout data at all."
parameters: []
responses: { }
tags:
- 'AI Visibility - Fanout Queries'
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/fanout/brand-mentioned':
post:
summary: 'Fanout Queries: Brand Mentioned'
operationId: fanoutQueriesBrandMentioned
description: "Donut-chart-shaped brand breakdown: each tracked brand's share of the executions/radar\npulls behind the in-scope fanout queries that mentioned it. Every slice sums to 100%, same\ninvariant as Visibility's share-of-voice donut."
parameters: []
responses: { }
tags:
- 'AI Visibility - Fanout Queries'
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/fanout/common-terms':
post:
summary: 'Fanout Queries: Common Terms'
operationId: fanoutQueriesCommonTerms
description: "Most frequent 2-3 word phrases across every fanout query's text in scope."
parameters: []
responses: { }
tags:
- 'AI Visibility - Fanout Queries'
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/fanout/export':
post:
summary: 'Fanout Queries: Export'
operationId: fanoutQueriesExport
description: 'CSV of the full filtered, grouped query list (not just the current page).'
parameters: []
responses: { }
tags:
- 'AI Visibility - Fanout Queries'
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/locations':
get:
summary: 'Visibility by location'
operationId: visibilityByLocation
description: "One visibility/SOV/rank row per location for the Home \"Locations\" card -- not a raw location\nlist (see workspaceSummary() for that). Empty for an org with no locations."
parameters:
-
in: query
name: date_from
description: 'Optional start date (Y-m-d). Default: 30 days ago.'
example: architecto
required: false
schema:
type: string
description: 'Optional start date (Y-m-d). Default: 30 days ago.'
example: architecto
-
in: query
name: date_to
description: 'Optional end date (Y-m-d). Default: today.'
example: architecto
required: false
schema:
type: string
description: 'Optional end date (Y-m-d). Default: today.'
example: architecto
responses:
401:
description: ''
content:
application/json:
schema:
type: object
example:
message: Unauthenticated.
properties:
message:
type: string
example: Unauthenticated.
tags:
- 'AI Visibility - Locations'
post:
summary: 'Add a location'
operationId: addALocation
description: "The address is resolved client-side first via the existing Google Places lookup endpoints\n(GooglePlacesLookupController, unchanged) -- this endpoint only persists the already-resolved\nfields, it never calls the Google Places API itself."
parameters: []
responses: { }
tags:
- 'AI Visibility - Locations'
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
name:
type: string
description: 'Display name for the location.'
example: 'Times Square'
country_code:
type: string
description: 'ISO 3166-1 alpha-2.'
example: US
locality:
type: string
description: '", " from the resolved Place.'
example: 'New York, United States'
formatted_address:
type: string
description: 'Full street address from the resolved Place.'
example: architecto
nullable: true
google_place_id:
type: string
description: "The resolved Place's id."
example: architecto
nullable: true
outlet_page_url:
type: string
description: "Optional outlet page URL, editable regardless of what the Place's own website_uri says."
example: 'http://www.bailey.biz/quos-velit-et-fugiat-sunt-nihil-accusantium-harum.html'
nullable: true
required:
- name
- country_code
- locality
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}/locations':
get:
summary: 'Workspace locations summary'
operationId: workspaceLocationsSummary
description: "Settings' \"Locations\" card and the multi-location gate (`useWorkspaceLocations`): the own\nbrand's workspace type, every location with its tracked prompt/competitor counts, and the\norg-wide Global (no-location) counts. A `single` workspace always returns an empty\n`locations` array, regardless of how many rows exist (there shouldn't be any)."
parameters: []
responses:
401:
description: ''
content:
application/json:
schema:
type: object
example:
message: Unauthenticated.
properties:
message:
type: string
example: Unauthenticated.
tags:
- 'AI Visibility - Locations'
post:
summary: 'Add a location (frontend contract)'
operationId: addALocationfrontendContract
description: "Same persistence path as store(), just the field names the dashboard's \"Add location\" modal\nactually sends (`address`/`city`/`place_id`/`outlet_url` instead of\n`formatted_address`/`locality`/`google_place_id`/`outlet_page_url`)."
parameters: []
responses: { }
tags:
- 'AI Visibility - Locations'
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
name:
type: string
description: ''
example: architecto
country_code:
type: string
description: 'ISO 3166-1 alpha-2.'
example: architecto
city:
type: string
description: ''
example: architecto
address:
type: string
description: 'Full formatted address.'
example: architecto
nullable: true
place_id:
type: string
description: ''
example: architecto
nullable: true
outlet_url:
type: string
description: ''
example: 'http://www.bailey.biz/quos-velit-et-fugiat-sunt-nihil-accusantium-harum.html'
nullable: true
required:
- name
- country_code
- city
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}/locations/{location}':
patch:
summary: 'Edit a location (frontend contract)'
operationId: editALocationfrontendContract
description: "No equivalent existed before this -- Phase 1 only ever shipped create/list. Same field\nrename as storeForFrontend()."
parameters: []
responses: { }
tags:
- 'AI Visibility - Locations'
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
name:
type: string
description: 'Must not be greater than 255 characters.'
example: b
country_code:
type: string
description: 'Must be 2 characters.'
example: ng
city:
type: string
description: 'Must not be greater than 150 characters.'
example: z
address:
type: string
description: 'Must not be greater than 500 characters.'
example: m
nullable: true
place_id:
type: string
description: 'Must not be greater than 255 characters.'
example: i
nullable: true
outlet_url:
type: string
description: 'Must be a valid URL. Must not be greater than 500 characters.'
example: 'http://www.okon.com/accusantium-harum-mollitia-modi-deserunt-aut-ab'
nullable: true
required:
- name
- country_code
- city
parameters:
-
in: path
name: organisation_uuid
description: 'The organisation UUID.'
example: 6ff8f7f6-1eb3-3525-be4a-3932c805afed
required: true
schema:
type: string
-
in: path
name: location
description: 'The location ID.'
example: 16
required: true
schema:
type: integer
'/api/organisations/{organisation_uuid}/ai-visibility/prompts/{prompt}/overview':
get:
summary: 'Prompt Overview'
operationId: promptOverview
description: ''
parameters: []
responses:
401:
description: ''
content:
application/json:
schema:
type: object
example:
message: Unauthenticated.
properties:
message:
type: string
example: Unauthenticated.
tags:
- 'AI Visibility - Prompt Detail'
parameters:
-
in: path
name: organisation_uuid
description: 'The organisation UUID.'
example: 660e8400-e29b-41d4-a716-446655440001
required: true
schema:
type: string
-
in: path
name: prompt
description: 'The prompt ID.'
example: 1
required: true
schema:
type: integer
'/api/organisations/{organisation_uuid}/ai-visibility/prompts/{prompt}/filters':
get:
summary: 'Prompt Filter Options'
operationId: promptFilterOptions
description: "Distinct providers, models, countries and languages that have actually run for this prompt.\nUsed to populate the filter dropdowns on the prompt detail page."
parameters: []
responses:
401:
description: ''
content:
application/json:
schema:
type: object
example:
message: Unauthenticated.
properties:
message:
type: string
example: Unauthenticated.
tags:
- 'AI Visibility - Prompt Detail'
parameters:
-
in: path
name: organisation_uuid
description: 'The organisation UUID.'
example: 660e8400-e29b-41d4-a716-446655440001
required: true
schema:
type: string
-
in: path
name: prompt
description: 'The prompt ID.'
example: 1
required: true
schema:
type: integer
'/api/organisations/{organisation_uuid}/ai-visibility/prompts/{prompt}/citations':
post:
summary: 'Prompt Citations'
operationId: promptCitations
description: 'Same shape as `POST /ai-visibility/citations`, scoped to this prompt.'
parameters: []
responses: { }
tags:
- 'AI Visibility - Prompt Detail'
parameters:
-
in: path
name: organisation_uuid
description: 'The organisation UUID.'
example: 660e8400-e29b-41d4-a716-446655440001
required: true
schema:
type: string
-
in: path
name: prompt
description: 'The prompt ID.'
example: 1
required: true
schema:
type: integer
'/api/organisations/{organisation_uuid}/ai-visibility/prompts/{prompt}/citations/domains':
post:
summary: 'Prompt Citation Domains (paginated)'
operationId: promptCitationDomainspaginated
description: 'Same shape as `POST /ai-visibility/citations/domains`, scoped to this prompt.'
parameters: []
responses: { }
tags:
- 'AI Visibility - Prompt Detail'
requestBody:
required: false
content:
application/json:
schema:
type: object
properties:
per_page:
type: integer
description: 'Optional - results per page. Default: 20.'
example: 16
page:
type: integer
description: 'Optional - page number. Default: 1.'
example: 16
parameters:
-
in: path
name: organisation_uuid
description: 'The organisation UUID.'
example: 660e8400-e29b-41d4-a716-446655440001
required: true
schema:
type: string
-
in: path
name: prompt
description: 'The prompt ID.'
example: 1
required: true
schema:
type: integer
'/api/organisations/{organisation_uuid}/ai-visibility/prompts/{prompt}/citations/urls':
post:
summary: 'Prompt Citation URLs (paginated)'
operationId: promptCitationURLspaginated
description: 'Same shape as `POST /ai-visibility/citations/urls`, scoped to this prompt.'
parameters: []
responses: { }
tags:
- 'AI Visibility - Prompt Detail'
requestBody:
required: false
content:
application/json:
schema:
type: object
properties:
per_page:
type: integer
description: 'Optional - results per page. Default: 20.'
example: 16
page:
type: integer
description: 'Optional - page number. Default: 1.'
example: 16
parameters:
-
in: path
name: organisation_uuid
description: 'The organisation UUID.'
example: 660e8400-e29b-41d4-a716-446655440001
required: true
schema:
type: string
-
in: path
name: prompt
description: 'The prompt ID.'
example: 1
required: true
schema:
type: integer
'/api/organisations/{organisation_uuid}/ai-visibility/prompts/{prompt}/citations/export':
post:
summary: 'Export Prompt Citation URLs'
operationId: exportPromptCitationURLs
description: ''
parameters: []
responses: { }
tags:
- 'AI Visibility - Prompt Detail'
parameters:
-
in: path
name: organisation_uuid
description: 'The organisation UUID.'
example: 660e8400-e29b-41d4-a716-446655440001
required: true
schema:
type: string
-
in: path
name: prompt
description: 'The prompt ID.'
example: 1
required: true
schema:
type: integer
'/api/organisations/{organisation_uuid}/ai-visibility/prompts/{prompt}/responses':
get:
summary: ''
operationId: getApiOrganisationsOrganisation_uuidAiVisibilityPromptsPromptResponses
description: ''
parameters: []
responses:
401:
description: ''
content:
application/json:
schema:
type: object
example:
message: Unauthenticated.
properties:
message:
type: string
example: Unauthenticated.
tags:
- 'AI Visibility - Prompt Detail'
parameters:
-
in: path
name: organisation_uuid
description: ''
example: 2958b10e-6a80-4903-868f-79b048b5e0c4
required: true
schema:
type: string
-
in: path
name: prompt
description: 'The prompt.'
example: architecto
required: true
schema:
type: string
'/api/organisations/{organisation_uuid}/ai-visibility/prompts/{prompt}/responses/{execution}':
get:
summary: 'Prompt Response Detail'
operationId: promptResponseDetail
description: 'Full detail for a single execution — backs the "AI Response" modal on the prompt responses tab.'
parameters: []
responses:
200:
description: ''
content:
application/json:
schema:
type: object
example:
data:
execution_id: 42
prompt_title: 'How does Notion help teams?'
provider: openai
model: gpt-4o
date: '2026-06-25 09:00:00'
detected_brands:
-
name: Notion
website: notion.com
sentiment: positive
is_brand_mentioned: true
response_content: 'Notion helps teams...'
citations:
-
rank: 1
url: 'https://notion.so/blog'
title: 'Notion Blog'
domain: notion.so
page_type: references
properties:
data:
type: object
properties:
execution_id:
type: integer
example: 42
prompt_title:
type: string
example: 'How does Notion help teams?'
provider:
type: string
example: openai
model:
type: string
example: gpt-4o
date:
type: string
example: '2026-06-25 09:00:00'
detected_brands:
type: array
example:
-
name: Notion
website: notion.com
items:
type: object
properties:
name:
type: string
example: Notion
website:
type: string
example: notion.com
sentiment:
type: string
example: positive
is_brand_mentioned:
type: boolean
example: true
response_content:
type: string
example: 'Notion helps teams...'
citations:
type: array
example:
-
rank: 1
url: 'https://notion.so/blog'
title: 'Notion Blog'
domain: notion.so
page_type: references
items:
type: object
properties:
rank:
type: integer
example: 1
url:
type: string
example: 'https://notion.so/blog'
title:
type: string
example: 'Notion Blog'
domain:
type: string
example: notion.so
page_type:
type: string
example: references
tags:
- 'AI Visibility - Prompt Detail'
parameters:
-
in: path
name: organisation_uuid
description: 'The organisation UUID.'
example: 660e8400-e29b-41d4-a716-446655440001
required: true
schema:
type: string
-
in: path
name: prompt
description: 'The prompt ID.'
example: 1
required: true
schema:
type: integer
-
in: path
name: execution
description: 'The execution ID.'
example: 42
required: true
schema:
type: integer
'/api/organisations/{organisation_uuid}/ai-visibility/prompts/{prompt}/radar-responses/{radarResponse}':
get:
summary: 'Full detail for a single radar (Google AI Overview) response.'
operationId: fullDetailForASingleRadarGoogleAIOverviewResponse
description: ''
parameters: []
responses:
401:
description: ''
content:
application/json:
schema:
type: object
example:
message: Unauthenticated.
properties:
message:
type: string
example: Unauthenticated.
tags:
- 'AI Visibility - Prompt Detail'
parameters:
-
in: path
name: organisation_uuid
description: ''
example: 6ff8f7f6-1eb3-3525-be4a-3932c805afed
required: true
schema:
type: string
-
in: path
name: prompt
description: ''
example: 16
required: true
schema:
type: integer
-
in: path
name: radarResponse
description: 'The radar_response id.'
example: 16
required: true
schema:
type: integer
'/api/organisations/{organisation_uuid}/ai-visibility/prompts/{prompt}/responses/export':
post:
summary: 'Export Prompt Responses'
operationId: exportPromptResponses
description: ''
parameters: []
responses: { }
tags:
- 'AI Visibility - Prompt Detail'
parameters:
-
in: path
name: organisation_uuid
description: 'The organisation UUID.'
example: 660e8400-e29b-41d4-a716-446655440001
required: true
schema:
type: string
-
in: path
name: prompt
description: 'The prompt ID.'
example: 1
required: true
schema:
type: integer
'/api/organisations/{organisation_uuid}/ai-visibility/executions/{execution}':
get:
summary: 'Execution Detail'
operationId: executionDetail
description: "Not scoped to a prompt and exempt from the dashboard cache — reads live, per Ticket 7's\nspec. 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
tag_ids:
type: array
description: ''
example:
- 16
items:
type: integer
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
location_scope:
type: string
description: "Franchise workspaces only -- a single-location org just never sends these and every\nprompt stays 'global', identical to today. Same enum/validation shape as\nAiVisibilitySuggestionsController::accept()'s location override."
example: architecto
location_ids:
type: array
description: ''
example:
- 16
items:
type: integer
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
locations:
type: array
description: "Optional, parallel to `prompts` by index -- franchise onboarding only. A prompt with\nno entry at its index (or this key omitted entirely) stays 'global', unchanged from\nbefore this parameter existed. Kept as its own array rather than nesting into\n`prompts.*` so a plain string array (every existing/dashboard caller) stays valid."
example: null
items:
type: object
properties:
scope:
type: string
description: 'This field is required when locations is present.'
example: architecto
location_ids:
type: array
description: ''
example:
- 16
items:
type: integer
topics:
type: array
description: 'Must not be greater than 255 characters.'
example:
- g
items:
type: string
nullable: true
tags:
type: array
description: 'Must not be greater than 255 characters.'
example:
-
- z
items:
type: array
description: ''
example:
- z
items:
type: string
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/bulk-delete':
post:
summary: 'Bulk Delete Prompts'
operationId: bulkDeletePrompts
description: "Deletes multiple tracked prompts at once, purging each one's historical visibility/\nsentiment/citation contribution the same way a single delete does (see\nAiVisibilityPromptDataPurger). An ID that doesn't belong to this organisation is silently\nskipped rather than failing the whole batch -- same \"unowned resource is invisible, not an\nerror\" posture as findPromptOrFail()'s 404 on the single-delete endpoint, just non-fatal\nhere since a bulk selection can't assume every ID is still valid by the time it's submitted."
parameters: []
responses: { }
tags:
- 'AI Visibility - Prompts'
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
prompt_ids:
type: array
description: 'Array of prompt IDs to delete. Max 500 items.'
example:
- 16
items:
type: integer
required:
- prompt_ids
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.\n\nreprocessing is a second, independent signal folded into the same `in_progress` flag: true\nwhile a brand/competitor/workspace rename's ReprocessAiVisibilityOrganisationJob fan-out is\nstill working through this org's historical executions/radar responses (see\nAiVisibilityReprocessingTracker). Surfaced separately from in_progress_count since it isn't\na count of prompts in flight -- the frontend banner shows different copy for it."
parameters: []
responses:
200:
description: ''
content:
application/json:
schema:
type: object
example:
data:
in_progress: true
in_progress_count: 2
reprocessing: false
in_progress_prompt_ids:
- '101'
- '104'
properties:
data:
type: object
properties:
in_progress:
type: boolean
example: true
in_progress_count:
type: integer
example: 2
reprocessing:
type: boolean
example: false
in_progress_prompt_ids:
type: array
example:
- '101'
- '104'
items:
type: string
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
tag_ids:
type: array
description: ''
example:
- 16
items:
type: integer
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, tags, 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
location_id: null
-
provider: perplexity
status: ready
location_id: null
-
provider: google_ai_overviews
status: ready
location_id: null
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
location_id: null
-
provider: perplexity
status: ready
location_id: null
-
provider: google_ai_overviews
status: ready
location_id: null
items:
type: object
properties:
provider:
type: string
example: openai
status:
type: string
example: in_progress
location_id:
type: string
example: null
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/saved-views':
get:
summary: 'List Saved Views'
operationId: listSavedViews
description: ''
parameters: []
responses:
200:
description: ''
content:
application/json:
schema:
type: object
example:
data:
-
id: 1
name: 'Weekly review'
filters:
date_preset: last_30_days
providers: []
brand_ids: []
topic_ids: []
tag_ids: []
regions: []
prompt_type: null
created_at: '2026-10-08T10:00:00.000000Z'
updated_at: '2026-10-08T10:00:00.000000Z'
properties:
data:
type: array
example:
-
id: 1
name: 'Weekly review'
filters:
date_preset: last_30_days
providers: []
brand_ids: []
topic_ids: []
tag_ids: []
regions: []
prompt_type: null
created_at: '2026-10-08T10:00:00.000000Z'
updated_at: '2026-10-08T10:00:00.000000Z'
items:
type: object
properties:
id:
type: integer
example: 1
name:
type: string
example: 'Weekly review'
filters:
type: object
properties:
date_preset:
type: string
example: last_30_days
providers:
type: array
example: []
brand_ids:
type: array
example: []
topic_ids:
type: array
example: []
tag_ids:
type: array
example: []
regions:
type: array
example: []
prompt_type:
type: string
example: null
created_at:
type: string
example: '2026-10-08T10:00:00.000000Z'
updated_at:
type: string
example: '2026-10-08T10:00:00.000000Z'
tags:
- 'AI Visibility - Saved Views'
post:
summary: 'Save Current View'
operationId: saveCurrentView
description: ''
parameters: []
responses:
201:
description: ''
content:
application/json:
schema:
type: object
example:
data:
id: 1
name: 'Weekly review'
filters:
date_preset: last_30_days
providers: []
brand_ids: []
topic_ids: []
tag_ids: []
regions: []
prompt_type: null
created_at: '2026-10-08T10:00:00.000000Z'
updated_at: '2026-10-08T10:00:00.000000Z'
properties:
data:
type: object
properties:
id:
type: integer
example: 1
name:
type: string
example: 'Weekly review'
filters:
type: object
properties:
date_preset:
type: string
example: last_30_days
providers:
type: array
example: []
brand_ids:
type: array
example: []
topic_ids:
type: array
example: []
tag_ids:
type: array
example: []
regions:
type: array
example: []
prompt_type:
type: string
example: null
created_at:
type: string
example: '2026-10-08T10:00:00.000000Z'
updated_at:
type: string
example: '2026-10-08T10:00:00.000000Z'
422:
description: ''
content:
application/json:
schema:
type: object
example:
message: 'The name field is required. (and 1 more error)'
errors:
name:
- '"Weekly review" already exists. Pick another name or use the existing view.'
properties:
message:
type: string
example: 'The name field is required. (and 1 more error)'
errors:
type: object
properties:
name:
type: array
example:
- '"Weekly review" already exists. Pick another name or use the existing view.'
items:
type: string
tags:
- 'AI Visibility - Saved Views'
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
name:
type: string
description: ''
example: 'Weekly review'
filters:
type: object
description: "Whatever the Overview page's filter bar currently holds -- stored and returned verbatim."
example: []
properties: { }
required:
- name
- filters
parameters:
-
in: path
name: organisation_uuid
description: 'The organisation UUID.'
example: 660e8400-e29b-41d4-a716-446655440001
required: true
schema:
type: string
'/api/organisations/{organisation_uuid}/ai-visibility/saved-views/{saved_view}':
delete:
summary: 'Delete Saved View'
operationId: deleteSavedView
description: "Owner-only -- unlike creating a view (Owner or Member), deleting one is restricted to\norganisation owners, regardless of who created it. See\nOrganisationPolicy::deleteAiVisibilitySavedView()."
parameters: []
responses:
204:
description: ''
content:
application/json:
schema:
type: object
example: { }
properties: { }
403:
description: ''
content:
application/json:
schema:
type: object
example:
message: 'You must be an organisation owner to delete a saved view.'
properties:
message:
type: string
example: 'You must be an organisation owner to delete a saved view.'
404:
description: ''
content:
application/json:
schema:
type: object
example:
message: 'Saved view not found.'
properties:
message:
type: string
example: 'Saved view not found.'
tags:
- 'AI Visibility - Saved Views'
parameters:
-
in: path
name: organisation_uuid
description: 'The organisation UUID.'
example: 660e8400-e29b-41d4-a716-446655440001
required: true
schema:
type: string
-
in: path
name: saved_view
description: 'The saved view 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 every prompt suggestion with status `completed` for the organisation, ordered by\nsource (radar first), then volume descending, then most recent. This backs the full\n\"Suggested\" tab (search + accept/dismiss over the whole set), so it is not capped the way\nthe onboarding preview endpoints are -- `meta.total` always equals `count(data)`."
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'
meta:
total: 1
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'
meta:
type: object
properties:
total:
type: integer
example: 1
tags:
- 'AI Visibility - Suggestions'
parameters:
-
in: path
name: organisation_uuid
description: 'The organisation UUID.'
example: 660e8400-e29b-41d4-a716-446655440001
required: true
schema:
type: string
'/api/organisations/{organisation_uuid}/ai-visibility/prompts/suggested/refetch':
post:
summary: 'Refetch suggestions'
operationId: refetchSuggestions
description: "A separate, standalone generation path from GenerateAiVisibilitySuggestionsJob (the\nonboarding job) — deliberately not shared code, so this can never change that job's\nbehavior. Two differences from onboarding generation: (1) previously dismissed suggestions\nare deleted first so a fresh batch can take their place — an upsert alone would otherwise\nleave a dismissed row's regenerated duplicate stuck as dismissed forever, since the upsert\nnever touches `status`; `completed`/`accepted` suggestions are left untouched. (2) an LLM\nfailure here is a real error, not silently swallowed into the generic fallback set — a user\nwho clicks \"refetch\" wants real, freshly-generated suggestions, and substituting the generic\nfallback would look like a genuine refresh when it isn't one.\n\nRate-limited to once per 10 minutes per org: a successful call holds its lock for the full\ncooldown (not released in a finally) so a second click can't trigger another LLM call too\nsoon; a failed call releases immediately since it never produced anything worth protecting.\nA request that lands inside an active cooldown isn't rejected — it gets back the same\nexisting (still-valid) suggestions with `cached: true`, rather than a bare error."
parameters: []
responses:
200:
description: ''
content:
application/json:
schema:
oneOf:
-
description: ''
type: object
example:
data:
topics:
-
id: 1
name: 'Feature Discovery'
description: ...
source: llm
status: completed
generated_at: '2026-06-01 00:00:00'
prompts:
-
id: 1
text: 'What is the best tool for notes and docs?'
category: awareness
source: llm
status: completed
generated_at: '2026-06-01 00:00:00'
properties:
data:
type: object
properties:
topics:
type: array
example:
-
id: 1
name: 'Feature Discovery'
description: ...
source: llm
status: completed
generated_at: '2026-06-01 00:00:00'
items:
type: object
properties:
id:
type: integer
example: 1
name:
type: string
example: 'Feature Discovery'
description:
type: string
example: ...
source:
type: string
example: llm
status:
type: string
example: completed
generated_at:
type: string
example: '2026-06-01 00:00:00'
prompts:
type: array
example:
-
id: 1
text: 'What is the best tool for notes and docs?'
category: awareness
source: llm
status: completed
generated_at: '2026-06-01 00:00:00'
items:
type: object
properties:
id:
type: integer
example: 1
text:
type: string
example: 'What is the best tool for notes and docs?'
category:
type: string
example: awareness
source:
type: string
example: llm
status:
type: string
example: completed
generated_at:
type: string
example: '2026-06-01 00:00:00'
-
description: ''
type: object
example:
data:
topics: []
prompts: []
cached: true
properties:
data:
type: object
properties:
topics:
type: array
example: []
prompts:
type: array
example: []
cached:
type: boolean
example: true
422:
description: ''
content:
application/json:
schema:
type: object
example:
message: 'No own brand found for this organisation.'
properties:
message:
type: string
example: 'No own brand found for this organisation.'
502:
description: ''
content:
application/json:
schema:
type: object
example:
message: 'Failed to generate suggestions. Please try again.'
properties:
message:
type: string
example: 'Failed to generate suggestions. Please try again.'
tags:
- 'AI Visibility - Suggestions'
parameters:
-
in: path
name: organisation_uuid
description: 'The organisation UUID.'
example: 660e8400-e29b-41d4-a716-446655440001
required: true
schema:
type: string
'/api/organisations/{organisation_uuid}/ai-visibility/prompts/suggested/{suggestion}/accept':
post:
summary: 'Accept a prompt suggestion'
operationId: acceptAPromptSuggestion
description: "Creates a live prompt from the suggestion, dispatches an immediate first run, and marks the\nsuggestion as `accepted`. Returns the new prompt's ID and settings."
parameters: []
responses:
201:
description: ''
content:
application/json:
schema:
type: object
example:
data:
prompt_id: 42
text: 'What is the best tool for notes and docs?'
topic_id: 3
frequency: daily
providers:
- openai
country: US
language: en
properties:
data:
type: object
properties:
prompt_id:
type: integer
example: 42
text:
type: string
example: 'What is the best tool for notes and docs?'
topic_id:
type: integer
example: 3
frequency:
type: string
example: daily
providers:
type: array
example:
- openai
items:
type: string
country:
type: string
example: US
language:
type: string
example: en
422:
description: ''
content:
application/json:
schema:
type: object
example:
message: 'Active prompt limit reached for this plan.'
properties:
message:
type: string
example: 'Active prompt limit reached for this plan.'
tags:
- 'AI Visibility - Suggestions'
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
frequency:
type: string
description: 'Execution frequency.'
example: daily
providers:
type: array
description: 'AI providers to enable.'
example:
- openai
items:
type: string
topic_id:
type: integer
description: 'Optional topic to attach the prompt to.'
example: 3
nullable: true
country:
type: string
description: 'Optional 2-letter country code.'
example: US
language:
type: string
description: 'Optional language code.'
example: en
skip_first_run:
type: boolean
description: ''
example: false
location_scope:
type: string
description: "Lets the user override the suggestion's pre-tagged default scope before accepting --\nfalls back to $row->location_scope/$row->location_id below when omitted."
example: architecto
location_ids:
type: array
description: ''
example:
- 16
items:
type: integer
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: ...
category: feature
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: ...
category: feature
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: ...
category:
type: string
example: feature
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'
requestBody:
required: false
content:
application/json:
schema:
type: object
properties:
location_scope:
type: string
description: ''
example: architecto
location_ids:
type: array
description: ''
example:
- 16
items:
type: integer
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'
category: feature
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'
category: feature
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'
category:
type: string
example: feature
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/tags':
get:
summary: 'List Tags'
operationId: listTags
description: ''
parameters: []
responses:
200:
description: ''
content:
application/json:
schema:
type: object
example:
data:
-
id: 1
name: Comparison
color: blue
created_at: '2026-06-05T10:00:00.000000Z'
updated_at: '2026-06-05T10:00:00.000000Z'
prompts_connected: 14
properties:
data:
type: array
example:
-
id: 1
name: Comparison
color: blue
created_at: '2026-06-05T10:00:00.000000Z'
updated_at: '2026-06-05T10:00:00.000000Z'
prompts_connected: 14
items:
type: object
properties:
id:
type: integer
example: 1
name:
type: string
example: Comparison
color:
type: string
example: blue
created_at:
type: string
example: '2026-06-05T10:00:00.000000Z'
updated_at:
type: string
example: '2026-06-05T10:00:00.000000Z'
prompts_connected:
type: integer
example: 14
tags:
- 'AI Visibility - Tags'
post:
summary: 'Create Tag'
operationId: createTag
description: ''
parameters: []
responses:
201:
description: ''
content:
application/json:
schema:
type: object
example:
data:
id: 1
name: Comparison
color: blue
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: Comparison
color:
type: string
example: blue
created_at:
type: string
example: '2026-06-05T10:00:00.000000Z'
updated_at:
type: string
example: '2026-06-05T10:00:00.000000Z'
422:
description: ''
content:
application/json:
schema:
type: object
example:
message: 'The name field is required. (and 1 more error)'
errors:
name:
- '"Comparison" already exists. Pick another name or use the existing tag.'
properties:
message:
type: string
example: 'The name field is required. (and 1 more error)'
errors:
type: object
properties:
name:
type: array
example:
- '"Comparison" already exists. Pick another name or use the existing tag.'
items:
type: string
tags:
- 'AI Visibility - Tags'
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
name:
type: string
description: 'Tag name.'
example: Comparison
color:
type: string
description: 'Optional swatch -- one of neutral, blue, emerald, amber, violet, rose. Defaults to neutral.'
example: blue
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/tags/{tag}':
patch:
summary: 'Update Tag'
operationId: updateTag
description: ''
parameters: []
responses:
200:
description: ''
content:
application/json:
schema:
type: object
example:
data:
id: 1
name: Comparison
color: blue
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: Comparison
color:
type: string
example: blue
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: 'Tag not found.'
properties:
message:
type: string
example: 'Tag not found.'
tags:
- 'AI Visibility - Tags'
requestBody:
required: false
content:
application/json:
schema:
type: object
properties:
name:
type: string
description: 'Optional tag name.'
example: Comparison
color:
type: string
description: 'Optional swatch -- one of neutral, blue, emerald, amber, violet, rose.'
example: blue
nullable: true
delete:
summary: 'Delete Tag'
operationId: deleteTag
description: "Removes the tag from every prompt it's attached to (ai_visibility_prompt_tags cascades) --\nthe prompts themselves, and their execution history/results, are untouched. Unlike\nAiVisibilityTopicController::destroy(), this is never blocked by active prompts: a tag is a\nloose label, not the single required grouping a topic is."
parameters: []
responses:
204:
description: ''
content:
application/json:
schema:
type: object
example: { }
properties: { }
404:
description: ''
content:
application/json:
schema:
type: object
example:
message: 'Tag not found.'
properties:
message:
type: string
example: 'Tag not found.'
tags:
- 'AI Visibility - Tags'
parameters:
-
in: path
name: organisation_uuid
description: 'The organisation UUID.'
example: 660e8400-e29b-41d4-a716-446655440001
required: true
schema:
type: string
-
in: path
name: tag
description: 'The tag 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
-
id: null
name: Unassigned
description: null
created_at: null
updated_at: null
active_count: 2
inactive_count: 0
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
-
id: null
name: Unassigned
description: null
created_at: null
updated_at: null
active_count: 2
inactive_count: 0
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/organisations/{organisation_uuid}/ai-visibility/addon-access':
get:
summary: 'GET /organisations/{organisation_uuid}/ai-visibility/addon-access'
operationId: gETorganisationsorganisationUuidaiVisibilityaddonAccess
description: ''
parameters: []
responses:
200:
description: success
content:
application/json:
schema:
type: object
example:
data:
enabled_addon_kinds:
- grok
purchased_addon_kinds:
- grok
- gemini
owned_addon_kinds: []
properties:
data:
type: object
properties:
enabled_addon_kinds:
type: array
example:
- grok
items:
type: string
purchased_addon_kinds:
type: array
example:
- grok
- gemini
items:
type: string
owned_addon_kinds:
type: array
example: []
tags:
- 'AI Visibility — Addon Access'
post:
summary: 'POST /organisations/{organisation_uuid}/ai-visibility/addon-access'
operationId: pOSTorganisationsorganisationUuidaiVisibilityaddonAccess
description: ''
parameters: []
responses:
200:
description: success
content:
application/json:
schema:
type: object
example:
data:
enabled_addon_kinds:
- grok
purchased_addon_kinds:
- grok
- gemini
owned_addon_kinds: []
properties:
data:
type: object
properties:
enabled_addon_kinds:
type: array
example:
- grok
items:
type: string
purchased_addon_kinds:
type: array
example:
- grok
- gemini
items:
type: string
owned_addon_kinds:
type: array
example: []
tags:
- 'AI Visibility — Addon Access'
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
kind:
type: string
description: 'One of grok, deepseek, anthropic, gemini, google_ai_mode.'
example: architecto
enabled:
type: boolean
description: ''
example: false
required:
- kind
- enabled
parameters:
-
in: path
name: organisation_uuid
description: ''
example: 2958b10e-6a80-4903-868f-79b048b5e0c4
required: true
schema:
type: string
/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: 36010381-2db5-4553-8459-ec8e089eb331
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: 36010381-2db5-4553-8459-ec8e089eb331
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: 36010381-2db5-4553-8459-ec8e089eb331
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: false
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: bearer
enum:
- none
- bearer
- custom
auth_token:
type: string
description: 'Must not be greater than 2048 characters.'
example: k
nullable: true
secret:
type: string
description: 'Must not be greater than 2048 characters.'
example: h
nullable: true
retry_config:
type: object
description: ''
example: null
properties: { }
nullable: true
is_active:
type: boolean
description: ''
example: false
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: bearer
enum:
- none
- bearer
- custom
auth_token:
type: string
description: 'Must not be greater than 2048 characters.'
example: k
nullable: true
secret:
type: string
description: 'Must not be greater than 2048 characters.'
example: h
nullable: true
retry_config:
type: object
description: ''
example: null
properties: { }
nullable: true
is_active:
type: boolean
description: ''
example: 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: 2958b10e-6a80-4903-868f-79b048b5e0c4
required: true
schema:
type: string
'/api/organisations/{organisation_uuid}/agents/{key}/suggested-prompts':
get:
summary: "Populates the run form's \"Try our AI Recommendations\" chips with real, org-specific\nobservations (see AgentSuggestedPromptGenerator) instead of the agent's static\nsuggestedPrompts() copy. Meaningful for root_cause_analysis and competitive_benchmark --\nother agents fall back to their registry-declared suggestedPrompts() unchanged since\nnothing generates dynamic ones for them yet."
operationId: populatesTheRunFormsTryOurAIRecommendationsChipsWithRealOrgSpecificObservationsseeAgentSuggestedPromptGeneratorInsteadOfTheAgentsStaticSuggestedPromptsCopyMeaningfulForRootCauseAnalysisAndCompetitiveBenchmarkOtherAgentsFallBackToTheirRegistryDeclaredSuggestedPromptsUnchangedSinceNothingGeneratesDynamicOnesForThemYet
description: ''
parameters: []
responses:
401:
description: ''
content:
application/json:
schema:
type: object
example:
message: Unauthenticated.
properties:
message:
type: string
example: Unauthenticated.
tags:
- Endpoints
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
brand_id:
type: integer
description: ''
example: 16
timeframe:
type: string
description: ''
example: 7d
enum:
- 7d
- 30d
- 90d
nullable: true
data_source:
type: array
description: ''
example:
- google_ai_mode
items:
type: string
enum:
- openai
- perplexity
- gemini
- grok
- deepseek
- anthropic
- google_ai_overviews
- bing_copilot
- google_ai_mode
- ai_visibility
required:
- brand_id
security: []
parameters:
-
in: path
name: organisation_uuid
description: ''
example: 2958b10e-6a80-4903-868f-79b048b5e0c4
required: true
schema:
type: string
-
in: path
name: key
description: ''
example: architecto
required: true
schema:
type: string
'/api/organisations/{organisation_uuid}/agents/{key}/runs':
post:
summary: ''
operationId: postApiOrganisationsOrganisation_uuidAgentsKeyRuns
description: ''
parameters: []
responses: { }
tags:
- Endpoints
security: []
parameters:
-
in: path
name: organisation_uuid
description: ''
example: 2958b10e-6a80-4903-868f-79b048b5e0c4
required: true
schema:
type: string
-
in: path
name: key
description: ''
example: architecto
required: true
schema:
type: string
'/api/organisations/{organisation_uuid}/agent-runs':
get:
summary: "Ticket 5's Recent Outputs listing endpoint -- never built alongside show()/download(),\nso that page has been stuck on hardcoded demo rows. Matches the mockup's columns exactly:\nFocus Topics, Agents, Status, Date created. Filterable by agent_type/status per Ticket 5's\ndefinition of done; paginated the same way AiVisibilityCompetitorController::index() is."
operationId: ticket5sRecentOutputsListingEndpointNeverBuiltAlongsideShowdownloadSoThatPageHasBeenStuckOnHardcodedDemoRowsMatchesTheMockupsColumnsExactlyFocusTopicsAgentsStatusDateCreatedFilterableByAgentTypestatusPerTicket5sDefinitionOfDonePaginatedTheSameWayAiVisibilityCompetitorControllerindexIs
description: ''
parameters: []
responses:
401:
description: ''
content:
application/json:
schema:
type: object
example:
message: Unauthenticated.
properties:
message:
type: string
example: Unauthenticated.
tags:
- Endpoints
security: []
parameters:
-
in: path
name: organisation_uuid
description: ''
example: 2958b10e-6a80-4903-868f-79b048b5e0c4
required: true
schema:
type: string
'/api/organisations/{organisation_uuid}/agent-runs/{run_id}':
get:
summary: "Pulled forward from Ticket 5's full listing/polling scope -- the Root Cause Analysis run\npage needs to poll run_plan step-by-step progress and read result once completed, and\nnothing else in Ticket 5 (agent listing, Recent Outputs table) is needed for that yet."
operationId: pulledForwardFromTicket5sFullListingpollingScopeTheRootCauseAnalysisRunPageNeedsToPollRunPlanStepByStepProgressAndReadResultOnceCompletedAndNothingElseInTicket5agentListingRecentOutputsTableIsNeededForThatYet
description: ''
parameters: []
responses:
401:
description: ''
content:
application/json:
schema:
type: object
example:
message: Unauthenticated.
properties:
message:
type: string
example: Unauthenticated.
tags:
- Endpoints
security: []
parameters:
-
in: path
name: organisation_uuid
description: ''
example: 2958b10e-6a80-4903-868f-79b048b5e0c4
required: true
schema:
type: string
-
in: path
name: run_id
description: 'The ID of the run.'
example: 17
required: true
schema:
type: integer
'/api/organisations/{organisation_uuid}/agent-runs/{run_id}/download':
get:
summary: "Ticket 4/5's own definition of done requires this endpoint, but it was never actually\nbuilt alongside RenderAgentReportPdfJob -- the PDF existed with nothing to fetch it\nthrough. Never returns a permanent URL, only a signed one generated on demand."
operationId: ticket45sOwnDefinitionOfDoneRequiresThisEndpointButItWasNeverActuallyBuiltAlongsideRenderAgentReportPdfJobThePDFExistedWithNothingToFetchItThroughNeverReturnsAPermanentURLOnlyASignedOneGeneratedOnDemand
description: "Backs both the frontend's Download button (open-and-forget) and Share button (copy the\nlink to hand to someone without a Subsig login) -- one URL serves both, so the expiry has\nto be long enough for a genuinely shared link to still work hours later, not just a quick\nclick. 24h, matching this feature's other \"good enough, not permanent\" TTLs (see\nAgentSuggestedPromptGenerator's suggestion cache)."
parameters:
-
in: query
name: format
description: "Optional - 'pdf' (default) or 'docx'."
example: docx
required: false
schema:
type: string
description: "Optional - 'pdf' (default) or 'docx'."
example: docx
responses:
401:
description: ''
content:
application/json:
schema:
type: object
example:
message: Unauthenticated.
properties:
message:
type: string
example: Unauthenticated.
tags:
- Endpoints
security: []
parameters:
-
in: path
name: organisation_uuid
description: ''
example: 2958b10e-6a80-4903-868f-79b048b5e0c4
required: true
schema:
type: string
-
in: path
name: run_id
description: 'The ID of the run.'
example: 17
required: true
schema:
type: integer
'/api/organisations/{organisation_uuid}/ai-visibility/radar/usage':
get:
summary: 'Radar Usage'
operationId: radarUsage
description: "Returns this month's SerpApi spend for the organisation's passive brand radar,\nbroken down against the plan budget cap (null when uncapped)."
parameters: []
responses:
401:
description: ''
content:
application/json:
schema:
type: object
example:
message: Unauthenticated.
properties:
message:
type: string
example: Unauthenticated.
tags:
- Endpoints
security: []
parameters:
-
in: path
name: organisation_uuid
description: 'The organisation UUID.'
example: 6ff8f7f6-1eb3-3525-be4a-3932c805afed
required: true
schema:
type: string
/api/appsumo/oauth/exchange:
post:
summary: "Exchange the single-use `code` AppSumo redirects buyers with (after \"Activate now\") for a\nlicense_key + status. Unauthenticated -- a brand-new buyer has no subsig session yet."
operationId: exchangeTheSingleUsecodeAppSumoRedirectsBuyersWithafterActivateNowForALicenseKey+StatusUnauthenticatedABrandNewBuyerHasNoSubsigSessionYet
description: "AppSumo's own `status` field flips to \"active\" as soon as our webhook responds 200 to their\n`activate` event -- which happens independently of whether this specific person has ever\nbeen through our OAuth flow (AppSumoWebhookController always 200s that event). So it does\nNOT mean \"this browser's user already has a subsig account\" -- every first-time buyer would\nincorrectly see \"active\" before ever signing up. `already_redeemed` is the real signal:\nwhether *our own* AppSumoLicense record has an organisation attached yet.\n\nFrontend branches on `already_redeemed` for new-vs-returning, and on `status === \"deactivated\"`\n(still AppSumo's own billing-state field, legitimately authoritative for this one case) to\nblock a cancelled/refunded license outright regardless of redemption state."
parameters: []
responses: { }
tags:
- Endpoints
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
code:
type: string
description: ''
example: architecto
required:
- code
security: []
/api/webhook:
post:
summary: ''
operationId: postApiWebhook
description: ''
parameters: []
responses: { }
tags:
- Endpoints
security: []
/api/v1/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: 59236191-44ef-4ab7-b2c7-acae8c37d895
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: 59236191-44ef-4ab7-b2c7-acae8c37d895
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: 32d32f0d-b949-47a7-b4ee-706454e30a96
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: 32d32f0d-b949-47a7-b4ee-706454e30a96
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: fb5734a9-b59c-444f-9624-0e1ce910644b
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: fb5734a9-b59c-444f-9624-0e1ce910644b
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: 9603b0bc-dbc0-4841-8b72-6ea70e36af5c
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: 9603b0bc-dbc0-4841-8b72-6ea70e36af5c
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: 93ef386b-6c04-42c5-a1d6-7308b6e284a8
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: 93ef386b-6c04-42c5-a1d6-7308b6e284a8
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: 7da1cf55-1913-4403-a15c-f6d2c6f74430
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: 7da1cf55-1913-4403-a15c-f6d2c6f74430
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: 58bd3bf8-f29f-4ad3-8e11-fd98fadcc9cb
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: 58bd3bf8-f29f-4ad3-8e11-fd98fadcc9cb
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: 926b8bec-4859-4ab6-86cf-73df513401e9
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: 926b8bec-4859-4ab6-86cf-73df513401e9
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: 67e50fff-e461-44b8-9668-556678c03ebf
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: 67e50fff-e461-44b8-9668-556678c03ebf
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: 865aa301-3553-4b57-a479-282b6c97e039
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: 865aa301-3553-4b57-a479-282b6c97e039
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: cd967501-0453-4af9-b139-734fc11d49df
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: cd967501-0453-4af9-b139-734fc11d49df
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: 8fd5e405-1d4d-4d4f-8684-b488fc96ef54
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: 8fd5e405-1d4d-4d4f-8684-b488fc96ef54
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: 14cea592-93af-4fa1-9aa2-b895aa01179c
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: 14cea592-93af-4fa1-9aa2-b895aa01179c
tags:
- Endpoints
security: []
parameters:
-
in: path
name: run_id
description: 'The ID of the run.'
example: 17
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: 2958b10e-6a80-4903-868f-79b048b5e0c4
required: true
schema:
type: string
'/api/organisations/{organisation_uuid}/agents/recommendations/summary':
get:
summary: "The Recommendations page's top stat-card row (High priority / Open recommendations /\nCompleted this month / AI readiness score). Pools Off-Page's Engage + Editorial cards and\nOn-Page's cards (Technical isn't included: its checks are a pass/fail audit, not\nDone/Decline task-state cards -- see recommendationsForTab()'s 'technical' branch). Always\na full, uncapped pull (limit: PHP_INT_MAX) since every card needs counting, not just the\nfirst page a tab would normally show."
operationId: theRecommendationsPagesTopStatCardRowHighPriorityOpenRecommendationsCompletedThisMonthAIReadinessScorePoolsOffPagesEngage+EditorialCardsAndOnPagesCardsTechnicalIsntIncludedItsChecksAreAPassfailAuditNotDoneDeclineTaskStateCardsSeeRecommendationsForTabstechnicalBranchAlwaysAFullUncappedPulllimitPHPINTMAXSinceEveryCardNeedsCountingNotJustTheFirstPageATabWouldNormallyShow
description: ''
parameters: []
responses:
401:
description: ''
content:
application/json:
schema:
type: object
example:
message: Unauthenticated.
properties:
message:
type: string
example: Unauthenticated.
tags:
- Home
security: []
parameters:
-
in: path
name: organisation_uuid
description: ''
example: 2958b10e-6a80-4903-868f-79b048b5e0c4
required: true
schema:
type: string
'/api/organisations/{organisation_uuid}/agents/recommendations/{tab}':
get:
summary: "Off-Page/On-Page/Technical recommendation tabs -- see\ndocs/zerorank-style-recommendations-plan.md. Separate from recommendations() above: that\nendpoint serves the existing flat trend-insight list (visibility drop, content-gap, etc),\nthis one serves the new tabbed, task-tracked cards. 'off_page' (Phase 2) and 'on_page'\n(Phase 3) are built; 'technical' is reserved by the route's tab constraint but returns 501\nuntil Phase 4 lands, rather than a misleading empty list."
operationId: offPageOnPageTechnicalRecommendationTabsSeeDocszerorankStyleRecommendationsPlanmdSeparateFromRecommendationsAboveThatEndpointServesTheExistingFlatTrendInsightListvisibilityDropContentGapEtcThisOneServesTheNewTabbedTaskTrackedCardsoffPagePhase2AndonPagePhase3AreBuilttechnicalIsReservedByTheRoutesTabConstraintButReturns501UntilPhase4LandsRatherThanAMisleadingEmptyList
description: ''
parameters: []
responses:
404:
description: ''
content:
application/json:
schema:
type: object
example:
message: 'The route api/organisations/2958b10e-6a80-4903-868f-79b048b5e0c4/agents/recommendations/off_page|on_page|technical could not be found.'
properties:
message:
type: string
example: 'The route api/organisations/2958b10e-6a80-4903-868f-79b048b5e0c4/agents/recommendations/off_page|on_page|technical could not be found.'
tags:
- Home
security: []
parameters:
-
in: path
name: organisation_uuid
description: ''
example: 2958b10e-6a80-4903-868f-79b048b5e0c4
required: true
schema:
type: string
-
in: path
name: tab
description: ''
example: off_page|on_page|technical
required: true
schema:
type: string
'/api/organisations/{organisation_uuid}/agents/recommendations/{tab}/sources':
get:
summary: "ZeroRank's per-domain \"Sources\" table, shown when a card in the Off-Page sidebar is\nselected. Same cache/501 shape as recommendationsForTab() above."
operationId: zeroRanksPerDomainSourcesTableShownWhenACardInTheOffPageSidebarIsSelectedSameCache501ShapeAsRecommendationsForTabAbove
description: ''
parameters: []
responses:
404:
description: ''
content:
application/json:
schema:
type: object
example:
message: 'The route api/organisations/2958b10e-6a80-4903-868f-79b048b5e0c4/agents/recommendations/off_page|on_page|technical/sources could not be found.'
properties:
message:
type: string
example: 'The route api/organisations/2958b10e-6a80-4903-868f-79b048b5e0c4/agents/recommendations/off_page|on_page|technical/sources could not be found.'
tags:
- Home
security: []
parameters:
-
in: path
name: organisation_uuid
description: ''
example: 2958b10e-6a80-4903-868f-79b048b5e0c4
required: true
schema:
type: string
-
in: path
name: tab
description: ''
example: off_page|on_page|technical
required: true
schema:
type: string
'/api/organisations/{organisation_uuid}/agents/recommendations/{tab}/action-cards':
get:
summary: "One domain's actionable cards (thread-specific, plus a subreddit-specific one for\nreddit.com) -- see AiVisibilityOffPageRecommendationService::cardsForDomain(). Same\ncache/501 shape as recommendationSources() above."
operationId: oneDomainsActionableCardsthreadSpecificPlusASubredditSpecificOneForRedditcomSeeAiVisibilityOffPageRecommendationServicecardsForDomainSameCache501ShapeAsRecommendationSourcesAbove
description: ''
parameters: []
responses:
404:
description: ''
content:
application/json:
schema:
type: object
example:
message: 'The route api/organisations/2958b10e-6a80-4903-868f-79b048b5e0c4/agents/recommendations/off_page|on_page|technical/action-cards could not be found.'
properties:
message:
type: string
example: 'The route api/organisations/2958b10e-6a80-4903-868f-79b048b5e0c4/agents/recommendations/off_page|on_page|technical/action-cards could not be found.'
tags:
- Home
security: []
parameters:
-
in: path
name: organisation_uuid
description: ''
example: 2958b10e-6a80-4903-868f-79b048b5e0c4
required: true
schema:
type: string
-
in: path
name: tab
description: ''
example: off_page|on_page|technical
required: true
schema:
type: string
'/api/organisations/{organisation_uuid}/agents/recommendations/{tab}/subreddits':
get:
summary: "ZeroRank's \"Top Subreddits\" panel -- only ever non-empty for a reddit.com domain. Same\ncache/501 shape as recommendationSources() above."
operationId: zeroRanksTopSubredditsPanelOnlyEverNonEmptyForARedditcomDomainSameCache501ShapeAsRecommendationSourcesAbove
description: ''
parameters: []
responses:
404:
description: ''
content:
application/json:
schema:
type: object
example:
message: 'The route api/organisations/2958b10e-6a80-4903-868f-79b048b5e0c4/agents/recommendations/off_page|on_page|technical/subreddits could not be found.'
properties:
message:
type: string
example: 'The route api/organisations/2958b10e-6a80-4903-868f-79b048b5e0c4/agents/recommendations/off_page|on_page|technical/subreddits could not be found.'
tags:
- Home
security: []
parameters:
-
in: path
name: organisation_uuid
description: ''
example: 2958b10e-6a80-4903-868f-79b048b5e0c4
required: true
schema:
type: string
-
in: path
name: tab
description: ''
example: off_page|on_page|technical
required: true
schema:
type: string
'/api/organisations/{organisation_uuid}/agents/recommendations/{tab}/who-recommends':
get:
summary: "\"Who AI recommends from X\" stacked bar -- own-brand vs named competitors vs \"Others\", for\none Off-Page domain. Off-Page only (On-Page/Technical have no equivalent chart in the\ndesign), same cache/501 shape as recommendationSubreddits() above."
operationId: WhoAIRecommendsFromXStackedBarOwnBrandVsNamedCompetitorsVsOthersForOneOffPageDomainOffPageOnlyOnPageTechnicalHaveNoEquivalentChartInTheDesignSameCache501ShapeAsRecommendationSubredditsAbove
description: ''
parameters: []
responses:
404:
description: ''
content:
application/json:
schema:
type: object
example:
message: 'The route api/organisations/2958b10e-6a80-4903-868f-79b048b5e0c4/agents/recommendations/off_page|on_page|technical/who-recommends could not be found.'
properties:
message:
type: string
example: 'The route api/organisations/2958b10e-6a80-4903-868f-79b048b5e0c4/agents/recommendations/off_page|on_page|technical/who-recommends could not be found.'
tags:
- Home
security: []
parameters:
-
in: path
name: organisation_uuid
description: ''
example: 2958b10e-6a80-4903-868f-79b048b5e0c4
required: true
schema:
type: string
-
in: path
name: tab
description: ''
example: off_page|on_page|technical
required: true
schema:
type: string
'/api/organisations/{organisation_uuid}/agents/recommendations/{tab}/content-insights':
get:
summary: "On-Page's \"Phrases, themes and domains\" panel -- bundles all three into one response since\nthe frontend always needs them together for the selected content type. Off-Page/Technical\nhave no equivalent, same 501 shape as the other tab-scoped endpoints."
operationId: onPagesPhrasesThemesAndDomainsPanelBundlesAllThreeIntoOneResponseSinceTheFrontendAlwaysNeedsThemTogetherForTheSelectedContentTypeOffPageTechnicalHaveNoEquivalentSame501ShapeAsTheOtherTabScopedEndpoints
description: ''
parameters: []
responses:
404:
description: ''
content:
application/json:
schema:
type: object
example:
message: 'The route api/organisations/2958b10e-6a80-4903-868f-79b048b5e0c4/agents/recommendations/off_page|on_page|technical/content-insights could not be found.'
properties:
message:
type: string
example: 'The route api/organisations/2958b10e-6a80-4903-868f-79b048b5e0c4/agents/recommendations/off_page|on_page|technical/content-insights could not be found.'
tags:
- Home
security: []
parameters:
-
in: path
name: organisation_uuid
description: ''
example: 2958b10e-6a80-4903-868f-79b048b5e0c4
required: true
schema:
type: string
-
in: path
name: tab
description: ''
example: off_page|on_page|technical
required: true
schema:
type: string
'/api/organisations/{organisation_uuid}/agents/recommendations/{tab}/recheck':
post:
summary: 'Technical tab''s "Re-check" button -- forces a fresh site audit, bypassing the 24h cache.'
operationId: technicalTabsReCheckButtonForcesAFreshSiteAuditBypassingThe24hCache
description: "Write-shaped (an on-demand, potentially slow re-crawl of the org's own site via PageSpeed\nInsights + several Http::get() calls), so requires manage access like updateRecommendation\nTaskStatus() below, not just view access."
parameters: []
responses: { }
tags:
- Home
security: []
parameters:
-
in: path
name: organisation_uuid
description: ''
example: 2958b10e-6a80-4903-868f-79b048b5e0c4
required: true
schema:
type: string
-
in: path
name: tab
description: ''
example: off_page|on_page|technical
required: true
schema:
type: string
'/api/organisations/{organisation_uuid}/agents/recommendations/{tab}/status':
patch:
summary: "Marks one Off-Page/On-Page/Technical recommendation card Done/Declined/Todo -- see\nAiVisibilityRecommendationTaskStateService's docblock for why \"todo\" deletes rather than\nstores a row. Write action (changes state other users with access to this org would also\nsee), so this requires manage access, not just view access."
operationId: marksOneOffPageOnPageTechnicalRecommendationCardDoneDeclinedTodoSeeAiVisibilityRecommendationTaskStateServicesDocblockForWhytodoDeletesRatherThanStoresARowWriteActionchangesStateOtherUsersWithAccessToThisOrgWouldAlsoSeeSoThisRequiresManageAccessNotJustViewAccess
description: ''
parameters: []
responses: { }
tags:
- Home
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
item_key:
type: string
description: 'Must not be greater than 255 characters.'
example: b
status:
type: string
description: ''
example: architecto
required:
- item_key
- status
security: []
parameters:
-
in: path
name: organisation_uuid
description: ''
example: 2958b10e-6a80-4903-868f-79b048b5e0c4
required: true
schema:
type: string
-
in: path
name: tab
description: ''
example: off_page|on_page|technical
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'
identity_source:
type: string
description: 'How the brand was identified: `website` (default) or `social`.'
example: website
product_website:
type: string
description: 'The product website URL. Required (nullable) when identity_source is `website`; must be omitted when `social`.'
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'
social_profiles:
type: array
description: 'Required when identity_source is `social`; must be omitted otherwise.'
example:
-
platform: instagram
url: 'https://instagram.com/acme'
items:
type: object
properties:
platform:
type: string
description: 'Free-form platform name, e.g. instagram, linkedin, x, facebook.'
example: instagram
url:
type: string
description: 'The profile URL.'
example: 'https://instagram.com/acme'
required:
- platform
- url
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'
identity_source:
type: string
description: 'How the brand was identified: `website` (default) or `social`.'
example: website
product_website:
type: string
description: 'The product website URL. Required (nullable) when identity_source is `website`; must be omitted when `social`.'
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'
social_profiles:
type: array
description: 'Required when identity_source is `social`; must be omitted otherwise.'
example:
-
platform: instagram
url: 'https://instagram.com/acme'
items:
type: object
properties:
platform:
type: string
description: 'Free-form platform name, e.g. instagram, linkedin, x, facebook.'
example: instagram
url:
type: string
description: 'The profile URL.'
example: 'https://instagram.com/acme'
required:
- platform
- url
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/onboarding/google-places/search:
get:
summary: 'Search Google Business/Maps listings'
operationId: searchGoogleBusinessMapsListings
description: 'Returns candidate Places for a picker UI, given a free-text query (e.g. a business name).'
parameters:
-
in: query
name: query
description: 'The search text.'
example: 'Aura Bangkok Clinic Asok'
required: true
schema:
type: string
description: 'The search text.'
example: 'Aura Bangkok Clinic Asok'
-
in: query
name: region
description: 'Optional ISO 3166-1 alpha-2 region code to bias results.'
example: TH
required: false
schema:
type: string
description: 'Optional ISO 3166-1 alpha-2 region code to bias results.'
example: TH
responses:
200:
description: ''
content:
application/json:
schema:
type: object
example:
data:
-
place_id: ChIJ...
name: 'Aura Bangkok Clinic Asok'
formatted_address: 'Exchange Tower, Bangkok, Thailand'
types:
- medical_clinic
properties:
data:
type: array
example:
-
place_id: ChIJ...
name: 'Aura Bangkok Clinic Asok'
formatted_address: 'Exchange Tower, Bangkok, Thailand'
types:
- medical_clinic
items:
type: object
properties:
place_id:
type: string
example: ChIJ...
name:
type: string
example: 'Aura Bangkok Clinic Asok'
formatted_address:
type: string
example: 'Exchange Tower, Bangkok, Thailand'
types:
type: array
example:
- medical_clinic
items:
type: string
tags:
- Onboarding
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
query:
type: string
description: 'Must not be greater than 255 characters.'
example: b
region:
type: string
description: 'Must be 2 characters.'
example: ng
nullable: true
required:
- query
/api/onboarding/google-places/resolve-url:
post:
summary: 'Resolve a pasted Google Maps/Place link'
operationId: resolveAPastedGoogleMapsPlaceLink
description: "For the \"paste a link\" flow (same input as Instagram/LinkedIn/etc.) -- detects a Google\nMaps/Place URL, resolves it to a real business via Text Search + Place Details, and returns\nthe same shape as show() for the frontend to autofill Brand Name / switch to Website mode."
parameters: []
responses:
200:
description: ''
content:
application/json:
schema:
type: object
example:
data:
place_id: ChIJ...
name: 'Aura Bangkok Clinic Asok'
primary_type: medical_clinic
types:
- medical_clinic
formatted_address: 'Exchange Tower, Bangkok, Thailand'
website_uri: 'https://aurabangkokclinic.com'
photo_names: []
properties:
data:
type: object
properties:
place_id:
type: string
example: ChIJ...
name:
type: string
example: 'Aura Bangkok Clinic Asok'
primary_type:
type: string
example: medical_clinic
types:
type: array
example:
- medical_clinic
items:
type: string
formatted_address:
type: string
example: 'Exchange Tower, Bangkok, Thailand'
website_uri:
type: string
example: 'https://aurabangkokclinic.com'
photo_names:
type: array
example: []
404:
description: ''
content:
application/json:
schema:
type: object
example:
message: 'Could not resolve a business from that link.'
properties:
message:
type: string
example: 'Could not resolve a business from that link.'
tags:
- Onboarding
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
url:
type: string
description: 'The pasted URL.'
example: 'https://www.google.com/maps/place/Aura+Bangkok+Clinic/@13.7,100.5,17z'
required:
- url
/api/onboarding/google-places/autocomplete:
get:
summary: 'Address autocomplete for the "Add location" modal'
operationId: addressAutocompleteForTheAddLocationModal
description: "Search-as-you-type predictions for a location's physical address, scoped to one country.\nDistinct from search() above (which finds a whole business listing) -- this is purely an\naddress/place-name typeahead for onboarding's multi-location \"Add location\" flow."
parameters:
-
in: query
name: input
description: 'The partial address/place text typed so far.'
example: '350 5th Ave'
required: true
schema:
type: string
description: 'The partial address/place text typed so far.'
example: '350 5th Ave'
-
in: query
name: country
description: 'ISO 3166-1 alpha-2 country code to scope results to.'
example: US
required: true
schema:
type: string
description: 'ISO 3166-1 alpha-2 country code to scope results to.'
example: US
-
in: query
name: session_token
description: "Caller-generated token shared across this typeahead session and the one details() call that follows it (Google's session-based billing)."
example: 3fa4159e-...
required: true
schema:
type: string
description: "Caller-generated token shared across this typeahead session and the one details() call that follows it (Google's session-based billing)."
example: 3fa4159e-...
responses:
200:
description: ''
content:
application/json:
schema:
type: object
example:
data:
-
place_id: ChIJ...
primary_text: '350 5th Ave'
secondary_text: 'New York, NY, USA'
properties:
data:
type: array
example:
-
place_id: ChIJ...
primary_text: '350 5th Ave'
secondary_text: 'New York, NY, USA'
items:
type: object
properties:
place_id:
type: string
example: ChIJ...
primary_text:
type: string
example: '350 5th Ave'
secondary_text:
type: string
example: 'New York, NY, USA'
tags:
- Onboarding
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
input:
type: string
description: 'Must not be greater than 200 characters.'
example: b
country:
type: string
description: 'Must be 2 characters.'
example: ng
session_token:
type: string
description: 'Must not be greater than 100 characters.'
example: z
required:
- input
- country
- session_token
/api/onboarding/google-places/details:
get:
summary: 'Address details for a picked autocomplete suggestion'
operationId: addressDetailsForAPickedAutocompleteSuggestion
description: "Resolves one chosen autocomplete prediction to its full address/coordinates, for the\nonboarding \"Add location\" modal. Must be called with the SAME session_token as the\nautocomplete() call that produced $placeId."
parameters:
-
in: query
name: place_id
description: 'The Place ID from the autocomplete results.'
example: ChIJ...
required: true
schema:
type: string
description: 'The Place ID from the autocomplete results.'
example: ChIJ...
-
in: query
name: session_token
description: 'The same session token used for the preceding autocomplete() call.'
example: 3fa4159e-...
required: true
schema:
type: string
description: 'The same session token used for the preceding autocomplete() call.'
example: 3fa4159e-...
responses:
200:
description: ''
content:
application/json:
schema:
type: object
example:
data:
place_id: ChIJ...
formatted_address: '350 5th Ave, New York, NY 10118, USA'
city: 'New York'
region: 'New York'
country_code: US
lat: 40.7484
lng: -73.9857
properties:
data:
type: object
properties:
place_id:
type: string
example: ChIJ...
formatted_address:
type: string
example: '350 5th Ave, New York, NY 10118, USA'
city:
type: string
example: 'New York'
region:
type: string
example: 'New York'
country_code:
type: string
example: US
lat:
type: number
example: 40.7484
lng:
type: number
example: -73.9857
tags:
- Onboarding
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
place_id:
type: string
description: 'Must not be greater than 300 characters.'
example: b
session_token:
type: string
description: 'Must not be greater than 100 characters.'
example: 'n'
required:
- place_id
- session_token
'/api/onboarding/google-places/{placeId}':
get:
summary: 'Get Google Business/Maps listing details'
operationId: getGoogleBusinessMapsListingDetails
description: "Resolves one chosen Place's details for prefilling the onboarding form."
parameters: []
responses:
200:
description: ''
content:
application/json:
schema:
type: object
example:
data:
place_id: ChIJ...
name: 'Aura Bangkok Clinic Asok'
primary_type: medical_clinic
types:
- medical_clinic
formatted_address: 'Exchange Tower, Bangkok, Thailand'
website_uri: 'https://aurabangkokclinic.com'
photo_names: []
properties:
data:
type: object
properties:
place_id:
type: string
example: ChIJ...
name:
type: string
example: 'Aura Bangkok Clinic Asok'
primary_type:
type: string
example: medical_clinic
types:
type: array
example:
- medical_clinic
items:
type: string
formatted_address:
type: string
example: 'Exchange Tower, Bangkok, Thailand'
website_uri:
type: string
example: 'https://aurabangkokclinic.com'
photo_names:
type: array
example: []
tags:
- Onboarding
parameters:
-
in: path
name: placeId
description: 'The Place ID from the search results.'
example: ChIJ...
required: true
schema:
type: string
/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'
provider_region_exceptions:
anthropic:
- IE
- LU
deepseek:
- IE
- LU
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'
provider_region_exceptions:
type: object
properties:
anthropic:
type: array
example:
- IE
- LU
items:
type: string
deepseek:
type: array
example:
- IE
- LU
items:
type: string
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'
identity_source:
type: string
description: 'How the brand is identified: `website` (default) or `social`.'
example: architecto
website:
type: string
description: 'Required (nullable) when identity_source is `website`; must be omitted when `social`.'
example: 'https://acme.com'
social_profiles:
type: array
description: 'Required when identity_source is `social`; must be omitted otherwise.'
example:
-
platform: instagram
url: 'https://instagram.com/acme'
items:
type: object
required:
- name
delete:
summary: 'Delete Organisation'
operationId: deleteOrganisation
description: "Delete an organisation and its projects (cascaded). Requires organisation owner role.\nCannot delete the authenticated user's current (primary) workspace; switch first."
parameters: []
responses:
200:
description: ''
content:
application/json:
schema:
type: object
example:
message: 'Organisation deleted successfully.'
properties:
message:
type: string
example: 'Organisation deleted successfully.'
403:
description: ''
content:
application/json:
schema:
oneOf:
-
description: 'No access'
type: object
example:
message: 'You do not have access to this organisation.'
properties:
message:
type: string
example: 'You do not have access to this organisation.'
-
description: 'Not owner'
type: object
example:
message: 'You must be an organisation admin to perform this action.'
properties:
message:
type: string
example: 'You must be an organisation admin to perform this action.'
422:
description: 'Current workspace'
content:
application/json:
schema:
type: object
example:
message: 'You cannot delete your current workspace. Switch to another organisation first.'
properties:
message:
type: string
example: 'You cannot delete your current workspace. Switch to another organisation first.'
tags:
- Organisations
parameters:
-
in: path
name: uuid
description: ''
example: 2958b10e-6a80-4903-868f-79b048b5e0c4
required: true
schema:
type: string
-
in: path
name: organisation
description: 'The organisation UUID.'
example: 550e8400-e29b-41d4-a716-446655440000
required: true
schema:
type: string
'/api/organisations/{organisation_uuid}/switch':
post:
summary: 'Switch Current Organisation'
operationId: switchCurrentOrganisation
description: "Set the user's current organisation for subsequent requests."
parameters: []
responses:
200:
description: ''
content:
application/json:
schema:
type: object
example:
message: 'Switched to organisation successfully.'
data:
uuid: 550e8400-e29b-41d4-a716-446655440000
name: 'Acme Inc'
properties:
message:
type: string
example: 'Switched to organisation successfully.'
data:
type: object
properties:
uuid:
type: string
example: 550e8400-e29b-41d4-a716-446655440000
name:
type: string
example: 'Acme Inc'
403:
description: 'No access'
content:
application/json:
schema:
type: object
example:
message: 'You do not have access to this organisation.'
properties:
message:
type: string
example: 'You do not have access to this organisation.'
tags:
- Organisations
parameters:
-
in: path
name: organisation_uuid
description: ''
example: 2958b10e-6a80-4903-868f-79b048b5e0c4
required: true
schema:
type: string
-
in: path
name: organisation
description: 'The organisation UUID.'
example: 550e8400-e29b-41d4-a716-446655440000
required: true
schema:
type: string
/api/platforms:
get:
summary: 'List All Platforms'
operationId: listAllPlatforms
description: 'Get all platforms.'
parameters: []
responses:
200:
description: ''
content:
application/json:
schema:
type: array
items:
type: object
properties:
uuid:
type: string
example: 550e8400-e29b-41d4-a716-446655440000
name:
type: string
example: G2
created_at:
type: string
example: '2026-01-08T10:00:00.000000Z'
updated_at:
type: string
example: '2026-01-08T10:00:00.000000Z'
example:
-
uuid: 550e8400-e29b-41d4-a716-446655440000
name: G2
created_at: '2026-01-08T10:00:00.000000Z'
updated_at: '2026-01-08T10:00:00.000000Z'
tags:
- Platforms
post:
summary: 'Create Platform'
operationId: createPlatform
description: 'Create a new platform.'
parameters: []
responses:
201:
description: ''
content:
application/json:
schema:
type: object
example:
message: 'Platform created successfully.'
data:
uuid: 550e8400-e29b-41d4-a716-446655440000
name: G2
created_at: '2026-01-08T10:00:00.000000Z'
updated_at: '2026-01-08T10:00:00.000000Z'
properties:
message:
type: string
example: 'Platform created successfully.'
data:
type: object
properties:
uuid:
type: string
example: 550e8400-e29b-41d4-a716-446655440000
name:
type: string
example: G2
created_at:
type: string
example: '2026-01-08T10:00:00.000000Z'
updated_at:
type: string
example: '2026-01-08T10:00:00.000000Z'
422:
description: 'Validation Error'
content:
application/json:
schema:
type: object
example:
message: 'The name has already been taken.'
errors:
name:
- 'The name has already been taken.'
properties:
message:
type: string
example: 'The name has already been taken.'
errors:
type: object
properties:
name:
type: array
example:
- 'The name has already been taken.'
items:
type: string
tags:
- Platforms
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
name:
type: string
description: 'The platform name.'
example: G2
required:
- name
'/api/platforms/{uuid}':
patch:
summary: 'Update Platform'
operationId: updatePlatform
description: "Update a platform's details."
parameters: []
responses:
200:
description: ''
content:
application/json:
schema:
type: object
example:
message: 'Platform updated successfully.'
data:
uuid: 550e8400-e29b-41d4-a716-446655440000
name: Capterra
created_at: '2026-01-08T10:00:00.000000Z'
updated_at: '2026-01-08T10:30:00.000000Z'
properties:
message:
type: string
example: 'Platform updated successfully.'
data:
type: object
properties:
uuid:
type: string
example: 550e8400-e29b-41d4-a716-446655440000
name:
type: string
example: Capterra
created_at:
type: string
example: '2026-01-08T10:00:00.000000Z'
updated_at:
type: string
example: '2026-01-08T10:30:00.000000Z'
404:
description: 'Not Found'
content:
application/json:
schema:
type: object
example:
message: 'Platform not found.'
properties:
message:
type: string
example: 'Platform not found.'
422:
description: 'Validation Error'
content:
application/json:
schema:
type: object
example:
message: 'The name has already been taken.'
errors:
name:
- 'The name has already been taken.'
properties:
message:
type: string
example: 'The name has already been taken.'
errors:
type: object
properties:
name:
type: array
example:
- 'The name has already been taken.'
items:
type: string
tags:
- Platforms
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
name:
type: string
description: 'The platform name.'
example: Capterra
required:
- name
delete:
summary: 'Delete Platform'
operationId: deletePlatform
description: 'Delete a platform.'
parameters: []
responses:
200:
description: ''
content:
application/json:
schema:
type: object
example:
message: 'Platform deleted successfully.'
properties:
message:
type: string
example: 'Platform deleted successfully.'
404:
description: 'Not Found'
content:
application/json:
schema:
type: object
example:
message: 'Platform not found.'
properties:
message:
type: string
example: 'Platform not found.'
tags:
- Platforms
parameters:
-
in: path
name: uuid
description: 'The platform UUID.'
example: 550e8400-e29b-41d4-a716-446655440000
required: true
schema:
type: string
'/api/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: 2958b10e-6a80-4903-868f-79b048b5e0c4
required: true
schema:
type: string
-
in: path
name: organisation
description: 'The organisation UUID.'
example: 550e8400-e29b-41d4-a716-446655440000
required: true
schema:
type: string
'/api/organisations/{organisation_uuid}/projects/{uuid}':
get:
summary: 'Get Project'
operationId: getProject
description: 'Get details of a specific project.'
parameters: []
responses:
200:
description: ''
content:
application/json:
schema:
type: object
example:
data:
uuid: 660e8400-e29b-41d4-a716-446655440001
product_name: 'Acme App'
product_website: 'https://acme.com'
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: 2958b10e-6a80-4903-868f-79b048b5e0c4
required: true
schema:
type: string
-
in: path
name: uuid
description: ''
example: b2b0131c-f2cb-42fa-ad1e-00605ce7c898
required: true
schema:
type: string
-
in: path
name: organisation
description: 'The organisation UUID.'
example: 550e8400-e29b-41d4-a716-446655440000
required: true
schema:
type: string
-
in: path
name: project
description: 'The project UUID.'
example: 660e8400-e29b-41d4-a716-446655440001
required: true
schema:
type: string
/api/integrations/slack/callback:
get:
summary: 'Slack OAuth Callback'
operationId: slackOAuthCallback
description: "Handles the OAuth callback from Slack after user authorization.\nExchanges the authorization code for an access token.\nThis endpoint is unauthenticated - uses cached state for auth context."
parameters:
-
in: query
name: code
description: 'The authorization code from Slack.'
example: 123456789.abcdef
required: false
schema:
type: string
description: 'The authorization code from Slack.'
example: 123456789.abcdef
-
in: query
name: state
description: 'The state parameter for CSRF protection.'
example: abc123...
required: false
schema:
type: string
description: 'The state parameter for CSRF protection.'
example: abc123...
-
in: query
name: error
description: 'OAuth error if user denied access.'
example: access_denied
required: false
schema:
type: string
description: 'OAuth error if user denied access.'
example: access_denied
responses:
302:
description: ''
content:
text/plain:
schema:
oneOf:
-
description: Success
type: string
example: 'Redirects to /dashboard/notification/overview?slack_connected=true&workspace={name}'
-
description: Error
type: string
example: 'Redirects to /dashboard/notification/overview?slack_error={error_code}'
-
description: ''
type: string
example: "\n\n \n \n \n\n Redirecting to https://app.subsig.com/dashboard/notification/overview?slack_error=architecto\n \n \n Redirecting to https://app.subsig.com/dashboard/notification/overview?slack_error=architecto.\n \n"
tags:
- 'Slack Integration'
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
code:
type: string
description: 'This field is required when error is not present.'
example: architecto
state:
type: string
description: 'Must be 64 characters.'
example: ngzmiyvdljnikhwaykcmyuwpwlvqwrsitcpscqldzsnrwtujwvlxjklqppwqbewt
error:
type: string
description: ''
example: architecto
required:
- state
security: []
/api/integrations/slack/connect:
get:
summary: 'Initiate Slack OAuth'
operationId: initiateSlackOAuth
description: "Starts the OAuth flow to connect a Slack workspace.\nRedirects to Slack's authorization page."
parameters: []
responses:
302:
description: 'Redirect to Slack'
content:
text/plain:
schema:
type: string
example: 'Redirects to Slack OAuth page'
400:
description: 'Not configured'
content:
application/json:
schema:
type: object
example:
error: configuration
message: 'Slack integration is not configured.'
properties:
error:
type: string
example: configuration
message:
type: string
example: 'Slack integration is not configured.'
401:
description: ''
content:
application/json:
schema:
type: object
example:
message: Unauthenticated.
properties:
message:
type: string
example: Unauthenticated.
403:
description: 'No organisation'
content:
application/json:
schema:
type: object
example:
message: 'Organisation context required.'
properties:
message:
type: string
example: 'Organisation context required.'
tags:
- 'Slack Integration'
/api/integrations/slack/status:
get:
summary: 'Get Slack Connection Status'
operationId: getSlackConnectionStatus
description: 'Returns the current Slack connection status for the organisation.'
parameters: []
responses:
200:
description: ''
content:
application/json:
schema:
oneOf:
-
description: Connected
type: object
example:
connected: true
team_id: T123456789
team_name: 'My Workspace'
scopes: 'chat:write,channels:read'
connected_at: '2025-01-01T12:00:00Z'
properties:
connected:
type: boolean
example: true
team_id:
type: string
example: T123456789
team_name:
type: string
example: 'My Workspace'
scopes:
type: string
example: 'chat:write,channels:read'
connected_at:
type: string
example: '2025-01-01T12:00:00Z'
-
description: 'Not connected'
type: object
example:
connected: false
properties:
connected:
type: boolean
example: false
-
description: 'Invalid token'
type: object
example:
connected: false
error: token_invalid
message: 'Slack connection needs to be re-authorized.'
properties:
connected:
type: boolean
example: false
error:
type: string
example: token_invalid
message:
type: string
example: 'Slack connection needs to be re-authorized.'
403:
description: 'No organisation'
content:
application/json:
schema:
type: object
example:
message: 'Organisation context required.'
properties:
message:
type: string
example: 'Organisation context required.'
tags:
- 'Slack Integration'
/api/integrations/slack/disconnect:
post:
summary: 'Disconnect Slack'
operationId: disconnectSlack
description: 'Removes the Slack workspace connection for the organisation.'
parameters: []
responses:
200:
description: ''
content:
application/json:
schema:
oneOf:
-
description: Success
type: object
example:
success: true
message: 'Slack connection removed.'
properties:
success:
type: boolean
example: true
message:
type: string
example: 'Slack connection removed.'
-
description: 'Not connected'
type: object
example:
success: true
message: 'No Slack connection found.'
properties:
success:
type: boolean
example: true
message:
type: string
example: 'No Slack connection found.'
403:
description: 'No organisation'
content:
application/json:
schema:
type: object
example:
message: 'Organisation context required.'
properties:
message:
type: string
example: 'Organisation context required.'
tags:
- 'Slack Integration'
/api/integrations/slack/channels:
get:
summary: 'List Slack Channels'
operationId: listSlackChannels
description: 'Fetches the list of channels from the connected Slack workspace.'
parameters: []
responses:
200:
description: Success
content:
application/json:
schema:
type: object
example:
ok: true
channels:
-
id: C123456789
name: general
is_member: true
-
id: C987654321
name: random
is_member: false
properties:
ok:
type: boolean
example: true
channels:
type: array
example:
-
id: C123456789
name: general
is_member: true
-
id: C987654321
name: random
is_member: false
items:
type: object
properties:
id:
type: string
example: C123456789
name:
type: string
example: general
is_member:
type: boolean
example: true
401:
description: ''
content:
application/json:
schema:
oneOf:
-
description: 'Not connected'
type: object
example:
ok: false
error: not_connected
message: 'Not connected to Slack.'
properties:
ok:
type: boolean
example: false
error:
type: string
example: not_connected
message:
type: string
example: 'Not connected to Slack.'
-
description: 'Invalid token'
type: object
example:
ok: false
error: token_invalid
message: 'Slack connection needs to be re-authorized.'
properties:
ok:
type: boolean
example: false
error:
type: string
example: token_invalid
message:
type: string
example: 'Slack connection needs to be re-authorized.'
403:
description: 'No organisation'
content:
application/json:
schema:
type: object
example:
message: 'Organisation context required.'
properties:
message:
type: string
example: 'Organisation context required.'
tags:
- 'Slack Integration'
/api/integrations/slack/send:
post:
summary: 'Send a message to Slack'
operationId: sendAMessageToSlack
description: "Accepts a payload in either Reviews format (data = array of reviews) or\nMentions format (data = object with posts and/or comments) and posts\na Block Kit message to the given Slack channel via the notification dispatcher."
parameters: []
responses:
200:
description: Success
content:
application/json:
schema:
type: object
example:
ok: true
message: 'Message sent.'
properties:
ok:
type: boolean
example: true
message:
type: string
example: 'Message sent.'
400:
description: 'Slack API error'
content:
application/json:
schema:
type: object
example:
ok: false
error: ...
message: ...
properties:
ok:
type: boolean
example: false
error:
type: string
example: ...
message:
type: string
example: ...
401:
description: 'Not connected'
content:
application/json:
schema:
type: object
example:
ok: false
error: not_connected
message: 'Not connected to Slack.'
properties:
ok:
type: boolean
example: false
error:
type: string
example: not_connected
message:
type: string
example: 'Not connected to Slack.'
422:
description: 'Invalid payload'
content:
application/json:
schema:
type: object
example:
message: 'Invalid payload: data must be reviews array or mentions object.'
properties:
message:
type: string
example: 'Invalid payload: data must be reviews array or mentions object.'
500:
description: 'Server error'
content:
application/json:
schema:
type: object
example:
message: 'Failed to send message.'
properties:
message:
type: string
example: 'Failed to send message.'
tags:
- 'Slack Integration'
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
channel_id:
type: string
description: 'Must not be greater than 32 characters.'
example: b
status:
type: string
description: ''
example: architecto
process_id:
type: string
description: ''
example: architecto
created_at:
type: string
description: ''
example: architecto
data:
type: object
description: ''
example: []
properties: { }
required:
- channel_id
- status
- process_id
- created_at
- data
/api/subscription-plans:
get:
summary: 'List Subscription Plans'
operationId: listSubscriptionPlans
description: 'Get all available subscription plans.'
parameters: []
responses:
200:
description: ''
content:
application/json:
schema:
type: object
example:
data:
-
id: 1
stripe_price_id: price_internal_free
name: Free
description: 'Post-trial free tier'
amount: 0
currency: usd
interval: month
plan_threshold: { }
properties:
data:
type: array
example:
-
id: 1
stripe_price_id: price_internal_free
name: Free
description: 'Post-trial free tier'
amount: 0
currency: usd
interval: month
plan_threshold: []
items:
type: object
properties:
id:
type: integer
example: 1
stripe_price_id:
type: string
example: price_internal_free
name:
type: string
example: Free
description:
type: string
example: 'Post-trial free tier'
amount:
type: integer
example: 0
currency:
type: string
example: usd
interval:
type: string
example: month
plan_threshold:
type: object
properties: { }
tags:
- Subscriptions
/api/subscriptions/threshold-usage:
get:
summary: 'Get Threshold Usage'
operationId: getThresholdUsage
description: "Returns plan soft limits in `threshold` 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