{"openapi":"3.1.0","info":{"title":"TofuBofu","version":"0.1.0"},"paths":{"/api/auth/register":{"post":{"summary":"Register","operationId":"register_api_auth_register_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RegisterRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/account/profile":{"post":{"summary":"Update Profile","operationId":"update_profile_api_account_profile_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProfileRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/account/password":{"post":{"summary":"Change Password","operationId":"change_password_api_account_password_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PasswordRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/auth/login":{"post":{"summary":"Login","operationId":"login_api_auth_login_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/auth/request-link":{"post":{"summary":"Request Link","description":"Passwordless login: email the user a magic link + a 6-digit code. Always\nreturns ok (don't reveal whether an account exists).\n\n`next` is the destination the sign-in was started FROM, and it has to\nsurvive the email or the round trip loses it. A team invite is the case that\nforced this: `/invite/{token}` sent a signed-out visitor to `/login` and\ndropped the token, so the emailed link landed them on `/reports` with the\ninvite unaccepted and nothing in the app pointing back at it.\n\nThe value is validated HERE, not only where it is finally used, because this\nis where it enters an email that will be clicked days later.","operationId":"request_link_api_auth_request_link_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LinkRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/auth/verify-code":{"post":{"summary":"Verify Code","operationId":"verify_code_api_auth_verify_code_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerifyCodeRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/auth/logout":{"post":{"summary":"Logout","operationId":"logout_api_auth_logout_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/auth/me":{"get":{"summary":"Me","operationId":"me_api_auth_me_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/research":{"post":{"summary":"Research Url","operationId":"research_url_api_research_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResearchRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/query-plan":{"post":{"summary":"Query Plan","description":"Generate the proposed query plan for the user to review/edit before scoring.","operationId":"query_plan_api_query_plan_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QueryPlanRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/reports":{"post":{"summary":"Start Report","description":"Authenticated ad-hoc scan. This has none of /api/free-scan's guards\n(email capture, verification, dedup, IP rate limit), so it must never run\nanonymously -- anonymous/public callers belong on /api/free-scan instead.","operationId":"start_report_api_reports_post","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReportRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"summary":"List All Reports","operationId":"list_all_reports_api_reports_get","parameters":[{"name":"brand","in":"query","required":false,"schema":{"type":"string","default":"","title":"Brand"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/free-scan":{"post":{"summary":"Free Scan","description":"Public email-only free scan. Captures the lead, dedups to 1 per email / 30\ndays (hands back the existing report), and rate-limits per IP as a backstop.\nCore pipeline is shared with the MCP server (app/services/free_scan.py).","operationId":"free_scan_api_free_scan_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/FreeScanRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/onboard/start":{"post":{"summary":"Onboard Start","description":"PLG onboarding: the free audit IS the signup. One email submit creates a\npasswordless soft account, logs this tab in, starts the scan attributed to\nthe account (no orphan lead), and emails the keepsake magic link. See\napp/services/onboarding.py for the full contract.","operationId":"onboard_start_api_onboard_start_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OnboardStartRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/team/join":{"post":{"summary":"Team Join","description":"Verified-domain team join (the Slack/Notion pattern): a verified user whose\nemail domain matches a workspace's domain joins it, and their solo workspace\n(brand + report) is absorbed into the team so nothing is stranded.","operationId":"team_join_api_team_join_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TeamJoinRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/overview":{"get":{"summary":"Overview Data","operationId":"overview_data_api_overview_get","parameters":[{"name":"brand","in":"query","required":false,"schema":{"type":"string","default":"","title":"Brand"}},{"name":"as_of","in":"query","required":false,"schema":{"type":"string","default":"","title":"As Of"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/websites/path-prefix":{"post":{"summary":"Set Path Prefix","description":"Tell us which section of a shared website this brand owns.\n\nOnly meaningful when two brands sit on one domain. With it set, the pages\nand the first-party visits under that path are that brand's and are reported\nas such; without it both brands see the website's figures and the screen\nsays so. Site-wide totals are never split, whatever is set here, because\n`gsc_daily` carries no path and a share of them would be invented.","operationId":"set_path_prefix_api_websites_path_prefix_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PathPrefixRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/workspaces":{"get":{"summary":"List Workspaces Route","description":"Every workspace this user belongs to, plus which one is active.\n\n`switchable` is what the UI gates the switcher on: one workspace means no\ncontrol at all, so a customer never sees an operator concept exists.\n\n`can_create` and `create_reason` ship with the list so the UI can SHOW the\ncontrol on Track and Fix with the upgrade reason attached, rather than\nhiding it. A hidden capability sells nothing, and a 403 with no visible\ncause reads as a bug.","operationId":"list_workspaces_route_api_workspaces_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}},"post":{"summary":"Create Workspace Route","description":"Create a workspace. Dominate and Rule only, comped exempt.\n\nTHE NAME IS REQUIRED and has no default, because a default is the whole\nreason this endpoint exists: two callers of `provision_workspace` defaulted\ndifferently and Arnav ended up with two workspaces called `arnav`.\n\n`provision_workspace` is CALLED, never reimplemented: the placeholder brand,\nthe empty company profile and the owner membership all follow the one path\nthat is already tested. It hardcodes `plan='free'`, and that is the point.\nA plan is set by the Dodo webhook after payment or by an admin comp, so the\npayer's plan never propagates into the workspace they just made.\n\nIt does NOT switch the caller into the new workspace. The client calls the\nexisting `POST /api/workspaces/active` for that, so creation and selection\nstay separable, and nothing here resolves anything from the active\nselection: this writes to the id it just minted and to nothing else.\n\nVERIFIED ONLY, like everything else on this card. A workspace IS the billing\nand access boundary, so minting one sits with billing, teammates and\npublishing rather than with viewing and scanning, which stay open to a soft\naccount on purpose. `/api/team/invite` renders inches below this control and\nalready gates; two controls on one card with two verification rules is the\nsame class of contradiction as the one this commit is fixing.","operationId":"create_workspace_route_api_workspaces_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkspaceNameRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/workspaces/{workspace_id}":{"patch":{"summary":"Rename Workspace Route","description":"Rename a workspace. Owner only, and the target is the PATH id.\n\nNever \"the active one\". `get_active_workspace` answers what the user is\nlooking at, and creating a workspace changes that answer, so a rename that\nread the selection would rename whichever account the operator last clicked.\nMembership is re-checked against THIS id.\n\n404 rather than 403 for a workspace the caller is not in, so the endpoint\nnever confirms that an id it was handed exists. An operator or a member gets\n403: they are in the workspace and are being told the action is not theirs,\nwhich leaks nothing they did not already know. Renaming the customer's own\ncompany is the owner's to do.\n\nVERIFIED ONLY, for the reason `create_workspace_route` gives: this writes to\nthe account boundary and every teammate sees the result, so it belongs with\nbilling and team rather than with the open view-and-scan paths. The check\nruns BEFORE the role lookup, so an unverified caller learns nothing about\nwhich ids exist.","operationId":"rename_workspace_route_api_workspaces__workspace_id__patch","parameters":[{"name":"workspace_id","in":"path","required":true,"schema":{"type":"string","title":"Workspace Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WorkspaceNameRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/workspaces/active":{"post":{"summary":"Set Active Workspace Route","description":"Switch which workspace this user is looking at.\n\nRefuses a workspace the user does not belong to rather than storing it and\nfiltering later, so the users table never holds an id that looks\nauthoritative and is not.","operationId":"set_active_workspace_route_api_workspaces_active_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ActiveWorkspaceRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/brands":{"get":{"summary":"List Brands Route","operationId":"list_brands_route_api_brands_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/brands/{brand_id}/colors":{"post":{"summary":"Set Brand Colors Route","description":"This brand's hero-image palette (spec 2026-09-04, D4). Brand-scoped, not\nworkspace-scoped: a workspace with several brands must not have one\nbrand's colours leak onto another's renders, which is why this writes\n`brands.brand_colors` and nothing on `workspaces`.\n\nMalformed entries are dropped rather than failing the whole request\n(`brands.normalize_colors`); the response carries what actually landed so\nthe caller can show it rather than assume the request round-tripped.","operationId":"set_brand_colors_route_api_brands__brand_id__colors_post","parameters":[{"name":"brand_id","in":"path","required":true,"schema":{"type":"string","title":"Brand Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BrandColorsRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/fixes":{"get":{"summary":"Fixes","operationId":"fixes_api_fixes_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/recommendations/{rec_id}/status":{"post":{"summary":"Set Rec Status","operationId":"set_rec_status_api_recommendations__rec_id__status_post","parameters":[{"name":"rec_id","in":"path","required":true,"schema":{"type":"string","title":"Rec Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatusRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/goal":{"post":{"summary":"Set Goal","operationId":"set_goal_api_goal_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GoalRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/usage":{"get":{"summary":"Usage","operationId":"usage_api_usage_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/profile":{"get":{"summary":"Get Profile","operationId":"get_profile_api_profile_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/scan/preview-questions":{"post":{"summary":"Preview Questions","description":"A sample of the buying questions the current answers produce.\n\nThe intake asks more than any competitor's and still reads like a form, so\nthe depth is invisible exactly where a sceptic decides whether we are\nserious. This makes the answers visibly do something: change SMB to\nEnterprise and the questions change.\n\nReads nothing, writes nothing, spends no scan quota and does not touch the\nquery budget. Cost is controlled inside question_preview: identical answers\nare cached, and a workspace has an hourly allowance past which the\ndeterministic templates stand in.","operationId":"preview_questions_api_scan_preview_questions_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuestionPreviewRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/scan/keyword-cloud":{"post":{"summary":"Keyword Cloud","description":"The terms this brand claims, the terms buyers search, and the gap.\n\nSpec 1.4. Pre-filled from TWO sources rather than one, because the\ndisagreement between them is the finding: a term the page leans on that\nnobody searches is copy talking to itself, and a term with real volume the\npage never says is a shelf the brand is not standing on.\n\nTHE COST RULES LIVE IN `demand_signals.keyword_cloud` AND ARE THE EXISTING\nONES. Both paid halves are cached and charged against a ledger BEFORE the\nprovider is called, and the ledgers are the two that already exist rather\nthan a third: the expansion budget this shares with the Demand tab is the\nsame unit and the same bill. What this endpoint adds is the HOURLY\nALLOWANCE, the same guard `/api/scan/question-set` carries, because a\nscreen is reachable in a loop and the page fetch below is not free either.\n\nNEVER RAISES for a data reason. A step that cannot pre-fill still lets a\ncustomer type their own terms, so every failure degrades to a stated\nshortfall rather than a 500.","operationId":"keyword_cloud_api_scan_keyword_cloud_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeywordCloudRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/scan/question-set":{"post":{"summary":"Question Set","description":"The plan's full buying-question set, each row carrying its provenance.\n\nTHE PRODUCT SELLS A PLAN-SIZED SET OF BUYING QUESTIONS A SCAN\n(`billing.bofu_budget_for_plan`, which is the only place that number is\ndecided), AND NOTHING EVER WROTE THEM BEFORE RUN WAS PRESSED. The box was\nseeded from `workspaces.list_saved_queries` and nothing else, so a brand\nwhose earlier scans ran free showed five and the founder typed the rest.\n\nTWO RULES, and the first one is a data-safety rule, not a product one.\n\n1. **What is already in the box LEADS and is never replaced.** This endpoint\n   fills the REMAINDER of the budget. Production `saved_queries` holds 280\n   rows across 30 brands, every one with an empty intent and none pinned, and\n   `scan_run` replaces the non-custom rows with whatever the form posts back.\n   So an endpoint that returned a fresh set for the form to swap in would\n   delete a customer's stored questions on the first press of Run, and would\n   break every trend line those questions carry. Filling around them costs\n   nothing and is also the right product answer to \"only 5-6 are showing\".\n\n2. **The accepted set is what runs**, because the form posts it back and\n   `scan_intake.pin_questions` pins it. That is a correctness rule rather\n   than a cost one: measured 2026-08-22, two identical calls to the question\n   generator returned 0, 0 and 5 identical questions out of 23 on three real\n   brands, so regenerating at scan time would replace almost every question\n   the customer just approved.\n\nCost is bounded the way `question_preview` bounds it: identical answers hit\na cache, and a workspace has an hourly allowance past which the last cached\nset stands.","operationId":"question_set_api_scan_question_set_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/QuestionSetRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/scan/run":{"post":{"summary":"Scan Run","operationId":"scan_run_api_scan_run_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScanRunRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/overview":{"get":{"summary":"Admin Overview","operationId":"admin_overview_api_admin_overview_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/dashboard":{"get":{"summary":"Admin Dashboard","operationId":"admin_dashboard_api_admin_dashboard_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/analytics":{"get":{"summary":"Admin Analytics","operationId":"admin_analytics_api_admin_analytics_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/workspace/{workspace_id}":{"get":{"summary":"Admin Workspace","operationId":"admin_workspace_api_admin_workspace__workspace_id__get","parameters":[{"name":"workspace_id","in":"path","required":true,"schema":{"type":"string","title":"Workspace Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/recommendations/{rec_id}/done":{"post":{"summary":"Admin Mark Done","operationId":"admin_mark_done_api_admin_recommendations__rec_id__done_post","parameters":[{"name":"rec_id","in":"path","required":true,"schema":{"type":"string","title":"Rec Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/workspace/{workspace_id}/plan":{"post":{"summary":"Admin Set Plan","operationId":"admin_set_plan_api_admin_workspace__workspace_id__plan_post","parameters":[{"name":"workspace_id","in":"path","required":true,"schema":{"type":"string","title":"Workspace Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminPlanRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/workspace/{workspace_id}/comped":{"post":{"summary":"Admin Set Comped","description":"Comp or un-comp a workspace, which exempts it from the per-plan brand cap.\n\n`/admin` READ this flag in four places and nothing anywhere WROTE it, so the\nexemption CLAUDE.md documents was unreachable without hand-editing SQLite on\na GCS volume. Admin-gated by `_require_admin`, the same boundary as every\nother write on this console.","operationId":"admin_set_comped_api_admin_workspace__workspace_id__comped_post","parameters":[{"name":"workspace_id","in":"path","required":true,"schema":{"type":"string","title":"Workspace Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminCompedRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/workspace/{workspace_id}/partner-logo":{"post":{"summary":"Admin Upload Partner Logo","description":"Store one partner lockup against a workspace. Admin only.\n\nThe TARGET IS THE PATH ID, never the caller's active workspace. An admin is\na member of several workspaces and their selection changes as they click, so\nreading it here would write a customer's logo onto whichever account had\nbeen opened last.","operationId":"admin_upload_partner_logo_api_admin_workspace__workspace_id__partner_logo_post","parameters":[{"name":"workspace_id","in":"path","required":true,"schema":{"type":"string","title":"Workspace Id"}}],"requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/Body_admin_upload_partner_logo_api_admin_workspace__workspace_id__partner_logo_post"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/workspace/{workspace_id}/partner-logo/{variant}":{"delete":{"summary":"Admin Clear Partner Logo","description":"Remove one partner lockup. The row is cleared FIRST, then the bytes.\n\nThat order is the safe one: a cleared row with a leftover file renders our\nown mark and wastes a few kilobytes, while a deleted file with a live row\nwould put a broken image on a customer's report.","operationId":"admin_clear_partner_logo_api_admin_workspace__workspace_id__partner_logo__variant__delete","parameters":[{"name":"workspace_id","in":"path","required":true,"schema":{"type":"string","title":"Workspace Id"}},{"name":"variant","in":"path","required":true,"schema":{"type":"string","title":"Variant"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/plan-config":{"get":{"summary":"Admin Plan Config","operationId":"admin_plan_config_api_admin_plan_config_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/plan-config/{plan_id}":{"post":{"summary":"Admin Set Plan Config","description":"Live-edit a plan's numbers/toggles. Takes effect immediately (in-memory\ncache refresh, no redeploy) for every new scan and every marketing page\nthat renders via get_plan().","operationId":"admin_set_plan_config_api_admin_plan_config__plan_id__post","parameters":[{"name":"plan_id","in":"path","required":true,"schema":{"type":"string","title":"Plan Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PlanConfigRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/drip-config":{"get":{"summary":"Admin Drip Config","description":"The drip's cadence and copy, plus the fields a template may reference.\n\nHolds no state of its own and reads only through `drip_config`, so moving\nthis page to its own admin surface later is a routing change.","operationId":"admin_drip_config_api_admin_drip_config_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/drip-config/{stage}":{"post":{"summary":"Admin Set Drip Config","description":"Live-edit one drip stage. Takes effect on the next scheduler tick, no\nredeploy. Validation happens HERE rather than at send time, because a broken\nplaceholder discovered while sending is an email nobody receives and nobody\nhears about.","operationId":"admin_set_drip_config_api_admin_drip_config__stage__post","parameters":[{"name":"stage","in":"path","required":true,"schema":{"type":"integer","title":"Stage"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DripConfigRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/schedules-backfill":{"get":{"summary":"Admin Schedules Backfill Plan","description":"What the backfill would create. Reads only.","operationId":"admin_schedules_backfill_plan_api_admin_schedules_backfill_get","parameters":[{"name":"plan","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Plan"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"summary":"Admin Schedules Backfill Run","description":"Create the missing recurring scans. Idempotent: a second run creates none.","operationId":"admin_schedules_backfill_run_api_admin_schedules_backfill_post","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SchedulesBackfillRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/content-dedupe":{"get":{"summary":"Admin Content Dedupe Plan","description":"What the cleanup would delete, and what it would keep and why. Reads only.","operationId":"admin_content_dedupe_plan_api_admin_content_dedupe_get","parameters":[{"name":"workspace_id","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Workspace Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"summary":"Admin Content Dedupe Run","description":"Delete the redundant rows. Idempotent: a second run deletes none.","operationId":"admin_content_dedupe_run_api_admin_content_dedupe_post","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ContentDedupeRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/scans/{report_id}/assign":{"post":{"summary":"Admin Assign Scan","description":"Hand a scan Arnav ran to the customer it is about.\n\nThe recipient is always emailed the magic link, per the spec's decision 3:\nwithout clicking it they stay unverified and cannot invite their own team.\n\nONE SCAN, and only where one scan is the whole relationship. See\n`_scan_level_assign_is_the_wrong_unit`.","operationId":"admin_assign_scan_api_admin_scans__report_id__assign_post","parameters":[{"name":"report_id","in":"path","required":true,"schema":{"type":"string","title":"Report Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssignScanRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/brands":{"get":{"summary":"Admin Brands","description":"Every brand, for the Brands tab. The handover's front door.\n\nIt did not exist, and that absence is the whole reason the brand-level\nhandover shipped with no screen: `/admin` listed accounts and scans, so the\nonly button anywhere near a customer's data was the scan-level one.","operationId":"admin_brands_api_admin_brands_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/admin/brands/{brand_id}/handover":{"get":{"summary":"Admin Hand Over Brand Preview","description":"What the handover WOULD move. Reads only.\n\nA different verb rather than a flag on the POST, the same separation the\nschedules backfill uses: a mistyped argument cannot turn a preview into a\nmove. Pass `?email=` to resolve where it would land without creating an\naccount for that person.","operationId":"admin_hand_over_brand_preview_api_admin_brands__brand_id__handover_get","parameters":[{"name":"brand_id","in":"path","required":true,"schema":{"type":"string","title":"Brand Id"}},{"name":"email","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Email"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"summary":"Admin Hand Over Brand","description":"Hand a customer their whole brand: history, content, credentials, plan.\n\nThe brand-level sibling of `POST /admin/scans/{id}/assign`. Use that one for\na cold prospect where a single scan is the whole relationship; use this one\nfor a customer, where moving one report leaves their competitors, questions\nand credentials in our account.","operationId":"admin_hand_over_brand_api_admin_brands__brand_id__handover_post","parameters":[{"name":"brand_id","in":"path","required":true,"schema":{"type":"string","title":"Brand Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HandOverBrandRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/prospects":{"get":{"summary":"Admin Prospects","operationId":"admin_prospects_api_admin_prospects_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}},"post":{"summary":"Admin Prospect Scan","description":"Scan a company that is not a customer.\n\nNo brand, no workspace, no quota, no lead row, no email. See\napp/services/prospects.py for why the absence is the mechanism.","operationId":"admin_prospect_scan_api_admin_prospects_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProspectScanRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/prospects/research":{"post":{"summary":"Admin Prospect Research","description":"Step one: what we think this company is, before anything is scanned.\n\nCosts a crawl, not a scan. /admin shows the answer as an editable form so\nthe operator confirms the competitors, market and category the way every\nother scan entry point in the product already makes them.","operationId":"admin_prospect_research_api_admin_prospects_research_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProspectResearchRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/scans/{report_id}/unassign":{"post":{"summary":"Admin Unassign Scan","description":"Undo an assignment, for the mistyped-email case.","operationId":"admin_unassign_scan_api_admin_scans__report_id__unassign_post","parameters":[{"name":"report_id","in":"path","required":true,"schema":{"type":"string","title":"Report Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/admin/health":{"get":{"summary":"Admin Health","operationId":"admin_health_api_admin_health_get","parameters":[{"name":"deep","in":"query","required":false,"schema":{"type":"integer","default":0,"title":"Deep"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/unsubscribe":{"get":{"summary":"Unsubscribe","description":"One-click unsubscribe from the lifecycle drip. Handles both a GET link-click\n(returns a confirmation page) and an RFC 8058 POST (Gmail/Apple native button).","operationId":"unsubscribe_api_unsubscribe_get","parameters":[{"name":"t","in":"query","required":false,"schema":{"type":"string","default":"","title":"T"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"summary":"Unsubscribe","description":"One-click unsubscribe from the lifecycle drip. Handles both a GET link-click\n(returns a confirmation page) and an RFC 8058 POST (Gmail/Apple native button).","operationId":"unsubscribe_api_unsubscribe_get","parameters":[{"name":"t","in":"query","required":false,"schema":{"type":"string","default":"","title":"T"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/billing/state":{"get":{"summary":"Billing State","description":"What the customer is actually on, and what they have actually paid.\n\nOur own database is the FLOOR: plan, usage and billing_status come from\n`workspaces` and are always returned. Dodo is asked for the live subscription\nand the payment history on top, and every one of those reads fails soft. A\nthird party being slow must never blank a billing screen.\n\n`subscription: null` means WE COULD NOT ASK, never \"there is no\nsubscription\". The screen says so rather than implying the account is\nunbilled, which is the same rule this repo applies to a silent engine.","operationId":"billing_state_api_billing_state_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/billing/portal":{"post":{"summary":"Billing Portal","description":"A hosted session where the customer updates their card, cancels, and\ndownloads their own invoices.\n\nOWNER ONLY. A member can read the plan; changing what the company pays is\nthe owner's, the same boundary `team/operator-publish` draws for publishing.\n\nThis replaces a `mailto:` that told customers self-serve billing was \"coming\nshortly\". We never see a card number and we do not render invoices ourselves.","operationId":"billing_portal_api_billing_portal_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/billing/checkout":{"post":{"summary":"Billing Checkout","description":"Start a hosted Dodo subscription checkout for the payer's own workspace.\n\nTHE WORKSPACE IS A DESTINATION HERE, NOT A VIEW, and until 2026-08-20 this\nresolved through `_current_workspace` -> `get_active_workspace`. A plan is\nwritten state: it is what they pay for, it lives on the workspace, and the\nid put into the checkout metadata is the id the webhook then writes it to.\nReading the switcher meant that for an operator, who is a member of every\ncustomer they run, whichever customer was last clicked was the workspace the\npayment upgraded. Three separate reviews caught this shape at three other\ncall sites before it was found on the money path.\n\n`resolve_destination_workspace` is also what the OTHER half of the money\npath already used: `workspaces.resolve_workspace_for_payer` resolves a\nsubscription that carries no `metadata.workspace_id` through it, and it\nexcludes the `operator` role by design. Both halves now answer the same\nquestion the same way, so a checkout and its email fallback cannot name two\ndifferent workspaces for one payment.","operationId":"billing_checkout_api_billing_checkout_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckoutRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/webhooks/dodo":{"post":{"summary":"Dodo Webhook","description":"Dodo subscription webhook: verify the signature, resolve the workspace,\nthen move the plan. Never grants access on an unverified payload.\n\nThree properties this handler has to hold, each written because it did not:\n\n1. **An unattributable payment is LOUD.** The whole body used to sit inside\n   `if workspace_id:`, and a subscription created outside our own checkout\n   carries no workspace id in its metadata. That path set no plan, sent no\n   alert, wrote no log line and returned 200, so Dodo never retried and\n   nobody found out until the customer complained. Now the workspace is\n   resolved from the payer's email when metadata has none, and when it still\n   cannot be resolved the handler logs `DODO-UNATTRIBUTED` and emails the\n   founder. An ambiguous email match is an alert, never a coin flip.\n2. **A replay does not double-apply.** Standard Webhooks stamps every\n   delivery with a unique `webhook-id` and Dodo retries anything non-2xx, so\n   the same event arrives more than once as a matter of course. The id is\n   claimed in `billing_events` before anything is applied.\n3. **A cancellation is checked against the workspace's CURRENT\n   subscription.** Cancelling a superseded subscription after an upgrade used\n   to drop a customer paying $499 back to free, as a direct consequence of us\n   tidying up.\n\nAlways returns 200 for a signed payload we understood. A 500 here is a\nretry loop, and retrying an event that already landed is worse than the\nevent we could not attribute.","operationId":"dodo_webhook_api_webhooks_dodo_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/schedules":{"get":{"summary":"Get Schedules","operationId":"get_schedules_api_schedules_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}},"post":{"summary":"Set Schedule","operationId":"set_schedule_api_schedules_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduleRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/reports/{report_id}/status":{"get":{"summary":"Report Status","description":"Scan status; pass ?events_after=N to also stream the live engine-by-engine\nreveal: appends {events: [...], next_cursor: M} of (query, engine) outcomes\nbeyond cursor N. Old clients that don't send the param see the same payload\nas before.\n\n`?plan=1` additionally returns `{\"plan\": {\"queries\": [...], \"engines\":\n[{\"id\", \"label\"}]}}`, which is the grid the waiting screen draws before any\nanswer arrives. It is asked for EXPLICITLY rather than sent on every poll:\nthe question set is up to 25 strings and does not change mid-scan, so\nrepeating it every two seconds would be the same payload 30 times over. The\nclient keeps the flag on only until a plan comes back, because a scan polled\nwhile it is still queued has no plan yet and a single first-poll send would\nmean that page never gets one. A scan with no plan yet returns NO `plan`\nkey, which the page reads as \"draw the skeleton\", not as \"the grid is\nempty\".","operationId":"report_status_api_reports__report_id__status_get","parameters":[{"name":"report_id","in":"path","required":true,"schema":{"type":"string","title":"Report Id"}},{"name":"events_after","in":"query","required":false,"schema":{"type":"integer","default":-1,"title":"Events After"}},{"name":"plan","in":"query","required":false,"schema":{"type":"integer","default":0,"title":"Plan"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/reports/{report_id}":{"get":{"summary":"Get Report Data","operationId":"get_report_data_api_reports__report_id__get","parameters":[{"name":"report_id","in":"path","required":true,"schema":{"type":"string","title":"Report Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/reports/{report_id}/pdf":{"get":{"summary":"Download Pdf","operationId":"download_pdf_api_reports__report_id__pdf_get","parameters":[{"name":"report_id","in":"path","required":true,"schema":{"type":"string","title":"Report Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/reports/{report_id}/llms-txt":{"get":{"summary":"Download Llms Txt","description":"A real, publishable llms.txt for the scanned brand's own site, generated\nfrom their profile + the real pages the site crawl found. Plain-text\ndownload so a customer can drop it straight at their site root.","operationId":"download_llms_txt_api_reports__report_id__llms_txt_get","parameters":[{"name":"report_id","in":"path","required":true,"schema":{"type":"string","title":"Report Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/site-file/pull-request":{"post":{"summary":"Open Site File Pr","description":"Put the llms.txt we already generated into the customer's repository.\n\nWE HAVE BEEN GENERATING THIS AND NEVER DELIVERING IT.\n`analyzer.generate_llms_txt` writes a real file from the customer's own\ncrawled sitemap on every scan, and until today the only way it reached a\nsite was a human downloading it and uploading it by hand. Nobody did.\n\nIT IS A STATIC FILE, which is the whole reason this one CAN be fixed by pull\nrequest while the base URL that prompted the work cannot: a build-time\nenvironment variable beats a repository value, and `llms.txt` has no build\nstep to lose to.\n\nTHE CONTENT IS THE ONE WE STORED, never composed here. It is generated at\nscan time from real crawled pages, so a second derivation on this path could\nhand a customer a file that disagrees with the one their report offers.","operationId":"open_site_file_pr_api_site_file_pull_request_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SiteFileRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/booking":{"post":{"summary":"Record Meeting Booking","description":"Record a booked meeting, UNCONDITIONALLY.\n\nTHE POINT OF THIS ENDPOINT IS THAT IT NEVER CONSULTS `tb_ai_ref`.\n`window.tbConversion` returns immediately when that marker is absent, so on\nits own it would have recorded only the bookings that came from an AI\nanswer, while every Google Ads click, outbound reply and direct visit\nrecorded silently nothing and the dashboard read as though it showed all of\nthem. The client fires BOTH: this write, which always happens, and the AI\ntag, which adds referral attribution when there is one. `ai_engine` here is\nthe same information as evidence, never a gate.\n\nNothing a caller sends is trusted as free text: `source` resolves against\nthe public half of the scan enum, `surface` against `booking.SURFACES`,\n`ai_engine` against the shared referral-engine set, and utm keys through the\nsame sanitiser the scan funnel uses.","operationId":"record_meeting_booking_api_booking_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookingRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/reports/{report_id}/intake":{"post":{"summary":"Report Intake","description":"Record an answer to one of the questions asked beside the live reveal.\n\nNEVER BLOCKING, in both directions. It does not wait for the scan, and the\nscan does not wait for it: an answer that lands before the diagnosis pass\nshapes this report, one that lands after it is stored on the brand and\nshapes the next. The response says which happened, so the UI can tell the\ntruth instead of implying the answer was used.","operationId":"report_intake_api_reports__report_id__intake_post","parameters":[{"name":"report_id","in":"path","required":true,"schema":{"type":"string","title":"Report Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScanIntakeRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/heard-about":{"get":{"summary":"Heard About Status","description":"Whether to ask this person how they heard about us. Ask-once is a rule,\nnot a nicety: the spec records that one dismissal IS an answer, so a\ndismissal counts as answered here and the question never returns.\n\nOnly signed-in people can be recognised across reports. A shareable report\nURL has no identity, so the client suppresses a repeat locally instead; the\nworst case there is asking a stranger twice on two different devices, which\nis better than storing anything to recognise them by.","operationId":"heard_about_status_api_heard_about_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/reports/{report_id}/feedback":{"post":{"summary":"Report Feedback","description":"Report-moment feedback that gives value back. Session optional (report\nURLs are shareable). When the viewer OWNS the report, corrections also\napply: removing a mis-attributed competitor or an off-base query sharpens\nthe brand's NEXT scan. Anonymous taps are recorded but never mutate data.","operationId":"report_feedback_api_reports__report_id__feedback_post","parameters":[{"name":"report_id","in":"path","required":true,"schema":{"type":"string","title":"Report Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReportFeedbackRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/connections":{"get":{"summary":"Connections List","description":"This website's connections, plus every website in the workspace.\n\n`connected` stays scoped to the brand in context, because that is what the\nIntegrations card acts on. `websites` is the workspace-wide grouping the\nWebsites section renders, and it is a VIEW over the brands: nobody creates a\nwebsite, it appears the moment a brand has a domain.","operationId":"connections_list_api_connections_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/connections/connect":{"post":{"summary":"Connections Connect","operationId":"connections_connect_api_connections_connect_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConnectRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/connections/{provider}/test":{"post":{"summary":"Connections Test","operationId":"connections_test_api_connections__provider__test_post","parameters":[{"name":"provider","in":"path","required":true,"schema":{"type":"string","title":"Provider"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/connections/{provider}/disconnect":{"post":{"summary":"Connections Disconnect","operationId":"connections_disconnect_api_connections__provider__disconnect_post","parameters":[{"name":"provider","in":"path","required":true,"schema":{"type":"string","title":"Provider"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/connections/webflow/sites":{"post":{"summary":"Webflow Sites","operationId":"webflow_sites_api_connections_webflow_sites_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebflowSitesRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/connections/webflow/collections":{"post":{"summary":"Webflow Collections","operationId":"webflow_collections_api_connections_webflow_collections_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebflowCollectionsRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/connections/{provider}/fields":{"post":{"summary":"Provider Fields","description":"This destination's real fields, plus what we would pre-select and why.\n\n`suggested` comes from the SAME function the silent publish path uses, so\nwhat the customer sees pre-selected is what a scheduled publish would have\ndone on its own. `guessed` names anything chosen by field ORDER rather than\nby a name match, which the screen turns into a visible warning: a body\nlanding in the right field by luck is the failure this endpoint exists to\nstop, and a customer cannot check a guess nobody told them about.","operationId":"provider_fields_api_connections__provider__fields_post","parameters":[{"name":"provider","in":"path","required":true,"schema":{"type":"string","title":"Provider"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProviderFieldsRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/connections/{provider}/field-map":{"post":{"summary":"Provider Field Map","description":"Change the field map on a connection that already exists.\n\nReconnecting would mean asking the customer for their credential again just\nto move the title into a different field, so this updates the stored row in\nplace. Everything else on it is carried over untouched.\n\nIT RETURNS THE SANITISED MAP, and the screen renders it. A role dropped here\n(a term deleted since, a slug that no longer exists, a type that cannot hold\nwhat we write) used to vanish with nothing said, because the browser called\n`loadIntegrations()` instead of reading the answer it already had.","operationId":"provider_field_map_api_connections__provider__field_map_post","parameters":[{"name":"provider","in":"path","required":true,"schema":{"type":"string","title":"Provider"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProviderFieldMapRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/connections/{provider}/relearn":{"post":{"summary":"Provider Relearn","description":"Read this destination again, and say what changed. Save only on confirm.\n\nTHE DEFECT THIS CLOSES. Discovery runs at CONNECT and nowhere else, so every\nlater improvement to how we read a customer's site reaches only customers who\nhappen to reconnect. Measured on the production database on 2026-09-01: the\ncorrected GitHub role map shipped in `tofubofu-00242-hfd` took one repository\nfrom 4 bound roles to 11, and it reached nobody until a human reconnected by\nhand. The one Webflow connection in production still holds the two-field map\nit was given on 2026-08-20.\n\n`apply=false` is a PREVIEW and stores nothing. Discovery is re-run on apply\nrather than a preview being stashed anywhere: a pending credential parked in\na session is a second copy of a customer's connection, and re-reading costs\none call to a provider the customer is sitting in front of. If their site\nchanged between the two calls, the answer that gets stored is the newer one\nand the screen we return is computed from it.\n\nIT MAY NOT WIDEN WHAT IS STORED. `connections` is a credential table, so\n`base.relearn_widens` refuses anything that adds a key outside the declared\ndiscovery results or that alters the credential itself.","operationId":"provider_relearn_api_connections__provider__relearn_post","parameters":[{"name":"provider","in":"path","required":true,"schema":{"type":"string","title":"Provider"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ProviderRelearnRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/connections/github/install":{"get":{"summary":"Github Install","description":"Where to send the customer to install the app, or why we cannot.\n\nThe signed state goes out TWICE: on the install URL, and in a short-lived\nfirst-party cookie. GitHub does not document that the state on\n`installations/new?state=` survives its authorize hop, so the cookie is the\ncopy we control. See `github.STATE_COOKIE` for the full reasoning; the check\nthe callback runs is identical either way.","operationId":"github_install_api_connections_github_install_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/connections/github/authorize":{"get":{"summary":"Github Authorize","description":"THE SECOND DOOR: where a customer goes to link an installation they did\nnot make, and could not have made.\n\nInstalling a GitHub App on an organisation is owner-only, so\n`github_install` above is unreachable for most customers: GitHub answers\n\"This action must be performed by an organization owner\". This authorizes\nthe PERSON instead, which any org member can complete, and the callback then\nasks GitHub which installations that person actually holds.\n\nIt grants us nothing new. No install, no repository change, no extra scope.","operationId":"github_authorize_api_connections_github_authorize_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/connections/github/installations":{"post":{"summary":"Github Installations","description":"The installations the link round trip just proved are this person's.\n\nNOTHING here is asserted by the caller. The list is read out of the sealed,\nexpiring, workspace-bound cookie the CALLBACK wrote after GitHub answered\n`GET /user/installations`, so a receipt minted per row re-encodes proof we\nalready hold rather than making a new claim.","operationId":"github_installations_api_connections_github_installations_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/connections/github/repos":{"post":{"summary":"Github Repos","operationId":"github_repos_api_connections_github_repos_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GitHubReposRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/connections/github/content-dirs":{"post":{"summary":"Github Content Dirs","operationId":"github_content_dirs_api_connections_github_content_dirs_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GitHubDirsRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/google/status":{"get":{"summary":"Google Status","description":"What Settings renders. `configured` false means this deployment has no\nOAuth client, which the UI states plainly instead of showing a button that\nwould fail on click.","operationId":"google_status_api_google_status_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/google/start":{"get":{"summary":"Google Start","description":"Returns the consent URL rather than redirecting, so the caller can show a\nreal message when Google is not configured instead of bouncing the browser\nto an error page.","operationId":"google_start_api_google_start_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/google/properties":{"get":{"summary":"Google Properties","description":"The picker. GA4 failing does not hide the Search Console list.","operationId":"google_properties_api_google_properties_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/google/select":{"post":{"summary":"Google Select","description":"Save the chosen properties, pull a fast recent window synchronously, then\ncontinue the full year in the background.\n\nSplit deliberately. A year is around 39 Search Console requests and takes\nlong enough that doing it inline would leave the customer watching a spinner\nat the exact moment they are deciding whether this thing works. 28 days\narrives in about three requests and is enough to draw a real trend, and the\nrest fills in behind it. If the background task dies, which on Cloud Run any\ndeploy will do, the daily pass re-runs the backfill because no completed\nbackfill is recorded.","operationId":"google_select_api_google_select_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GoogleSelectRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/google/sync":{"post":{"summary":"Google Sync","operationId":"google_sync_api_google_sync_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/google/disconnect":{"post":{"summary":"Google Disconnect","description":"One click, and the stored Google data goes with the credential. The\nprivacy page says so in public, so the delete happens before the best-effort\nrevoke and does not depend on it.","operationId":"google_disconnect_api_google_disconnect_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/google/traffic":{"get":{"summary":"Google Traffic","description":"Everything the Traffic page draws from Google, plus the comparison.\n\nDeliberately does NOT touch the Tofu Index. Google numbers sit\nbeside it and never inside it: the Index is a mention-rate measure, and\nblending search data into it would make a number customers trend against\nmean something different from one month to the next.","operationId":"google_traffic_api_google_traffic_get","parameters":[{"name":"days","in":"query","required":false,"schema":{"type":"integer","default":30,"title":"Days"}},{"name":"from_date","in":"query","required":false,"schema":{"type":"string","default":"","title":"From Date"}},{"name":"to_date","in":"query","required":false,"schema":{"type":"string","default":"","title":"To Date"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/referrals":{"get":{"summary":"Referrals","description":"AI-referral analytics for the Overview + the install snippets for Settings.\nTwo snippets: the referral tag (drop-in, on every page) and the conversion\nsnippet (call window.tbConversion('signup') or similar on a thank-you/\nsuccess page to attribute it back to an AI referral, if one brought that\nvisitor within the last 30 days).\n\nBRAND-SCOPED as of 2026-08-19, for the same reason /api/traffic is. The\nOverview traffic card sits under the brand selector and this endpoint read\nthe whole workspace, so every brand in a multi-brand workspace was shown\nevery other brand's AI referrals as its own.","operationId":"referrals_api_referrals_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/traffic":{"get":{"summary":"Traffic","description":"Unified Traffic surface. First-party analytics from our own tag (which\nworks with zero setup, for customers who have no GSC or GA4) plus, once a\nworkspace connects Google, the state of that connection.\n\nThe Google DETAIL lives at /api/google/traffic, not here: this endpoint must\nstay cheap and must keep working unchanged for the majority of workspaces\nthat have never connected Google.\n\nBRAND-SCOPED as of 2026-08-19. It used to return the whole workspace's tag\ndata under whichever brand was selected, so a brand chip reading BotGauge\nsat beside 565 pageviews that were tofubofu.com's. Now the read is narrowed\nto the brand's own site, and what the narrowing excluded is REPORTED rather\nthan dropped, so a number that just fell can be explained.\n\n`tag_state` replaces the old boolean `tag_connected`, because that boolean\nhad no way to say \"the tag is reporting, but not for this brand\", and a\nfalse there rendered as an install prompt to a customer whose tag was\nworking fine.","operationId":"traffic_api_traffic_get","parameters":[{"name":"days","in":"query","required":false,"schema":{"type":"integer","default":30,"title":"Days"}},{"name":"from_date","in":"query","required":false,"schema":{"type":"string","default":"","title":"From Date"}},{"name":"to_date","in":"query","required":false,"schema":{"type":"string","default":"","title":"To Date"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/ai-presence":{"get":{"summary":"Ai Presence","description":"The three signals per engine, side by side. Nobody else holds all three.\n\nAn analytics product knows arrivals. A log tool knows crawls. Only a product\nthat ALSO runs the scan knows whether the engine actually named you. Joined,\nthey separate four situations that look identical in GA4 and have completely\ndifferent fixes: never crawled, read but not cited, cited but no clicks, and\nworking.\n\nReads stored data only, so this costs nothing at scan time.\n\nNO CALLER SINCE 2026-09-23, AND THAT IS PARKED RATHER THAN FORGOTTEN.\n`traffic.html` was its only consumer and the AI-crawlers tab came off the\npage with the two-tab rebuild\n(ops/specs/2026-09-23-the-traffic-page-becomes-two-tabs.md): the crawl half\nof this join is EMPTY for everyone, 0 rows across 0 workspaces, because the\nonly writer is `POST /api/v1/crawler-hits` and that asks a founder to ship\nus server logs. Nobody has.\n\nIt is left standing, unrouted to any screen, because the spec's argument is\nthat the SIGNAL is worth more than anything else that pane could hold (an\nengine crawls you before it cites you, so it is the leading indicator of\nthe Tofu Index and the only number that moves in days) and the conclusion\nis to BUILD THE INSTRUMENT, not to keep the empty tab. The route worth\nbuilding is a Cloudflare connect, queued separately; when it lands, this is\nthe endpoint it fills and its correlation logic is the part worth keeping.\nDelete it only together with `ai_crawlers.correlate`, and note that\n`ai_crawlers.traffic_reading` is NOT dead: `/api/traffic` reads it for the\nengine table's visit column.","operationId":"ai_presence_api_ai_presence_get","parameters":[{"name":"days","in":"query","required":false,"schema":{"type":"integer","default":30,"title":"Days"}},{"name":"from_date","in":"query","required":false,"schema":{"type":"string","default":"","title":"From Date"}},{"name":"to_date","in":"query","required":false,"schema":{"type":"string","default":"","title":"To Date"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/team":{"get":{"summary":"Team List","operationId":"team_list_api_team_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/team/operator-publish":{"post":{"summary":"Team Operator Publish","description":"Let the account team publish to this site, or stop them.\n\nOwner-only, and deliberately not admin-settable: the whole point of the\ndefault-off grant is that pushing to somebody's own property is THEIR call.\nA control we could flip from our side would be the same permission with a\nlonger path to it.","operationId":"team_operator_publish_api_team_operator_publish_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/OperatorPublishRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/team/invite":{"post":{"summary":"Team Invite","operationId":"team_invite_api_team_invite_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/TeamInviteRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/team/{user_id}/remove":{"post":{"summary":"Team Remove","operationId":"team_remove_api_team__user_id__remove_post","parameters":[{"name":"user_id","in":"path","required":true,"schema":{"type":"string","title":"User Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/team/invite/{token}":{"get":{"summary":"Team Invite Lookup","description":"Public: lets the invite landing page show who/what before requiring login.","operationId":"team_invite_lookup_api_team_invite__token__get","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/team/invite/{token}/accept":{"post":{"summary":"Team Invite Accept","operationId":"team_invite_accept_api_team_invite__token__accept_post","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/deliverables":{"get":{"summary":"Deliverables List","operationId":"deliverables_list_api_deliverables_get","parameters":[{"name":"brand","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Brand"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"summary":"Deliverables Add","description":"Add an idea to the board, as a topic or a page to work from.\n\nAttached to the brand's latest completed report, because a deliverable\nbelongs to a brand THROUGH its report (there is no brand_id column) and an\nidea with no report is invisible to every brand-scoped read on the board.\nThat also means the grounding precondition can be checked when it is drafted:\nan idea we cannot ground is one we will decline to write, and the customer is\nbetter off learning that at the point they add it.","operationId":"deliverables_add_api_deliverables_post","parameters":[{"name":"brand","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Brand"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddIdeaRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/deliverables/{d_id}/generate":{"post":{"summary":"Deliverables Generate","operationId":"deliverables_generate_api_deliverables__d_id__generate_post","parameters":[{"name":"d_id","in":"path","required":true,"schema":{"type":"string","title":"D Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/deliverables/{d_id}/hero/regenerate":{"post":{"summary":"Deliverables Hero Regenerate","description":"Redraw the hero image pair and NOTHING ELSE, with no new art direction.\n\nNO LONGER SURFACED IN THE UI (spec 2026-09-04-unified-edit-and-brand-\npalette): the dedicated \"Redraw hero image\" button that called this is\nremoved, because it took no input and Arnav's own words were \"on what\nbasis are we redrawing the image without any input of what the user\nwants.\" Editing the hero now goes exclusively through the chat box\n(`/deliverables/{d_id}/revise`, which threads a customer's instruction\ninto the render via `image_brief`). This endpoint STAYS as API surface --\na bare \"redraw with no new instruction, same brief\" action is still\nuseful from an integration -- and its own behaviour here is unchanged.\n\nThe gap this closes (spec 2026-09-03-edit-a-piece-after-it-exists):\n`hero_image.schedule` was called in exactly one place, at first draft\ngeneration, so a deliverable that already existed when heroes shipped had\n`hero_image` NULL with no code path that would ever fill it. Arnav hit it\non production on day one.\n\nThe article's approved copy is untouchable here: regenerating a picture is\nnot a reason to re-run a writer over text a customer has already accepted.\n`hero_image.schedule` only ever writes the four hero columns, and the test\nasserts `body_markdown`, `title` and `meta_description` byte-identical.\n\nGeneration stays a background step (79 to 111 seconds measured against the\nreal API), a publish never blocks on an image, and `TB_HERO_IMAGES` governs\nthis path exactly as it governs the first draw.","operationId":"deliverables_hero_regenerate_api_deliverables__d_id__hero_regenerate_post","parameters":[{"name":"d_id","in":"path","required":true,"schema":{"type":"string","title":"D Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/deliverables/{d_id}/revisions":{"get":{"summary":"Deliverables Revisions","description":"This piece's edit history, oldest first, from the table rather than the\nlast response, so it survives a close and a reload. Read-only, so it is not\ngated on `content_production`: a workspace that can see the piece can see\nwhat was done to it.","operationId":"deliverables_revisions_api_deliverables__d_id__revisions_get","parameters":[{"name":"d_id","in":"path","required":true,"schema":{"type":"string","title":"D Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/deliverables/{d_id}/dismiss":{"post":{"summary":"Deliverables Dismiss","description":"Refuse an idea, with a reason. The row stays; only its state changes.","operationId":"deliverables_dismiss_api_deliverables__d_id__dismiss_post","parameters":[{"name":"d_id","in":"path","required":true,"schema":{"type":"string","title":"D Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DismissDeliverableRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/deliverables/{d_id}/restore":{"post":{"summary":"Deliverables Restore","description":"Undo a dismissal. A mistake has to be reversible, which is half the reason\ndismissal is a state rather than a delete.","operationId":"deliverables_restore_api_deliverables__d_id__restore_post","parameters":[{"name":"d_id","in":"path","required":true,"schema":{"type":"string","title":"D Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/deliverables/{d_id}/schedule":{"post":{"summary":"Deliverables Schedule","description":"Set or clear the planning date on a deliverable card.\n\nNOT gated on `content_production`, matching dismiss/restore just above and\nfor the same reason: this creates nothing, costs nothing, and spends\nnothing against any plan limit, so there is no reason to block it.","operationId":"deliverables_schedule_api_deliverables__d_id__schedule_post","parameters":[{"name":"d_id","in":"path","required":true,"schema":{"type":"string","title":"D Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScheduleDeliverableRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/deliverables/calendar-plan":{"get":{"summary":"Deliverables Calendar Plan","description":"What auto-scheduling would assign, and what it could not place. Reads only.","operationId":"deliverables_calendar_plan_api_deliverables_calendar_plan_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/deliverables/auto-schedule":{"post":{"summary":"Deliverables Auto Schedule","description":"Assign the planning dates. Idempotent, and it never touches a date a\nperson set.","operationId":"deliverables_auto_schedule_api_deliverables_auto_schedule_post","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/deliverables/{d_id}/performance":{"get":{"summary":"Deliverables Performance","description":"What happened after this post went live, from data already ingested.\n\nA READ of our own stored Search Console rows, so it needs no publish grant.\n`connected: false` means Search Console is not connected for this workspace,\nwhich the screen must SAY rather than rendering as zeros: no rows is not the\nsame measurement as no clicks.","operationId":"deliverables_performance_api_deliverables__d_id__performance_get","parameters":[{"name":"d_id","in":"path","required":true,"schema":{"type":"string","title":"D Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/deliverables/{d_id}/export":{"get":{"summary":"Deliverables Export","description":"One finished draft as a file: HTML, DOCX or PDF.\n\nThe human review step. Our engine invented \"2.3 times more leads\" about a\nreal customer and it is live on their site twice, one instance inside a FAQ\nanswer, because the only way a draft could leave the product was as pasted\nmarkdown and nobody read it properly.\n\nNot gated on the source floor. Reading a failing draft in Word is exactly how\na customer decides what to ask for, and blocking the export would leave them\nwith a piece they can neither publish nor properly review.","operationId":"deliverables_export_api_deliverables__d_id__export_get","parameters":[{"name":"d_id","in":"path","required":true,"schema":{"type":"string","title":"D Id"}},{"name":"format","in":"query","required":false,"schema":{"type":"string","default":"html","title":"Format"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/deliverables/{d_id}/revise":{"post":{"summary":"Deliverables Revise","description":"Change a draft by instruction, and report what the change did.\n\nThis replaces the regenerate button. Regenerate threw the piece away and\nrolled again, so a customer had no way to tell an improvement from a\ndifferent roll. The response carries before/after measurements of the real\nstored body, so the edit is reviewable.","operationId":"deliverables_revise_api_deliverables__d_id__revise_post","parameters":[{"name":"d_id","in":"path","required":true,"schema":{"type":"string","title":"D Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReviseDeliverableRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/deliverables/{d_id}/body":{"post":{"summary":"Deliverables Save Body","description":"The customer's own edit to the text, saved from the drawer.\n\nTHIS IS A PERSON'S EDIT, NOT THE WRITER'S, and the stamp has to say so.\n`updated_at` alone cannot: the model's own rewrite moves it too, which is\nexactly why `updated_by` exists and why the board can mark the pieces\nsomebody has been working on. So this passes the user's id.\n\nIT SAVES THE BODY AND NOTHING ELSE, INCLUDING THE STATUS.\n`save_deliverable_body` writes two columns. The first cut of this went\nthrough `save_deliverable_asset`, which opens `UPDATE ... SET status='draft'`\nunconditionally, so correcting a typo on a LIVE article silently\nun-published it: the card left the Published lane, the \"N are live\" count\ndropped, the Cited badge went with it, and the toast said \"Saved.\" Found by\ndriving a real stored row, and there was no way back in the product.","operationId":"deliverables_save_body_api_deliverables__d_id__body_post","parameters":[{"name":"d_id","in":"path","required":true,"schema":{"type":"string","title":"D Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeliverableBodyRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/deliverables/{d_id}/publish":{"post":{"summary":"Deliverables Publish","operationId":"deliverables_publish_api_deliverables__d_id__publish_post","parameters":[{"name":"d_id","in":"path","required":true,"schema":{"type":"string","title":"D Id"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PublishDeliverableRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/deliverables/{d_id}/unpublish":{"post":{"summary":"Deliverables Unpublish","description":"Take a published post back.\n\nGated exactly like publishing, and for the same reason: closing a pull\nrequest on a customer's repository is an action on their property, so an\noperator may only do it where the account owner has allowed publishing.","operationId":"deliverables_unpublish_api_deliverables__d_id__unpublish_post","parameters":[{"name":"d_id","in":"path","required":true,"schema":{"type":"string","title":"D Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/deliverables/{d_id}/reconcile":{"post":{"summary":"Deliverables Reconcile","description":"Ask the destination what state this piece is really in. Nothing else.\n\nTHE DRAWER CALLS THIS THE MOMENT IT OPENS on a piece we have published,\nwhich is the whole reason it is a separate endpoint from `/verify`.\n`/verify` also fetches the customer's public page, which is a second\nnetwork round trip to a third party, and a panel opening is not a request\nto go and crawl somebody's website. This asks one API one question.\n\nA READ, so it needs no publish grant, exactly as `/verify` does not: an\noperator who may not push to the customer's site may still look at what\ntheir destination says. Workspace membership is the boundary, checked by\n`_current_workspace` and by the row's own `workspace_id` inside\n`reconcile_publication`.\n\n`changed: false` with `state: null` is the normal, uninteresting answer and\nmeans we could not ask. It is never an error: the panel simply renders what\nit already had.","operationId":"deliverables_reconcile_api_deliverables__d_id__reconcile_post","parameters":[{"name":"d_id","in":"path","required":true,"schema":{"type":"string","title":"D Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/deliverables/{d_id}/verify":{"post":{"summary":"Deliverables Verify","description":"Read the live page back, on demand.\n\nNOT OPTIONAL, and not a duplicate of the automatic check. The normal Webflow\nworkflow is staged as a draft and published by a human in the CMS minutes\nlater, so the publish that we saw made no page and the page that exists we\nnever saw. Without this control the feature misses the workflow it was\nwritten for.\n\nA READ of a public page, so it needs no publish grant: an operator who may\nnot push to the customer's site may still look at it. Workspace membership\nis the boundary, checked by `_current_workspace` plus the row's own\nworkspace_id below.","operationId":"deliverables_verify_api_deliverables__d_id__verify_post","parameters":[{"name":"d_id","in":"path","required":true,"schema":{"type":"string","title":"D Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/shortlists":{"get":{"summary":"Shortlists Data","description":"The Queries page: one item per buying question, both for the row-per-\nquestion list and (ops/specs/2026-09-04-queries-matrix-view.md) the\nquestion-by-engine matrix view built on the same payload.\n\n`as_of` = a report_id to view the page as it stood at that past scan,\nsame \"as of\" contract as monitor.get_overview: everything below (the\nlist, the matrix, movement, tracked competitors) reflects that scan, and\nthe trend history/series never leaks scans that came after it. Defaults\nto the latest scan when omitted or unrecognised, exactly the prior\nbehaviour.","operationId":"shortlists_data_api_shortlists_get","parameters":[{"name":"brand","in":"query","required":false,"schema":{"type":"string","default":"","title":"Brand"}},{"name":"as_of","in":"query","required":false,"schema":{"type":"string","default":"","title":"As Of"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/queries":{"get":{"summary":"Custom Queries List","operationId":"custom_queries_list_api_queries_get","parameters":[{"name":"brand","in":"query","required":false,"schema":{"type":"string","default":"","title":"Brand"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"post":{"summary":"Custom Queries Add","operationId":"custom_queries_add_api_queries_post","parameters":[{"name":"brand","in":"query","required":false,"schema":{"type":"string","default":"","title":"Brand"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomQueryIn"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/queries/{query_id}":{"put":{"summary":"Custom Queries Edit","description":"Rewrite a pinned query in place.\n\nEditing exists because the manage-queries screen does not any more: pin,\nedit and remove all happen on the grid row, beside the answer that made you\nwant to change the question. Delete-then-add would have done the same job\nand lost the row's position, which decides which query the plan cap drops.","operationId":"custom_queries_edit_api_queries__query_id__put","parameters":[{"name":"query_id","in":"path","required":true,"schema":{"type":"string","title":"Query Id"}},{"name":"brand","in":"query","required":false,"schema":{"type":"string","default":"","title":"Brand"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomQueryIn"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"summary":"Custom Queries Delete","operationId":"custom_queries_delete_api_queries__query_id__delete","parameters":[{"name":"query_id","in":"path","required":true,"schema":{"type":"string","title":"Query Id"}},{"name":"brand","in":"query","required":false,"schema":{"type":"string","default":"","title":"Brand"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/queries/non-vendor":{"post":{"summary":"Custom Queries Drop Non Vendor","description":"Remove questions no engine answered with a vendor, on a brand whose set\nwas GENERATED rather than accepted.\n\nWHY THIS EXISTS, AND WHY DELETING A PIN WAS NOT ENOUGH. Arnav, 2026-09-14:\n\"give an option to the user to remove queries in the summary page itself.\"\nThe re-scan card already names the flagged questions\n(`scan_summary.last_scan`), and on a brand that has ACCEPTED its set\n`DELETE /api/queries/{id}` really removes one, because\n`scan_plan.overlay_pins(exact=True)` makes the pinned list the whole set.\n\nOn a brand that has NOT, it does not. `_reuse_previous_plan` rebuilds the\nnext set from the last scan's stored rows, so a flagged question sits in\nthat generated half whether or not it is also pinned, and deleting the pin\nleaves it running while the screen claims it is gone. Measured on\nproduction: all seven of Lemon Law Group Partners' flagged questions are\npinned, all seven pins were deleted through the expression the scan uses,\nand all seven still ran. So the card withheld the control rather than lie.\n\nTHE ONLY MECHANISM THAT REALLY STOPS A GENERATED QUESTION IS ACCEPTANCE, and\nthe spec names it: \"the existing edit path for generated ones\". This is that\npath, without making the customer walk the whole question editor to use it.\nIt pins the SURVIVING questions from the last scan, by their exact stored\ntext so every per-query trend line still lines up, and stamps\n`brands.questions_curated`. `report_engine._reuse_previous_plan` is untouched,\nwhich the dead-queries spec requires.\n\nTHE CONSEQUENCE IS REAL AND THE SCREEN STATES IT BEFORE THE CLICK: an\naccepted set is the whole set, so nothing is written to refill the freed\nslots, on this scan or a later one. That is what the customer is asking for\nhere, and it is also why `scan.html` asks `tbConfirm` first.\n\nTHE SERVER DECIDES WHAT SURVIVES. The request carries only which flagged\nquestions to drop; the set itself is rebuilt here from the stored scan, with\nthe same arguments `/api/profile` renders the card from. A client that sent\na short list could otherwise curate a brand down to it.","operationId":"custom_queries_drop_non_vendor_api_queries_non_vendor_post","parameters":[{"name":"brand","in":"query","required":false,"schema":{"type":"string","default":"","title":"Brand"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/NonVendorRemovalIn"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/competitors":{"get":{"summary":"Competitors Data","operationId":"competitors_data_api_competitors_get","parameters":[{"name":"brand","in":"query","required":false,"schema":{"type":"string","default":"","title":"Brand"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/competitors/demand":{"get":{"summary":"Competitors Demand","description":"Branded monthly search volume for every company on the leaderboard.\n\nThe sharpest demand finding we can produce and the one AI data structurally\ncannot: a rival can sit level with you on AI share of voice while carrying\nthree times your branded demand, which is not \"AI likes them\", it is\n\"buyers want them\". The two numbers answer different questions and this\nendpoint keeps them in different columns.","operationId":"competitors_demand_api_competitors_demand_get","parameters":[{"name":"brand","in":"query","required":false,"schema":{"type":"string","default":"","title":"Brand"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/traffic/demand":{"get":{"summary":"Traffic Demand","description":"Category keyword demand: the ZERO-INTEGRATION surface on Traffic.\n\nEvery other number on that page needs the customer to connect Search\nConsole, connect GA4, or install our tag. This one needs none of them, so a\nbrand-new customer sees something real on day one instead of an empty tab.\nIt is CATEGORY demand, not their traffic, and the template says so.\n\nKEYWORDS COME FROM THREE REAL SOURCES, and until this release they came\nfrom one templated string builder that produced at most three phrases:\n\n  1. Search Console queries, when Google is connected and the property is\n     this brand's. Already ingested, already paid for, and the strongest of\n     the three because it is demand this site actually received.\n  2. The brand's own scan queries, which are what the Tofu Index is\n     measured over, so the AI column on those rows is a real reading rather\n     than a blank.\n  3. DataForSEO expansion of 1 and 2, which is `fetch_demand_phrases`, a\n     function that has existed and gone uncalled on this path since it\n     shipped.\n\nVOLUME LEADS WITH THE AI NUMBER. `ai_search_volume` measures how often a\nphrase is put to an AI engine and `search_volume` is Google Ads volume; an\nAEO product's demand table leads with the first and shows the second beside\nit, not the other way round.\n\nSORTED BY WHAT AI MISSES, never by volume. A list sorted by volume is a\nkeyword tool; the finding is the volume sitting on phrases where no engine\nnamed you.","operationId":"traffic_demand_api_traffic_demand_get","parameters":[{"name":"brand","in":"query","required":false,"schema":{"type":"string","default":"","title":"Brand"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/competitors/track":{"post":{"summary":"Competitor Add","operationId":"competitor_add_api_competitors_track_post","parameters":[{"name":"brand","in":"query","required":false,"schema":{"type":"string","default":"","title":"Brand"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CompetitorIn"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/competitors/track/{competitor_id}":{"delete":{"summary":"Competitor Delete","operationId":"competitor_delete_api_competitors_track__competitor_id__delete","parameters":[{"name":"competitor_id","in":"path","required":true,"schema":{"type":"string","title":"Competitor Id"}},{"name":"brand","in":"query","required":false,"schema":{"type":"string","default":"","title":"Brand"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/competitors/head-to-head":{"post":{"summary":"Competitor Head To Head","description":"Which tracked rivals go into the head-to-head probe (spec 2026-09-26, 2b).\n\nArnav: \"the user should choose the BOFU queries.\" T-070 already settled that\nthe Bofu Index is measured against the competitors the customer chose; this\nis the same rule one level deeper, WHICH of them, and until now it was\ndecided by the order `req.competitors` happened to be concatenated in.\n\nIT TAKES EFFECT ON THE NEXT SCAN, NOT ON THE STORED ONE. The probe is engine\ncalls, so a selection cannot rescore a report that has already run, and the\npanel says so rather than letting a customer tick a box and wonder why the\ngrid did not move.\n\nTHE CAP IS `prompt_generator.H2H_MAX_RIVALS`, read from the module that runs\nthe probe. An empty list clears the selection and returns the brand to list\norder, which is the documented default.","operationId":"competitor_head_to_head_api_competitors_head_to_head_post","parameters":[{"name":"brand","in":"query","required":false,"schema":{"type":"string","default":"","title":"Brand"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/HeadToHeadIn"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/analyst/meta":{"get":{"summary":"Analyst Meta","operationId":"analyst_meta_api_analyst_meta_get","parameters":[{"name":"brand","in":"query","required":false,"schema":{"type":"string","default":"","title":"Brand"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/analyst/chat":{"post":{"summary":"Analyst Chat","operationId":"analyst_chat_api_analyst_chat_post","parameters":[{"name":"brand","in":"query","required":false,"schema":{"type":"string","default":"","title":"Brand"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AnalystChatRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/index/subscribe":{"post":{"summary":"Index Subscribe","operationId":"index_subscribe_api_index_subscribe_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/IndexSubscribeRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/keys":{"get":{"summary":"Api Keys List","operationId":"api_keys_list_api_keys_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}},"post":{"summary":"Api Keys Create","operationId":"api_keys_create_api_keys_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/KeyCreateRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/keys/{key_id}":{"delete":{"summary":"Api Keys Revoke","operationId":"api_keys_revoke_api_keys__key_id__delete","parameters":[{"name":"key_id","in":"path","required":true,"schema":{"type":"string","title":"Key Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/webhook":{"post":{"summary":"Webhook Set","operationId":"webhook_set_api_webhook_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/WebhookSetRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"delete":{"summary":"Webhook Delete","operationId":"webhook_delete_api_webhook_delete","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/content-autonomy":{"get":{"summary":"Content Autonomy Get","operationId":"content_autonomy_get_api_content_autonomy_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}},"post":{"summary":"Content Autonomy Set","operationId":"content_autonomy_set_api_content_autonomy_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AutonomyRequest"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/tools/{tool}":{"post":{"summary":"Run Free Tool","operationId":"run_free_tool_api_tools__tool__post","parameters":[{"name":"tool","in":"path","required":true,"schema":{"type":"string","title":"Tool"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ToolRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/connections/github/installed":{"get":{"summary":"Github Installed","description":"Where GitHub sends the browser back, from EITHER door.\n\nTwo round trips end here and they are told apart by one thing: the install\ndoor returns an `installation_id`, the link door returns only a `code`,\nbecause it installed nothing. Both are held to the same three checks below,\nand both end at the same receipt.\n\nTHE PATH IS FIXED, not chosen: it is the app's **Callback URL**, registered\nin the app's settings on GitHub, and changing it here means changing it\nthere first. Not the Setup URL, which this docstring said until 2026-08-26\nand which is wrong in a way that would have wasted whoever configures the\nApp: GitHub's registration docs say of that field, read at source, \"If you\nselected Request user authorization (OAuth) during installation in an\nearlier step, you will not be able to enter a URL here. Users will instead\nbe redirected to the Callback URL as part of the authorization flow\". We\nrequire that option, so the Setup URL field is not even available to us.\nRegister this path as the FIRST callback URL.\n\nIt stores nothing. It mints a RECEIPT and hands it, with the installation id,\nback to the Integrations page, which continues the connect flow. The id only\nbecomes a connection once the customer picks a repository and a folder and\npresses Connect, so the write stays in one place rather than a redirect\ncreating a connection as a side effect.\n\nTHREE CHECKS, IN THIS ORDER, and the third is the one that was missing.\n\n1. The signed `state` proves the round trip started in THIS browser and this\n   workspace. It proves nothing about the installation. It is read from the\n   query string if GitHub sent it back and from our own cookie if it did\n   not, because GitHub documents neither behaviour and the check must not\n   depend on which one is true.\n2. `setup_action == \"request\"` means no installation exists yet.\n3. `code` is GitHub's one-time proof of WHO returned. We exchange it for a\n   user token, ask GitHub whether that user holds this installation, and\n   discard the token. Without this step the callback would sign whatever\n   integer the URL carried: measured against the pre-fix branch, a caller\n   holding only their own signed state got a 303 carrying installation\n   987654, which they had never touched.","operationId":"github_installed_connections_github_installed_get","parameters":[{"name":"installation_id","in":"query","required":false,"schema":{"type":"string","default":"","title":"Installation Id"}},{"name":"setup_action","in":"query","required":false,"schema":{"type":"string","default":"","title":"Setup Action"}},{"name":"state","in":"query","required":false,"schema":{"type":"string","default":"","title":"State"}},{"name":"code","in":"query","required":false,"schema":{"type":"string","default":"","title":"Code"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/google/callback":{"get":{"summary":"Google Callback","description":"Where Google sends the browser back. Always redirects to Settings with a\nshort status, never renders an error page of its own.\n\nTHE PATH IS FIXED, not chosen. It is registered on the OAuth client as\n`https://tofubofu.com/auth/google/callback` and\n`http://localhost:3000/auth/google/callback`, and Google rejects any\nredirect_uri that does not match one of those exactly. Moving this route\nmeans editing the client in the Google console first.","operationId":"google_callback_auth_google_callback_get","parameters":[{"name":"code","in":"query","required":false,"schema":{"type":"string","default":"","title":"Code"}},{"name":"state","in":"query","required":false,"schema":{"type":"string","default":"","title":"State"}},{"name":"error","in":"query","required":false,"schema":{"type":"string","default":"","title":"Error"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/ping":{"get":{"summary":"Ping","operationId":"ping_api_v1_ping_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/v1/crawler-hits":{"post":{"summary":"Report Crawler Hits","description":"Report AI crawler requests seen in your own server logs.\n\nThis is the half of AI attribution that no JavaScript analytics product can\nreach, ours included: GPTBot, ClaudeBot and PerplexityBot do not run\nJavaScript, so GA4 never sees them and neither does our /t.js tag. The\nrequest is only observable where it lands, which is your server.\n\nSend aggregated counts, not one call per request: group by bot, path and\nday. Unknown or unrecognised user agents are dropped rather than stored, so\nthe table only holds crawlers we can name and explain.\n\nA crawl is a precondition for a citation, not proof of one. The scan is what\nmeasures whether the engine actually named you.","operationId":"report_crawler_hits_api_v1_crawler_hits_post","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CrawlerHitsRequest"}}}},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}},"get":{"summary":"Get Crawler Hits","description":"What the AI crawlers have read, and which engines have never looked.","operationId":"get_crawler_hits_api_v1_crawler_hits_get","parameters":[{"name":"days","in":"query","required":false,"schema":{"type":"integer","default":30,"title":"Days"}},{"name":"site","in":"query","required":false,"schema":{"type":"string","default":"","title":"Site"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/reports":{"get":{"summary":"List Reports","operationId":"list_reports_api_v1_reports_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/v1/reports/{report_id}":{"get":{"summary":"Get Report","operationId":"get_report_api_v1_reports__report_id__get","parameters":[{"name":"report_id","in":"path","required":true,"schema":{"type":"string","title":"Report Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/scans":{"post":{"summary":"Start Scan","description":"Start a scan for this workspace's brand, on the account's own plan.\n\nMirrors POST /scan/run: resolves the brand, enforces the monthly per-brand\nquota from the workspace plan, and assembles the ReportRequest from the\nplan's query/sentiment/sampling/engine entitlements. Attributed to the\nworkspace, not a user (there is no user behind an API key).","operationId":"start_scan_api_v1_scans_post","requestBody":{"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ScanCreate"}}},"required":true},"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/api/v1/summary":{"get":{"summary":"Summary","description":"Compact, normalized snapshot of the latest completed scan. Same shape the\nMCP get_visibility_report tool returns, so clients (WordPress admin widget,\nChrome extension popup) don't parse the full report JSON.","operationId":"summary_api_v1_summary_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/api/v1/llms-txt":{"get":{"summary":"Llms Txt","description":"The latest completed scan's generated llms.txt (llmstxt.org format), as\ntext/plain. The WordPress plugin fetches this and serves it at the site's\n/llms.txt web root, the one thing the WP REST publisher cannot do.","operationId":"llms_txt_api_v1_llms_txt_get","responses":{"200":{"description":"Successful Response","content":{"text/plain":{"schema":{"type":"string"}}}}}}},"/robots.txt":{"get":{"summary":"Robots","operationId":"robots_robots_txt_get","responses":{"200":{"description":"Successful Response","content":{"text/plain":{"schema":{"type":"string"}}}}}}},"/llms.txt":{"get":{"summary":"Llms Txt","operationId":"llms_txt_llms_txt_get","responses":{"200":{"description":"Successful Response","content":{"text/plain":{"schema":{"type":"string"}}}}}}},"/.well-known/mcp-registry-auth":{"get":{"summary":"Mcp Registry Auth","description":"Domain-ownership proof for publishing com.tofubofu/* to the official MCP\nRegistry (registry.modelcontextprotocol.io). The value is the PUBLIC half of\nan Ed25519 keypair; the private key lives only on Arnav's machine at\n~/.config/tofubofu/mcp-registry-key.pem (publish runbook: docs/mcp-listing.md).","operationId":"mcp_registry_auth__well_known_mcp_registry_auth_get","responses":{"200":{"description":"Successful Response","content":{"text/plain":{"schema":{"type":"string"}}}}}}},"/media/deliverable/{d_id}.svg":{"get":{"summary":"Deliverable Hero Svg","operationId":"deliverable_hero_svg_media_deliverable__d_id__svg_get","parameters":[{"name":"d_id","in":"path","required":true,"schema":{"type":"string","title":"D Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/media/deliverable/{d_id}-og.webp":{"get":{"summary":"Deliverable Og Webp","description":"The social card, which is the one image that carries the headline.","operationId":"deliverable_og_webp_media_deliverable__d_id__og_webp_get","parameters":[{"name":"d_id","in":"path","required":true,"schema":{"type":"string","title":"D Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/media/deliverable/{d_id}.webp":{"get":{"summary":"Deliverable Hero Webp","description":"The rendered article hero. Served for the in-app preview and as the\nfallback for a destination that cannot host a file of its own.\n\nRead straight off the volume rather than out of the database: the bytes are a\nfile, and `hero_image.filename_for` is what refuses an id that is really a\npath traversal.","operationId":"deliverable_hero_webp_media_deliverable__d_id__webp_get","parameters":[{"name":"d_id","in":"path","required":true,"schema":{"type":"string","title":"D Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/media/partner/{name}":{"get":{"summary":"Partner Logo Media","description":"A co-branding partner's own logo, served off the volume beside the hero\nimages and with the same long cache.\n\nTHE NAME IS THE WHOLE KEY AND IT IS CHECKED, NOT TRUSTED. It arrives as a\nURL path segment, and `partner_logo.load_bytes` refuses anything that is not\nthe exact shape we generate (workspace id, variant, content digest,\nextension), which is what stops a dot or a slash here being a path traversal\nwearing a logo's clothes.\n\nTHE DIGEST IN THE NAME IS WHY THE CACHE CAN BE LONG. Replacing a logo\nproduces a different filename and therefore a different URL, so no browser\nor CDN can keep serving the previous picture. This is deliberately public:\nthe image renders on anon-shareable report pages and inside link previews\nthat scrapers fetch with no session at all.","operationId":"partner_logo_media_media_partner__name__get","parameters":[{"name":"name","in":"path","required":true,"schema":{"type":"string","title":"Name"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/t.js":{"get":{"summary":"Tracking Tag","operationId":"tracking_tag_t_js_get","parameters":[{"name":"w","in":"query","required":false,"schema":{"type":"string","default":"","title":"W"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/t.gif":{"get":{"summary":"Tracking Beacon","operationId":"tracking_beacon_t_gif_get","parameters":[{"name":"w","in":"query","required":false,"schema":{"type":"string","default":"","title":"W"}},{"name":"e","in":"query","required":false,"schema":{"type":"string","default":"","title":"E"}},{"name":"p","in":"query","required":false,"schema":{"type":"string","default":"/","title":"P"}},{"name":"v","in":"query","required":false,"schema":{"type":"string","default":"referrer","title":"V"}},{"name":"h","in":"query","required":false,"schema":{"type":"string","default":"","title":"H"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/t-pv.gif":{"get":{"summary":"Pageview Beacon","operationId":"pageview_beacon_t_pv_gif_get","parameters":[{"name":"w","in":"query","required":false,"schema":{"type":"string","default":"","title":"W"}},{"name":"p","in":"query","required":false,"schema":{"type":"string","default":"/","title":"P"}},{"name":"r","in":"query","required":false,"schema":{"type":"string","default":"","title":"R"}},{"name":"h","in":"query","required":false,"schema":{"type":"string","default":"","title":"H"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/t-conv.js":{"get":{"summary":"Conversion Tag","operationId":"conversion_tag_t_conv_js_get","parameters":[{"name":"w","in":"query","required":false,"schema":{"type":"string","default":"","title":"W"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/t-conv.gif":{"get":{"summary":"Conversion Beacon","operationId":"conversion_beacon_t_conv_gif_get","parameters":[{"name":"w","in":"query","required":false,"schema":{"type":"string","default":"","title":"W"}},{"name":"e","in":"query","required":false,"schema":{"type":"string","default":"","title":"E"}},{"name":"p","in":"query","required":false,"schema":{"type":"string","default":"/","title":"P"}},{"name":"label","in":"query","required":false,"schema":{"type":"string","default":"conversion","title":"Label"}},{"name":"h","in":"query","required":false,"schema":{"type":"string","default":"","title":"H"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/sitemap.xml":{"get":{"summary":"Sitemap","operationId":"sitemap_sitemap_xml_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/":{"get":{"summary":"Home","operationId":"home__get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/audit":{"get":{"summary":"Audit","operationId":"audit_audit_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/how-it-works":{"get":{"summary":"How It Works","operationId":"how_it_works_how_it_works_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/pricing":{"get":{"summary":"Pricing","operationId":"pricing_pricing_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/integrations":{"get":{"summary":"Integrations Page","operationId":"integrations_page_integrations_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/features":{"get":{"summary":"Features Page","operationId":"features_page_features_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/docs":{"get":{"summary":"Docs Index","operationId":"docs_index_docs_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/docs/{slug}":{"get":{"summary":"Docs Detail","operationId":"docs_detail_docs__slug__get","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/developers":{"get":{"summary":"Developers Page","operationId":"developers_page_developers_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/privacy":{"get":{"summary":"Privacy Page","operationId":"privacy_page_privacy_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/terms":{"get":{"summary":"Terms Page","operationId":"terms_page_terms_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/about":{"get":{"summary":"About Page","operationId":"about_page_about_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/contact":{"get":{"summary":"Contact Page","description":"The scheduler's own page, and the site's first contact surface.\n\nWorth having for EEAT independently of booking: `/about` existed and named a\nfounder, while the only way to reach anyone was a `mailto:` in the footer.\nAn organisation with no contact page is a weaker entity to an engine reading\nthe site, and every listing directory asks for one.","operationId":"contact_page_contact_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/login":{"get":{"summary":"Login","operationId":"login_login_get","parameters":[{"name":"next","in":"query","required":false,"schema":{"type":"string","default":"","title":"Next"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/auth/verify":{"get":{"summary":"Auth Verify","description":"Magic-link landing: verify the token (this stamps the account verified),\nstart a session, and land wherever the link intended (the onboarding welcome\nemail points back at the report), defaulting to the app.","operationId":"auth_verify_auth_verify_get","parameters":[{"name":"token","in":"query","required":false,"schema":{"type":"string","default":"","title":"Token"}},{"name":"next","in":"query","required":false,"schema":{"type":"string","default":"","title":"Next"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/register":{"get":{"summary":"Register","operationId":"register_register_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/overview":{"get":{"summary":"Overview","operationId":"overview_overview_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/reports":{"get":{"summary":"Reports Page","operationId":"reports_page_reports_get","parameters":[{"name":"upgraded","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Upgraded"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/app":{"get":{"summary":"App Redirect","operationId":"app_redirect_app_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/dashboard":{"get":{"summary":"Dashboard Redirect","operationId":"dashboard_redirect_dashboard_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/monitor":{"get":{"summary":"Monitor Redirect","operationId":"monitor_redirect_monitor_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/analyst":{"get":{"summary":"Analyst Redirect","operationId":"analyst_redirect_analyst_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/foundation":{"get":{"summary":"Foundation Redirect","operationId":"foundation_redirect_foundation_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/narrative":{"get":{"summary":"Narrative Redirect","operationId":"narrative_redirect_narrative_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/queries":{"get":{"summary":"Handler","operationId":"handler_queries_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/shortlists":{"get":{"summary":"Handler","operationId":"handler_shortlists_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/competitors":{"get":{"summary":"Handler","operationId":"handler_competitors_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/content-engine":{"get":{"summary":"Handler","operationId":"handler_content_engine_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/fixes":{"get":{"summary":"Handler","operationId":"handler_fixes_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/traffic":{"get":{"summary":"Handler","operationId":"handler_traffic_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/scan":{"get":{"summary":"Scan","operationId":"scan_scan_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/settings":{"get":{"summary":"Settings","operationId":"settings_settings_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/website":{"get":{"summary":"Website Page","description":"This brand's website: publishing, tracking and scan cadence.\n\nNOT `/integrations`. That slug is a PUBLIC MARKETING PAGE (main.py:623),\ncarried in the sitemap at priority 0.8, in `llms.txt` and in the site footer.\nRegistering a second route on it silently shadowed this page behind the\nmarketing one, which a browser render caught and no test would have. The\nlabel matches the slug, per the rule set on 2026-07-22.\n\nSplit off Settings on 2026-08-25. Every surface here is keyed by site or\nbrand (`connections.site`, `google_connections.site`, `web_events.site_host`,\n`schedules.brand_id`), and Settings suppresses the brand switcher because it\nis account-scoped. Keeping them together meant the page configured a website\nthe user could not see, and a second site could not be connected at all.","operationId":"website_page_website_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/invite/{token}":{"get":{"summary":"Invite Landing","description":"Public: no login required to view. The invite is emailed as a plain\nlink (workspace_invites.token), same shape as the magic-link login.","operationId":"invite_landing_invite__token__get","parameters":[{"name":"token","in":"path","required":true,"schema":{"type":"string","title":"Token"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/admin":{"get":{"summary":"Admin Page","operationId":"admin_page_admin_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/admin/status":{"get":{"summary":"Admin Status Page","operationId":"admin_status_page_admin_status_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/tools":{"get":{"summary":"Tools Hub","operationId":"tools_hub_tools_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/tools/{slug}":{"get":{"summary":"Tool Page","operationId":"tool_page_tools__slug__get","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/industries":{"get":{"summary":"Industries Hub","operationId":"industries_hub_industries_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/for/{vertical}":{"get":{"summary":"Vertical Page","operationId":"vertical_page_for__vertical__get","parameters":[{"name":"vertical","in":"path","required":true,"schema":{"type":"string","title":"Vertical"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/solutions":{"get":{"summary":"Solutions Index","operationId":"solutions_index_solutions_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/solutions/{slug}":{"get":{"summary":"Solution Page","operationId":"solution_page_solutions__slug__get","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/engines":{"get":{"summary":"Engines Index","operationId":"engines_index_engines_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/engines/{engine}":{"get":{"summary":"Engine Page","operationId":"engine_page_engines__engine__get","parameters":[{"name":"engine","in":"path","required":true,"schema":{"type":"string","title":"Engine"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/leaderboard":{"get":{"summary":"Leaderboard Index","operationId":"leaderboard_index_leaderboard_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/leaderboard/{vertical}":{"get":{"summary":"Leaderboard Vertical","operationId":"leaderboard_vertical_leaderboard__vertical__get","parameters":[{"name":"vertical","in":"path","required":true,"schema":{"type":"string","title":"Vertical"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/aeo":{"get":{"summary":"Pillar Aeo","operationId":"pillar_aeo_aeo_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/geo":{"get":{"summary":"Pillar Geo","operationId":"pillar_geo_geo_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/ai-visibility":{"get":{"summary":"Pillar Ai Visibility","operationId":"pillar_ai_visibility_ai_visibility_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/tofu-mofu-bofu":{"get":{"summary":"Pillar Tofu Mofu Bofu","operationId":"pillar_tofu_mofu_bofu_tofu_mofu_bofu_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/research":{"get":{"summary":"Research Index","operationId":"research_index_research_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/research/msp-ai-visibility-2026":{"get":{"summary":"Research Msp Report 2026","operationId":"research_msp_report_2026_research_msp_ai_visibility_2026_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/research/{slug}":{"get":{"summary":"Research Detail","operationId":"research_detail_research__slug__get","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/report/{report_id}/cited-card.svg":{"get":{"summary":"Cited Card Svg","description":"Shareable proof card for the 'Cited' win, rendered from the report's\nstored celebration (scan_ingest writes insights_json['celebrations']).","operationId":"cited_card_svg_report__report_id__cited_card_svg_get","parameters":[{"name":"report_id","in":"path","required":true,"schema":{"type":"string","title":"Report Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/report/{report_id}/preview.png":{"get":{"summary":"Report Preview Png","description":"The link-preview image a scraper fetches for /report/{id}.\n\nKept deliberately cheap: one DB row, then a cached Pillow draw. A scraper\nthat times out caches the failure, so this must never do slow work.","operationId":"report_preview_png_report__report_id__preview_png_get","parameters":[{"name":"report_id","in":"path","required":true,"schema":{"type":"string","title":"Report Id"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/research/{slug}/badge/{company}.svg":{"get":{"summary":"Research Badge Svg","operationId":"research_badge_svg_research__slug__badge__company__svg_get","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}},{"name":"company","in":"path","required":true,"schema":{"type":"string","title":"Company"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/research/{slug}/badge/{company}":{"get":{"summary":"Research Badge Page","operationId":"research_badge_page_research__slug__badge__company__get","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}},{"name":"company","in":"path","required":true,"schema":{"type":"string","title":"Company"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/blog":{"get":{"summary":"Blog Index","operationId":"blog_index_blog_get","responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}}}}},"/blog/tofu-mofu-bofu-b2b-funnel-guide":{"get":{"summary":"Blog Funnel Guide Moved","operationId":"blog_funnel_guide_moved_blog_tofu_mofu_bofu_b2b_funnel_guide_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}}}}},"/blog/{slug}":{"get":{"summary":"Blog Post","operationId":"blog_post_blog__slug__get","parameters":[{"name":"slug","in":"path","required":true,"schema":{"type":"string","title":"Slug"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}},"/report/{report_id}":{"get":{"summary":"Report Page","operationId":"report_page_report__report_id__get","parameters":[{"name":"report_id","in":"path","required":true,"schema":{"type":"string","title":"Report Id"}}],"responses":{"200":{"description":"Successful Response","content":{"text/html":{"schema":{"type":"string"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}}}}}},"components":{"schemas":{"ActiveWorkspaceRequest":{"properties":{"workspace_id":{"type":"string","title":"Workspace Id"}},"type":"object","required":["workspace_id"],"title":"ActiveWorkspaceRequest"},"AddIdeaRequest":{"properties":{"topic":{"type":"string","title":"Topic","default":""},"url":{"type":"string","title":"Url","default":""}},"type":"object","title":"AddIdeaRequest"},"AdminCompedRequest":{"properties":{"comped":{"type":"boolean","title":"Comped"}},"type":"object","required":["comped"],"title":"AdminCompedRequest"},"AdminPlanRequest":{"properties":{"plan":{"type":"string","title":"Plan"}},"type":"object","required":["plan"],"title":"AdminPlanRequest"},"AnalystChatRequest":{"properties":{"messages":{"items":{},"type":"array","title":"Messages"},"page":{"type":"string","title":"Page","default":""}},"type":"object","required":["messages"],"title":"AnalystChatRequest"},"AssignScanRequest":{"properties":{"email":{"type":"string","title":"Email"},"stay_as_operator":{"type":"boolean","title":"Stay As Operator","default":true}},"type":"object","required":["email"],"title":"AssignScanRequest"},"AutonomyRequest":{"properties":{"level":{"type":"string","title":"Level"}},"type":"object","required":["level"],"title":"AutonomyRequest"},"Body_admin_upload_partner_logo_api_admin_workspace__workspace_id__partner_logo_post":{"properties":{"variant":{"type":"string","title":"Variant"},"file":{"type":"string","contentMediaType":"application/octet-stream","title":"File"}},"type":"object","required":["variant","file"],"title":"Body_admin_upload_partner_logo_api_admin_workspace__workspace_id__partner_logo_post"},"BookingRequest":{"properties":{"surface":{"type":"string","title":"Surface","default":""},"source":{"type":"string","title":"Source","default":"web"},"path":{"type":"string","title":"Path","default":""},"referrer":{"type":"string","title":"Referrer","default":""},"landing_path":{"type":"string","title":"Landing Path","default":""},"utm":{"additionalProperties":true,"type":"object","title":"Utm","default":{}},"ai_engine":{"type":"string","title":"Ai Engine","default":""}},"type":"object","title":"BookingRequest","description":"One meeting booked, posted by `booking.js` off Calendly's own\n`calendly.event_scheduled` message. Carries no name and no email: those stay\nwith Calendly, and what this records is which surface and which origin\nproduced a meeting."},"BrandColorsRequest":{"properties":{"colors":{"items":{"type":"string"},"type":"array","title":"Colors","default":[]}},"type":"object","title":"BrandColorsRequest"},"CheckoutRequest":{"properties":{"plan":{"type":"string","title":"Plan"}},"type":"object","required":["plan"],"title":"CheckoutRequest"},"CompetitorIn":{"properties":{"name":{"type":"string","title":"Name"},"website":{"type":"string","title":"Website","default":""}},"type":"object","required":["name"],"title":"CompetitorIn"},"ConnectRequest":{"properties":{"provider":{"type":"string","title":"Provider"},"credentials":{"additionalProperties":true,"type":"object","title":"Credentials","default":{}},"label":{"type":"string","title":"Label","default":""},"site":{"type":"string","title":"Site","default":""}},"type":"object","required":["provider"],"title":"ConnectRequest"},"ContentDedupeRequest":{"properties":{"expected_delete":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Expected Delete"},"workspace_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Workspace Id"}},"type":"object","title":"ContentDedupeRequest"},"CrawlerHit":{"properties":{"bot":{"type":"string","title":"Bot"},"path":{"type":"string","title":"Path","default":"/"},"day":{"type":"string","title":"Day","default":""},"hits":{"type":"integer","title":"Hits","default":1},"status":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Status"}},"type":"object","required":["bot"],"title":"CrawlerHit"},"CrawlerHitsRequest":{"properties":{"hits":{"items":{"$ref":"#/components/schemas/CrawlerHit"},"type":"array","title":"Hits","default":[]},"site":{"type":"string","title":"Site","default":""}},"type":"object","title":"CrawlerHitsRequest"},"CustomQueryIn":{"properties":{"text":{"type":"string","title":"Text"}},"type":"object","required":["text"],"title":"CustomQueryIn"},"DeliverableBodyRequest":{"properties":{"body_markdown":{"type":"string","title":"Body Markdown"}},"type":"object","required":["body_markdown"],"title":"DeliverableBodyRequest"},"DismissDeliverableRequest":{"properties":{"reason":{"type":"string","title":"Reason","default":""}},"type":"object","title":"DismissDeliverableRequest"},"DripConfigRequest":{"properties":{"delay_hours":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Delay Hours"},"subject":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Subject"},"body":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Body"}},"type":"object","title":"DripConfigRequest"},"FreeScanRequest":{"properties":{"brand_name":{"type":"string","title":"Brand Name"},"domain":{"type":"string","title":"Domain"},"industry":{"type":"string","title":"Industry","default":""},"description":{"type":"string","title":"Description","default":""},"competitors":{"items":{"type":"string"},"type":"array","title":"Competitors","default":[]},"keywords":{"items":{"type":"string"},"type":"array","title":"Keywords","default":[]},"email":{"type":"string","title":"Email"},"geo_scope":{"type":"string","title":"Geo Scope","default":""},"locations":{"items":{"type":"string"},"type":"array","title":"Locations","default":[]},"intake":{"additionalProperties":true,"type":"object","title":"Intake","default":{}},"source":{"type":"string","title":"Source","default":""},"referrer":{"type":"string","title":"Referrer","default":""},"landing_path":{"type":"string","title":"Landing Path","default":""},"utm":{"additionalProperties":{"type":"string"},"type":"object","title":"Utm","default":{}}},"type":"object","required":["brand_name","domain","email"],"title":"FreeScanRequest"},"GitHubDirsRequest":{"properties":{"installation_id":{"type":"string","title":"Installation Id"},"repo":{"type":"string","title":"Repo"},"base_branch":{"type":"string","title":"Base Branch","default":""},"receipt":{"type":"string","title":"Receipt","default":""}},"type":"object","required":["installation_id","repo"],"title":"GitHubDirsRequest"},"GitHubReposRequest":{"properties":{"installation_id":{"type":"string","title":"Installation Id"},"receipt":{"type":"string","title":"Receipt","default":""}},"type":"object","required":["installation_id"],"title":"GitHubReposRequest"},"GoalRequest":{"properties":{"goal":{"type":"integer","title":"Goal"}},"type":"object","required":["goal"],"title":"GoalRequest"},"GoogleSelectRequest":{"properties":{"gsc_site_url":{"type":"string","title":"Gsc Site Url","default":""},"ga4_property_id":{"type":"string","title":"Ga4 Property Id","default":""},"ga4_property_name":{"type":"string","title":"Ga4 Property Name","default":""}},"type":"object","title":"GoogleSelectRequest"},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"HandOverBrandRequest":{"properties":{"email":{"type":"string","title":"Email"},"plan":{"type":"string","title":"Plan"},"operator_can_publish":{"type":"boolean","title":"Operator Can Publish"},"colleagues":{"items":{"type":"string"},"type":"array","title":"Colleagues","default":[]},"stay_as_operator":{"type":"boolean","title":"Stay As Operator","default":true},"retired_deliverables":{"type":"string","title":"Retired Deliverables","default":"migrate"},"schedule":{"type":"boolean","title":"Schedule","default":true},"rescan":{"type":"boolean","title":"Rescan","default":true}},"type":"object","required":["email","plan","operator_can_publish"],"title":"HandOverBrandRequest"},"HeadToHeadIn":{"properties":{"ids":{"items":{"type":"string"},"type":"array","title":"Ids","default":[]}},"type":"object","title":"HeadToHeadIn"},"IndexSubscribeRequest":{"properties":{"email":{"type":"string","title":"Email"},"slug":{"type":"string","title":"Slug","default":""}},"type":"object","required":["email"],"title":"IndexSubscribeRequest"},"KeyCreateRequest":{"properties":{"label":{"type":"string","title":"Label","default":"API key"}},"type":"object","title":"KeyCreateRequest"},"KeywordCloudRequest":{"properties":{"brand_name":{"type":"string","title":"Brand Name","default":""},"domain":{"type":"string","title":"Domain","default":""},"industry":{"type":"string","title":"Industry","default":""},"geo_scope":{"type":"string","title":"Geo Scope","default":"global"},"locations":{"items":{"type":"string"},"type":"array","title":"Locations","default":[]},"intake":{"additionalProperties":true,"type":"object","title":"Intake","default":{}},"brand_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Brand Id"},"competitors":{"items":{"type":"string"},"type":"array","title":"Competitors","default":[]}},"type":"object","title":"KeywordCloudRequest","description":"What the keyword step needs to pre-fill itself from two sources."},"LinkRequest":{"properties":{"email":{"type":"string","title":"Email"},"next":{"type":"string","title":"Next","default":""}},"type":"object","required":["email"],"title":"LinkRequest"},"LoginRequest":{"properties":{"email":{"type":"string","title":"Email"},"password":{"type":"string","title":"Password"}},"type":"object","required":["email","password"],"title":"LoginRequest"},"NonVendorRemovalIn":{"properties":{"texts":{"items":{"type":"string"},"type":"array","title":"Texts"}},"type":"object","required":["texts"],"title":"NonVendorRemovalIn"},"OnboardStartRequest":{"properties":{"email":{"type":"string","title":"Email"},"brand_name":{"type":"string","title":"Brand Name","default":""},"domain":{"type":"string","title":"Domain"},"industry":{"type":"string","title":"Industry","default":""},"description":{"type":"string","title":"Description","default":""},"competitors":{"items":{"type":"string"},"type":"array","title":"Competitors","default":[]},"keywords":{"items":{"type":"string"},"type":"array","title":"Keywords","default":[]},"geo_scope":{"type":"string","title":"Geo Scope","default":""},"locations":{"items":{"type":"string"},"type":"array","title":"Locations","default":[]},"geo_confirmed":{"type":"boolean","title":"Geo Confirmed","default":false},"locations_confirmed":{"type":"boolean","title":"Locations Confirmed","default":false},"segment":{"items":{"type":"string"},"type":"array","title":"Segment","default":[]},"tech_confirmed":{"type":"string","title":"Tech Confirmed","default":""},"referrer":{"type":"string","title":"Referrer","default":""},"landing_path":{"type":"string","title":"Landing Path","default":""},"utm":{"additionalProperties":{"type":"string"},"type":"object","title":"Utm","default":{}}},"type":"object","required":["email","domain"],"title":"OnboardStartRequest"},"OperatorPublishRequest":{"properties":{"enabled":{"type":"boolean","title":"Enabled"}},"type":"object","required":["enabled"],"title":"OperatorPublishRequest"},"PasswordRequest":{"properties":{"current_password":{"type":"string","title":"Current Password"},"new_password":{"type":"string","title":"New Password"}},"type":"object","required":["current_password","new_password"],"title":"PasswordRequest"},"PathPrefixRequest":{"properties":{"brand_id":{"type":"string","title":"Brand Id"},"path_prefix":{"type":"string","title":"Path Prefix","default":""}},"type":"object","required":["brand_id"],"title":"PathPrefixRequest"},"PlanConfigRequest":{"properties":{"scans_per_month":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Scans Per Month"},"scan_frequency":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Scan Frequency"},"brands_allowed":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Brands Allowed"},"queries":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Queries"},"sentiment_enabled":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Sentiment Enabled"},"sampling_depth":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Sampling Depth"},"engines":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Engines"},"articles_per_month":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Articles Per Month"},"youtube_scripts":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Youtube Scripts"},"newsletters":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Newsletters"},"priority_support":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Priority Support"},"feature_recommendations":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Feature Recommendations"},"feature_scheduling":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Feature Scheduling"},"feature_alerts":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Feature Alerts"},"feature_content_production":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Feature Content Production"},"feature_managed_fixes":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Feature Managed Fixes"}},"type":"object","title":"PlanConfigRequest"},"ProfileRequest":{"properties":{"name":{"type":"string","title":"Name"}},"type":"object","required":["name"],"title":"ProfileRequest"},"ProspectResearchRequest":{"properties":{"domain":{"type":"string","title":"Domain"}},"type":"object","required":["domain"],"title":"ProspectResearchRequest"},"ProspectScanRequest":{"properties":{"domain":{"type":"string","title":"Domain"},"tier":{"type":"string","title":"Tier","default":"free"},"note":{"type":"string","title":"Note","default":""},"confirmed":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Confirmed"},"segments":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Segments"}},"type":"object","required":["domain"],"title":"ProspectScanRequest"},"ProviderFieldMapRequest":{"properties":{"field_map":{"additionalProperties":true,"type":"object","title":"Field Map","default":{}}},"type":"object","title":"ProviderFieldMapRequest"},"ProviderFieldsRequest":{"properties":{"draft":{"additionalProperties":true,"type":"object","title":"Draft","default":{}},"token":{"type":"string","title":"Token","default":""},"collection_id":{"type":"string","title":"Collection Id","default":""}},"type":"object","title":"ProviderFieldsRequest"},"ProviderRelearnRequest":{"properties":{"apply":{"type":"boolean","title":"Apply","default":false}},"type":"object","title":"ProviderRelearnRequest"},"PublishDeliverableRequest":{"properties":{"provider":{"type":"string","title":"Provider"},"status":{"type":"string","title":"Status","default":"draft"},"republish":{"type":"boolean","title":"Republish","default":false}},"type":"object","required":["provider"],"title":"PublishDeliverableRequest"},"QueryPlan":{"properties":{"category_tofu":{"items":{"type":"string"},"type":"array","title":"Category Tofu","default":[]},"category_bofu":{"items":{"type":"string"},"type":"array","title":"Category Bofu","default":[]},"longtail_info":{"items":{"type":"string"},"type":"array","title":"Longtail Info","default":[]},"longtail_tool":{"items":{"type":"string"},"type":"array","title":"Longtail Tool","default":[]},"brand_sentiment":{"items":{"type":"string"},"type":"array","title":"Brand Sentiment","default":[]}},"type":"object","title":"QueryPlan"},"QueryPlanRequest":{"properties":{"brand_name":{"type":"string","title":"Brand Name"},"domain":{"type":"string","title":"Domain"},"industry":{"type":"string","title":"Industry","default":""},"description":{"type":"string","title":"Description","default":""},"competitors":{"items":{"type":"string"},"type":"array","title":"Competitors","default":[]}},"type":"object","required":["brand_name","domain"],"title":"QueryPlanRequest"},"QuestionPreviewRequest":{"properties":{"brand_name":{"type":"string","title":"Brand Name","default":""},"industry":{"type":"string","title":"Industry","default":""},"description":{"type":"string","title":"Description","default":""},"competitors":{"items":{"type":"string"},"type":"array","title":"Competitors","default":[]},"geo_scope":{"type":"string","title":"Geo Scope","default":"global"},"locations":{"items":{"type":"string"},"type":"array","title":"Locations","default":[]},"intake":{"additionalProperties":true,"type":"object","title":"Intake","default":{}}},"type":"object","title":"QuestionPreviewRequest"},"QuestionSetRequest":{"properties":{"brand_name":{"type":"string","title":"Brand Name","default":""},"domain":{"type":"string","title":"Domain","default":""},"industry":{"type":"string","title":"Industry","default":""},"description":{"type":"string","title":"Description","default":""},"competitors":{"items":{"type":"string"},"type":"array","title":"Competitors","default":[]},"geo_scope":{"type":"string","title":"Geo Scope","default":"global"},"locations":{"items":{"type":"string"},"type":"array","title":"Locations","default":[]},"intake":{"additionalProperties":true,"type":"object","title":"Intake","default":{}},"existing":{"items":{"type":"string"},"type":"array","title":"Existing","default":[]},"brand_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Brand Id"},"path_prefix":{"type":"string","title":"Path Prefix","default":""}},"type":"object","title":"QuestionSetRequest","description":"What the Questions step needs to fill the plan's budget.\n\n`existing` is what is already in the box, which is the brand's stored\nquestions plus anything typed. It is sent so the server can fill AROUND it\nrather than propose a replacement: see the docstring on the endpoint."},"RegisterRequest":{"properties":{"name":{"type":"string","title":"Name"},"email":{"type":"string","title":"Email"},"password":{"type":"string","title":"Password"}},"type":"object","required":["name","email","password"],"title":"RegisterRequest"},"ReportFeedbackRequest":{"properties":{"kind":{"type":"string","title":"Kind"},"target":{"type":"string","title":"Target","default":""},"value":{"type":"string","title":"Value","default":""}},"type":"object","required":["kind"],"title":"ReportFeedbackRequest"},"ReportRequest":{"properties":{"brand_name":{"type":"string","title":"Brand Name"},"domain":{"type":"string","title":"Domain"},"path_prefix":{"type":"string","title":"Path Prefix","default":""},"parent_brand":{"type":"string","title":"Parent Brand","default":""},"industry":{"type":"string","title":"Industry"},"description":{"type":"string","title":"Description","default":""},"competitors":{"items":{"type":"string"},"type":"array","title":"Competitors"},"h2h_rivals":{"items":{"type":"string"},"type":"array","title":"H2H Rivals","default":[]},"keywords":{"items":{"type":"string"},"type":"array","title":"Keywords","default":[]},"products":{"items":{"type":"string"},"type":"array","title":"Products","default":[]},"geo_scope":{"type":"string","title":"Geo Scope","default":"global"},"locations":{"items":{"type":"string"},"type":"array","title":"Locations","default":[]},"scan_tier":{"type":"string","title":"Scan Tier","default":"full"},"bofu_count":{"type":"integer","title":"Bofu Count","default":5},"sentiment_enabled":{"type":"boolean","title":"Sentiment Enabled","default":true},"sampling_depth":{"type":"string","title":"Sampling Depth","default":"adaptive"},"engines":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Engines"},"notify_email":{"type":"string","title":"Notify Email","default":""},"query_plan":{"anyOf":[{"$ref":"#/components/schemas/QueryPlan"},{"type":"null"}]},"custom_queries":{"items":{"type":"string"},"type":"array","title":"Custom Queries","default":[]},"questions_curated":{"type":"boolean","title":"Questions Curated","default":false},"intake":{"additionalProperties":true,"type":"object","title":"Intake","default":{}}},"type":"object","required":["brand_name","domain","industry","competitors"],"title":"ReportRequest"},"ResearchRequest":{"properties":{"url":{"type":"string","title":"Url"}},"type":"object","required":["url"],"title":"ResearchRequest"},"ReviseDeliverableRequest":{"properties":{"instruction":{"type":"string","title":"Instruction","default":""},"url":{"type":"string","title":"Url","default":""}},"type":"object","title":"ReviseDeliverableRequest"},"ScanCreate":{"properties":{"geo_scope":{"type":"string","title":"Geo Scope","description":"Where do you sell? Getting this wrong skews the whole report: a national firm scored on one city's queries looks invisible, and a local one scored nationally looks unwinnable. One of: global (Anywhere); national (Across one country); national_local (National, with a local angle); local (My own city or region). Shapes every buying query. This is the single highest-leverage answer here. Omit it rather than guessing: an omitted answer is recorded as unknown, a guessed one is indistinguishable from a real answer.","default":""},"locations":{"items":{"type":"string"},"type":"array","title":"Locations","description":"Which places? The country you sell across, or the cities you are bound to. Shapes which geography goes into a localized buying query. Omit it rather than guessing: an omitted answer is recorded as unknown, a guessed one is indistinguishable from a real answer."},"competitors":{"items":{"type":"string"},"type":"array","title":"Competitors","description":"Who would a buyer otherwise pick? Companies the user says a buyer would pick instead of them. We track whoever the engines name either way, so this is the user's own view rather than the whole comparison set. Shapes the comparison set, the mismatch between who you name and who the engines do, and which of those two a given finding is about. Omit it rather than guessing: an omitted answer is recorded as unknown, a guessed one is indistinguishable from a real answer."},"buyer_questions":{"items":{"type":"string"},"type":"array","title":"Buyer Questions","description":"Queries your buyers ask. Queries this company's buyers actually ask. They are put to the engines verbatim and tracked scan over scan, which is what makes a trend line mean anything. Shapes the queries themselves, pinned ahead of the generated ones. Omit it rather than guessing: an omitted answer is recorded as unknown, a guessed one is indistinguishable from a real answer."},"capacity":{"type":"string","title":"Capacity","description":"Who will do the fixes? We size and sequence the plan to match. A solo founder does not get a list built for a five-person team. One of: solo (Just me); one_marketer (One marketer); small_team (A small team, 2 to 4); full_team (A full team, 5 or more). Shapes the fix plan, which is generated in the tail after the engines answer. Omit it rather than guessing: an omitted answer is recorded as unknown, a guessed one is indistinguishable from a real answer.","default":""},"domain":{"type":"string","title":"Domain","default":""},"brand_name":{"type":"string","title":"Brand Name","default":""},"brand_id":{"type":"string","title":"Brand Id","default":""},"industry":{"type":"string","title":"Industry","default":""},"description":{"type":"string","title":"Description","default":""},"source":{"type":"string","title":"Source","default":""}},"type":"object","title":"ScanCreate"},"ScanIntakeRequest":{"properties":{"intake":{"additionalProperties":true,"type":"object","title":"Intake","default":{}}},"type":"object","title":"ScanIntakeRequest","description":"An intake answer given while a scan is running."},"ScanRunRequest":{"properties":{"brand_name":{"type":"string","title":"Brand Name"},"domain":{"type":"string","title":"Domain"},"industry":{"type":"string","title":"Industry","default":""},"description":{"type":"string","title":"Description","default":""},"competitors":{"items":{"type":"string"},"type":"array","title":"Competitors","default":[]},"keywords":{"items":{"type":"string"},"type":"array","title":"Keywords","default":[]},"goals":{"type":"string","title":"Goals","default":""},"geo_scope":{"type":"string","title":"Geo Scope","default":"global"},"locations":{"items":{"type":"string"},"type":"array","title":"Locations","default":[]},"capacity":{"type":"string","title":"Capacity","default":"solo"},"path_prefix":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Path Prefix"},"parent_brand":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Parent Brand"},"brand_id":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Brand Id"},"questions_accepted":{"type":"boolean","title":"Questions Accepted","default":false},"intake":{"additionalProperties":true,"type":"object","title":"Intake","default":{}}},"type":"object","required":["brand_name","domain"],"title":"ScanRunRequest"},"ScheduleDeliverableRequest":{"properties":{"date":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Date"}},"type":"object","title":"ScheduleDeliverableRequest"},"ScheduleRequest":{"properties":{"report_id":{"type":"string","title":"Report Id"},"frequency":{"type":"string","title":"Frequency"}},"type":"object","required":["report_id","frequency"],"title":"ScheduleRequest"},"SchedulesBackfillRequest":{"properties":{"expected_create":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Expected Create"},"plan":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Plan"}},"type":"object","title":"SchedulesBackfillRequest"},"SiteFileRequest":{"properties":{"name":{"type":"string","title":"Name","default":"llms_txt"}},"type":"object","title":"SiteFileRequest","description":"Which allowlisted static file to open a pull request for.\n\nA NAME, NEVER A PATH. `github.SITE_FILES` is the whole vocabulary and the\nadapter refuses anything not in it before a token is minted; this model\nexists so the wire format cannot carry a path either."},"StatusRequest":{"properties":{"status":{"type":"string","title":"Status"}},"type":"object","required":["status"],"title":"StatusRequest"},"TeamInviteRequest":{"properties":{"email":{"type":"string","title":"Email"},"role":{"type":"string","title":"Role","default":"member"}},"type":"object","required":["email"],"title":"TeamInviteRequest"},"TeamJoinRequest":{"properties":{"workspace_id":{"type":"string","title":"Workspace Id"}},"type":"object","required":["workspace_id"],"title":"TeamJoinRequest"},"ToolRequest":{"properties":{"domain":{"type":"string","title":"Domain"}},"type":"object","required":["domain"],"title":"ToolRequest"},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"},"input":{"title":"Input"},"ctx":{"type":"object","title":"Context"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"},"VerifyCodeRequest":{"properties":{"email":{"type":"string","title":"Email"},"code":{"type":"string","title":"Code"}},"type":"object","required":["email","code"],"title":"VerifyCodeRequest"},"WebflowCollectionsRequest":{"properties":{"token":{"type":"string","title":"Token"},"site_id":{"type":"string","title":"Site Id"}},"type":"object","required":["token","site_id"],"title":"WebflowCollectionsRequest"},"WebflowSitesRequest":{"properties":{"token":{"type":"string","title":"Token"}},"type":"object","required":["token"],"title":"WebflowSitesRequest"},"WebhookSetRequest":{"properties":{"url":{"type":"string","title":"Url"}},"type":"object","required":["url"],"title":"WebhookSetRequest"},"WorkspaceNameRequest":{"properties":{"name":{"type":"string","title":"Name"}},"type":"object","required":["name"],"title":"WorkspaceNameRequest"}}}}