{"openapi":"3.1.0","info":{"title":"DevTune API","version":"2.0.0","description":"API for programmatic access to your AI visibility data, webhook subscriptions, and automation workflows. Use this API to integrate DevTune data into CI/CD pipelines, BI tools, AI agents, and operational systems."},"servers":[{"url":"https://devtune.ai/api/v2","description":"Production"}],"security":[{"bearerAuth":[]}],"tags":[{"name":"Connection and usage","description":"The project a credential is bound to, its scopes and features, usage, and code-mode receipts."},{"name":"Setup","description":"Brands, domains, brand terms, platforms, content sources and pausing a project."},{"name":"Prompts and topics","description":"Tracked prompts, their topics, and prompt-set analysis."},{"name":"Visibility","description":"Visibility metrics, AI answers, sentiment, competitive position and the project timeline."},{"name":"Citations","description":"Citation statistics, the most-cited domains and pages, and cited lookalike sites."},{"name":"Positioning","description":"How well AI answers match your positioning statements."},{"name":"Accuracy","description":"Claims AI answers make about your brand, and contradictions."},{"name":"Content gaps","description":"Topics, sources and brands where competitors are cited."},{"name":"AI Traffic","description":"AI referrals, answer fetches, crawls, 404 Demand, Direct lift and Search Console totals."},{"name":"Adoption","description":"npm downloads and GitHub stars for project domains."},{"name":"Audit and pages","description":"Audit findings, Google eligibility, page metrics, and locked pages."},{"name":"Actions","description":"Suggested and accepted work, briefs, drafts and pull requests."},{"name":"Results","description":"Content changes, measured results and what works."},{"name":"Agents","description":"Managed agents, their schedules and runs."},{"name":"Dex","description":"Private Dex conversations, specialist handoffs and memory."},{"name":"Agent Picks","description":"Agent Picks setup, tests, schedules and results."},{"name":"Library and Knowledge","description":"Library items and the Knowledge Profile."},{"name":"Webhooks","description":"Webhook subscriptions and signing secrets."},{"name":"Agent registration","description":"Setup drafts for an unclaimed agent registration."}],"components":{"securitySchemes":{"oauthBearer":{"type":"http","scheme":"bearer","description":"Project-granted OAuth access token from the authorization server advertised at /.well-known/oauth-protected-resource/api/v2. Uses the same browser consent and current project permissions as MCP. Only operations explicitly listing oauthBearer accept these tokens."},"agentBearer":{"type":"http","scheme":"bearer","description":"Access token from an unclaimed agent registration (dtk_agent_ prefix), holding only setup.draft. Register at /agent/identity and exchange the identity assertion at /oauth2/token, as /auth.md describes. Only operations listing agentBearer accept it. Once a person claims the registration, its tokens act as the claimed API key under bearerAuth instead."},"bearerAuth":{"type":"http","scheme":"bearer","description":"API key with dtk_live_ prefix. Obtain it from API Keys in the account sidebar. New keys start with all supported scopes selected for the chosen project, and you can narrow them to specific read/write scopes as needed."}},"schemas":{"TrackedTopicsPage":{"type":"object","additionalProperties":false,"required":["items","includeInactive","pageSize","hasMore","nextCursor","pagination"],"properties":{"pagination":{"$ref":"#/components/schemas/ListPagination"},"items":{"type":"array","maxItems":100,"description":"The project’s topics, ordered by id. Switched-off topics only with includeInactive; deleted topics never.","items":{"type":"object","additionalProperties":false,"required":["id","name","description","isActive","branded","createdAt","updatedAt"],"properties":{"id":{"type":"string","format":"uuid","description":"Pass as topicId to listPrompts, getVisibilitySummary or getCompetitivePosition."},"name":{"type":"string"},"description":{"type":["string","null"],"description":"The topic description. Null or empty when it has none."},"isActive":{"type":"boolean","description":"Whether the topic is switched on."},"branded":{"type":"boolean","description":"True when every live prompt in the topic names the project's own brand. Visibility reads with promptScope=unbranded leave these topics out."},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}}},"includeInactive":{"type":"boolean","description":"Whether switched-off topics are listed, as requested."},"pageSize":{"type":"integer","minimum":1,"maximum":100,"description":"Rows per page."},"hasMore":{"type":"boolean","description":"More rows follow this page."},"nextCursor":{"type":["string","null"],"description":"Pass back as cursor with includeInactive unchanged; null on the last page."}}},"TrackedPromptsPage":{"type":"object","additionalProperties":false,"required":["items","topicId","includeInactive","pageSize","hasMore","nextCursor","pagination"],"properties":{"pagination":{"$ref":"#/components/schemas/ListPagination"},"items":{"type":"array","maxItems":100,"description":"Tracked prompts, ordered by id: by default only prompts that are on under a topic that is on. Deleted prompts are never listed.","items":{"type":"object","additionalProperties":false,"required":["id","promptText","topicId","topicName","isActive","topicActive","namesBrand","pack","disabledReason","disabledAt","createdAt","updatedAt"],"properties":{"id":{"type":"string","format":"uuid","description":"Pass as promptId to createPromptAction, listPromptActions or listVisibilityResponseHistory."},"promptText":{"type":"string","description":"The exact tracked question."},"topicId":{"type":"string","format":"uuid"},"topicName":{"type":"string"},"isActive":{"type":"boolean","description":"The prompt's own switch."},"topicActive":{"type":"boolean","description":"Whether the prompt’s topic is switched on."},"namesBrand":{"type":"boolean","description":"True when the prompt text names the project's own brand or one of its brand terms, retired ones included, as a whole word in any letter case."},"pack":{"type":["string","null"],"enum":["navigational",null],"description":"The fixed prompt pack the prompt belongs to: navigational for the download, install, sign-in and pricing questions that show which sites AI answers send buyers to. Pack prompts do not count toward the prompt allowance, and a project keeps at most six of them on. Null for every other prompt."},"disabledReason":{"type":["string","null"],"enum":["manual","plan_limit","quota_exceeded",null],"description":"Why the prompt was switched off: manual (by a person), plan_limit (the plan now allows fewer active prompts) or quota_exceeded (the prompt limit was reached). Null when it is switched on, or when it was deleted while on."},"disabledAt":{"type":["string","null"],"format":"date-time","description":"When the prompt was switched off. Null when it is switched on, when it was deleted while on, or when no switch-off time was recorded."},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}}},"topicId":{"type":["string","null"],"format":"uuid","description":"The topicId filter, as requested. Null when the list covers every topic."},"includeInactive":{"type":"boolean","description":"Whether switched-off prompts, and prompts under a switched-off topic, are listed, as requested."},"pageSize":{"type":"integer","minimum":1,"maximum":100,"description":"Rows per page."},"hasMore":{"type":"boolean","description":"More rows follow this page."},"nextCursor":{"type":["string","null"],"description":"Pass back as cursor with topicId and includeInactive unchanged; null on the last page."}}},"TrackedPromptRecord":{"type":"object","additionalProperties":false,"description":"The tracked prompt after the write.","required":["id","promptText","topicId","isActive","namesBrand","pack","disabledReason","disabledAt","deletedAt","createdAt","updatedAt"],"properties":{"id":{"type":"string","format":"uuid"},"promptText":{"type":"string","description":"The exact tracked question."},"topicId":{"type":"string","format":"uuid"},"isActive":{"type":"boolean","description":"The prompt's own switch."},"namesBrand":{"type":"boolean","description":"True when the prompt text names the project's own brand or one of its brand terms, retired ones included, as a whole word in any letter case."},"pack":{"type":["string","null"],"enum":["navigational",null],"description":"The fixed prompt pack the prompt belongs to: navigational for the download, install, sign-in and pricing questions that show which sites AI answers send buyers to. Pack prompts do not count toward the prompt allowance, and a project keeps at most six of them on. Null for every other prompt."},"disabledReason":{"type":["string","null"],"enum":["manual","plan_limit","quota_exceeded",null],"description":"Why the prompt was switched off: manual (by a person), plan_limit (the plan now allows fewer active prompts) or quota_exceeded (the prompt limit was reached). Null when it is switched on, or when it was deleted while on."},"disabledAt":{"type":["string","null"],"format":"date-time","description":"When the prompt was switched off. Null when it is switched on, when it was deleted while on, or when no switch-off time was recorded."},"deletedAt":{"type":["string","null"],"format":"date-time","description":"When the prompt was deleted; null while it is tracked. Deleted prompts keep their history and can be restored."},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"TrackedPromptWrite":{"type":"object","additionalProperties":false,"required":["prompt"],"properties":{"prompt":{"$ref":"#/components/schemas/TrackedPromptRecord"}}},"TrackedPromptDeletion":{"type":"object","additionalProperties":false,"required":["prompt","requestId","activeSlotsFreed"],"properties":{"prompt":{"$ref":"#/components/schemas/TrackedPromptRecord"},"requestId":{"type":"string","format":"uuid","description":"Send it again to get this result back instead of deleting again."},"activeSlotsFreed":{"type":"integer","minimum":0,"description":"Prompt allowance slots this deletion freed."}}},"TrackedPromptRestore":{"type":"object","additionalProperties":false,"required":["prompt","requestId","restored","alreadyRestored","topicRestored"],"properties":{"prompt":{"$ref":"#/components/schemas/TrackedPromptRecord"},"requestId":{"type":"string","format":"uuid","description":"Send it again to get this result back instead of restoring again."},"restored":{"type":"boolean","description":"Whether this request brought the prompt back."},"alreadyRestored":{"type":"boolean","description":"The prompt was not deleted, so nothing changed."},"topicRestored":{"type":"boolean","description":"Its deleted topic came back with it."}}},"TrackedTopicRecord":{"type":"object","additionalProperties":false,"description":"The topic after the write.","required":["id","name","description","isActive","branded","createdAt","updatedAt"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"description":{"type":["string","null"],"description":"The topic description. Null or empty when it has none."},"isActive":{"type":"boolean","description":"Whether the topic is switched on."},"branded":{"type":"boolean","description":"True when every live prompt in the topic names the project's own brand. Visibility reads with promptScope=unbranded leave these topics out."},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"TrackedTopicWrite":{"type":"object","additionalProperties":false,"required":["topic"],"properties":{"topic":{"$ref":"#/components/schemas/TrackedTopicRecord"}}},"TrackedTopicDeletion":{"type":"object","additionalProperties":false,"required":["topicId","deleted","deletedPromptCount","activeSlotsFreed"],"properties":{"topicId":{"type":"string","format":"uuid"},"deleted":{"type":"boolean","const":true},"deletedPromptCount":{"type":"integer","minimum":0,"description":"Prompts deleted with the topic."},"activeSlotsFreed":{"type":"integer","minimum":0,"description":"Prompt allowance slots the deletion freed."}}},"SetupBrand":{"type":"object","additionalProperties":false,"description":"A brand the project tracks: its own or a competitor.","required":["id","name","brandType","archivedAt","createdAt","updatedAt"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"brandType":{"type":"string","enum":["primary","competitor"],"description":"primary for the project's own brand, competitor for a rival."},"archivedAt":{"type":["string","null"],"format":"date-time","description":"When the brand was archived; null while it is tracked. An archived brand keeps its recorded history and can be restored."},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"SetupBrandsPage":{"type":"object","additionalProperties":false,"required":["items","includeArchived","pageSize","hasMore","nextCursor","pagination"],"properties":{"pagination":{"$ref":"#/components/schemas/ListPagination"},"items":{"type":"array","maxItems":100,"description":"The project’s brands, sorted by id. Archived brands only with includeArchived.","items":{"$ref":"#/components/schemas/SetupBrand"}},"includeArchived":{"type":"boolean","description":"Whether archived brands are listed, as requested."},"pageSize":{"type":"integer","minimum":1,"maximum":100,"description":"Rows per page."},"hasMore":{"type":"boolean","description":"More rows follow this page."},"nextCursor":{"type":["string","null"],"description":"Pass as cursor to read the next page. Null on the last page."}}},"SetupBrandWrite":{"type":"object","additionalProperties":false,"required":["brand"],"description":"The brand after the write.","properties":{"brand":{"$ref":"#/components/schemas/SetupBrand"}}},"SetupContentSources":{"type":"object","additionalProperties":false,"required":["sources"],"properties":{"sources":{"type":"array","description":"Live sources, by urlPrefix. A page belongs to the source with the longest matching prefix.","items":{"oneOf":[{"type":"object","additionalProperties":false,"required":["id","provider","urlPrefix","repository","branch","folder","format","status","reconnectReason","autoPullRequests","githubAccount"],"properties":{"id":{"type":"string","format":"uuid"},"provider":{"type":"string","enum":["github"]},"urlPrefix":{"type":"string","description":"A lowercase host and a path ending in a slash, with no scheme, such as docs.example.com/ or example.com/blog/."},"repository":{"type":"string","description":"The GitHub repository, as owner/name."},"branch":{"type":"string","description":"The repository branch the source reads."},"folder":{"type":"string","description":"The folder in the repository, with no leading or trailing slash. Empty is the repository root."},"format":{"type":"string","enum":["mintlify","markdown"],"description":"mintlify for a Mintlify docs folder, markdown for plain Markdown files."},"status":{"type":"string","enum":["active","needs_reconnect"],"description":"needs_reconnect when DevTune lost access to the repository; reconnect GitHub in the app."},"reconnectReason":{"type":["string","null"],"enum":["installation","repository",null],"description":"Why a needs_reconnect source broke: installation when the GitHub App was suspended or uninstalled, repository when the repository was removed from the app. Null while active."},"autoPullRequests":{"type":"boolean","description":"Whether new drafts for pages under this source open pull requests automatically."},"githubAccount":{"type":["string","null"],"description":"The GitHub organization or user the app is installed on."}}},{"type":"object","additionalProperties":false,"required":["id","provider","urlPrefix","space","connection","format","status","reconnectReason","autoPullRequests"],"properties":{"id":{"type":"string","format":"uuid"},"provider":{"type":"string","enum":["gitbook"]},"urlPrefix":{"type":"string","description":"A lowercase host and a path ending in a slash, with no scheme, such as docs.example.com/."},"space":{"type":"object","additionalProperties":false,"required":["id","title"],"description":"The GitBook space the prefix maps onto.","properties":{"id":{"type":"string"},"title":{"type":"string"}}},"connection":{"type":"object","additionalProperties":false,"required":["organization","gitbookUser"],"description":"The GitBook connection the source uses: the organization, and the GitBook user whose token it is.","properties":{"organization":{"type":"string"},"gitbookUser":{"type":"string"}}},"format":{"type":"string","enum":["gitbook"]},"status":{"type":"string","enum":["active","needs_reconnect"],"description":"needs_reconnect when the GitBook token stopped working or lost edit access to the space; replace the token in the app."},"reconnectReason":{"type":["string","null"],"enum":["token","space",null],"description":"Why a needs_reconnect source broke: token when GitBook rejected the token, space when the token can no longer edit the space. Null while active."},"autoPullRequests":{"type":"boolean","description":"Whether new drafts for pages under this source open change requests automatically."}}}]}}}},"SetupSearchPlatforms":{"type":"object","additionalProperties":false,"required":["platforms"],"properties":{"platforms":{"type":"array","items":{"type":"object","additionalProperties":false,"required":["platform","enabled","available"],"properties":{"platform":{"type":"string","enum":["chatgpt-search","perplexity","google-ai","google-ai-mode","bing-copilot-search","claude","xai-search"]},"enabled":{"type":"boolean","description":"Whether scheduled probes run on this platform now: switched on for the project, included in the plan, the project not paused and, for xai-search, Grok collection running. False for every platform while the project is paused."},"available":{"type":"boolean","description":"Whether the account's plan includes this platform, whatever the project's setting or pause state."}}},"description":"claude and xai-search are listed only to accounts that can use them."}}},"PageLockWrite":{"type":"object","additionalProperties":false,"required":["domainCanonical","pagePath","locked"],"properties":{"domainCanonical":{"type":"string","description":"The tracked domain the page is on."},"pagePath":{"type":"string","description":"The path as stored, starting with /."},"locked":{"type":"boolean","description":"True when the page is now locked: suggestions steer around it."}}},"ProjectPausedState":{"type":"object","additionalProperties":false,"required":["projectId","paused","pausedAt"],"properties":{"projectId":{"type":"string","format":"uuid"},"paused":{"type":"boolean","description":"Whether the project is paused after the write."},"pausedAt":{"type":["string","null"],"format":"date-time","description":"When the project was paused. Null while it is not."}}},"SetupBrandDeletion":{"type":"object","additionalProperties":false,"required":["brandId","deleted","archived"],"description":"The archived brand. deleted stays true for clients written against the earlier delete.","properties":{"brandId":{"type":"string","format":"uuid"},"deleted":{"type":"boolean","const":true},"archived":{"type":"boolean","const":true}}},"SetupDomain":{"type":"object","additionalProperties":false,"description":"A domain the project tracks, assigned to one of its brands or unassigned.","required":["id","domain","brandId","category","domainType","siteType","createdAt","updatedAt"],"properties":{"id":{"type":"string","format":"uuid"},"domain":{"type":"string","description":"The tracked domain, with a path when one was given."},"brandId":{"type":["string","null"],"description":"The brand the domain belongs to; null when unassigned."},"category":{"type":"string","enum":["primary","secondary","other"],"description":"primary for the project's own brand, secondary for a competitor's."},"domainType":{"type":"string","description":"The kind of site the domain serves, such as docs or blog."},"siteType":{"type":["string","null"],"enum":["content","api_application",null],"description":"How Audit scores one of the primary brand's domains: as a content site or an API application. Null when no site type is set."},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"SetupDomainsPage":{"type":"object","additionalProperties":false,"required":["items","pageSize","hasMore","nextCursor","pagination"],"properties":{"pagination":{"$ref":"#/components/schemas/ListPagination"},"items":{"type":"array","maxItems":100,"description":"The project’s tracked domains, sorted by id.","items":{"$ref":"#/components/schemas/SetupDomain"}},"pageSize":{"type":"integer","minimum":1,"maximum":100,"description":"Rows per page."},"hasMore":{"type":"boolean","description":"More rows follow this page."},"nextCursor":{"type":["string","null"],"description":"Pass as cursor to read the next page. Null on the last page."}}},"SetupDomainCreate":{"type":"object","additionalProperties":false,"required":["domain","previousBrandId"],"properties":{"domain":{"$ref":"#/components/schemas/SetupDomain"},"previousBrandId":{"type":["string","null"],"description":"The brand the domain belonged to before, when the project already held it. Null when the domain is new to the project or had no brand."}}},"SetupDomainWrite":{"type":"object","additionalProperties":false,"required":["domain"],"properties":{"domain":{"$ref":"#/components/schemas/SetupDomain"}}},"SetupDomainDeletion":{"type":"object","additionalProperties":false,"required":["domainId","deleted"],"properties":{"domainId":{"type":"string","format":"uuid"},"deleted":{"type":"boolean","const":true}}},"SetupBrandTerm":{"type":"object","additionalProperties":false,"description":"A word or phrase DevTune matches as a brand.","required":["id","term","brandId","isActive","createdAt","updatedAt"],"properties":{"id":{"type":"string","format":"uuid"},"term":{"type":"string","description":"The word or phrase, matched in any case."},"brandId":{"type":["string","null"],"description":"The brand the term is assigned to. Null when unassigned."},"isActive":{"type":"boolean","description":"false for a retired term."},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}}},"SetupBrandTermsPage":{"type":"object","additionalProperties":false,"required":["items","includeInactive","pageSize","hasMore","nextCursor","pagination"],"properties":{"pagination":{"$ref":"#/components/schemas/ListPagination"},"items":{"type":"array","maxItems":100,"description":"The project’s brand terms, sorted by id. Retired terms only with includeInactive.","items":{"$ref":"#/components/schemas/SetupBrandTerm"}},"includeInactive":{"type":"boolean","description":"Whether retired terms are listed, as requested."},"pageSize":{"type":"integer","minimum":1,"maximum":100,"description":"Rows per page."},"hasMore":{"type":"boolean","description":"More rows follow this page."},"nextCursor":{"type":["string","null"],"description":"Pass as cursor to read the next page. Null on the last page."}}},"SetupBrandTermCreate":{"type":"object","additionalProperties":false,"required":["brandTerm","restored"],"properties":{"brandTerm":{"$ref":"#/components/schemas/SetupBrandTerm"},"restored":{"type":"boolean","description":"true when a retired term was restored."}}},"SetupBrandTermWrite":{"type":"object","additionalProperties":false,"required":["brandTerm"],"properties":{"brandTerm":{"$ref":"#/components/schemas/SetupBrandTerm"}}},"SetupAgentPicksCatalogue":{"type":"object","additionalProperties":false,"required":["categories","environments","agents","max_competitor_tool_keys","max_agent_models"],"properties":{"categories":{"type":"array","description":"Published categories, by name. Their ids are the category_id saveAgentPicksSetup and the source_category_id saveAgentPicksJob accept.","items":{"type":"object","additionalProperties":false,"required":["id","slug","name","tools"],"properties":{"id":{"type":"string","format":"uuid"},"slug":{"type":"string","description":"The category slug; setup selectors accept it."},"name":{"type":"string"},"tools":{"type":"array","description":"The category’s active tools, by display name: the tool_key and competitor_tool_keys a setup in this category may name.","items":{"type":"object","additionalProperties":false,"required":["tool_key","display_name","brand_domain"],"properties":{"tool_key":{"type":"string","description":"The key setups and results use for the tool."},"display_name":{"type":"string"},"brand_domain":{"type":["string","null"],"description":"The tool’s own website domain. Null when none is recorded."}}}}}}},"environments":{"type":"array","description":"The test apps agents start in that the setup wizard offers for environment_ids, by name. One with category_ids is valid only with those categories. A retired test app is not listed: a new setup cannot add it, and a setup that already holds it keeps it.","items":{"type":"object","additionalProperties":false,"required":["id","name","framework","default_selected","prompt_prefix","starts_with"],"properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"framework":{"type":["string","null"],"description":"null for the empty folder."},"default_selected":{"type":"boolean","description":"Whether a new setup includes the test app. false for a test app offered unticked, such as one with hosting or requirements: include its id in environment_ids to test it."},"prompt_prefix":{"type":["string","null"],"description":"What agents are told before every request in this test app, such as a requirement the team has. Null when they are told nothing extra."},"starts_with":{"type":["string","null"],"description":"The host whose own deploy setup the project starts with, by name. Null for a plain starter."},"category_ids":{"type":"array","items":{"type":"string","format":"uuid"},"description":"Present only for a test app that runs in some categories: the published categories it may be saved with. Absent means every category."}}}},"agents":{"type":"array","description":"The agents a setup may test, each with the models agent_models accepts for it.","items":{"type":"object","additionalProperties":false,"required":["agent","name","included_in_plan","models"],"properties":{"agent":{"type":"string","description":"The agent key, such as claude_code."},"name":{"type":"string"},"included_in_plan":{"type":"boolean","description":"false when the account’s Agent Picks plan does not include the agent: saving it is refused with 403 agent_not_in_plan."},"models":{"type":"array","description":"The model ids agent_models accepts for the agent.","items":{"type":"string"}}}}},"max_competitor_tool_keys":{"type":"integer","minimum":1,"description":"The most competitor_tool_keys a setup may save."},"max_agent_models":{"type":"integer","minimum":1,"description":"The most agent_models entries a setup may save."}}},"SetupSearchPlatformCatalogue":{"type":"object","additionalProperties":false,"required":["platforms"],"properties":{"platforms":{"type":"array","description":"The AI platforms DevTune probes, in display order.","items":{"type":"object","additionalProperties":false,"required":["platform","name","includedInPlan"],"properties":{"platform":{"type":"string","description":"The platform key other endpoints use."},"name":{"type":"string"},"includedInPlan":{"type":"boolean","description":"Whether the account’s plan includes it."}}}}}},"AgentSetupDraft":{"type":"object","additionalProperties":false,"required":["product"],"description":"An unclaimed agent registration’s project setup, replaced whole. visibility is required for ai-search and both and refused otherwise; agent_picks is required for agent-picks and both and refused otherwise. Probed platforms are not part of a draft. Plan limits are applied when a person claims the registration.","properties":{"product":{"description":"What the project is set up for: ai-search, agent-picks or both.","type":"string","enum":["ai-search","agent-picks","both"]},"trial_plan":{"type":["string","null"],"description":"The Agent Picks plan a new account’s trial starts on: local-runner (Local Runner monthly) or a Local Runner price id, at one project and with no units; cloud (Cloud monthly at 100 credits), a Cloud price id, or a Cloud price id and its units of 20 credits, such as price_…:9 for 180 credits. The retired plus and pro, and their price ids, select Cloud at 100 and 180 credits. Rejected for ai-search. For agent-picks a new account’s trial starts on it; for both it is accepted but not used. Null when none is chosen."},"visibility":{"description":"The AI search setup. Null unless product is ai-search or both.","nullable":true,"type":"object","additionalProperties":false,"required":["domain"],"properties":{"domain":{"type":"string","minLength":1,"maxLength":2048,"description":"The project's website domain."},"brand_context":{"description":"A description of the company. Null when none is given.","type":["string","null"],"minLength":20,"maxLength":10000},"competitors":{"type":"array","maxItems":25,"description":"Under both, the one competitor list both products share.","items":{"type":"object","additionalProperties":false,"required":["name","urls"],"properties":{"name":{"type":"string","minLength":1,"maxLength":100},"urls":{"description":"The competitor's website URLs.","type":"array","minItems":1,"maxItems":10,"items":{"type":"string","minLength":1,"maxLength":2048}}}}},"prompts":{"description":"Prompts to track, as written.","type":"array","maxItems":50,"items":{"type":"string","minLength":10,"maxLength":1000}}}},"agent_picks":{"description":"The Agent Picks setup: a public category's setup (use_category) or the project's own job (start_from_scratch). Null unless product is agent-picks or both.","nullable":true,"oneOf":[{"type":"object","additionalProperties":false,"required":["starting_point","setup"],"properties":{"starting_point":{"type":"string","const":"use_category"},"setup":{"type":"object","description":"The body saveAgentPicksSetup accepts, with ids from getPublicAgentPicksCatalogue."}}},{"type":"object","additionalProperties":false,"required":["starting_point","job"],"properties":{"starting_point":{"type":"string","const":"start_from_scratch"},"job":{"type":"object","description":"The body saveAgentPicksJob accepts, with ids from getPublicAgentPicksCatalogue."}}}]}}},"AgentSetupDraftState":{"type":"object","additionalProperties":false,"required":["draft","updated_at","expires_at"],"properties":{"draft":{"nullable":true,"allOf":[{"$ref":"#/components/schemas/AgentSetupDraft"}],"description":"null until a draft is saved."},"updated_at":{"type":["string","null"],"format":"date-time","description":"When the draft was last saved. Null until one is."},"expires_at":{"type":"string","format":"date-time","description":"When the unclaimed registration expires. The next hourly cleanup deletes it with its draft and claim attempts."}}},"AgentPicksPublicCatalogue":{"type":"object","additionalProperties":false,"required":["categories","environments","agents","max_competitor_tool_keys","max_agent_models"],"description":"The Agent Picks catalogue a draft may name: getAgentPicksCatalogue without included_in_plan, since a registration has no account until it is claimed.","properties":{"categories":{"$ref":"#/components/schemas/SetupAgentPicksCatalogue/properties/categories"},"environments":{"$ref":"#/components/schemas/SetupAgentPicksCatalogue/properties/environments"},"agents":{"description":"The coding agents a setup can test, with their models.","type":"array","items":{"type":"object","additionalProperties":false,"required":["agent","name","models"],"properties":{"agent":{"type":"string"},"name":{"type":"string"},"models":{"type":"array","items":{"type":"string"},"description":"The models a setup can pair with the agent."}}}},"max_competitor_tool_keys":{"type":"integer","minimum":1,"description":"The most competitor tools a setup can name."},"max_agent_models":{"type":"integer","minimum":1,"description":"The most agent and model pairs a setup can save."}}},"SearchPlatformPublicCatalogue":{"type":"object","additionalProperties":false,"required":["platforms"],"properties":{"platforms":{"type":"array","description":"The AI platforms DevTune probes, in display order.","items":{"type":"object","additionalProperties":false,"required":["platform","name"],"properties":{"platform":{"type":"string"},"name":{"type":"string"}}}}}},"SetupBrandTermDeletion":{"type":"object","additionalProperties":false,"required":["brandTermId","deleted","retired"],"properties":{"brandTermId":{"type":"string","format":"uuid"},"deleted":{"type":"boolean","const":true},"retired":{"type":"boolean","description":"true when the term was assigned to a brand and is kept, retired."}}},"VisibilityResponseHistory":{"type":"object","additionalProperties":false,"required":["items","window","population","pageSize","pageCursor","nextCursor","hasMore","pagination"],"properties":{"pagination":{"$ref":"#/components/schemas/ListPagination"},"items":{"type":"array","maxItems":100,"description":"Runs of tracked prompts on AI platforms, newest first. Read each one with the response detail endpoint.","items":{"type":"object","additionalProperties":false,"required":["executionId","promptId","platform","status","startedAt","completedAt"],"properties":{"executionId":{"type":"string","format":"uuid"},"promptId":{"type":"string","format":"uuid"},"platform":{"type":"string"},"status":{"type":"string","enum":["pending","running","success","failed","rate_limited"],"description":"pending or running until the run finishes; success when the platform answered; failed or rate_limited when it did not."},"startedAt":{"type":"string","format":"date-time"},"completedAt":{"type":["string","null"],"format":"date-time","description":"When the run finished. Null while it is pending or running."}}}},"window":{"type":"object","additionalProperties":false,"description":"The time range the list covers, by startedAt.","required":["days","from","before","timeBasis"],"properties":{"days":{"type":"integer","enum":[30,90]},"from":{"type":"string","format":"date-time","description":"Inclusive lower bound."},"before":{"type":"string","format":"date-time","description":"Exclusive anchor set on the first page."},"timeBasis":{"type":"string","enum":["started_at"]}}},"population":{"type":"object","additionalProperties":false,"description":"Which runs the list includes.","required":["kind","consistency","answerEvidence","promptId","platform","status","enterprisePlatformsIncluded"],"properties":{"kind":{"type":"string","enum":["still_present_executions"]},"consistency":{"type":"string","enum":["live"],"description":"The listing is live: retention, deletion and lifecycle status can change while you page."},"answerEvidence":{"type":"string","enum":["read_each_response"]},"promptId":{"type":["string","null"],"format":"uuid","description":"The promptId filter, as requested. Null when not filtered."},"platform":{"type":["string","null"],"description":"The platform filter, as requested. Null when not filtered."},"status":{"type":["string","null"],"enum":["pending","running","success","failed","rate_limited",null],"description":"The status filter, as requested. Null when not filtered."},"enterprisePlatformsIncluded":{"type":"boolean","description":"Whether runs on platforms that need Enterprise search platform access are listed. When false, the account does not have that access and those runs are left out."}}},"pageSize":{"type":"integer","minimum":1,"maximum":100,"description":"Rows per page."},"pageCursor":{"type":"string","description":"Replay this page with its original window anchor. Deduplicate previously exported execution IDs after a mid-page interruption."},"nextCursor":{"type":["string","null"],"description":"Pass as cursor to read the next page. Null on the last page."},"hasMore":{"type":"boolean","description":"More rows follow this page."}}},"BrandSentiment":{"type":"object","description":"Sentiment of AI answers towards the primary brand, for the window or one day.","properties":{"averageScore":{"type":["number","null"],"minimum":-1,"maximum":1,"description":"Mean score of the scored answers, from -1 to 1. Null when no answer was scored or the day is withheld, never 0."},"label":{"type":["string","null"],"enum":["positive","neutral","negative",null],"description":"averageScore with the scorer cut: above 0.3 positive, below -0.3 negative, otherwise neutral. Null when no answer was scored."},"positive":{"type":["integer","null"],"minimum":0,"description":"Scored answers above 0.3. Null on a withheld day."},"neutral":{"type":["integer","null"],"minimum":0,"description":"Scored answers from -0.3 to 0.3. Null on a withheld day."},"negative":{"type":["integer","null"],"minimum":0,"description":"Scored answers below -0.3. Null on a withheld day."},"coverage":{"type":"object","description":"How many answers there were and how many were scored.","properties":{"responses":{"type":["integer","null"],"minimum":0,"description":"AI answers in the window or day. Null on a withheld day."},"scoredResponses":{"type":["integer","null"],"minimum":0,"description":"Answers with a sentiment score: positive plus neutral plus negative. Null on a withheld day."}},"required":["responses","scoredResponses"]}},"required":["averageScore","label","positive","neutral","negative","coverage"]},"TimelineEvent":{"type":"object","additionalProperties":false,"properties":{"id":{"type":"string","format":"uuid"},"source":{"type":"string","enum":["external","team","deterministic","detected","measured_action"],"description":"Where the event comes from. external: a sourced fact DevTune published, such as a platform model change or outage. team: a fact the DevTune team published. deterministic: a change recorded in the project, such as prompts added. detected: a change DevTune found in citations or AI Traffic. measured_action: a step in measuring a shipped action."},"kind":{"type":"string","enum":["change_detected","change_claimed","measurement_opened","outcome_measured","prompts_added","prompts_edited","prompts_paused","prompts_resumed","prompts_deleted","prompts_restored","topic_deleted","tracked_urls_changed","platform_model_change","platform_outage","data_collection_gap","search_algorithm_update","citation_share_change","ai_traffic_change","devtune_methodology_change"],"description":"What happened. Measured actions use change_detected, change_claimed, measurement_opened or outcome_measured. Detected citation-share changes use citation_share_change and detected AI Traffic changes use ai_traffic_change. Platform, data-collection and DevTune changes, and prompt or topic changes, have their own kinds."},"occurredAt":{"type":"string","format":"date-time","description":"When it happened. A detected change for this project is placed at 03:00 UTC on the day it was first seen."},"endedAt":{"type":["string","null"],"format":"date-time","description":"When a fact DevTune published that spans time, such as an outage, ended. Null when the fact has no end time, and on every project event."},"platformKeys":{"description":"The AI platforms the event concerns. Null when it is not about particular platforms, and on AI Traffic changes; read trafficChange.platform for those.","anyOf":[{"type":"array","items":{"type":"string"}},{"type":"null"}]},"titleKey":{"type":["string","null"],"description":"Always null. Titles come from evidence."},"evidence":{"type":"object","additionalProperties":true,"properties":{"measuredAction":{"type":"object","additionalProperties":false,"properties":{"actionId":{"type":"string","format":"uuid"},"actionTitle":{"type":"string"},"agentPicks":{"type":"object","additionalProperties":true,"description":"Structured Agent Picks Action rationale for localized titles. Only present with agent_picks enabled; reason Actions include the reason and all covered cells."},"interventionId":{"type":["string","null"],"format":"uuid","description":"The measured action’s before/after window."},"changeEventId":{"type":["string","null"],"format":"uuid","description":"The content change behind the event. Null when a result has no linked content change. Absent on a measurement opening."},"url":{"type":["string","null"],"description":"The page the action changed. An empty string when the action names no page."},"result":{"type":"string","enum":["improved","mixed","no_clear_change","declined","still_measuring","unmeasured"],"description":"What the before/after window shows. It records what moved, not what caused it."},"measurementOpenedAt":{"type":["string","null"],"format":"date-time","description":"When the before/after window opened. Absent on a change event."},"windowClosedAt":{"type":["string","null"],"format":"date-time","description":"When the window closed. Only on a result event. Null when no window close was recorded."},"method":{"type":"string","enum":["v1","v2"],"description":"Method used for an outcome_measured result."},"agentPicksMethod":{"type":"string","enum":["v1","v2"],"description":"On an Agent Picks outcome_measured event, the method its stored result was decided by: v2 compares the re-test with its baseline by the 90% interval on their difference, v1 by 95% ranges. A result stored without one is v1."}},"required":["interventionId","url"],"description":"The measured action behind the event. Changes and results carry changeEventId; openings and results carry measurementOpenedAt. Only results carry result and windowClosedAt, and a result event is placed when Results measured it."},"detectedEvidence":{"description":"A detected citation-share change: across every tracked project, or for this project. Only on detected citation-share events.","anyOf":[{"type":"object","additionalProperties":false,"properties":{"platform":{"type":"string"},"domain":{"type":"string","description":"The cited domain whose share changed."},"direction":{"type":"string","enum":["up","down"],"description":"Whether the share rose or fell."},"currentShare":{"type":"number","minimum":0,"maximum":1,"description":"The domain's share of the platform's citations in the observation period, from 0 to 1."},"baselineShare":{"type":"number","minimum":0,"maximum":1,"description":"The same share in the baseline period, from 0 to 1."},"absoluteChange":{"type":"number","description":"currentShare minus baselineShare, on the same 0 to 1 scale."},"relativeChange":{"type":["number","null"],"description":"The change relative to baselineShare. Null when baselineShare is 0."},"observationStart":{"type":"string","format":"date","description":"First day of the observation period."},"observationEnd":{"type":"string","format":"date","description":"Last day of the observation period."},"projectCount":{"type":"integer","minimum":0,"description":"Tracked projects across DevTune where the change was seen."},"projectFraction":{"type":"number","minimum":0,"maximum":1,"description":"projectCount as a share of the projects that could be compared on this platform, from 0 to 1."}},"required":["platform","domain","direction","currentShare","baselineShare","absoluteChange","relativeChange","observationStart","observationEnd","projectCount","projectFraction"]},{"type":"object","additionalProperties":false,"properties":{"platform":{"type":"string"},"domain":{"type":"string","description":"The cited domain whose share changed."},"direction":{"type":"string","enum":["up","down"],"description":"Whether the share rose or fell."},"currentShare":{"type":"number","minimum":0,"maximum":1,"description":"The domain's share of the platform's citations in the observation period, from 0 to 1."},"baselineShare":{"type":"number","minimum":0,"maximum":1,"description":"The same share in the baseline period, from 0 to 1."},"absoluteChange":{"type":"number","description":"currentShare minus baselineShare, on the same 0 to 1 scale."},"relativeChange":{"type":["number","null"],"description":"The change relative to baselineShare. Null when baselineShare is 0."},"observationStart":{"type":"string","format":"date","description":"First day of the observation period."},"observationEnd":{"type":"string","format":"date","description":"Last day of the observation period."},"scope":{"type":"string","enum":["project"]},"domainRole":{"type":"string","enum":["own","competitor","other"],"description":"own for a primary brand domain, competitor for a tracked competitor’s, other for any other source."},"brandName":{"type":["string","null"],"description":"The brand the domain belongs to. Null when domainRole is other, or when the brand has since been deleted."},"incidentId":{"type":"string","format":"uuid"},"baselineStart":{"type":"string","format":"date","description":"First day of the baseline period."},"baselineEnd":{"type":"string","format":"date","description":"Last day of the baseline period."},"cohortPromptCount":{"type":"integer","minimum":0,"description":"Prompts compared across both periods."},"activePromptCount":{"type":"integer","minimum":0,"description":"Prompts active in the observation period."},"status":{"type":"string","enum":["active","resolved"],"description":"active while the change holds, resolved once it no longer does."},"resolvedAt":{"type":["string","null"],"format":"date-time","description":"When the change resolved. Null while active."}},"required":["platform","domain","direction","currentShare","baselineShare","absoluteChange","relativeChange","observationStart","observationEnd","scope","domainRole","brandName","incidentId","baselineStart","baselineEnd","cohortPromptCount","activePromptCount","status","resolvedAt"]},{"type":"null"}]},"trafficChange":{"type":"object","additionalProperties":false,"properties":{"incidentId":{"type":"string","format":"uuid"},"platform":{"type":"string","description":"AI Traffic provider key, for example openai or anthropic."},"classGroup":{"type":"string","enum":["answer_fetches","crawls"],"description":"answer_fetches for answer fetchers, crawls for training and index crawlers."},"direction":{"type":"string","enum":["up","down"],"description":"Whether requests rose or fell."},"observedEvents":{"type":"integer","minimum":0,"description":"Requests in the observation period."},"baselineEvents":{"type":"integer","minimum":0,"description":"Requests in the baseline period."},"expectedEvents":{"type":"integer","minimum":0,"description":"baselineEvents scaled to the observation period’s length."},"relativeChange":{"type":["number","null"],"description":"observedEvents minus expectedEvents, divided by expectedEvents. Null when the baseline has no events."},"observationStart":{"type":"string","format":"date","description":"First day of the observation period."},"observationEnd":{"type":"string","format":"date","description":"Last day of the observation period."},"baselineStart":{"type":"string","format":"date","description":"First day of the baseline period."},"baselineEnd":{"type":"string","format":"date","description":"Last day of the baseline period."},"status":{"type":"string","enum":["active","resolved"],"description":"active while the change holds, resolved once it no longer does."},"resolvedAt":{"type":["string","null"],"format":"date-time","description":"When the change resolved. Null while active."}},"required":["incidentId","platform","classGroup","direction","observedEvents","baselineEvents","expectedEvents","relativeChange","observationStart","observationEnd","baselineStart","baselineEnd","status","resolvedAt"],"description":"Detected AI Traffic change for one provider and class group. answer_fetches counts answer fetchers; crawls counts training and index crawlers. expectedEvents is the baseline scaled to the observation window length; relativeChange is null when the baseline has no events. Only high-confidence changes measured with complete sensor coverage surface, and only where AI Traffic is available."}},"description":"What the event is based on. Detected AI Traffic changes (ai_traffic_change) return trafficChange. Detected facts return global aggregate or project incident detectedEvidence. Project evidence includes domainRole (own, competitor or other), brandName (null for other, or once the brand is deleted), scope, incidentId, baseline dates, prompt counts, status and resolvedAt; only global evidence includes projectCount and projectFraction. External facts return title, description, and sourceUrl. Prompt additions include promptCount, topic, and requestedBy. Prompt edits (prompts_edited) and prompts switched off (prompts_paused) or on (prompts_resumed) return the same fields, one event per kind per day, counting each prompt once; edits and switches made before DevTune recorded them are not returned. Deletions also include jobId and activeSlotsFreed; restores include jobId and topicRestored. Prompt names are not returned. Tracked-URL changes (tracked_urls_changed, one per day) include changeCount, brands (up to five affected brand names), brandCount, requestedBy and requestedByCredential (the API key reference and name, or MCP connection reference, when every change has the same credential); they apply to citations collected from that day on, and earlier citations keep the labels they were recorded with."}},"required":["id","source","kind","occurredAt","endedAt","platformKeys","titleKey","evidence"]},"CitabilitySectionAssessment":{"type":"object","description":"An editorial review of up to six sections of a page, each read as an excerpt an AI answer might quote on its own. Findings are suggestions, not measured citation effects.","properties":{"version":{"type":"string","description":"Version of the review method."},"contentHash":{"type":"string","minLength":1,"description":"A hash of the crawled page content reviewed."},"inputHash":{"type":"string","minLength":1,"description":"A hash of the exact Markdown and review method used."},"assessedAt":{"type":"string","format":"date-time"},"status":{"type":"string","enum":["complete","partial","unavailable","failed"],"description":"complete: every selected section was reviewed and every finding kept. partial: sections were sampled, the page or a section was cut short, or a finding was dropped because its excerpt was not in the section. unavailable: the page has no substantive sections. failed: the review could not run."},"reason":{"type":["string","null"],"enum":["no_substantive_sections","model_failure","invalid_excerpts",null],"description":"no_substantive_sections with status unavailable, model_failure with failed, invalid_excerpts with partial when a finding was dropped. Null otherwise."},"selection":{"type":"object","description":"How the reviewed sections were chosen from the page.","properties":{"scannedChars":{"type":"integer","minimum":0,"description":"Characters of the page Markdown scanned for sections, at most 120,000."},"sourceChars":{"type":"integer","minimum":0,"description":"Length of the page Markdown, in characters."},"candidateCount":{"type":"integer","minimum":0,"description":"Substantive sections found in the scanned text."},"sampled":{"type":"boolean","description":"True when more than 6 sections were found, so 6 evenly spaced ones were reviewed."},"truncated":{"type":"boolean","description":"True when the page is longer than the scanned text, or a section was cut at 2,000 characters."}},"required":["scannedChars","sourceChars","candidateCount","sampled","truncated"],"additionalProperties":false},"sections":{"type":"array","maxItems":6,"description":"The reviewed sections, in page order.","items":{"type":"object","properties":{"locator":{"type":"string","maxLength":100,"description":"markdown:start:end. Findings name their section by it."},"heading":{"type":"string","maxLength":200,"description":"The heading the section sits under. Empty when no heading comes before it."},"start":{"type":"integer","minimum":0,"description":"Character offset in the page Markdown where the section starts."},"end":{"type":"integer","minimum":0,"description":"Character offset where the returned source ends, exclusive."},"source":{"type":"string","maxLength":2000,"description":"The section Markdown exactly as crawled, up to 2,000 characters."},"truncated":{"type":"boolean","description":"True when the section is longer than 2,000 characters and source holds only the start."}},"required":["locator","heading","start","end","source","truncated"],"additionalProperties":false}},"findings":{"type":"array","maxItems":3,"description":"Up to 3 places where a section is unclear when read alone. Empty when the reviewed sections need no change, or when status is unavailable or failed.","items":{"type":"object","properties":{"locator":{"type":"string","maxLength":100,"description":"The locator of the section the finding is in."},"kind":{"type":"string","enum":["subject_clarity","direct_answer","evidence_proximity","conditions_or_limitations"],"description":"subject_clarity: the section does not say what it is about. direct_answer: it lacks a direct answer for its purpose. evidence_proximity: evidence sits too far from the claim it supports. conditions_or_limitations: conditions or limits sit too far from the claim."},"excerpt":{"type":"string","minLength":1,"maxLength":600,"description":"An exact quote from the section's source, Markdown included."},"explanation":{"type":"string","minLength":1,"maxLength":400,"description":"What becomes unclear when the excerpt is read alone."},"remedy":{"type":"string","maxLength":400,"description":"A suggested edit that only restructures or clarifies text already in the section. Empty when the suggestion was removed for adding facts the section does not contain, unless the page is locked. On a locked page, including one DevTune protects as high-stakes, every remedy, empty ones included, is an instruction to leave the page unchanged and consider separate supporting content."}},"required":["locator","kind","excerpt","explanation","remedy"],"additionalProperties":false}}},"required":["version","contentHash","inputHash","assessedAt","status","reason","selection","sections","findings"],"additionalProperties":false},"Error":{"type":"object","properties":{"error":{"type":"string","description":"Machine-readable error code, such as bad_request, unauthorized, forbidden, not_found, conflict, payment_required, rate_limit_exceeded or internal_error. Some endpoints return their own codes, listed in their descriptions."},"message":{"type":"string","description":"What went wrong, for a person to read. Do not parse it; use error and reason."},"reason":{"type":"string","description":"Why the request was refused, for an error with more than one cause, such as a refused content-draft start."},"details":{"type":"object","additionalProperties":true,"description":"Machine-readable facts about the failure or refusal, such as retryable or shortfall_credits.","properties":{"retryable":{"type":"boolean","description":"Read this field before retrying. Present on every 5xx and 429 response: true when another attempt may succeed, false for a permanent refusal even on a 5xx. Other refusals may also provide this field."},"retryAfterSeconds":{"type":"integer","minimum":1,"description":"Seconds to wait when retryable is true on a 429 or 503 response, also sent in Retry-After. Uses the provider retry time or limiter reset when available; otherwise 60 seconds for 429 and GitHub throttles or 30 seconds for other 503 responses. GitHub secondary limits wait at least 60 seconds."}}},"status":{"type":"integer","description":"The HTTP status code, repeated in the body."}},"required":["error","message","status"]},"Meta":{"type":"object","description":"Sent beside data on every successful response that returns data.","properties":{"timestamp":{"type":"string","format":"date-time","description":"When the response was produced."},"projectId":{"type":"string","format":"uuid"}}},"Pagination":{"type":"object","properties":{"hasMore":{"type":"boolean","description":"More rows follow this page."},"nextCursor":{"type":["string","null"],"description":"Pass as cursor to read the next page. Null on the last page."},"page":{"type":"integer","description":"The page returned, counting from 1."},"pageSize":{"type":"integer","description":"Rows per page."},"totalCount":{"type":"integer","description":"Rows across every page."},"totalPages":{"type":"integer","description":"Pages at this pageSize. 0 when there are no rows."}},"required":["hasMore","nextCursor"]},"ListPagination":{"type":"object","properties":{"hasMore":{"type":"boolean","description":"More rows follow this page."},"nextCursor":{"type":["string","null"],"description":"Pass as cursor to read the next page. Null on the last page."}},"required":["hasMore","nextCursor"]},"CursorPagination":{"type":"object","properties":{"pageSize":{"type":"integer","minimum":1,"maximum":100,"description":"Rows per page."},"cursor":{"type":["string","null"],"description":"The cursor this page was read with. Null on the first page."},"nextCursor":{"type":["string","null"],"description":"Pass as cursor to read the next page. Null on the last page."},"hasMore":{"type":"boolean","description":"More rows follow this page."}},"required":["pageSize","cursor","nextCursor","hasMore"]},"LibraryVersionHistoryItem":{"type":"object","additionalProperties":false,"properties":{"id":{"type":"string","format":"uuid"},"title":{"type":"string"},"status":{"type":"string","enum":["current","superseded"]},"createdAt":{"type":"string","format":"date-time"}},"required":["id","title","status","createdAt"]},"LibraryItemMetadata":{"type":"object","additionalProperties":false,"properties":{"actionId":{"type":["string","null"],"format":"uuid","description":"The Action the item belongs to. Null when it has none."},"agentKey":{"type":["string","null"],"description":"The agent that produced the item: managed-agent: and its workflow for a managed agent's work, otherwise the generator. Null when generatorKey is."},"createdAt":{"type":"string","format":"date-time"},"draftKind":{"type":["string","null"],"enum":["page","section_edits",null],"description":"For a content_draft item, page for a whole new page or section_edits for a change list. Null for every other kind."},"generatorKey":{"type":["string","null"],"description":"What produced the item, such as upload. Items with the same generatorKey and subjectKey are versions of one another."},"generatorType":{"type":["string","null"],"description":"Who produced the item, such as user, agent or system."},"groundingEnabled":{"type":"boolean","description":"Whether agents may use the item as context. Items with it off stay searchable. Always false for a content draft."},"id":{"type":"string","format":"uuid"},"indexingStatus":{"type":"string","description":"indexed once the item's text is searchable, pending while it waits, unindexed when no readable text was found."},"kind":{"type":"string","enum":["report","brief","action_draft","upload","memory","content_draft"],"description":"content_draft is returned only when the account has content drafts and the caller holds actions.read."},"managedAgentTaskId":{"type":["string","null"],"format":"uuid","description":"The agent task that produced the item. Null when no agent task did."},"rankScore":{"type":"number","description":"Relevance to the search query; results come highest first. 0 without a query."},"status":{"type":"string","enum":["current","superseded"],"description":"current, or superseded once a newer version replaces it."},"summary":{"type":["string","null"],"description":"A summary of the item, up to 280 characters. Empty when none is recorded."},"title":{"type":"string"},"updatedAt":{"type":"string","format":"date-time"},"versionNumber":{"type":"integer","minimum":1,"description":"This version's place in its history, from 1 for the oldest."},"versionCount":{"type":"integer","minimum":1,"description":"Versions in the item's history, this one included."},"versionHistory":{"description":"The item's other versions, newest first, up to 20.","type":"array","items":{"$ref":"#/components/schemas/LibraryVersionHistoryItem"}}},"required":["actionId","agentKey","createdAt","draftKind","generatorKey","generatorType","groundingEnabled","id","indexingStatus","kind","managedAgentTaskId","rankScore","status","summary","title","updatedAt","versionNumber","versionCount","versionHistory"]},"LibraryItemDetail":{"type":"object","additionalProperties":false,"properties":{"actionId":{"type":["string","null"],"format":"uuid","description":"The Action the item belongs to. Null when it has none."},"contentMarkdown":{"type":["string","null"],"description":"The item as Markdown. Null when it has only plain text."},"contentText":{"type":"string","description":"The item's plain text, as search reads it."},"createdAt":{"type":"string","format":"date-time"},"expiresAt":{"type":["string","null"],"format":"date-time","description":"When a memory item expires. Null for every other kind."},"generatorKey":{"type":["string","null"],"description":"What produced the item, such as upload. Items with the same generatorKey and subjectKey are versions of one another."},"groundingEnabled":{"type":"boolean","description":"Whether agents may use the item as context. Items with it off stay searchable. Always false for a content draft."},"id":{"type":"string","format":"uuid"},"kind":{"type":"string","enum":["report","brief","action_draft","upload","memory","content_draft"],"description":"content_draft is returned only when the account has content drafts and the caller holds actions.read."},"managedAgentTaskId":{"type":["string","null"],"format":"uuid","description":"The agent task that produced the item. Null when no agent task did."},"provenance":{"type":"object","additionalProperties":true,"description":"Where the item came from, as recorded with it. For a content draft: briefGeneratedAt, briefId, changeList, draftKind, factCheck and runId."},"status":{"type":"string","enum":["current","superseded"],"description":"current, or superseded once a newer version replaces it."},"subjectKey":{"type":["string","null"],"description":"What the item is about, within its generator. Null when generatorKey is."},"supersededByItemId":{"type":["string","null"],"format":"uuid","description":"The newer version that replaced this one. Null while it is current, when that version was deleted, or after its successor link is cleared."},"title":{"type":"string"},"updatedAt":{"type":"string","format":"date-time"},"versionNumber":{"type":"integer","minimum":1,"description":"This version's place in its history, from 1 for the oldest."},"versionCount":{"type":"integer","minimum":1,"description":"Versions in the item's history, this one included."},"versionHistory":{"description":"The item's other versions, newest first, up to 20.","type":"array","items":{"$ref":"#/components/schemas/LibraryVersionHistoryItem"}}},"required":["actionId","contentMarkdown","contentText","createdAt","expiresAt","generatorKey","groundingEnabled","id","kind","managedAgentTaskId","provenance","status","subjectKey","supersededByItemId","title","updatedAt","versionNumber","versionCount","versionHistory"]},"LibraryItemsData":{"type":"object","properties":{"items":{"description":"Matching items, most relevant first, newest first on a tie. A page can come back short when it skips items the caller cannot read.","type":"array","items":{"$ref":"#/components/schemas/LibraryItemMetadata"}},"pagination":{"$ref":"#/components/schemas/CursorPagination"}},"required":["items","pagination"]},"LibraryItemData":{"allOf":[{"$ref":"#/components/schemas/LibraryItemDetail"}]},"KnowledgeData":{"type":"object","properties":{"companyContext":{"type":"string","description":"Canonical company description shared by project workflows and agents."},"profile":{"description":"The project's profile. Null when none has been saved.","anyOf":[{"$ref":"#/components/schemas/KnowledgeProfile"},{"type":"null"}]},"proposals":{"description":"Proposed profile changes, newest first.","type":"array","items":{"$ref":"#/components/schemas/KnowledgeProposalData"}},"pagination":{"$ref":"#/components/schemas/CursorPagination"}},"required":["companyContext","profile","proposals","pagination"]},"KnowledgeProfile":{"type":"object","properties":{"companyContext":{"type":"string","description":"Canonical company context returned after a direct update."},"id":{"type":"string","format":"uuid"},"positioning":{"type":["string","null"],"deprecated":true,"description":"Deprecated: read companyContext. Null when not set."},"productsServices":{"type":["string","null"],"deprecated":true,"description":"Deprecated: read companyContext. Null when not set."},"tone":{"type":["string","null"],"description":"The voice and style agent tasks for the project write in. Null when not set."},"savedInstructions":{"type":["string","null"],"description":"Instructions included in the project's agent tasks to guide recommendations, writing and sources. Null when not set."},"updatedAt":{"type":"string","format":"date-time"},"updatedBy":{"type":["string","null"],"format":"uuid","description":"The person who last changed the profile. Null when no person has, or their account was deleted."}},"required":["id","positioning","productsServices","tone","savedInstructions","updatedAt","updatedBy"]},"KnowledgeProposalData":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"targetField":{"type":"string","description":"The profile field the change is for: company_context, positioning, products_services, tone or saved_instructions."},"proposedValue":{"type":"string","description":"The proposed text, in full."},"source":{"type":"string","description":"What proposed it: crawl_diff (a change found on the site), run_learning (an agent run) or user."},"provenance":{"type":"object","additionalProperties":true,"description":"What the proposal was drawn from, as recorded with it. Once it is approved or dismissed, reviewActor also records who reviewed it."},"status":{"description":"pending until reviewed, then approved or dismissed. Restoring a dismissed proposal returns it to pending.","type":"string","enum":["pending","approved","dismissed"]},"reviewedBy":{"type":["string","null"],"format":"uuid","description":"Who reviewed it. Null while pending, when no person reviewed it, or when their account was deleted."},"reviewedAt":{"type":["string","null"],"format":"date-time","description":"When it was reviewed. Null while pending."},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"}},"required":["id","targetField","proposedValue","source","provenance","status","reviewedBy","reviewedAt","createdAt","updatedAt"]},"Webhook":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Pass it as webhookId to delete the subscription or rotate its signing secret."},"url":{"type":"string","format":"uri","description":"The URL DevTune posts events to."},"events":{"$ref":"#/components/schemas/WebhookEvents"},"isActive":{"type":"boolean","description":"True while the subscription receives events. Deleting a subscription turns it off, and lists return only active subscriptions."},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time","description":"When the subscription last changed, including a secret rotation. Absent from the create response."}}},"WebhookEventType":{"type":"string","enum":["search-tracking.completed","visibility.changed","action.created","action.updated","action.recommendation.created","action.recommendation.updated","action.brief.ready","action.draft.ready","agent.run.completed","outcome.measured"]},"WebhookEvents":{"type":"array","description":"The event types the subscription receives.","items":{"$ref":"#/components/schemas/WebhookEventType"},"minItems":1,"maxItems":10,"uniqueItems":true},"PublicActionStatus":{"type":"string","enum":["suggested","accepted","live","archived"],"description":"suggested: DevTune proposed it and nobody has decided. accepted: the team wants it done. live: the page is published and its measurement window is open. archived: taken off the queue by the team."},"CitationDefenceCompositionSide":{"type":"object","additionalProperties":false,"description":"Prompt-platform cells present in only one of the two windows.","required":["cellCount","citationCount"],"properties":{"cellCount":{"type":"integer","minimum":0,"description":"Prompt-platform cells present in only this comparison window."},"citationCount":{"type":"integer","minimum":0,"description":"The page's citations in those cells."}}},"FlexibleRollupItem":{"type":"object","additionalProperties":true,"description":"One row of the figures the matching DevTune screen shows. Fields vary by report."},"OffsetPagination":{"type":"object","properties":{"hasMore":{"type":"boolean","description":"More rows follow this page."},"nextCursor":{"type":["string","null"],"description":"Pass as cursor to read the next page. Null on the last page."},"offset":{"type":"integer","minimum":0,"description":"Rows before this page."},"pageSize":{"type":"integer","minimum":1,"maximum":100,"description":"Rows per page."},"totalCount":{"type":"integer","minimum":0,"description":"Rows across every page."}},"required":["hasMore","nextCursor","offset","pageSize","totalCount"]},"RollupCursorPagination":{"type":"object","properties":{"hasMore":{"type":"boolean","description":"More rows follow this page."},"nextCursor":{"type":["string","null"],"description":"Pass as cursor to read the next page while hasMore is true. It can still be set on the last page, so stop on hasMore, not on a null cursor."},"pageSize":{"type":"integer","minimum":1,"maximum":100,"description":"Rows per page."},"totalCount":{"type":"integer","minimum":0,"description":"Rows across every page."}},"required":["hasMore","nextCursor","pageSize","totalCount"]},"GscMeasurement":{"type":"object","description":"Whether the Search Console totals are verified and current.","required":["availability","status","isCurrent","asOfDate","explanation","contributors","coveredRanges","storedEvidence","sourceDateFrom","sourceDateTo","finalityBoundary"],"properties":{"availability":{"type":"string","enum":["unavailable","partial","verified_zero","measured"],"description":"measured: the totals are verified for the whole source window. verified_zero: verified, and clicks and impressions are both 0. partial: some days are verified or DevTune holds unverified data for the window, or the totals are earlier ones, not current. unavailable: nothing is verified or stored; report the period as unmeasured."},"status":{"type":"string","enum":["unmeasured","measured"],"description":"measured when availability is measured or verified_zero. Use availability instead when reporting evidence."},"isCurrent":{"type":"boolean","description":"False when these are earlier totals rather than current ones, or when no totals have been published yet."},"asOfDate":{"type":["string","null"],"format":"date-time","description":"When the Search Console totals were published. Null when none have been."},"explanation":{"type":"string","description":"A sentence describing availability, for a person to read. Do not parse it."},"coverageFrom":{"type":["string","null"],"format":"date","description":"First day every contributing Search Console connection has verified data for. Null when any of them has none, or when no totals have been published."},"coverageThrough":{"type":["string","null"],"format":"date","description":"Last day every contributing Search Console connection has verified data for. Null when any of them has none, or when no totals have been published."},"sourceDateFrom":{"type":["string","null"],"format":"date","description":"First day of the Search Console window, in Pacific time."},"sourceDateTo":{"type":["string","null"],"format":"date","description":"Last day of the Search Console window, in Pacific time."},"finalityBoundary":{"type":["string","null"],"format":"date","description":"The last day whose Search Console data is final, in Pacific time, for both the current and prior periods."},"contributors":{"type":"array","description":"One entry for each Search Console connection behind the totals, with the days it covers, followed by a storedData entry for the data DevTune holds and a publication entry. Only the publication entry is present when no totals have been published.","items":{"type":"object","additionalProperties":true}},"coveredRanges":{"type":"array","description":"Each contributing connection's verified days, clipped to the window. Ranges that overlap do not show that the whole project is covered.","items":{"type":"object","properties":{"connectionId":{"type":["string","null"],"description":"The contributing Search Console connection. Null when it was not recorded."},"dateFrom":{"type":"string","format":"date","description":"First verified day in the window."},"dateTo":{"type":"string","format":"date","description":"Last verified day in the window."}}}},"storedEvidence":{"type":"array","description":"The first and last days DevTune holds any data for. It does not mean every day between them is covered, and it is not part of the verified totals.","items":{"type":"object","additionalProperties":true}}}},"GscWindowPeriod":{"type":"object","description":"One Search Console period: its totals and its dates.","required":["aggregates","window"],"properties":{"aggregates":{"type":"object","description":"Search Console totals for the project's properties.","properties":{"clicks":{"type":["integer","null"],"description":"Clicks from Google Search in the period. Null when no verified total has been published for the period. While measurement.status is unmeasured, a value here is from the last published totals, not current ones."},"impressions":{"type":["integer","null"],"description":"Impressions in Google Search in the period. Null when no verified total has been published for the period. While measurement.status is unmeasured, a value here is from the last published totals, not current ones."},"ctr":{"type":["number","null"],"description":"Clicks over impressions, from 0 to 1; 0 when there were no impressions. Null when no verified total has been published for the period. While measurement.status is unmeasured, a value here is from the last published totals, not current ones."},"avgPosition":{"type":["number","null"],"description":"Average ranking position, weighted by impressions; 1 is the top result, and 0 means there were no impressions. Null when no verified total has been published for the period. While measurement.status is unmeasured, a value here is from the last published totals, not current ones."},"measurement":{"$ref":"#/components/schemas/GscMeasurement"}}},"window":{"type":"object","description":"The period, in Pacific-time Search Console days.","properties":{"days":{"type":"integer","enum":[7,30,90]},"dateFrom":{"type":"string","format":"date","description":"First day of the period."},"dateTo":{"type":"string","format":"date","description":"Last day of the period, included."}}}}},"GscWindowPeriods":{"type":"object","required":["current","prior","delta"],"description":"Search Console totals for the current period and the same-length period before it.","properties":{"current":{"$ref":"#/components/schemas/GscWindowPeriod"},"prior":{"$ref":"#/components/schemas/GscWindowPeriod"},"delta":{"oneOf":[{"type":"null"},{"type":"object","properties":{"clicks":{"type":"integer","description":"Current clicks minus prior clicks."},"impressions":{"type":"integer","description":"Current impressions minus prior impressions."}}}],"description":"Change in project totals from prior to current. Null unless both periods are measured."}}},"PageMetricsData":{"type":"object","properties":{"analyticsReportingContext":{"$ref":"#/components/schemas/AnalyticsReportingContext"},"searchConsole":{"$ref":"#/components/schemas/GscWindowPeriods"},"items":{"type":"array","description":"One row per page with its Search Console, AI Traffic, citation and Audit figures for the window.","items":{"$ref":"#/components/schemas/FlexibleRollupItem"}},"pagination":{"$ref":"#/components/schemas/RollupCursorPagination"},"summary":{"$ref":"#/components/schemas/FlexibleRollupItem"},"windowDays":{"type":"integer","enum":[7,30,90]}},"required":["items","pagination","summary","windowDays","searchConsole"]},"PageGuardRailItem":{"type":"object","additionalProperties":true,"description":"One page guard-rail classification with the evidence it was computed from.","properties":{"domainCanonical":{"type":"string","description":"The tracked domain the row is held under, with its path prefix when it has one."},"pagePath":{"type":"string","description":"The page path, as stored."},"isUserLocked":{"type":"boolean","description":"Whether a person locked this page. A locked page refuses changes."},"computedAt":{"type":"string","format":"date-time","description":"When the classification run that last changed this page’s state or evidence computed it. A daily run that finds nothing changed leaves it as it was, so an older value does not mean the classification is out of date."},"updatedAt":{"type":"string","format":"date-time","description":"When this row last changed for any reason: a new state or evidence, or a lock placed or removed."}}},"PageGuardRailsData":{"type":"object","properties":{"items":{"type":"array","description":"This page of guard-rail rows.","items":{"$ref":"#/components/schemas/PageGuardRailItem"}},"pagination":{"$ref":"#/components/schemas/OffsetPagination"},"target":{"type":"object","description":"Present only when url was passed: the host and path the URL was read as. Host is lowercased without www. or port; the path keeps its case and percent-encoding and drops the query and fragment.","properties":{"host":{"type":"string","description":"The host the URL was read as."},"pagePath":{"type":"string","description":"The path the URL was read as."}},"required":["host","pagePath"]}},"required":["items","pagination"]},"PositioningStatement":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"kind":{"type":"string","enum":["positioning"]},"statement":{"type":"string","description":"The positioning statement: the customer's own wording, or a DevTune suggestion the customer approved."},"relevant_intents":{"type":["array","null"],"description":"The prompt intents whose answers are checked against the statement, such as comparison_evaluation. Null when every intent is.","items":{"type":"string"}},"status":{"type":"string","enum":["active","archived"],"description":"active while answers are checked against the statement; archived when it is no longer checked and keeps its history."},"created_at":{"type":"string","format":"date-time"}},"required":["id","kind","statement","relevant_intents","status","created_at"]},"PositioningMatchCell":{"type":"object","description":"Which brands AI answers give one positioning statement to, over the window or one day.","properties":{"statement_id":{"type":"string","format":"uuid"},"statement":{"type":"string","description":"The positioning statement: the customer's own wording, or a DevTune suggestion the customer approved."},"kind":{"type":"string","enum":["positioning"]},"status":{"type":"string","enum":["active","archived"],"description":"active while answers are checked against the statement; archived when it is no longer checked and keeps its history."},"series_started_on":{"type":"string","format":"date","description":"The first day in the window with results for the statement under the current competitor set. The window’s first day when there are none."},"eligible":{"type":"integer","minimum":0,"description":"Successful AI answers that mention a tracked brand, from prompts with an intent the statement covers, under the filters. On statement totals, only days, platforms and intents with answers scored under the current competitor set count."},"scored":{"type":"integer","minimum":0,"description":"Answers checked against the statement, abstained ones included."},"abstained":{"type":"integer","minimum":0,"description":"Of scored, answers the check was not confident enough to show."},"brand_counts":{"type":"object","description":"Shown answers that give the statement to each tracked brand, keyed by brand id.","additionalProperties":{"type":"integer","minimum":0}},"another_product":{"type":"integer","minimum":0,"description":"Shown answers that give the statement to a product DevTune does not track."},"several":{"type":"integer","minimum":0,"description":"Shown answers that give the statement to several products."},"nobody":{"type":"integer","minimum":0,"description":"Shown answers that give the statement to no product."},"brands":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":["string","null"],"description":"The brand name. Null when the brand has been removed."}},"required":["id","name"]},"description":"The brands in brand_counts, with their names."},"share_of_statement":{"type":["number","null"],"minimum":0,"maximum":1,"description":"Primary brand count divided by brand_counts plus another_product. Null below ten assignments or when the primary brand is unavailable."},"day":{"type":"string","format":"date","description":"Present on daily cells."},"prior_status":{"type":"string","enum":["comparable","competitor_changed","incomplete"],"description":"On statement totals only: whether the previous window of the same length compares with this one. competitor_changed: either window has answers scored against another competitor set. incomplete: the previous window has a day, platform and intent with eligible answers and none scored, or no eligible answers at all."},"prior":{"type":["object","null"],"description":"On statement totals only: the previous window’s totals for the current competitor set, under the same filters. Null unless prior_status is comparable, so a change is never computed against a different set or a partly scored window.","properties":{"scored":{"type":"integer","minimum":0,"description":"Answers checked against the statement."},"brand_counts":{"type":"object","description":"Shown answers that give the statement to each tracked brand, keyed by brand id.","additionalProperties":{"type":"integer","minimum":0}},"another_product":{"type":"integer","minimum":0,"description":"Shown answers that give the statement to a product DevTune does not track."},"several":{"type":"integer","minimum":0,"description":"Shown answers that give the statement to several products."},"nobody":{"type":"integer","minimum":0,"description":"Shown answers that give the statement to no product."},"share_of_statement":{"type":["number","null"],"minimum":0,"maximum":1,"description":"The primary brand’s share in the previous window, computed as share_of_statement is. Null below ten assignments."}},"required":["scored","brand_counts","another_product","several","nobody","share_of_statement"]}},"required":["statement_id","statement","kind","status","series_started_on","eligible","scored","abstained","brand_counts","another_product","several","nobody","brands","share_of_statement"]},"PositioningMatchDailyCell":{"allOf":[{"$ref":"#/components/schemas/PositioningMatchCell"},{"type":"object","properties":{"competitor_set_id":{"type":"string","format":"uuid","description":"Competitor set used for this trend point. The zero UUID means no competitor set was recorded."},"is_current_set":{"type":"boolean","description":"Whether this trend point uses the current competitor set."}},"required":["day","competitor_set_id","is_current_set"]}]},"ConnectionContextData":{"type":"object","required":["project","credential","scopes","features","checkedAt"],"properties":{"project":{"type":"object","required":["id","name","paused","domains"],"description":"The project the credential is bound to.","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"},"paused":{"type":"boolean","description":"A paused project gives credentials read access only until it is resumed in the app."},"domains":{"type":"array","maxItems":500,"description":"The project's own domains, ordered by domain. Competitor domains are not included.","items":{"type":"object","required":["domain","type"],"properties":{"domain":{"type":"string","description":"Canonical host, with a path when the property lives under one."},"type":{"type":"string","enum":["docs","api-ref","blog","github","package","tutorial","changelog","community","forum","qna","social","review","examples","media","home","other"],"description":"The kind of site the domain serves, such as docs or blog."}}}}}},"credential":{"type":"object","required":["type"],"description":"The credential that made the request.","properties":{"type":{"type":"string","enum":["api_key","oauth"],"description":"api_key for an API key, oauth for an OAuth connection."}}},"scopes":{"type":"array","items":{"type":"string"},"description":"Scopes the credential may call now, sorted. A paused project keeps only read scopes and setup.write, which can resume it; an OAuth connection keeps only what both its approved grant and the user's current team permissions allow."},"features":{"type":"object","description":"Account features that gate operations independently of scopes. A scope whose feature is off still refuses.","required":["attributeMatch","agentPicks","contentDrafts","contentPullRequests","mcpCodeMode","mcpCodeModeWrites"],"properties":{"attributeMatch":{"type":"boolean","description":"Positioning and Accuracy reads."},"agentPicks":{"type":"boolean","description":"Agent Picks reads and settings."},"contentDrafts":{"type":"boolean","description":"Action draft reads and starts. Some Agent Picks actions can be drafted without it."},"contentPullRequests":{"type":"boolean","description":"Content sources and draft pull requests."},"mcpCodeMode":{"type":"boolean","description":"MCP code mode and its execution receipts. Always true; every account has code mode."},"mcpCodeModeWrites":{"type":"boolean","description":"Approved project writes in MCP code mode. Always true; each write still needs its scope and human approval."}}},"checkedAt":{"type":"string","format":"date-time","description":"When access was read."}}},"ProjectUsageData":{"type":"object","required":["projectId","paused","prompts","credits","aiTrafficEvents","trackedPages","deletion"],"properties":{"projectId":{"type":"string","format":"uuid"},"paused":{"type":"boolean","description":"A paused project starts no probes, agents, crawls, audits or data syncs and drops new AI Traffic. Credentials keep read access only until it is resumed in the app. Its prompts and project slot do not count against the account while paused."},"prompts":{"type":"object","required":["active"],"properties":{"active":{"type":"integer","minimum":0}}},"credits":{"type":"object","required":["used","periodStart","periodEnd"],"description":"Credits spent in the account credit period, including credits held by running work until it settles.","properties":{"used":{"type":"number","minimum":0},"periodStart":{"type":"string","format":"date","description":"First day of the credit period."},"periodEnd":{"type":"string","format":"date","description":"Exclusive end of the period."}}},"aiTrafficEvents":{"type":"object","required":["count","periodStart","periodEnd"],"description":"AI Traffic events this UTC calendar month: AI crawler, assistant and answer-fetch requests, plus visits from AI answers. Other human visits are not counted. Across projects this can differ slightly from the account’s usage meter, which counts events as they arrive.","properties":{"count":{"type":"integer","minimum":0},"periodStart":{"type":"string","format":"date","description":"First day of the month."},"periodEnd":{"type":"string","format":"date","description":"Exclusive end of the period."}}},"trackedPages":{"type":["object","null"],"required":["used","limit"],"description":"Tracked pages against the page limit. Null until the first page-tracking status is computed.","properties":{"used":{"type":"integer","minimum":0},"limit":{"type":"integer","minimum":0,"description":"The most pages the project can track on its plan."}}},"deletion":{"type":"object","required":["projectDeletesAt","accountDeletesAt"],"description":"When inactive data is scheduled to be deleted. A paused project is deleted six months after it was paused; a team account is deleted after its plan or trial ends, with its projects. Both are null when nothing is scheduled. Resuming the project or resubscribing cancels the deletion.","properties":{"projectDeletesAt":{"type":["string","null"],"format":"date-time","description":"When this project will be deleted, or null if it is not scheduled."},"accountDeletesAt":{"type":["string","null"],"format":"date-time","description":"When the team account and all its projects will be deleted, or null if it is not scheduled."}}}}},"AgentPickRate":{"type":"object","properties":{"outcome":{"type":"string","enum":["installed","built_its_own","no_provider"],"description":"installed: the agent installed tool_key. built_its_own: it wrote its own solution. no_provider: it wired in a hosted service (a database or mail server URL the user fills in) without naming a provider. built_its_own and no_provider are separate rates, never merged."},"tool_key":{"type":["string","null"],"description":"Null when the agent built its own or chose no provider; outcome says which."},"picks":{"type":"integer","minimum":0,"description":"Completed runs that picked it: every installed outcome, a stated pick (named as the agent’s choice without being written into the code) included. A count; over several scenarios picks / runs is not the rate."},"runs":{"type":"integer","minimum":0,"description":"Completed runs in the scope, summed over its scenarios."},"rate":{"type":["number","null"],"minimum":0,"maximum":1,"description":"The pick rate and the headline rate: each scenario’s picks over its completed runs, averaged over the scenarios, so a scenario counts once however many runs it took. In one scenario, picks / runs. Null when there are no runs."},"rate_low":{"type":["number","null"],"minimum":0,"maximum":1,"description":"Lower bound of the Wilson 90% interval, over the scenarios (in one scenario, over its runs). Null when there are no runs."},"rate_high":{"type":["number","null"],"minimum":0,"maximum":1,"description":"Upper bound of the Wilson 90% interval, over the scenarios (in one scenario, over its runs). Null when there are no runs."},"scenarios":{"type":"integer","minimum":0,"description":"Scenarios (test app, request, agent and model) with completed runs: the scenarios the rate averages and its range counts."},"directional":{"type":"boolean","description":"True when the rate rests on fewer than 10 scenarios (in one scenario, fewer than 10 runs): report it as directional."},"stated_picks":{"type":"integer","minimum":0,"description":"Of picks, the runs that named the tool as their pick without writing it into the code. Always 0 for built_its_own and no_provider."},"installs":{"type":["integer","null"],"minimum":0,"description":"Completed runs that wrote the tool into the code: picks - stated_picks. Null for built_its_own and no_provider, which name no tool."},"install_rate":{"type":["number","null"],"minimum":0,"maximum":1,"description":"The install rate, shown beside the pick rate: each scenario’s installs over its runs, averaged over the scenarios like rate. Null for built_its_own and no_provider, when there are no runs, or when the response’s install_rate_hidden_reason is set."},"install_rate_low":{"type":["number","null"],"minimum":0,"maximum":1,"description":"Lower bound of the install rate’s Wilson 90% interval; null with install_rate."},"install_rate_high":{"type":["number","null"],"minimum":0,"maximum":1,"description":"Upper bound of the install rate’s Wilson 90% interval; null with install_rate."},"found":{"oneOf":[{"$ref":"#/components/schemas/AgentPicksFoundIn"},{"type":"null"}],"description":"Found in: how many completed runs reached the tool (searched for it, fetched its site or a package of it, or tried to install it). Never feeds the pick rate. Null for built_its_own and no_provider, a tool the test did not record, and a test with no Found in measurement, such as one completed before it was recorded."}},"required":["outcome","tool_key","picks","runs","rate","rate_low","rate_high","scenarios","directional","stated_picks","installs","install_rate","install_rate_low","install_rate_high","found"]},"AgentPicksFoundIn":{"type":"object","description":"Found in over the rate’s runs: of the completed runs, how many reached the tool before choosing, and how many had no trail to decide from. Model-side web search and fetch bypass the sandbox, so a run that did not reach the tool was not seen reaching it in the sandbox.","required":["completed_runs","reached_runs","unavailable_runs","rate","rate_low","rate_high","scenarios"],"properties":{"completed_runs":{"type":"integer","minimum":1,"description":"Completed runs Found in is over."},"reached_runs":{"type":"integer","minimum":0,"description":"Completed runs whose trail searched for the tool, fetched its site or a registry package of it, or tried to install it."},"unavailable_runs":{"type":"integer","minimum":0,"description":"Completed runs whose trail had no events to decide from. They are left out of the rate."},"rate":{"type":["number","null"],"minimum":0,"maximum":1,"description":"In one scenario, reached_runs / (completed_runs - unavailable_runs); over several, each scenario’s rate averaged, so a scenario counts once. Null when every completed run is unavailable."},"rate_low":{"type":["number","null"],"minimum":0,"maximum":1,"description":"Lower bound of rate's Wilson 90% range. Null when rate is."},"rate_high":{"type":["number","null"],"minimum":0,"maximum":1,"description":"Upper bound of rate's Wilson 90% range. Null when rate is."},"scenarios":{"type":"integer","minimum":0,"description":"Scenarios with a run that had a trail: the scenarios rate averages and its range counts. 1 for a cell."}}},"AgentPickCoverage":{"type":"object","description":"How many of the planned runs completed. Rates use completed runs only.","properties":{"planned":{"type":"integer","minimum":0,"description":"Every run the test queued."},"completed":{"type":"integer","minimum":0,"description":"Runs that completed. Rates use these only."},"failed":{"type":"integer","minimum":0,"description":"Runs that ran without completing."},"not_run":{"type":"integer","minimum":0,"description":"Runs cancelled before they started because the test reached its budget."},"fallback_runs":{"type":"integer","minimum":1,"description":"Completed runs whose pick was an unmatched-code guess, which loss verdicts leave out. Absent when there are none."}},"required":["planned","completed","failed","not_run"]},"AgentPicksRunList":{"type":"object","additionalProperties":false,"required":["items","pagination","setup","self_run_uploads"],"properties":{"setup":{"$ref":"#/components/schemas/AgentPicksSetupUsed"},"self_run_uploads":{"description":"The project's self-run uploads switch on Local Runner, which a member who manages billing and is on the project changes in the project's Agent Picks settings; uploads to a project whose switch is off are refused with project_uploads_off. Null on every other plan: Cloud and Enterprise accept uploads to every project, and Plus and Pro accept none.","type":["object","null"],"additionalProperties":false,"required":["on","turns_off_at"],"properties":{"on":{"type":"boolean","description":"Whether uploads are on, including while a turn-off is pending."},"turns_off_at":{"type":["string","null"],"description":"When a pending turn-off takes effect: the end of the Agent Picks subscription period. Null while uploads stay on, and once they are off; without an Agent Picks subscription a turn-off takes effect at once, so none is pending."}}},"items":{"description":"Runs, newest first.","type":"array","items":{"type":"object","additionalProperties":false,"required":["id","test_id","test_kind","test_status","agent","model","stage","model_calls","repetition","status","created_at","started_at","completed_at","cell_agreed","origin","series"],"properties":{"id":{"type":"string","format":"uuid"},"test_id":{"type":"string","format":"uuid"},"test_kind":{"description":"What started the test: manual, scheduled, model_release, starter or retest.","type":"string"},"test_status":{"type":"string","enum":["queued","running","failed","cancelled","completed"]},"agent":{"description":"The coding agent, such as claude_code or codex.","type":"string"},"stage":{"type":["string","null"],"enum":["sandbox","setup","agent","judging",null],"description":"Current runner stage; null for queued, finished or older runs."},"model_calls":{"type":["integer","null"],"description":"Model calls the run made over all attempts; null when no activity was recorded."},"model":{"type":"string"},"repetition":{"description":"The run's repetition number, counting from 1. In a re-test of several cells, the numbers run across all its cells in turn.","type":"integer"},"status":{"description":"Where the run is. completed, failed and cancelled are final.","type":"string","enum":["queued","running","failed","cancelled","completed"]},"created_at":{"type":"string","format":"date-time"},"started_at":{"description":"When the run started. Null while it is queued, and for a run cancelled or failed before it started.","type":["string","null"]},"completed_at":{"description":"When the run ended. Null until then.","type":["string","null"]},"origin":{"$ref":"#/components/schemas/AgentPicksOrigin"},"series":{"$ref":"#/components/schemas/AgentPicksRunSeries"},"cell_agreed":{"description":"True when the run was cancelled before it started because its cell’s first 4 runs picked the same thing. It was not needed: not an error, not missing coverage, and not charged.","type":"boolean"}}}},"pagination":{"description":"Where this page sits in the runs.","type":"object","additionalProperties":false,"required":["hasMore","nextCursor","has_more","next_cursor","limit"],"properties":{"hasMore":{"type":"boolean","description":"More rows follow this page."},"nextCursor":{"type":["string","null"],"description":"Pass as cursor to read the next page. Null on the last page."},"has_more":{"description":"More rows follow this page.","type":"boolean"},"next_cursor":{"description":"Pass as cursor to read the next page. Null on the last page.","type":["string","null"]},"limit":{"description":"Rows per page, as requested.","type":"integer","minimum":1,"maximum":100}}}}},"AgentPicksRunTrace":{"type":"object","additionalProperties":false,"required":["availability","attempts","items","unplaced","unplaced_total","unplaced_pagination","totals","pagination"],"properties":{"availability":{"type":"string","enum":["stored","pending","not_retained"],"description":"stored: the run has stored lines. pending: none yet, because the run is still running or finished within the last hour, or recovery is pending. not_retained: none, and none is expected, because the run finished over an hour ago without stored lines, as older runs did."},"attempts":{"type":"array","description":"Each attempt the run started, whether its lines are stored, and recovery status: pending, stored, unavailable, or null when unknown. Pending recovery keeps the trace incomplete until it finishes.","items":{"type":"object","additionalProperties":false,"required":["attempt","stored","recovery"],"properties":{"attempt":{"type":"integer","minimum":1,"description":"The attempt, counting from 1."},"stored":{"type":"boolean","description":"Whether the attempt's lines are stored."},"recovery":{"description":"Recovery of the attempt's lines: pending, stored or unavailable. Null when unknown.","type":["string","null"],"enum":["pending","stored","unavailable",null]}}}},"items":{"description":"The trace lines on this page, in order, with model calls placed after the line they follow.","type":"array","items":{"type":"object","additionalProperties":false,"required":["attempt","seq","at","kind","preview","preview_truncated","detail"],"properties":{"attempt":{"type":"integer","minimum":1},"seq":{"type":"integer","minimum":1,"description":"The line within its attempt. A model_call item carries the seq of the line it follows."},"at":{"type":"string","format":"date-time","description":"The time the agent gave the line. Lines it did not time, including the prompt, setup output and every claude_code and codex line, carry the run's start time. A model_call item carries when the call finished."},"kind":{"description":"What the line is: prompt, narration, tool_call, tool_result, file_write, file_edit, error or model_call.","type":"string","enum":["prompt","narration","tool_call","tool_result","file_write","file_edit","error","model_call"]},"preview":{"type":"string","description":"The line's first 65,536 characters, redacted. Empty for a model_call item."},"preview_truncated":{"type":"boolean","description":"The stored text is longer than the preview."},"text":{"type":"string","description":"The line's whole stored text, up to 1 MiB (detail.truncated beyond). Only with full=true."},"detail":{"type":"object","additionalProperties":true,"description":"Tool name, arguments summary, path, command, URL, status, exit code, line counts, packages and provider ids when the agent reported them. A model_call item has model_call_id, response_id, tool_call_ids, status, tokens, started_at and finished_at."}}}},"unplaced":{"description":"Model calls that could not be placed among the lines, up to 200 a page.","type":"array","maxItems":500,"items":{"type":"object","additionalProperties":false,"required":["model_call_id","attempt","reason","response_id","tool_call_ids","status","input_tokens","output_tokens","cache_read_tokens","cache_write_tokens","started_at","finished_at"],"properties":{"model_call_id":{"type":"string","format":"uuid"},"attempt":{"type":["integer","null"],"description":"The attempt that made the call. Null when not recorded."},"reason":{"description":"failed: the call failed. no_ids: the call carries no ids to match a line, or the run's agent is codex, whose lines carry none. pending: the run is still going. unmatched: no line matched it.","type":"string","enum":["failed","no_ids","pending","unmatched"]},"response_id":{"type":["string","null"],"description":"The model provider's response id. Null when it reported none."},"tool_call_ids":{"type":"array","items":{"type":"string"},"description":"The tool calls the response asked for."},"status":{"type":"string","enum":["completed","failed"]},"input_tokens":{"type":"integer","description":"Uncached input tokens the call used. Cached input is in cache_read_tokens and cache_write_tokens."},"output_tokens":{"type":"integer"},"cache_read_tokens":{"type":"integer","description":"Input tokens read from the provider's cache."},"cache_write_tokens":{"type":"integer","description":"Input tokens written to the provider's cache."},"started_at":{"type":"string","format":"date-time"},"finished_at":{"type":"string","format":"date-time"}}}},"unplaced_total":{"type":["integer","null"],"minimum":0,"description":"Null on a page that carries no unplaced calls."},"unplaced_pagination":{"type":["object","null"],"additionalProperties":false,"required":["after_id","has_more"],"description":"Null on a page that carries no unplaced calls. Pass after_id as after_unplaced for the next 200.","properties":{"after_id":{"type":["string","null"],"format":"uuid","description":"Pass as after_unplaced for the next page while has_more is true. Null when this page carries no unplaced calls."},"has_more":{"type":"boolean","description":"More unplaced calls follow."}}},"totals":{"type":"object","additionalProperties":false,"description":"Every model call the run made, placed or not.","required":["calls_started","calls_finished","input_tokens","output_tokens","cache_read_tokens","cache_write_tokens"],"properties":{"calls_started":{"type":"integer"},"calls_finished":{"type":"integer"},"input_tokens":{"type":"integer","description":"Uncached input tokens across every call. Cached input is in cache_read_tokens and cache_write_tokens."},"output_tokens":{"type":"integer","description":"Output tokens across every call."},"cache_read_tokens":{"type":"integer","description":"Cached input tokens read across every call."},"cache_write_tokens":{"type":"integer","description":"Cached input tokens written across every call."}}},"pagination":{"description":"Where this page sits in the lines.","type":"object","additionalProperties":false,"required":["after_attempt","after_seq","has_more","limit"],"properties":{"after_attempt":{"type":"integer","description":"Pass as after_attempt, with after_seq, for the next page."},"after_seq":{"type":"integer","description":"Pass as after_seq for the next page."},"has_more":{"type":"boolean","description":"More lines follow this page."},"limit":{"type":"integer","description":"Lines per page, as requested."}}}}},"AgentPicksRunDetail":{"type":"object","additionalProperties":false,"required":["run","context","pick","integration","evidence","pagination"],"properties":{"pagination":{"$ref":"#/components/schemas/ListPagination"},"run":{"type":"object","additionalProperties":false,"required":["id","test_id","test_kind","test_status","agent","model","stage","model_calls","repetition","status","created_at","started_at","completed_at","early_stopped","wall_ms","has_error","cell_agreed","origin","series"],"properties":{"origin":{"$ref":"#/components/schemas/AgentPicksOrigin"},"series":{"$ref":"#/components/schemas/AgentPicksRunSeries"},"id":{"type":"string","format":"uuid"},"test_id":{"type":"string","format":"uuid"},"test_kind":{"description":"What started the test: manual, scheduled, model_release, starter or retest.","type":"string"},"test_status":{"type":"string","enum":["queued","running","failed","cancelled","completed"]},"agent":{"description":"The coding agent, such as claude_code or codex.","type":"string"},"stage":{"type":["string","null"],"enum":["sandbox","setup","agent","judging",null],"description":"Current runner stage; null for queued, finished or older runs."},"model_calls":{"type":["integer","null"],"description":"Model calls the run made over all attempts; null when no activity was recorded."},"model":{"type":"string"},"repetition":{"description":"Which repetition of its cell the run is, counting from 1. In a re-test that covers several cells, the run's position in the whole re-test, from 1 to 40.","type":"integer"},"status":{"description":"Where the run is. completed, failed and cancelled are final.","type":"string","enum":["queued","running","failed","cancelled","completed"]},"created_at":{"type":"string","format":"date-time"},"started_at":{"description":"When the run started. Null while it is queued, and for a run cancelled or failed before it started.","type":["string","null"]},"completed_at":{"description":"When the run ended. Null until then.","type":["string","null"]},"early_stopped":{"description":"True when the runner stopped the agent before it exited on its own.","type":"boolean"},"wall_ms":{"description":"How long the agent ran, in milliseconds. Null when not recorded.","type":["number","null"]},"has_error":{"description":"True when the run recorded an error. False for a cell_agreed run, whose cancel is not an error.","type":"boolean"},"cell_agreed":{"description":"True when the run was cancelled before it started because its cell’s first 4 runs picked the same thing. It was not needed: not an error, not missing coverage, and not charged.","type":"boolean"}}},"context":{"description":"What the run was given. The request, intent, job, classifier version and time limit are as recorded when its test was queued; environment is the catalogue's current entry for the run's test app.","type":"object","additionalProperties":false,"required":["environment","request","request_withheld","job_withheld","intent","job","classifier_version","agent_timeout_sec"],"properties":{"environment":{"description":"The test app the run used.","anyOf":[{"type":"object","additionalProperties":false,"required":["id","slug","name","framework"],"properties":{"id":{"type":"string"},"slug":{"type":"string"},"name":{"type":"string"},"framework":{"description":"The framework. Null for the empty folder.","type":["string","null"]}}},{"type":"null"}]},"request_withheld":{"type":"boolean","description":"True when the request text exists but is not returned."},"job_withheld":{"type":"boolean","description":"True when the job description exists but is not returned."},"request":{"description":"Always null: the request text is withheld.","type":["string","null"]},"intent":{"description":"The request intent: recommend, add, hosted or improve. Null when not recorded.","type":["string","null"]},"job":{"description":"Always null: the job description is withheld.","type":["string","null"]},"classifier_version":{"description":"The classifier version the test used. Null when not recorded.","type":["string","null"]},"agent_timeout_sec":{"description":"How long the agent was allowed to run, in seconds. Null when not recorded.","type":["number","null"]}}},"pick":{"description":"What the agent picked, as the classifier judged it. Null until the run is classified.","anyOf":[{"type":"object","additionalProperties":false,"required":["outcome","tool_key","tool_display_name","tool_brand_domain","confidence","classifier_version","classifier_accuracy","category_precision","decided_by","tool_packages","tool_packages_withheld","other_tools","other_tools_withheld","overlapping_tools","custom_job","diff_truncated"],"properties":{"outcome":{"description":"What the agent did: installed, built_its_own, no_provider, other_tool, none or unclassified.","type":"string"},"tool_key":{"description":"The tool the agent installed. Null unless outcome is installed.","type":["string","null"]},"tool_display_name":{"type":["string","null"],"description":"That tool's name. Null when there is no tool or no name is recorded."},"tool_brand_domain":{"type":["string","null"],"description":"That tool's website domain. Null when none is recorded."},"confidence":{"description":"How sure the classifier was, from 0 to 1. Null when not recorded.","type":["number","null"]},"classifier_version":{"description":"The classifier version that judged the run.","type":"string"},"classifier_accuracy":{"anyOf":[{"$ref":"#/components/schemas/AgentPicksClassifierAccuracy"},{"type":"null"}]},"category_precision":{"anyOf":[{"$ref":"#/components/schemas/AgentPicksCategoryPrecision"},{"type":"null"}]},"decided_by":{"description":"What decided the outcome, such as package, stated_pick, config or nothing_found. Null when not recorded.","type":["string","null"]},"statement_index":{"type":"integer","minimum":0,"maximum":10000,"description":"Optional sentence index in the redacted final answer; absent when no statement evidence is retained."},"tool_packages_withheld":{"type":"boolean","description":"True when some packages the agent added are left out of tool_packages because they are not the tool's known packages."},"tool_packages":{"description":"The tool's known packages the agent added, as ecosystem:name.","type":"array","items":{"type":"string"}},"other_tools_withheld":{"type":"boolean","description":"True when the agent used other tools, which are not named."},"other_tools":{"type":"array","items":{"type":"string"},"description":"Always empty: other tools are withheld."},"overlapping_tools":{"type":"array","description":"When decided_by is ambiguous, the test’s tools the run used, none of which decided the pick; empty otherwise. On a custom job, saying which runs inside the other (signals.yieldsTo) decides such runs in later tests.","items":{"type":"object","additionalProperties":false,"required":["tool_key","display_name"],"properties":{"tool_key":{"type":"string","description":"A tool among several the run installed, when the classifier could not pick one."},"display_name":{"type":"string"}}}},"custom_job":{"type":"boolean","description":"Whether the run’s test is the project’s own job and that job is still the project’s setup, so its tools and relationships can be edited. False for a ready-made category, and for a job the project has since switched away from."},"diff_truncated":{"description":"True when the agent's code changes were too long to read in full.","type":"boolean"}}},{"type":"null"}]},"integration":{"description":"Recorded observations for the picked tool, independent of the event page. Null for legacy runs or unavailable facts. Archive downloads do not prove installation; docs counts cover recognised documentation hosts/paths under the confirmed vendor domains saved for this run. Request schemes and ports are not retained, so docs identify hosts only. For scheduled project tests and DevTune board tests, registry.latest_version is registry metadata captured before queue insertion, valid only at registry.captured_at; it does not describe current registry state. New on-demand tests, free starter tests and Action re-tests do not capture registry metadata; comparison and deprecation remain unknown with no_snapshot. Previously retained snapshots remain readable. A comparison of differs does not mean outdated. These observations do not establish functional success or working use.","anyOf":[{"type":"object","additionalProperties":false,"required":["version","tool_key","coverage","packages","vendor_commands","docs","truncated"],"properties":{"version":{"type":"integer","const":2},"tool_key":{"type":"string","description":"The installed tool the observations are about."},"coverage":{"description":"Whether the agent transcript and the network log were captured in full. Where one was not, what it does not show is unknown, not no.","type":"object","additionalProperties":false,"required":["transcript","proxy"],"properties":{"transcript":{"type":"boolean","description":"True when the agent transcript was captured in full. False when it is missing or incomplete, including an OpenCode run that handed work to a child session."},"proxy":{"type":"boolean","description":"True when the run's network log was captured in full. False when it is missing, has an unreadable line or recorded a failed TLS connection."}}},"packages":{"description":"The tool's packages the run's install log or downloads show, with the version seen. A download alone does not prove an install.","type":"array","maxItems":100,"items":{"type":"object","additionalProperties":false,"required":["name","version","source","registry"],"properties":{"registry":{"description":"How the installed version compares with the package registry.","type":"object","additionalProperties":false,"required":["captured_at","source","latest_version","comparison","comparison_reason","deprecated","deprecation_reason"],"properties":{"captured_at":{"description":"When the registry was read. Null when it was not.","type":["string","null"],"format":"date-time"},"source":{"description":"The registry read: registry.npmjs.org, pypi.org or rubygems.org. Null when none was.","type":["string","null"],"enum":["registry.npmjs.org","pypi.org","rubygems.org",null]},"latest_version":{"type":["string","null"],"description":"The registry's latest version then. Null when the registry did not return one."},"comparison":{"description":"matches_registry_latest, differs, or unknown.","type":"string","enum":["matches_registry_latest","differs","unknown"]},"comparison_reason":{"type":"string","enum":["available","unavailable","unsupported","wildcard","malformed","capped","timeout","not_found","lookup_identity_unknown","redirected","no_snapshot","no_observed_version","version_not_captured","non_version","compared","version_captured"]},"deprecated":{"type":["boolean","null"],"description":"Whether npm marks the installed version deprecated. Null when not checked."},"deprecation_reason":{"description":"Why deprecated is what it is.","type":"string","enum":["available","unavailable","unsupported","wildcard","malformed","capped","timeout","not_found","lookup_identity_unknown","redirected","no_snapshot","no_observed_version","version_not_captured","non_version","compared","version_captured"]}}},"name":{"type":"string"},"version":{"type":["string","null"],"description":"The installed version. Null when source is conflicting."},"source":{"description":"Where the version was seen: install_log when the install log names it, download when only a download does, or conflicting when the sightings name different versions and none is clearly the latest.","type":"string","enum":["install_log","download","conflicting"]}}}},"vendor_commands":{"description":"The tool's own package commands that ran successfully.","type":"array","maxItems":100,"items":{"type":"object","additionalProperties":false,"required":["package","count"],"properties":{"package":{"type":"string","description":"The package whose command ran."},"count":{"type":"integer","minimum":0,"description":"How many times it ran successfully."}}}},"docs":{"description":"Requests for the tool's documentation pages in the run's network log, by host: a docs host such as docs.example.com, or a docs path such as /docs on the tool's domain.","type":"array","maxItems":100,"items":{"type":"object","additionalProperties":false,"required":["host","requests","failures","repeated_requests"],"properties":{"host":{"type":"string"},"requests":{"type":"integer","minimum":0,"description":"Documentation page requests to it."},"failures":{"type":"integer","minimum":0,"description":"Of those, failed requests."},"repeated_requests":{"type":"integer","minimum":0,"description":"Of those, repeats of an earlier request."}}}},"truncated":{"type":"boolean","description":"True when some observations were left out."}}},{"type":"null"}]},"evidence":{"description":"What the agent did during the run, step by step.","type":"object","additionalProperties":false,"required":["availability","items","pagination"],"properties":{"availability":{"description":"recorded when steps are returned, end_of_trail when a later page has none, not_retained when the run kept none.","type":"string","enum":["recorded","not_retained","end_of_trail"]},"items":{"description":"Steps in order.","type":"array","items":{"type":"object","additionalProperties":false,"required":["id","seq","kind","url","status_code","recorded_at","observed_at","detail"],"properties":{"id":{"type":"string","format":"uuid"},"seq":{"description":"The step's position in the run.","type":"integer"},"kind":{"description":"search, fetch, read, command, install or write.","type":"string","enum":["search","fetch","read","command","install","write"]},"url":{"description":"The origin a fetch went to. Null for other steps.","type":["string","null"]},"status_code":{"description":"The HTTP status of a fetch. Null when none.","type":["number","null"]},"recorded_at":{"type":"string"},"observed_at":{"description":"When the step happened in the run, in epoch milliseconds. Null for a step from a transcript without timestamps, such as Codex's; a step seen only in the network log keeps its time.","type":["number","null"]},"detail":{"description":"What the step shows; most text is withheld.","type":"object","additionalProperties":false,"required":["queries","path","packages","source","method","exit_code","failed","command_withheld","text_withheld","packages_withheld"],"properties":{"queries":{"description":"Always empty: search queries are withheld.","type":"array","items":{"type":"string"}},"path":{"description":"Always null: file paths are withheld.","type":["string","null"]},"packages":{"description":"For an install, the tool's known packages installed.","type":"array","items":{"type":"string"}},"source":{"description":"For a fetch, what made it: fetch_tool, command, provider or proxy. Null otherwise.","type":["string","null"]},"method":{"description":"For a fetch the network log recorded, the HTTP method. Null otherwise.","type":["string","null"]},"exit_code":{"description":"For a command, its exit code when the agent reported one. Null otherwise.","type":["number","null"]},"failed":{"description":"For a command, whether it failed, when the agent reported its result. Null otherwise.","type":["boolean","null"]},"text_withheld":{"type":"boolean","description":"True for a search, read or write, whose text is withheld."},"packages_withheld":{"type":"boolean","description":"True when an install named packages that are not the tool's known packages."},"command_withheld":{"description":"True for a command or install, whose command line is withheld.","type":"boolean"}}}}}},"pagination":{"type":"object","additionalProperties":false,"required":["has_more","next_cursor","limit"],"properties":{"has_more":{"description":"More steps follow this page.","type":"boolean"},"next_cursor":{"description":"Pass as cursor to read the next page. Null on the last page.","type":["string","null"]},"limit":{"description":"Steps per page, as requested.","type":"integer","minimum":1,"maximum":100}}}}}}},"AgentPicksTestProject":{"type":"object","description":"A test app a test added or removed against the full test before it. id and framework are null for a test app no longer in the catalogue, whose name is then its slug.","required":["id","slug","name","framework"],"properties":{"id":{"type":["string","null"],"format":"uuid"},"slug":{"type":"string"},"name":{"type":"string"},"framework":{"type":["string","null"]}}},"AgentPicksRequestChange":{"type":"object","description":"A request whose text changed against the full test before it. before is null for a request added or asked again; after is null for a request left out.","required":["intent","before","after"],"properties":{"intent":{"type":"string","enum":["add","recommend","hosted","improve"]},"before":{"type":["string","null"]},"after":{"type":["string","null"]}}},"AgentPicksEndingChange":{"type":["object","null"],"description":"How requests end, before and after, when it changed against the full test before it; a test from before the ending was recorded reads as DevTune’s ending. Null when unchanged, for a directional test or a re-test, and for a first full test.","required":["before","after"],"properties":{"before":{"type":"string"},"after":{"type":"string"}}},"AgentPicksOrigin":{"type":"string","enum":["managed","self_run"],"description":"Where the runs ran: managed, in DevTune’s sandboxes, or self_run, on the customer’s own machine with the agentpicks CLI and uploaded. A self-run is never counted in a managed rate."},"AgentPicksTestSeries":{"type":["object","null"],"additionalProperties":false,"description":"A self-run test’s series: the context its runs share. Rates within a self-run test compare runs of this series only. Null for a managed test.","required":["key","period","agent","agent_version","model","stack","lock_digest","repository","category","intent"],"properties":{"key":{"type":"string","description":"The series key: a SHA-256, in hex, of the agent, version, model, starter and lock digest (or repository), category and request intent. Runs with the same key are one series."},"period":{"type":"string","format":"date","description":"The first day of the UTC month the test holds. A series has one test per month."},"agent":{"type":"string","description":"The coding agent the runs used, such as claude_code."},"agent_version":{"type":["string","null"],"description":"The agent version installed where the runs ran."},"model":{"type":"string","description":"The model the agent was asked for; default is the agent’s own default."},"stack":{"type":["string","null"],"description":"The starter test app the runs were laid down from. Null for a series of runs on the customer’s repository."},"lock_digest":{"type":["string","null"],"description":"SHA-256 of the starter’s committed files and lockfiles. Null for a repository series."},"repository":{"type":["string","null"],"description":"The customer’s repository, as host and path. Null for a starter series."},"category":{"type":["string","null"],"description":"The slug of the category or project job the runs were given a request from."},"intent":{"type":"string","description":"The intent of that request: add, recommend, hosted or improve."}}},"AgentPicksRunSeries":{"type":["object","null"],"additionalProperties":false,"description":"A self-run run’s series, as its test’s, with the commit it tested. Null for a managed run.","required":["key","period","agent","agent_version","model","stack","lock_digest","repository","category","intent","commit"],"properties":{"key":{"type":"string","description":"The series key: a SHA-256, in hex, of the agent, version, model, starter and lock digest (or repository), category and request intent. Runs with the same key are one series."},"period":{"type":"string","format":"date","description":"The first day of the UTC month the test holds. A series has one test per month."},"agent":{"type":"string","description":"The coding agent the runs used, such as claude_code."},"agent_version":{"type":["string","null"],"description":"The agent version installed where the runs ran."},"model":{"type":"string","description":"The model the agent was asked for; default is the agent’s own default."},"stack":{"type":["string","null"],"description":"The starter test app the runs were laid down from. Null for a series of runs on the customer’s repository."},"lock_digest":{"type":["string","null"],"description":"SHA-256 of the starter’s committed files and lockfiles. Null for a repository series."},"repository":{"type":["string","null"],"description":"The customer’s repository, as host and path. Null for a starter series."},"category":{"type":["string","null"],"description":"The slug of the category or project job the runs were given a request from."},"intent":{"type":"string","description":"The intent of that request: add, recommend, hosted or improve."},"commit":{"type":["string","null"],"description":"The repository commit the run tested. It is not part of the series key, so a series following a branch spans commits. Null for a starter run."}}},"AgentPicksSetupUsed":{"type":["object","null"],"description":"The Agent Picks setup this response used: its id and its category slug (null for a setup saved without a category). null when the project has no setup.","required":["id","category_slug"],"properties":{"id":{"type":"string","format":"uuid"},"category_slug":{"type":["string","null"],"description":"The setup’s category slug. Null for a setup saved without a category."}}},"AgentPicksQuoteData":{"type":"object","required":["purpose","estimate","excluded_agents","quote_token","expires_at","setup"],"properties":{"setup":{"$ref":"#/components/schemas/AgentPicksSetupUsed"},"purpose":{"type":"string","enum":["paid_test","schedule"],"description":"paid_test for a test or re-test quote, schedule for one run of a schedule."},"test_id":{"type":"string","format":"uuid","description":"The id the start reserves under, for a test quote. Follow it with the test read."},"action_id":{"type":["string","null"],"format":"uuid","description":"The Action a re-test quote measures. Null for any other quote."},"cadence":{"type":"string","enum":["daily","weekly","monthly"],"description":"The cadence a schedule quote enables."},"estimate":{"description":"What the quoted test or run is expected to cost.","type":"object","required":["expected","high","runs"],"properties":{"expected":{"type":"number","description":"Credits. A Full test, or a Custom test at 8 runs per cell (no preset), assumes cells stop early, at about 4.6 runs per cell; a cell re-run at 8 does not stop early."},"high":{"type":"number","description":"Credits the start reserves, pricing every planned run; settlement never charges more."},"runs":{"type":"integer","description":"Planned runs the quote covers. In a Full test, or a Custom test at 8 runs per cell (no preset), a cell stops after 4 runs that agree, so fewer may run."}}},"excluded_agents":{"type":"array","items":{"type":"string"},"description":"Saved agents the account’s Agent Picks plan does not include, which the test leaves out."},"quote_token":{"type":"string","description":"Signed for this API key or OAuth connection and project. Works once."},"expires_at":{"type":"string","format":"date-time","description":"When quote_token stops working."}}},"AgentPicksStartedTestData":{"type":"object","required":["status","test_id","setup"],"properties":{"setup":{"$ref":"#/components/schemas/AgentPicksSetupUsed"},"status":{"type":"string","enum":["queued","existing"],"description":"existing when a retry with the same quote token found the test it already queued; nothing more is reserved."},"test_id":{"type":"string","format":"uuid"}}},"AgentPicksBundleManifest":{"type":"object","description":"A self-run bundle's manifest.json as the agentpicks CLI writes it. Its context is reported by the customer's machine and not verified. It names stack or repository, never both.","required":["bundleVersion","runId","runner","agent","host","sandbox","request","startedAt","finishedAt","artifacts"],"properties":{"bundleVersion":{"type":"integer","enum":[1]},"runId":{"type":"string","format":"uuid","description":"Generated by the runner; the upload is keyed on it."},"runner":{"type":"object","required":["name","version"],"properties":{"name":{"type":"string","enum":["@devtune/agentpicks"]},"version":{"type":"string"}}},"agent":{"type":"object","required":["kind","version","model"],"properties":{"kind":{"type":"string"},"version":{"type":"string"},"model":{"type":"string"}}},"host":{"type":"object","required":["os","arch"],"properties":{"os":{"type":"string"},"arch":{"type":"string"}}},"sandbox":{"type":"object","required":["provider","imageDigest","egressIp"],"properties":{"provider":{"type":"string","enum":["docker"]},"imageDigest":{"type":"string"},"egressIp":{"type":["string","null"]}}},"stack":{"type":"object","required":["slug","starter","lockDigest"],"properties":{"slug":{"type":"string"},"starter":{"type":"string"},"lockDigest":{"type":"string"}}},"repository":{"type":"object","required":["remoteUrl","commit"],"properties":{"remoteUrl":{"type":["string","null"]},"commit":{"type":"string"}}},"request":{"type":"object","required":["category","intent","prompt"],"properties":{"category":{"type":"string","description":"A published category's slug, or one of the project's jobs."},"intent":{"type":"string","enum":["add","recommend","hosted","improve"]},"prompt":{"type":"string"}}},"startedAt":{"type":"string","format":"date-time"},"finishedAt":{"type":"string","format":"date-time"},"artifacts":{"type":"array","items":{"type":"object","required":["name","bytes","sha256"],"properties":{"name":{"type":"string"},"bytes":{"type":"integer","minimum":0},"sha256":{"type":"string"}}}}}},"AgentPicksUploadData":{"type":"object","required":["run_id","status","expires_at","completed_at","run"],"properties":{"run_id":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["open","completed","expired"],"description":"open: artifacts are uploading. completed: the run is recorded. expired: not completed within 24 hours, or an artifact was stored at the wrong size."},"expires_at":{"type":"string","format":"date-time"},"completed_at":{"type":["string","null"],"format":"date-time"},"run":{"type":["object","null"],"description":"The recorded run; null until completed.","required":["id","test_id","status","analysis","analysis_error"],"properties":{"id":{"type":"string","format":"uuid"},"test_id":{"type":"string","format":"uuid"},"status":{"type":"string"},"analysis":{"type":"string","enum":["pending","analysed","failed"],"description":"pending until the run is analysed, then analysed. failed: the run could not be analysed, and nothing retries it; analysis_error says why."},"analysis_error":{"type":["string","null"],"description":"Why the analysis failed; null unless analysis is failed."}}}}},"AgentPicksCreatedUploadData":{"allOf":[{"$ref":"#/components/schemas/AgentPicksUploadData"},{"type":"object","required":["uploads"],"properties":{"uploads":{"type":"array","description":"One signed address per declared artifact while the upload is open; empty once completed.","items":{"type":"object","required":["name","bytes","path","url"],"properties":{"name":{"type":"string"},"bytes":{"type":"integer"},"path":{"type":"string","description":"self-run/<projectId>/<runId>/<name>"},"url":{"type":["string","null"],"format":"uri","description":"PUT the artifact's bytes here within two hours. Storage refuses a PUT to a path that already holds an object. null when the artifact is already stored."}}}}}}]},"AgentPicksDeletedUploadData":{"type":"object","required":["run_id","status"],"properties":{"run_id":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["deleted"]}}},"AgentPicksCancelledTestData":{"type":"object","required":["status","test_id"],"properties":{"status":{"type":"string","enum":["cancelled"]},"test_id":{"type":"string","format":"uuid"}}},"AgentPicksStarterTestStartedData":{"type":"object","required":["status","test_id","runs","setup"],"properties":{"setup":{"$ref":"#/components/schemas/AgentPicksSetupUsed"},"status":{"type":"string","enum":["queued"]},"test_id":{"type":"string","format":"uuid"},"runs":{"type":"integer","description":"Runs queued."}}},"AgentPicksSetupListData":{"type":"object","required":["setups","catalogue"],"properties":{"setups":{"type":"array","items":{"type":"object","required":["id","category_slug","name","kind","source_category_slug","snapshot_digest","requests","tools","own_tool_key","competitor_tool_keys","environments","agent_models","building_blocks"],"properties":{"id":{"type":"string","format":"uuid"},"category_slug":{"type":"string","description":"The slug a self-run upload names as its manifest’s request.category to file its runs on this setup."},"name":{"type":"string","description":"The category’s name, or the job line."},"kind":{"type":"string","enum":["job","copy","category"]},"source_category_slug":{"type":["string","null"],"description":"The public category a copy or category setup is of; null for a job."},"snapshot_digest":{"type":"string","pattern":"^[a-f0-9]{64}$"},"requests":{"type":"object","description":"The active request per intent (add, recommend, hosted, improve); null for one left out.","additionalProperties":{"type":["string","null"]}},"tools":{"type":"array","items":{"type":"object","required":["tool_key","display_name","brand_domain","signals"],"properties":{"tool_key":{"type":"string"},"display_name":{"type":"string"},"brand_domain":{"type":["string","null"]},"signals":{"type":["object","null"],"additionalProperties":true,"description":"In the shape saveAgentPicksJob takes, for a tool the customer authored. Null for a tool whose key is in DevTune’s catalogue, whose signals are DevTune’s: name it by key."}}}},"own_tool_key":{"type":["string","null"]},"competitor_tool_keys":{"type":"array","items":{"type":"string"}},"environments":{"type":"array","items":{"type":"object","required":["id","slug"],"properties":{"id":{"type":"string","format":"uuid"},"slug":{"type":"string"}}}},"agent_models":{"type":"array","items":{"$ref":"#/components/schemas/AgentPicksAgentModel"}},"building_blocks":{"type":["object","null"],"additionalProperties":true,"description":"A job’s building blocks. Null for a copy or category setup, whose building blocks are the catalogue’s."}}}},"catalogue":{"type":"object","required":["categories","environments","agents"],"properties":{"categories":{"type":"array","description":"Published public categories.","items":{"type":"object","required":["id","slug"],"properties":{"id":{"type":"string","format":"uuid"},"slug":{"type":"string"}}}},"environments":{"type":"array","description":"Test apps a new setup may name; retired ones are left out.","items":{"type":"object","required":["id","slug"],"properties":{"id":{"type":"string","format":"uuid"},"slug":{"type":"string"}}}},"agents":{"type":"array","items":{"type":"object","required":["agent","models","included_in_plan"],"properties":{"agent":{"type":"string","enum":["claude_code","codex","opencode","pi","muse_code"]},"models":{"type":"array","items":{"type":"string"}},"included_in_plan":{"type":"boolean"}}}}}}}},"AgentPicksSetupData":{"type":"object","required":["category_id","tool_key","competitor_tool_keys","environment_ids","enabled_agent_models","snapshot_digest","setup"],"properties":{"setup":{"$ref":"#/components/schemas/AgentPicksSetupUsed"},"snapshot_digest":{"type":"string","pattern":"^[a-f0-9]{64}$","description":"The setup’s snapshot digest as this save committed it. Send it as snapshot_digest on the next save to have a save made since refused as setup_changed."},"category_id":{"type":"string","format":"uuid"},"tool_key":{"type":["string","null"],"description":"The project's own tool, as saved. Null when saved without one."},"competitor_tool_keys":{"type":"array","items":{"type":"string"},"description":"Competitor tools, as saved."},"environment_ids":{"description":"The test apps the setup tests, as saved.","type":"array","items":{"type":"string","format":"uuid"}},"enabled_agent_models":{"description":"The agent and model pairs, as saved, in order.","type":"array","items":{"$ref":"#/components/schemas/AgentPicksAgentModel"}},"requests":{"type":["object","null"],"description":"The active request per intent, as saved. A left-out request is absent. Null for a public board’s setup.","additionalProperties":{"type":"string"}},"request_ending":{"type":["string","null"],"description":"The text appended to each request, as saved. Null for a public board’s setup."},"recognition":{"type":["object","null"],"description":"Each active tool’s signals and the building blocks, as saved, for what the customer authored: a tool whose key is in DevTune’s catalogue is left out, and a copy of a public category returns no building blocks, since both are the catalogue’s. Null for a public board’s setup.","properties":{"tools":{"type":"array","items":{"type":"object","properties":{"tool_key":{"type":"string"},"signals":{"type":"object","additionalProperties":true}}}},"building_blocks":{"type":"object","additionalProperties":true}}}}},"AgentPicksSetupClearedData":{"type":"object","required":["cleared","setup"],"properties":{"cleared":{"type":"boolean","enum":[true]},"setup":{"$ref":"#/components/schemas/AgentPicksSetupUsed"}}},"AgentPicksJobSavedData":{"type":"object","required":["saved","snapshot_digest","setup"],"properties":{"saved":{"type":"boolean","enum":[true]},"snapshot_digest":{"type":"string","pattern":"^[a-f0-9]{64}$","description":"The job’s snapshot digest as this save committed it. Send it as snapshot_digest on the next save to have a save made since refused as setup_changed."},"setup":{"$ref":"#/components/schemas/AgentPicksSetupUsed"}}},"AgentPicksDraftRejection":{"type":"object","required":["field","value","reason"],"properties":{"field":{"type":"string","description":"The draft field the proposal was for, such as buildingBlocks.codePatterns or relationships."},"value":{"type":"string","description":"The proposal dropped."},"reason":{"type":"string","description":"Why it was dropped, as a short key such as not_in_evidence, not_in_registry, not_tool_specific, invalid_name or prefix, or the name of the check the value failed."}}},"AgentPicksJobDraftData":{"type":"object","required":["draft_id","created_at","status","job","requests","building_blocks","competitors","rejected","error"],"properties":{"draft_id":{"type":"string","format":"uuid"},"created_at":{"type":"string","format":"date-time"},"status":{"type":"string","enum":["drafted","failed"],"description":"drafted, or failed. A failed draft returns only the job passed, as one add request, for the customer to fill in."},"job":{"type":"string","description":"The job line: the one passed, or the drafted one when job was null. Empty when a draft with no job passed failed."},"requests":{"description":"The requests the job's tests would give agents.","type":"array","items":{"type":"object","required":["intent","text"],"properties":{"intent":{"type":"string","enum":["add","recommend","hosted"]},"text":{"type":"string"}}}},"building_blocks":{"type":"object","additionalProperties":true,"description":"packages by ecosystem, codePatterns and the optional record, handler, endpoint, service and local patterns, as the job save takes them."},"competitors":{"description":"Up to six suggested competitors, never the project's primary domain.","type":"array","items":{"type":"object","required":["name","domain"],"properties":{"name":{"type":"string"},"domain":{"type":"string","description":"The competitor's main domain."}}}},"rejected":{"description":"Proposals verification dropped, and why.","type":"array","items":{"$ref":"#/components/schemas/AgentPicksDraftRejection"}},"error":{"type":["string","null"],"description":"Why the draft failed, in a sentence. Null when drafted."}}},"AgentPicksToolDraftData":{"type":"object","required":["draft_id","created_at","status","tool_key","signals","relationships","sources","rejected","error","failure"],"properties":{"draft_id":{"type":"string","format":"uuid"},"created_at":{"type":"string","format":"date-time"},"status":{"type":"string","enum":["drafted","failed"],"description":"drafted, or failed. A failed draft returns empty signals, with the tool's domain as its only brand domain, for the customer to fill in."},"tool_key":{"type":"string","description":"The tool drafted, as passed."},"signals":{"type":"object","additionalProperties":true,"description":"packages by ecosystem, configNames, importPatterns, brandDomains and, when the vendor’s pages show them, configFiles and configPatterns, as the job save takes them."},"relationships":{"type":"array","description":"Proposed from the drafted tool’s own site: tool_key, another of the job’s tools, yields to yields_to, the drafted tool, so a run that uses both counts as yields_to. quote is the sentence read there that says the drafted tool runs inside tool_key, and url the page. Registry text never proposes one. Apply one by adding yields_to to that tool’s signals.yieldsTo before saving the job.","items":{"type":"object","additionalProperties":false,"required":["tool_key","yields_to","quote","url"],"properties":{"tool_key":{"type":"string","description":"Another of the job's tools."},"yields_to":{"type":"string","description":"The drafted tool."},"quote":{"type":"string","description":"The sentence on the drafted tool's site that says it runs inside tool_key."},"url":{"type":"string","description":"The page the sentence is on."}}}},"sources":{"description":"What the draft read: each registry query or lookup and each page of the vendor's site.","type":"array","items":{"type":"object","required":["kind","url","ok"],"properties":{"kind":{"type":"string","enum":["registry","docs"],"description":"registry for a package registry read, docs for a page of the vendor's site."},"url":{"type":"string"},"ok":{"type":"boolean","description":"Whether the URL answered with a readable success response. A registry search that found no packages is still ok."}}}},"rejected":{"description":"Proposals verification dropped, and why.","type":"array","items":{"$ref":"#/components/schemas/AgentPicksDraftRejection"}},"error":{"type":["string","null"],"description":"Why the draft failed, in a sentence. Null when drafted."},"failure":{"type":["string","null"],"description":"Why the draft failed: nothing_to_read when no registry result or vendor page could be read, nothing_found when no domain was given and nothing proposed appears in what was read, model_failed when the model failed or timed out, unexpected for anything else. Null when drafted."}}},"AgentPicksSavedScheduleData":{"type":"object","required":["cadence","next_run_at","setup"],"properties":{"setup":{"$ref":"#/components/schemas/AgentPicksSetupUsed"},"cadence":{"type":"string","enum":["off","daily","weekly","monthly"],"description":"How often a scheduled test runs: daily at midnight UTC, weekly on Monday, monthly on the first, or off."},"next_run_at":{"description":"When the next scheduled test is due. Null when the schedule is off.","type":["string","null"],"format":"date-time"}}},"AgentPicksScheduleData":{"type":"object","required":["cadence","next_run_at","last_skipped","estimate","setup"],"properties":{"setup":{"$ref":"#/components/schemas/AgentPicksSetupUsed"},"cadence":{"type":"string","enum":["off","daily","weekly","monthly"],"description":"How often a scheduled test runs: daily at midnight UTC, weekly on Monday, monthly on the first, or off."},"next_run_at":{"type":["string","null"],"format":"date-time","description":"When the next scheduled test is due. Null when the schedule is off or the project has no setup."},"last_skipped":{"type":["object","null"],"description":"The newest scheduled period, when it was skipped, with how to fix it. Null when the schedule is off, no period has come due yet, the newest period ran, or a skip has since been fixed by saving the setup or changing plan. Credit and permission skips stay until the next period runs.","required":["due_at","reason","shortfall_micros","action","action_href"],"properties":{"due_at":{"type":"string","format":"date-time"},"reason":{"type":"string","enum":["insufficient_credits","permission_changed","no_settings","estimate_changed","agent_not_in_plan","hosted_tests_not_in_plan","test_in_flight","unavailable"],"description":"insufficient_credits: the account could not cover the run. permission_changed: the member who set the schedule can no longer manage or edit the project. no_settings: the setup is incomplete. estimate_changed: the fresh estimate is above the confirmed one. agent_not_in_plan: the plan includes none of the saved agents. hosted_tests_not_in_plan: the plan runs tests on its own machines (Local Runner); scheduled tests need Agent Picks Cloud. test_in_flight: another test was still running. unavailable: the test could not run for another reason; nothing needs fixing."},"action":{"type":["string","null"],"enum":["buy_credits","manage_project","complete_setup","confirm_estimate","view_plans",null],"description":"What fixes the skip. Null for test_in_flight and unavailable."},"action_href":{"type":["string","null"],"description":"The DevTune app path where action is done: billing for buy_credits and view_plans, the project’s Agent Picks settings otherwise. Null when action is null."},"shortfall_micros":{"type":["number","null"],"description":"How many credits the account was short, in credit micros (1 credit = 1,000,000). Null when the skip was not for credits."}}},"estimate":{"type":["object","null"],"description":"What the saved setup would cost on a schedule, in credits. Null when the setup cannot be quoted.","properties":{"fingerprint":{"type":"string","description":"Identifies the setup and prices this estimate used."},"month":{"type":"string","description":"The calendar month per_month projects, as YYYY-MM in UTC: next month."},"included_credits":{"type":"number","description":"Credits the account’s active Agent Picks subscription includes for the month; 0 while trialing."},"per_run":{"type":"object","description":"One scheduled test.","properties":{"expected":{"type":"number","description":"Credits the test is expected to cost."},"high":{"type":"number","description":"Credits a run would reserve; settlement never charges more."},"runs":{"type":"integer","description":"Agent runs in one test."}}},"per_month":{"type":"object","description":"For each cadence (off, daily, weekly, monthly), the tests it would run in month and their cost in credits.","additionalProperties":{"type":"object","properties":{"runs":{"type":"integer","description":"Scheduled tests in the month."},"expected":{"type":"number","description":"per_run.expected times runs."},"high":{"type":"number","description":"per_run.high times runs."}}}}}}}},"AgentPicksCellTool":{"type":"object","required":["outcome","tool_key","runs","picks","rate","rate_low","rate_high","stated_picks","installs","install_rate","install_rate_low","install_rate_high","measured","is_own","is_competitor","found"],"properties":{"outcome":{"description":"installed for a named tool; built_its_own when the agent wrote the capability itself; no_provider when it chose a hosted service but no vendor.","type":"string","enum":["installed","built_its_own","no_provider"]},"tool_key":{"type":["string","null"],"description":"The tool, for installed. Null for built_its_own and no_provider."},"runs":{"type":["integer","null"],"minimum":0,"description":"Completed runs in the cell the rate is over; 0 when measured is false. Null unless the test completed."},"picks":{"type":["integer","null"],"minimum":0,"description":"Completed runs with this outcome, a stated pick included. Null unless the test completed."},"rate":{"type":["number","null"],"minimum":0,"maximum":1,"description":"The pick rate: picks (every installed outcome, a stated pick included) over runs. Null when measured is false or the cell has no completed runs."},"rate_low":{"type":["number","null"],"minimum":0,"maximum":1,"description":"Lower bound of rate's Wilson 90% range. Null when rate is."},"rate_high":{"type":["number","null"],"minimum":0,"maximum":1,"description":"Upper bound of rate's Wilson 90% range. Null when rate is."},"stated_picks":{"type":["integer","null"],"minimum":0,"description":"Of picks, the runs that named the tool without writing it into the code. Null unless the test completed."},"installs":{"type":["integer","null"],"minimum":0,"description":"picks - stated_picks: runs that wrote the tool into the code. Null unless the test completed, and for built_its_own and no_provider, which name no tool."},"install_rate":{"type":["number","null"],"minimum":0,"maximum":1,"description":"installs / runs with its Wilson 90% range, beside the pick rate. Null when rate is, for built_its_own and no_provider, or when the test’s install_rate_hidden_reason is set."},"install_rate_low":{"description":"Lower bound of install_rate's Wilson 90% range. Null when install_rate is.","type":["number","null"],"minimum":0,"maximum":1},"install_rate_high":{"description":"Upper bound of install_rate's Wilson 90% range. Null when install_rate is.","type":["number","null"],"minimum":0,"maximum":1},"measured":{"type":"boolean","description":"False when the test is not completed or the tool was not recognised. Rates are null; counts and verdicts are also null for non-completed tests."},"is_own":{"type":"boolean","description":"True for the project's own tool."},"is_competitor":{"description":"True for a competitor the setup named when the test was queued or copied.","type":"boolean"},"found":{"oneOf":[{"$ref":"#/components/schemas/AgentPicksFoundIn"},{"type":"null"}],"description":"Found in: how many completed runs reached the tool (searched for it, fetched its site or a package of it, or tried to install it). Never feeds the pick rate. Null for built_its_own and no_provider, a tool the test did not record, and a test with no Found in measurement, such as one completed before it was recorded."}}},"AgentPicksCellDiscovery":{"type":"object","required":["completed_runs","reached_runs","unavailable_runs","rate","rate_low","rate_high"],"properties":{"completed_runs":{"type":"integer","minimum":1,"description":"Completed runs in the cell."},"reached_runs":{"type":"integer","minimum":0,"description":"Completed runs whose trail searched for, fetched or installed your tool: its docs, package or registry entry."},"unavailable_runs":{"type":"integer","minimum":0,"description":"Completed runs whose trail had no events to decide from. They are left out of the rate."},"rate":{"type":["number","null"],"minimum":0,"maximum":1,"description":"reached_runs / (completed_runs - unavailable_runs), with its Wilson 90% range. Null when every completed run is unavailable. Model-side web search and fetch bypass the sandbox, so a run that did not reach your tool was not seen reaching it in the sandbox."},"rate_low":{"type":["number","null"],"minimum":0,"maximum":1,"description":"Lower bound of rate's Wilson 90% range. Null when rate is."},"rate_high":{"type":["number","null"],"minimum":0,"maximum":1,"description":"Upper bound of rate's Wilson 90% range. Null when rate is."}}},"AgentPicksModelCell":{"type":"object","required":["environment_id","framework","intent","agent","model","planned","completed","own_picks","fallback_runs","top_outcome","top_tool_key","top_picks","is_loss","own_led","none_runs","unclassified_runs","other_tool_runs","tools"],"properties":{"environment_id":{"type":"string","format":"uuid"},"framework":{"type":"string","description":"The cell's framework, or its test app's slug when the test app has none."},"intent":{"type":"string","enum":["add","recommend","hosted","improve"]},"agent":{"type":"string","enum":["claude_code","codex","cursor","pi","opencode","muse_code"]},"model":{"type":"string"},"planned":{"type":"integer","minimum":0},"completed":{"type":"integer","minimum":0,"description":"Coverage including unmatched-code guesses; displayed rates retain these runs."},"own_picks":{"type":["integer","null"],"minimum":0,"description":"Completed runs that picked the project's own tool, a stated pick included. 0 when the test has no own tool. Null unless the test completed."},"fallback_runs":{"type":["integer","null"],"minimum":0,"description":"Completed runs an older job test judged built_its_own only because they added code no tool matched. Loss verdicts and top_picks leave them out; rates keep them. Null unless the test completed."},"top_outcome":{"description":"The outcome with the most picks once fallback_runs are left out; on a tie, a rival install or built_its_own comes first. When no run picked anything, built_its_own with top_picks 0. Null unless the test completed, or when the cell has no completed runs.","type":["string","null"],"enum":["installed","built_its_own","no_provider",null]},"top_tool_key":{"type":["string","null"],"description":"The tool, when top_outcome is installed. Null otherwise."},"top_picks":{"type":["integer","null"],"minimum":0,"description":"Top pick count, leaving out unmatched-code guesses as loss decisions do. Null unless the test completed."},"is_loss":{"type":["boolean","null"],"description":"The shared loss Action rule, excluding unmatched-code guesses, by decision_method. v2: a competing tool or built its own was picked clearly more often at 90%. v1: one was a top pick, ties included, and the own tool had no majority. Null unless the test completed."},"own_led":{"type":["boolean","null"],"description":"By decision_method. v2: the own tool was picked clearly more often than every other outcome at 90%. v1: it is the strict top pick or was installed in a majority of completed runs. Null unless the test completed."},"decision_method":{"type":"string","enum":["v1","v2"],"description":"The test’s method, the rule is_loss and own_led were decided by. Missing means v1."},"none_runs":{"type":["integer","null"],"minimum":0,"description":"Completed runs where the agent chose no tool and built nothing. Null when the test's outcome_counts_available is false."},"unclassified_runs":{"type":["integer","null"],"minimum":0,"description":"Completed runs the classifier could not decide. Null when the test's outcome_counts_available is false."},"other_tool_runs":{"type":["integer","null"],"minimum":0,"description":"Completed runs of a job test that added a tool the job does not name. Null when the test's outcome_counts_available is false."},"runs_without_trail":{"type":["integer","null"],"minimum":0,"description":"Completed runs in this cell that recorded no trail event. Null where trail coverage cannot be determined, including a cloned public test, which has no runs of its own."},"discovery":{"oneOf":[{"$ref":"#/components/schemas/AgentPicksCellDiscovery"},{"type":"null"}],"description":"Whether runs reached your tool before choosing. Null when the test has no discovery measurement: it is not completed, it finished before discovery was recorded, or it has no own tool."},"tools":{"description":"Every outcome recorded in the cell, plus the tools the test recognises and the test's own and competitor tools, with their rates. In a completed test, a tool no run picked has 0 picks.","type":"array","items":{"$ref":"#/components/schemas/AgentPicksCellTool"}}}},"AgentPicksCellsData":{"type":"object","required":["tests"],"properties":{"tests":{"description":"The requested tests, in the order given.","type":"array","maxItems":8,"items":{"type":"object","required":["test_id","status","comparison_key","completed_at","outcome_counts_available","install_rate_hidden_reason","cells","integration","projects_added","projects_removed","requests_changed","ending_changed","origin","series"],"properties":{"test_id":{"type":"string","format":"uuid"},"origin":{"$ref":"#/components/schemas/AgentPicksOrigin"},"series":{"$ref":"#/components/schemas/AgentPicksTestSeries"},"install_rate_hidden_reason":{"type":["string","null"],"enum":["no_installable_tool",null],"description":"no_installable_tool when none of the test’s tools has a package, config name or import pattern an agent could install (a hosted dashboard has only brand domains). Every install_rate, install_rate_low and install_rate_high is then null, never 0, and only the pick rate is shown. Null when install rates are shown."},"status":{"type":"string","enum":["queued","running","completed","failed","cancelled"],"description":"Only completed tests expose measurements. Other statuses retain coverage with measured false and null counts, rates, ranges and verdicts."},"judging_key":{"type":["string","null"],"description":"Classifier version and stated-pick policy identity, plus the recorded recognition for a test of confirmed tools, so a recognition edit is a judging change. Null means unavailable; do not compare. Equal keys compare only within one origin and, for self-run tests, one series.key: two series can share a judging key."},"comparison_key":{"type":"string","description":"Opaque key identifying what a test measured: its judging, category, tools and recorded requests, request ending and job, and for a self-run test its origin and series, so a self-run never shares a key with a managed test or another series. Compare keys for equality only. Show a pairwise delta only between tests with equal keys. Execution settings do not change the key. For older tests without recorded requests, the key still covers category and tools, but request edits cannot be detected. In trends, keep each test’s measured points and break lines where judging changes; also keep managed and self-run tests on separate lines, and each self-run test on the line of its series.key."},"projects_added":{"description":"Test apps this test ran that the full test before it did not, ordered by name. Show them as an event on the trend; the line stays joined, and cells still compare per test app under an equal comparison_key. Empty when nothing changed, for a directional test or a re-test, and for a first full test.","type":"array","items":{"$ref":"#/components/schemas/AgentPicksTestProject"}},"projects_removed":{"description":"Test apps the full test before this one ran that this test did not, ordered by name. Empty in the same cases as projects_added.","type":"array","items":{"$ref":"#/components/schemas/AgentPicksTestProject"}},"requests_changed":{"description":"Requests whose text this test changed against the full test before it, in intent order: reworded, left out (after null) or asked again (before null). Show them as an event on the trend; the line stays joined, and the comparison_key changes. Empty when nothing changed, for a directional test or a re-test, for a first full test, and when either test recorded no requests.","type":"array","items":{"$ref":"#/components/schemas/AgentPicksRequestChange"}},"ending_changed":{"$ref":"#/components/schemas/AgentPicksEndingChange"},"completed_at":{"type":["string","null"],"format":"date-time","description":"When the test ended. Null while it is queued or running."},"outcome_counts_available":{"type":"boolean","description":"True only for a completed test whose completed run detail covers every cell. The three outcome run counts are null when false."},"runs_without_trail":{"type":["integer","null"],"minimum":0,"description":"The test's completed runs that recorded no trail event, summed over its cells. Null where trail coverage cannot be determined, including a cloned public test, which has no runs of its own."},"cells":{"description":"One cell per test app, request intent, agent and model.","type":"array","items":{"$ref":"#/components/schemas/AgentPicksModelCell"}},"integration":{"description":"What the test’s install runs were observed doing with each installed tool: completed runs whose outcome installed the tool, stated picks excluded. Null while the test is not completed, while this read is unavailable, or when the test’s installs are not all covered by computed observations (not computed); an empty tools list only when no run installed a tool.","oneOf":[{"type":"object","required":["tools"],"properties":{"tools":{"description":"One entry per installed tool.","type":"array","items":{"allOf":[{"$ref":"#/components/schemas/AgentPicksIntegrationAggregate"},{"type":"object","required":["tool_key","by_agent"],"properties":{"tool_key":{"type":"string","description":"The installed tool."},"by_agent":{"type":"array","description":"The same figures per agent, present when the test ran more than one agent; empty otherwise.","items":{"allOf":[{"$ref":"#/components/schemas/AgentPicksIntegrationAggregate"},{"type":"object","required":["agent"],"properties":{"agent":{"type":"string"}}}]}}}}]}}}},{"type":"null"}]}}}}}},"AgentPicksIntegrationRate":{"type":"object","required":["numerator","denominator","unknown","rate"],"description":"numerator of denominator runs answered yes; denominator counts the runs that could be answered yes or no. unknown counts runs in the base that could not be answered either way (not captured, malformed or truncated), and is never counted as no. Across several scenarios (test app, request, agent and model), rate is the average of each scenario's own rate, so every scenario counts once and numerator and denominator are run counts, not its fraction. Null when denominator is 0.","properties":{"numerator":{"type":"integer","minimum":0,"description":"Runs answered yes."},"denominator":{"type":"integer","minimum":0,"description":"Runs that could be answered yes or no."},"unknown":{"type":"integer","minimum":0,"description":"Runs that could not be answered either way."},"rate":{"type":["number","null"],"minimum":0,"maximum":1,"description":"The average of each scenario's numerator / denominator over the scenarios with a run that could be answered; in one scenario, numerator / denominator. Null when denominator is 0."}}},"AgentPicksIntegrationAggregate":{"type":"object","required":["install_runs","observed_runs","unavailable_runs","malformed_runs","truncated_runs","installed_version","registry_comparison","deprecated","docs","invocations"],"properties":{"install_runs":{"type":"integer","minimum":0,"description":"Completed runs that installed the tool. observed_runs + unavailable_runs + malformed_runs = install_runs."},"observed_runs":{"type":"integer","minimum":0,"description":"Install runs with a valid retained observation."},"unavailable_runs":{"type":"integer","minimum":0,"description":"Install runs with no retained observation, such as runs recorded before observations were kept."},"malformed_runs":{"type":"integer","minimum":0,"description":"Install runs whose retained observation failed validation. They are excluded from every count and counted here."},"truncated_runs":{"type":"integer","minimum":0,"description":"Observed runs with a list limited to 100 entries; what they did not show stays unknown."},"installed_version":{"$ref":"#/components/schemas/AgentPicksIntegrationRate","description":"Of install_runs: a single version of the tool’s package was observed. A run with conflicting versions or no transcript or proxy capture is unknown."},"registry_comparison":{"description":"Of runs with an observed version: numerator matched the registry latest pinned when the test was queued; differs did not, which does not mean outdated. unknown includes no_snapshot, runs with no pinned registry snapshot (on-demand tests), which are never reported as matching or differing.","allOf":[{"$ref":"#/components/schemas/AgentPicksIntegrationRate"},{"type":"object","required":["differs","no_snapshot"],"properties":{"differs":{"type":"integer","minimum":0,"description":"Runs whose observed version differs from that registry latest."},"no_snapshot":{"type":"integer","minimum":0,"description":"Runs with no pinned registry snapshot. Counted in unknown."}}}]},"deprecated":{"$ref":"#/components/schemas/AgentPicksIntegrationRate","description":"Of runs with an observed version: an observed npm version was recorded deprecated in the pinned registry snapshot."},"docs":{"description":"Of install_runs: at least one recognised documentation request on a confirmed vendor domain. A run without proxy capture and no request is unknown. requests sums the requests.","allOf":[{"$ref":"#/components/schemas/AgentPicksIntegrationRate"},{"type":"object","required":["requests"],"properties":{"requests":{"type":"integer","minimum":0,"description":"Recognised documentation requests, summed over the runs."}}}]},"invocations":{"description":"Of install_runs: at least one successful command of the tool’s package. A run without transcript capture and no command is unknown. count sums the commands.","allOf":[{"$ref":"#/components/schemas/AgentPicksIntegrationRate"},{"type":"object","required":["count"],"properties":{"count":{"type":"integer","minimum":0,"description":"Successful commands of the tool’s package, summed over the runs."}}}]}}},"AgentPicksHistoryData":{"type":"object","required":["tests","next_offset","pagination","setup"],"properties":{"setup":{"$ref":"#/components/schemas/AgentPicksSetupUsed"},"next_offset":{"type":["integer","null"],"deprecated":true,"description":"Deprecated: pass pagination.nextCursor as cursor. Null on the last page."},"pagination":{"$ref":"#/components/schemas/ListPagination"},"tests":{"description":"The setup's paid and self-run tests, newest first, or the one test test_id names. Each test's rates are its own runs'.","type":"array","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"origin":{"$ref":"#/components/schemas/AgentPicksOrigin"},"series":{"$ref":"#/components/schemas/AgentPicksTestSeries"},"status":{"type":"string","description":"Where the test is: queued, running, completed, failed or cancelled."},"directional":{"type":"boolean","description":"Partial matrix, cell re-run, re-test, starter test, or fewer than three runs per cell; excluded from loss Actions, re-test baselines and full-test trends."},"created_at":{"type":"string","format":"date-time"},"completed_at":{"type":["string","null"],"format":"date-time","description":"When the test ended. Null while it is queued or running."},"scheduled_for":{"description":"The schedule slot the test ran for. Null for a test the schedule did not start.","type":["string","null"],"format":"date-time"},"planned":{"type":"integer"},"completed":{"type":["integer","null"],"description":"Runs completed. Null while the test is queued or running, and for a cancelled test other than a re-test."},"picks":{"type":"integer","description":"Completed runs that picked the project tool, a stated pick included."},"stated_picks":{"type":"integer","description":"Of picks, the runs that named the tool without writing it into the code."},"installs":{"type":"integer","description":"picks - stated_picks."},"tool_key":{"type":["string","null"],"description":"The project's own tool, when the test included it. Null otherwise, and then no rate is returned."},"scenarios":{"type":"integer","minimum":0,"description":"Scenarios (test app, request, agent and model) with completed runs: the scenarios rate averages and its range counts."},"rate":{"type":["number","null"],"description":"The project tool’s pick rate, each scenario’s picks over its completed runs averaged over the scenarios (in one scenario, picks / completed); present once the test completed with tool_key set."},"rate_directional":{"type":"boolean","description":"True when rate rests on fewer than 10 scenarios (in one scenario, fewer than 10 runs): report it as directional. Present with rate. Separate from directional, which describes how the test was run."},"rate_low":{"description":"Lower bound of rate's Wilson 90% range, over the scenarios. Present with rate.","type":["number","null"]},"rate_high":{"description":"Upper bound of rate's Wilson 90% range, over the scenarios. Present with rate.","type":["number","null"]},"install_rate":{"type":["number","null"],"description":"The install rate beside the pick rate; null when install_rate_hidden_reason is set."},"install_rate_low":{"type":["number","null"],"description":"Lower bound of install_rate's Wilson 90% range. Null when install_rate is."},"install_rate_high":{"type":["number","null"],"description":"Upper bound of install_rate's Wilson 90% range. Null when install_rate is."},"install_rate_hidden_reason":{"type":["string","null"],"enum":["no_installable_tool",null],"description":"no_installable_tool when none of the test’s tools has a package, config name or import pattern an agent could install (a hosted dashboard has only brand domains). Every install_rate, install_rate_low and install_rate_high is then null, never 0, and only the pick rate is shown. Null when install rates are shown."},"type":{"type":"string","enum":["full","quick","custom","retest"],"description":"How the test was run: full (every test app, request and agent with three or more runs per cell), quick (one test app, one run per cell), custom (any other scope or repetition) or retest (an Action’s re-test)."},"settled_micros":{"type":["integer","null"],"description":"Credits the test has charged so far, in micros (1 credit = 1000000). Starts at 0 and grows as its runs settle."},"judging_key":{"type":["string","null"],"description":"Classifier version and stated-pick policy identity, plus the recorded recognition for a test of confirmed tools, so a recognition edit is a judging change. Compare only equal non-null keys, within one origin and, for self-run tests, one series.key: two series can share a judging key."},"judging_changed":{"type":"boolean","description":"A judging boundary from the preceding full test, computed before pagination. Break the line here. Lines are also split by origin and, for self-run tests, by series.key."},"projects_added":{"description":"Test apps this test ran that the preceding full test did not, ordered by name and computed before pagination. Mark them as an event on the trend without breaking the series. Empty when nothing changed, for a directional test or a re-test, and for a first full test.","type":"array","items":{"$ref":"#/components/schemas/AgentPicksTestProject"}},"projects_removed":{"description":"Test apps the preceding full test ran that this test did not, ordered by name. Empty in the same cases as projects_added.","type":"array","items":{"$ref":"#/components/schemas/AgentPicksTestProject"}},"requests_changed":{"description":"Requests whose text this test changed against the preceding full test, in intent order: reworded, left out (after null) or asked again (before null). Mark them as an event on the trend without breaking the series. Empty when nothing changed, for a directional test or a re-test, for a first full test, and when either test recorded no requests.","type":"array","items":{"$ref":"#/components/schemas/AgentPicksRequestChange"}},"ending_changed":{"$ref":"#/components/schemas/AgentPicksEndingChange"},"trend_eligible":{"type":"boolean","description":"A completed non-directional full test in the current category. Retain historical judging segments, with visible breaks."},"baseline_eligible":{"type":"boolean","description":"A completed full test in the current category whose non-null judging_key matches the latest full test. False without a setup or known judging. Tests with the same judging key remain eligible even with a different judging key between them. Coverage does not change it. Always false for a self-run test."},"action_id":{"type":["string","null"],"format":"uuid","description":"The Action a re-test measures; null for every other test."},"open_loss_actions":{"type":["integer","null"],"description":"Loss Actions this test created that are still suggested or accepted."}}}}}},"AgentPicksInstallMeasurement":{"type":"object","description":"A re-test baseline or result for the Action’s tool. picks counts every completed run that picked it (every installed outcome, a stated pick included), and rate is that pick rate with its Wilson range, on both sides of the comparison. The range is 90% in the Action’s evidence. In a measured re-test result (baseline, after and cells) it is at the confidence of the result’s method, the one its verdict was decided at: 95% for v1, 90% for v2.","required":["picks","runs","planned","rate","rate_low","rate_high","completed_at"],"properties":{"picks":{"type":"integer","description":"Completed runs that picked the tool, a stated pick included."},"runs":{"type":"integer","description":"The runs rate is over: completed runs, less unmatched-code guesses on the baseline side."},"planned":{"type":"integer"},"rate":{"type":"number","description":"picks / runs in one cell. Over several cells (scenarios set), each cell’s picks over its runs, averaged."},"scenarios":{"type":"integer","minimum":1,"description":"The cells a measurement over several pools, each counted once; its range counts them. Absent for one cell and for measurements recorded before it."},"rate_low":{"type":"number","description":"Lower bound of rate's Wilson range, at the confidence described above."},"rate_high":{"type":"number","description":"Upper bound of rate's Wilson range, at the confidence described above."},"completed_at":{"type":"string","format":"date-time","description":"When the measured test ended."},"failed":{"type":"integer","description":"Runs that failed. Re-test measurements only."},"not_run":{"type":"integer","description":"Planned runs that never ran. Re-test measurements only."}}},"AgentPicksMeasuredShare":{"type":"object","additionalProperties":false,"description":"count labelled test cases out of of; rate is count / of, rounded to four decimal places.","required":["rate","count","of"],"properties":{"rate":{"type":"number","minimum":0,"maximum":1,"description":"count / of."},"count":{"type":"integer","minimum":0,"description":"Labelled cases counted."},"of":{"type":"integer","minimum":1,"description":"Labelled cases the share is over."}}},"AgentPicksReportedShare":{"type":"object","additionalProperties":false,"description":"count right out of of runs reported as this outcome. rate is count / of rounded to four decimal places, and null when of is below minimum_reported, too few to state a rate.","required":["rate","count","of"],"properties":{"rate":{"type":["number","null"],"minimum":0,"maximum":1,"description":"count / of. Null when of is below minimum_reported."},"count":{"type":"integer","minimum":0,"description":"Of those runs, the ones a person labelled the same."},"of":{"type":"integer","minimum":0,"description":"Labelled runs the classifier reported as this outcome."}}},"AgentPicksCategoryPrecision":{"type":"object","additionalProperties":false,"description":"How often the classifier version that judged this run was right in this run's category, measured on a hand-labelled sample of DevTune's own published test runs, not on your runs. precision is right over reported for each outcome kind a category reports; a kind reported fewer than minimum_reported times has a null rate. Null for a category or version with no labelled sample.","required":["classifier_version","category_slug","basis","note","sampled_runs","undecidable_runs","minimum_reported","precision"],"properties":{"classifier_version":{"type":"string","description":"The classifier version the figures are for."},"category_slug":{"type":"string","description":"The category the sample comes from."},"basis":{"type":"string","enum":["labelled_published_runs"]},"note":{"type":"string","description":"The basis, the sample size and the rate threshold, in plain sentences."},"sampled_runs":{"type":"integer","minimum":0,"description":"Runs in the sample, undecidable ones included."},"undecidable_runs":{"type":"integer","minimum":0,"description":"Runs in the sample a person could not decide. They are not counted."},"minimum_reported":{"type":"integer","minimum":1,"description":"How many times an outcome must be reported before it has a rate."},"precision":{"description":"Right over reported, per outcome.","type":"object","additionalProperties":false,"required":["installed","built_its_own","no_provider","none"],"properties":{"installed":{"$ref":"#/components/schemas/AgentPicksReportedShare"},"built_its_own":{"$ref":"#/components/schemas/AgentPicksReportedShare"},"no_provider":{"$ref":"#/components/schemas/AgentPicksReportedShare"},"none":{"$ref":"#/components/schemas/AgentPicksReportedShare"}}}}},"AgentPicksClassifierAccuracy":{"type":"object","additionalProperties":false,"description":"How often the classifier version that judged this run was right on a labelled test set, measured with the rule-based stated-pick judge. Not measured on production runs. precision is right over reported for each outcome kind, null for a kind the classifier never reported on the set; unclassified is decidable cases left unclassified; stated_pick_recall is stated picks found.","required":["classifier_version","basis","note","labelled_cases","decidable_cases","precision","unclassified","stated_pick_recall"],"properties":{"classifier_version":{"type":"string","description":"The classifier version the figures are for."},"basis":{"type":"string","enum":["labelled_test_set"]},"note":{"type":"string","description":"The basis and the set size, in one plain sentence."},"labelled_cases":{"type":"integer","minimum":0,"description":"Cases in the labelled test set."},"decidable_cases":{"type":"integer","minimum":0,"description":"Labelled cases with one right outcome, rather than two tools or too little evidence."},"precision":{"description":"Right over reported, per outcome.","type":"object","additionalProperties":false,"required":["installed","stated_pick","built_its_own","no_provider","other_tool","none"],"properties":{"installed":{"description":"Right over reported for installed. Null when the classifier never reported it on the set.","anyOf":[{"$ref":"#/components/schemas/AgentPicksMeasuredShare"},{"type":"null"}]},"stated_pick":{"description":"Right over reported for stated_pick. Null when the classifier never reported it on the set.","anyOf":[{"$ref":"#/components/schemas/AgentPicksMeasuredShare"},{"type":"null"}]},"built_its_own":{"description":"Right over reported for built_its_own. Null when the classifier never reported it on the set.","anyOf":[{"$ref":"#/components/schemas/AgentPicksMeasuredShare"},{"type":"null"}]},"no_provider":{"description":"Right over reported for no_provider. Null when the classifier never reported it on the set.","anyOf":[{"$ref":"#/components/schemas/AgentPicksMeasuredShare"},{"type":"null"}]},"other_tool":{"description":"Right over reported for other_tool. Null when the classifier never reported it on the set.","anyOf":[{"$ref":"#/components/schemas/AgentPicksMeasuredShare"},{"type":"null"}]},"none":{"description":"Right over reported for none. Null when the classifier never reported it on the set.","anyOf":[{"$ref":"#/components/schemas/AgentPicksMeasuredShare"},{"type":"null"}]}}},"unclassified":{"description":"Decidable cases left unclassified, over decidable cases. Null when the set has no decidable cases.","anyOf":[{"$ref":"#/components/schemas/AgentPicksMeasuredShare"},{"type":"null"}]},"stated_pick_recall":{"description":"Stated picks found, over the stated-pick cases. Null when the set has no stated-pick cases.","anyOf":[{"$ref":"#/components/schemas/AgentPicksMeasuredShare"},{"type":"null"}]}}},"AgentPicksActionsData":{"type":"object","required":["actions","next_offset","incomplete","pagination","setup"],"properties":{"setup":{"$ref":"#/components/schemas/AgentPicksSetupUsed"},"incomplete":{"type":"boolean","description":"Some stored Action rows could not be read. Valid rows remain available."},"next_offset":{"type":["integer","null"],"deprecated":true,"description":"Deprecated: pass pagination.nextCursor as cursor. Null on the last page."},"pagination":{"$ref":"#/components/schemas/ListPagination"},"actions":{"description":"Agent Picks Actions for the setup, newest first.","type":"array","items":{"type":"object","required":["action","retest","result"],"properties":{"action":{"type":"object","required":["id","title","description","state","portfolio_rank","evidence","agent_prompt"],"properties":{"id":{"type":"string","format":"uuid"},"title":{"type":"string"},"description":{"type":"string","description":"What the test found behind the Action, such as how many completed runs picked the tool in each cell, and any locked pages the fix steers around."},"state":{"type":["string","null"],"description":"The Action's status in Actions: suggested, accepted, live or archived. Null when it has none."},"portfolio_rank":{"type":["integer","null"],"description":"One-based position in the project recommendation queue while state is suggested. An Action that has left the queue keeps the position it last held."},"agent_prompt":{"type":["string","null"],"description":"The drafted fix as a Markdown prompt for a coding agent: the brief, what agents picked instead and how often, target files or pages, the acceptance check and every locked page the fix steers around. Null unless the key also holds actions.read, which brief reads require, and until the Action brief is ready and still current. It is a draft for human review and publishes nothing."},"evidence":{"type":"object","description":"Pinned baseline test_id, origin, series, loss_key, tool_key, install measurement, run_ids, failed_fetches and locked_urls. A self-run Action (origin self_run) names its series, with repository always null (read the test for it), and its loss_key names that series, so it never folds into a managed Action or another series'; it has no managed re-test. An Action kept after its self-run test was deleted has no run_ids, no run evidence (run_evidence_available false) and an empty failed_fetches; a run deleted from a test that keeps others leaves its Actions' run_ids, runs and failed_fetches. A cell Action has cell; a reason Action has reason and cells, aggregate counts and run evidence, without a first-cell claim. An observation reason (own_url_failed, own_install_failed, own_tool_not_observed or own_tool_tried) also has observation_counts, each cell's winners and, optionally, context_failures by URL origin; an own_tool_not_observed reason also lists fetched_origins, the URL origins its agents fetched; its failed_fetches is empty and its cells carry only their identity, baseline, winners and run_ids. On every Action, every failed_fetches url is an origin only, and so is a fetch reason's url unless the reason has an own target; an own_url_failed url, and a fetch reason’s with an own target, keeps its path; no URL carries userinfo, a query or a fragment; run evidence keeps only decidedBy, reason_key, candidates, toolPackages, buildingBlockPackages, diffEmpty and diffTruncated. Rates and re-tests use the full-cell baselines, never the observation counts.","required":["test_id","origin","series","loss_key","tool_key","baseline","failed_fetches","locked_urls"],"anyOf":[{"required":["cell"],"properties":{"kind":{"enum":["cell"]}}},{"required":["kind","reason","cells"],"properties":{"kind":{"const":"reason"}}}],"properties":{"test_id":{"type":"string","format":"uuid"},"origin":{"$ref":"#/components/schemas/AgentPicksOrigin"},"series":{"$ref":"#/components/schemas/AgentPicksTestSeries"},"loss_key":{"type":"string","description":"Identifies the loss the Action addresses."},"kind":{"type":"string","enum":["cell","reason"]},"reason":{"type":"object","description":"fetch and classifier name a shared failed fetch or recorded reason; a fetch on the customer’s own domain or registry package carries that target, and a classifier key that is not a short key is empty. own_url_failed is an own URL (no query or fragment) that returned an error, with its domain or registry package target, its most frequent status_code and status_counts, every status with its runs; own_install_failed is an exact own package whose install failed; own_tool_not_observed is a job whose trails show no fetch, install or search of the own tool; own_tool_tried is a job whose trails show the own tool searched for, fetched or installed before the run picked another tool or built its own, with its most common trails.","required":["kind"],"properties":{"kind":{"description":"The kind of reason, as described above.","type":"string","enum":["fetch","classifier","own_url_failed","own_install_failed","own_tool_not_observed","own_tool_tried"]},"url":{"type":"string","description":"The failed URL, for a fetch or own_url_failed reason."},"status_code":{"type":"integer","description":"Its most frequent HTTP status."},"status_counts":{"type":"array","description":"own_url_failed only: each status the grouped runs failed with and how many runs did.","items":{"type":"object","required":["status_code","runs"],"properties":{"status_code":{"type":"integer","description":"An HTTP status the runs failed with."},"runs":{"type":"integer","description":"Runs that failed with it."}}}},"key":{"type":"string","description":"The recorded reason key, for a classifier reason. Empty when it is not a short key."},"target":{"description":"The customer's own domain or registry package the reason concerns.","type":"object","required":["kind"],"properties":{"kind":{"type":"string","enum":["domain","package"]},"domain":{"type":"string","description":"The domain, for a domain target."},"ecosystem":{"description":"The package registry, for a package target.","type":"string","enum":["npm","gem","pypi"]},"name":{"type":"string"}}},"package":{"description":"The own package whose install failed, for own_install_failed.","type":"object","required":["ecosystem","name"],"properties":{"ecosystem":{"description":"The package registry.","type":"string","enum":["npm","gem","pypi"]},"name":{"type":"string"}}},"job":{"type":"string","description":"The job the runs were given, for a job reason."},"frameworks":{"description":"The test apps the runs used.","type":"array","items":{"type":"string"}},"trails":{"type":"array","description":"own_tool_tried only: up to five of the most common trails, most runs first. steps are what the run did with the own tool, each once in the order it first happened: searched (a search naming it), fetched (its domain or registry package), installed (a plain install that succeeded) or install_unconfirmed (any other install of its package). outcome is what the run ended on: installed (after the last own-tool step of the run, a plain install of a package of that tool that succeeded) or picked (any other pick of it, a stated pick included), naming tool_key and tool_name, or built_its_own. These are observations, not causes.","items":{"type":"object","required":["steps","outcome","tool_key","tool_name","runs"],"properties":{"steps":{"description":"What the run did with the own tool, in order.","type":"array","items":{"type":"string","enum":["searched","fetched","installed","install_unconfirmed"]}},"outcome":{"description":"What the run ended up doing.","type":"string","enum":["installed","picked","built_its_own"]},"tool_key":{"type":"string","nullable":true,"description":"The tool the run picked instead. Null when it built its own."},"tool_name":{"type":"string","nullable":true,"description":"That tool's name. Null when it built its own."},"runs":{"type":"integer","description":"Runs that followed this trail."}}}},"coverage":{"type":"object","description":"own_tool_not_observed only: the docs coverage check its title follows, absent until the check runs after the test completes. missing means none of the pages_checked owned pages mentions framework; unchecked means anything else, including no owned content held, and its pages are candidates that mention framework and the tool or a job term, claimed as nothing more. After the test completes, the candidates are judged: exists_not_fetched means a candidate in pages documents the job in framework and agents never fetched it, using the entry_points instead (empty when they used none of yours); missing with a judgment means every candidate was judged not to document it. entry_points on unchecked are owned pages agents in the group's runs fetched.","required":["status","pages"],"properties":{"status":{"description":"The coverage check's result, as described above.","type":"string","enum":["missing","exists_not_fetched","unchecked"]},"framework":{"type":"string","description":"The framework the check looked for."},"pages":{"description":"The owned pages the result names.","type":"array","items":{"type":"string"}},"pages_checked":{"type":"integer","description":"missing and unchecked: how many owned pages the check read."},"entry_points":{"type":"array","description":"Plain labels for owned pages agents fetched successfully, such as npm README or llms.txt, never URLs or paths.","items":{"type":"string"}},"pages_incomplete":{"type":"boolean","description":"unchecked only: true when some candidate is not listed in pages."},"judgment":{"type":"object","description":"missing and exists_not_fetched only: present when the result comes from judging the candidates, not from a keyword match.","required":["model","question_version","threshold","pages_judged","judged_at"],"properties":{"model":{"type":"string","description":"The judging model release."},"question_version":{"type":"integer","description":"Version of the judging question."},"threshold":{"type":"number","description":"The confidence a verdict needed; pages judged below it decided nothing."},"pages_judged":{"type":"integer","description":"How many candidate pages the judgment answered; a page whose check failed is not counted."},"judged_at":{"type":"string","format":"date-time"}}}}}}},"observation_counts":{"type":"object","description":"Runs and cells where an observation reason holds, and the runs counted apart from it.","required":["runs","cells","own_tool_seen_runs","evidence_unavailable_runs","recovery_unknown_runs"],"properties":{"runs":{"type":"integer","description":"Runs where the observation holds."},"cells":{"type":"integer","description":"Cells those runs fall in."},"own_tool_seen_runs":{"type":"integer","description":"Runs in the covered cells whose trail shows the own tool searched for, fetched or installed. On an own_tool_tried reason these are the reason's own runs; on any other reason they are counted apart from it."},"evidence_unavailable_runs":{"type":"integer","description":"Runs whose trail is missing or incomplete, counted apart from the observation."},"recovery_unknown_runs":{"type":"integer","description":"Runs whose trail recovery is unknown, counted apart."}}},"context_failures":{"type":"array","description":"Other parties' failed fetches on an observation reason Action, by URL origin only.","items":{"type":"object","required":["origin","status_code","runs"],"properties":{"origin":{"type":"string"},"status_code":{"type":"integer"},"runs":{"type":"integer","description":"Runs that hit it."}}}},"fetched_origins":{"type":"array","description":"The URL origins an own_tool_not_observed reason Action's agents fetched, by origin only, most fetched first.","items":{"type":"object","required":["origin","runs"],"properties":{"origin":{"type":"string","description":"The URL origin."},"runs":{"type":"integer","description":"Runs that fetched it."}}}},"cells":{"description":"The losing cells the reason covers.","type":"array","items":{"type":"object","required":["test_id","loss_key","cell","baseline"],"properties":{"test_id":{"type":"string","format":"uuid"},"loss_key":{"type":"string","description":"Identifies the cell's loss."},"cell":{"description":"The cell: test app, intent, agent and model.","type":"object","additionalProperties":true},"baseline":{"$ref":"#/components/schemas/AgentPicksInstallMeasurement"},"winners":{"type":"array","description":"What won the cell's runs. Always present on an observation reason's cells.","items":{"type":"object","required":["outcome","tool_key","tool_name","runs"],"properties":{"outcome":{"description":"installed or built_its_own.","type":"string","enum":["installed","built_its_own"]},"tool_key":{"type":["string","null"],"description":"The winning tool. Null when the runs built their own."},"tool_name":{"type":["string","null"],"description":"Its name. Null when the runs built their own."},"runs":{"type":"integer","description":"Runs it won."}}}}}}},"tool_key":{"type":"string","description":"The customer's own tool the Action is about."},"cell":{"description":"The losing cell, for a cell Action.","type":"object","properties":{"environment_id":{"type":"string","format":"uuid"},"environment_name":{"type":"string","description":"The test app."},"intent":{"type":"string","enum":["add","recommend","hosted","improve"]},"agent":{"type":"string"},"model":{"type":"string"}}},"baseline":{"$ref":"#/components/schemas/AgentPicksInstallMeasurement"},"runs":{"description":"The runs behind the Action, with what each picked.","type":"array","items":{"type":"object","properties":{"run_id":{"type":"string","format":"uuid"},"outcome":{"type":"string","description":"What the agent did."},"tool_key":{"type":["string","null"],"description":"The tool it picked: installed, or named as its choice without being written into the code. Null unless outcome is installed."},"confidence":{"type":["number","null"],"description":"How sure the classifier was, from 0 to 1. Null when not recorded."},"classifier_version":{"type":"string","description":"The classifier version that judged the run."},"evidence":{"description":"What the classifier saw.","type":"object","additionalProperties":true}}}},"run_evidence_available":{"type":"boolean","description":"Whether the runs' evidence can be read."},"run_ids":{"description":"The runs behind the Action.","type":"array","items":{"type":"string","format":"uuid"}},"reason_run_ids":{"type":"array","description":"A fetch reason's runs that hit its exact URL and status. Failed fetch URLs are origins only, so this is the reason's run count; each covered cell carries its own.","items":{"type":"string","format":"uuid"}},"failed_fetches":{"description":"Fetches in the Action's runs that returned 400 or above. Empty on an own_url_failed, own_install_failed, own_tool_not_observed or own_tool_tried reason.","type":"array","items":{"type":"object","properties":{"run_id":{"type":"string","format":"uuid"},"url":{"type":"string","description":"The URL origin."},"status_code":{"type":"integer"}}}},"locked_urls":{"description":"Locked pages the fix steers around.","type":"array","items":{"type":"string"}},"locked_urls_withheld":{"type":"integer","description":"Locked pages left out of locked_urls because they cannot be shown safely. They are still steered around. Absent when every locked page is listed."}}}}},"retest":{"description":"The Action's latest re-test. Null when none has started.","type":["object","null"],"properties":{"id":{"type":"string","format":"uuid"},"status":{"type":"string"},"baseline_test_id":{"type":"string","format":"uuid"},"action_id":{"type":"string","format":"uuid"},"batch":{"type":"integer","description":"Which batch of 8 runs the re-test is on, from 1 to 5. A re-test of several cells shares each batch among them."},"reserved_micros":{"type":"integer","description":"Credits the re-test reserved, in credit micros (1 credit = 1,000,000)."},"settled_micros":{"type":["integer","null"],"description":"Credits the re-test has charged so far, in credit micros. Starts at 0 and grows as its runs settle."},"refunded_micros":{"type":"integer","description":"Unused credits returned, in credit micros."},"method":{"type":"string","enum":["v1","v2"],"description":"How the re-test decides its result, fixed when it starts: v2, or v1 for a re-test started before v2."}}},"result":{"description":"The re-test result. Null until the Action is marked done; then unmeasured until a re-test starts, still_measuring while it runs, and the re-test's result once it ends.","type":["object","null"],"properties":{"result":{"description":"What the re-test shows against the baseline: improved, mixed, no_clear_change, declined, still_measuring or unmeasured.","type":"string","enum":["improved","mixed","no_clear_change","declined","still_measuring","unmeasured"]},"test_id":{"type":"string","format":"uuid"},"baseline_test_id":{"type":"string","format":"uuid"},"baseline":{"$ref":"#/components/schemas/AgentPicksInstallMeasurement"},"after":{"description":"The re-test's measurement. Null until it has one.","anyOf":[{"$ref":"#/components/schemas/AgentPicksInstallMeasurement"},{"type":"null"}]},"cells":{"type":"array","description":"Per-cell measurements for a reason-level Action.","items":{"type":"object","required":["cell","baseline","after","result"],"properties":{"cell":{"type":"object","additionalProperties":{"type":"string"}},"baseline":{"$ref":"#/components/schemas/AgentPicksInstallMeasurement"},"after":{"description":"The cell's re-test measurement. Null until it has one.","anyOf":[{"$ref":"#/components/schemas/AgentPicksInstallMeasurement"},{"type":"null"}]},"result":{"type":"string","enum":["improved","mixed","no_clear_change","declined","still_measuring","unmeasured"]}}}},"coverage_complete":{"type":"boolean","description":"True when the re-test completed and every cell has a measured result."},"stop_reason":{"type":["string","null"],"description":"ranges_separated when every cell's result was already clear at a batch check, so any runs still queued were cancelled. Null otherwise."},"measured_at":{"description":"When the result was measured. Null until then.","type":["string","null"],"format":"date-time"},"method":{"type":"string","enum":["v1","v2"],"description":"How the result was decided. v2 compares each cell’s pick rate with its baseline by the 90% interval on their difference, and its ranges are 90%. v1, a result from a re-test started before v2, compares 95% ranges, and its ranges are 95%; a v1 result is never restated."}}}}}}}},"AgentPicksPaidTestData":{"type":"object","required":["test","runs","setup"],"properties":{"setup":{"$ref":"#/components/schemas/AgentPicksSetupUsed"},"test":{"type":["object","null"],"description":"The paid or self-run test test_id names, or else the setup’s latest paid test other than a re-test. Null when there is none.","required":["id","status","estimate","directional","reserved_micros","settled_micros","refunded_micros","released_at","created_at","completed_at","origin","series"],"properties":{"origin":{"$ref":"#/components/schemas/AgentPicksOrigin"},"series":{"$ref":"#/components/schemas/AgentPicksTestSeries"},"id":{"type":"string","format":"uuid","description":"Pass it as test_id to read this test again, or in the path to cancel it."},"directional":{"type":"boolean","description":"True for a partial matrix, a cell re-run, a re-test, a starter test, or fewer than three runs per cell. Such a test is left out of loss Actions, re-test baselines and full-test trends."},"status":{"type":"string","enum":["queued","running","completed","failed","cancelled"],"description":"Where the test is. completed, failed and cancelled are final."},"estimate":{"type":["object","null"],"description":"The quote the test started from, in credits. Null for a self-run test, which reserves none.","required":["expected","high","runs"],"properties":{"expected":{"type":"number","description":"Credits the test was expected to cost."},"high":{"type":"number","description":"Credits the start reserved against."},"runs":{"type":"integer","description":"Agent runs the test planned."}}},"reserved_micros":{"type":["integer","null"],"description":"Credits the test reserved, in credit micros (1 credit = 1,000,000)."},"settled_micros":{"type":["integer","null"],"description":"Credits the test has charged so far, in credit micros. Starts at 0 and grows as its runs settle; it stops changing once released_at is set."},"refunded_micros":{"type":"integer","description":"Unused credits returned, in credit micros. released_at says when."},"released_at":{"type":["string","null"],"format":"date-time","description":"When the unused reservation was returned. Null until it is."},"created_at":{"type":"string","format":"date-time","description":"When the test was started."},"completed_at":{"type":["string","null"],"format":"date-time","description":"When the test ended. Null while it is queued or running."}}},"runs":{"type":"array","description":"Every run of the test. Empty when test is null.","items":{"type":"object","required":["id","environment_id","environment_name","intent","request","agent","model","stage","model_calls","started_at","repetition","status","outcome","tool_key","settled_micros","cell_agreed","origin","series"],"properties":{"origin":{"$ref":"#/components/schemas/AgentPicksOrigin"},"series":{"$ref":"#/components/schemas/AgentPicksRunSeries"},"id":{"type":"string","format":"uuid","description":"Pass it as runId to read the run or its trace."},"environment_id":{"type":"string","format":"uuid","description":"The test app, as the Agent Picks catalogue lists it."},"environment_name":{"type":"string","description":"The test app the run used."},"intent":{"type":"string","description":"The intent of the request the agent was given."},"request":{"type":"string","description":"The request text the agent was given."},"agent":{"type":"string","description":"The coding agent: claude_code, codex, cursor, pi, opencode or muse_code."},"stage":{"type":["string","null"],"enum":["sandbox","setup","agent","judging",null],"description":"Current runner stage; null for queued, finished or older runs."},"model_calls":{"type":["integer","null"],"description":"Model calls the run made over all attempts; null when no activity was recorded."},"model":{"type":"string"},"started_at":{"description":"When the run started. Null while it is queued, and for a run cancelled or failed before it started.","type":["string","null"],"format":"date-time"},"repetition":{"description":"Which repetition of its cell the run is, counting from 1. In a re-test that covers several cells, the run's position in the whole re-test, from 1 to 40.","type":"integer"},"status":{"type":"string","description":"queued, running, completed, failed or cancelled."},"outcome":{"type":["string","null"],"description":"What the agent did: installed (it picked tool_key, by installing it or by naming it as its choice without writing it into the code), built_its_own (it wrote its own solution), no_provider (it wired in a hosted service without naming a provider), other_tool (in a job test, it added a tool the job does not name), none (it picked no tool and built nothing) or unclassified (the evidence could not decide). Null until the run is classified."},"tool_key":{"type":["string","null"],"description":"The tool the agent installed. Null unless outcome is installed."},"settled_micros":{"type":["integer","null"],"description":"Credits this run charged, in credit micros. Null until it settles."},"cell_agreed":{"description":"True when the run was cancelled before it started because its cell’s first 4 runs picked the same thing. It was not needed: not an error, not missing coverage, and not charged.","type":"boolean"}}}}}},"AgentPicksStarterTestData":{"type":"object","properties":{"test":{"type":["object","null"],"description":"The project’s latest starter test; null when it has none.","properties":{"id":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["queued","running","completed","failed","cancelled"]},"category_id":{"type":"string","format":"uuid"},"created_at":{"type":"string","format":"date-time"},"completed_at":{"type":["string","null"],"format":"date-time","description":"When the test ended. Null while it is queued or running."},"planned_runs":{"type":"integer","minimum":0,"description":"Every run the test queued."},"completed_runs":{"type":"integer","minimum":0,"description":"Runs with status completed."},"environment":{"description":"The test app the test ran in. Null when the test has no runs.","type":["object","null"],"properties":{"name":{"type":"string"},"framework":{"type":["string","null"],"description":"The framework. Null for the empty folder."}},"required":["name","framework"]}},"required":["id","status","category_id","created_at","completed_at","planned_runs","completed_runs","environment"]},"runs":{"type":"array","description":"The test’s runs by request (add, recommend, hosted, improve) then agent. Empty when there is no test. A failed or cancelled test keeps its runs.","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"intent":{"type":"string","description":"add (build it), recommend (recommend one), hosted (hosted service) or improve (improve existing)."},"request":{"type":"string","description":"The request the agent was given."},"agent":{"type":"string"},"stage":{"type":["string","null"],"enum":["sandbox","setup","agent","judging",null],"description":"Current runner stage; null for queued, finished or older runs."},"model_calls":{"type":["integer","null"],"description":"Model calls the run made over all attempts; null when no activity was recorded."},"model":{"type":"string"},"status":{"description":"Where the run is. completed, failed and cancelled are final.","type":"string","enum":["queued","running","completed","failed","cancelled"]},"outcome":{"type":["string","null"],"description":"installed, built_its_own, no_provider, other_tool, none or unclassified; null until the run is classified."},"tool_key":{"type":["string","null"],"description":"The installed tool; null for every other outcome and before classification."},"other_tools":{"type":"array","items":{"type":"string"},"description":"For other_tool: the packages and hosts the run added that no tool in the test names."},"sandbox_attempts":{"type":"integer","minimum":0,"description":"Sandboxes the run started, as recorded; 0 before its first starts."}},"required":["id","intent","request","agent","model","stage","model_calls","status","outcome","tool_key","other_tools","sandbox_attempts"]}},"tools":{"type":["array","null"],"description":"The tools the test ran with, as recorded when it was queued, with a display name and logo domain to label each tool_key. The labels can reflect catalogue edits made since; the list of tools does not. Null for a test queued before tests recorded their tools, whose tool list is unknown; empty when there is no test.","items":{"type":"object","properties":{"tool_key":{"type":"string"},"display_name":{"type":"string"},"brand_domain":{"type":["string","null"],"description":"The domain the tool's logo comes from. Null when none is known."}},"required":["tool_key","display_name","brand_domain"]}}},"required":["test","runs","tools"]},"AgentPicksModelSettings":{"type":"object","properties":{"setup":{"$ref":"#/components/schemas/AgentPicksSetupUsed"},"enabled_agents":{"type":"array","items":{"type":"string"},"description":"Coding agents the setup tests. Empty when none is chosen."},"enabled_agent_models":{"description":"The saved agent and model pairs, in order.","type":"array","items":{"$ref":"#/components/schemas/AgentPicksAgentModel"}}},"required":["enabled_agents","enabled_agent_models","setup"]},"AgentPicksAgentModel":{"type":"object","additionalProperties":false,"properties":{"agent":{"type":"string","enum":["claude_code","codex","opencode","pi","muse_code"],"description":"Every Agent Picks plan includes every agent."},"model":{"type":"string","description":"A priced model offered for the selected agent. Claude Code: claude-sonnet-5-5, claude-opus-5-5, claude-fable-5-1, claude-haiku-5-5. Codex: gpt-6.1-sol, gpt-6-astra, gpt-6-luna. Pi and OpenCode: any of those seven, and zai/glm-5.3, zai/glm-5.3-flash, deepseek/deepseek-v4.1-flash-1017 and deepseek/deepseek-v4-pro-0813. Muse Code: meta/muse-wonder-1.0."}},"required":["agent","model"]},"AgentPicksSummaryData":{"type":"object","properties":{"setup":{"$ref":"#/components/schemas/AgentPicksSetupUsed"},"status":{"type":"string","enum":["no_settings","no_test","ready"],"description":"ready when a test result is shown; no_test when there is no completed test yet; no_settings when the project has no setup."},"category":{"type":["object","null"],"description":"The project’s category. When the project tests its own job instead, the job’s own category: a slug starting job- and name the job line. Null when the project has no setup, or its setup has no category.","properties":{"id":{"type":"string","format":"uuid"},"slug":{"type":"string"},"name":{"type":"string"}},"required":["id","slug","name"]},"job":{"type":["object","null"],"description":"The project’s own job, when it tests one instead of a public category: the work agents are asked to do, the requests its tests give them, what decides built_its_own and no_provider, and the tools it compares with the signals that name each one in a run, as the customer confirmed them. When a test is shown, the job line, requests, building blocks and tools are as that test recorded them when it was queued, so a later edit to the job never relabels its numbers. An agent drafted them from package registries and each vendor’s own site. A job has no public test, so test.scope is always project for it. Null for a public category.","properties":{"job":{"type":"string","description":"The job's one-line description, as the shown test recorded it, else as saved. Agents are given the requests, not this line."},"source_category":{"type":["object","null"],"description":"The public category the job was started from. Null when it was not started from one.","properties":{"id":{"type":"string","format":"uuid"},"slug":{"type":"string"},"name":{"type":"string"}},"required":["id","slug","name"]},"requests":{"description":"The requests the job's tests give agents.","type":"array","items":{"type":"object","properties":{"intent":{"type":"string","enum":["add","recommend","hosted"]},"text":{"type":"string"}},"required":["intent","text"]}},"building_blocks":{"type":"object","additionalProperties":true,"description":"Packages and code patterns that decide built_its_own, and endpoint and local patterns that decide no_provider. Empty lists when nothing does: then no run of the job is built_its_own."},"tools":{"description":"The tools the job compares, with the signals that name each in a run.","type":"array","items":{"type":"object","properties":{"tool_key":{"type":"string"},"display_name":{"type":"string"},"brand_domain":{"type":["string","null"],"description":"The tool's main website domain. Null when none is recorded."},"is_own":{"type":"boolean","description":"True for the customer's own tool."},"packages":{"type":"object","additionalProperties":{"type":"array","items":{"type":"string"}},"description":"Package names by ecosystem (npm, pypi, gem)."},"config_names":{"type":"array","items":{"type":"string"},"description":"Config names, such as environment variables, that name the tool in code."},"import_patterns":{"description":"Import patterns that name the tool in code.","type":"array","items":{"type":"string"}},"brand_domains":{"type":"array","items":{"type":"string"},"description":"The tool's website domains, as recorded for the job. A host on one of them in the added code names the tool."}},"required":["tool_key","display_name","brand_domain","is_own","packages","config_names","import_patterns","brand_domains"]}}},"required":["job","source_category","requests","building_blocks","tools"]},"own_tool_key":{"type":["string","null"],"description":"Your tool in the shown test, as the test recorded it. Older tests that did not record it fall back to your current setup’s tool; when the test recorded its job’s tools, the fallback is kept only if they include it. Null when there is no own tool."},"competitor_tool_keys":{"type":"array","items":{"type":"string"},"description":"The competitors in the shown test, as the test recorded them. Older tests that did not record them fall back to your current setup’s competitors in the same way as own_tool_key."},"enabled_environment_ids":{"type":"array","items":{"type":"string","format":"uuid"},"description":"Test apps the project’s setup tests. Empty when none is chosen."},"enabled_frameworks":{"type":"array","items":{"type":"string"},"description":"Frameworks of the enabled test apps. Empty when none is chosen."},"enabled_agent_models":{"type":"array","description":"The saved agent and model pairs, in order, including model ids no longer offered. Paid tests accept only the offered priced models.","items":{"type":"object","properties":{"agent":{"type":"string"},"model":{"type":"string"}},"required":["agent","model"],"description":"One saved coding agent and model id."}},"enabled_agents":{"type":"array","items":{"type":"string","enum":["claude_code","codex","cursor","pi","opencode","muse_code"]},"description":"Coding agents the project’s setup tests. Empty when none is chosen."},"test":{"type":["object","null"],"description":"The test test_id names; without test_id, the latest completed non-directional own test; if none exists, the latest completed directional own test, flagged directional=true; otherwise the newest eligible cloned or published public edition. Directional results never displace a full own result and never feed loss Actions, re-test baselines or trends. Action re-tests are never shown. Null when there is none.","properties":{"id":{"type":"string","format":"uuid"},"origin":{"$ref":"#/components/schemas/AgentPicksOrigin","description":"self_run only for a self-run test that test_id names, whose rates are its own runs’; without test_id, the summary reads managed tests only."},"series":{"$ref":"#/components/schemas/AgentPicksTestSeries"},"scope":{"type":"string","enum":["project","public"],"description":"public when the project has no completed own result and its category’s public test is shown: the copy cloned into the project (kind cloned) or, without one, the latest public test."},"kind":{"type":"string","description":"What started the shown test: manual, scheduled, model_release, starter or cloned."},"directional":{"type":"boolean","description":"Partial matrix, cell re-run, re-test, starter test, or fewer than three runs per cell; excluded from loss Actions, re-test baselines and full-test trends."},"roles_source":{"type":"string","enum":["snapshot","job_signals","current_settings"],"description":"Where the shown test’s tool roles came from: snapshot, recorded with the test; job_signals, current settings limited to the tools the test recorded; current_settings, the setup as it is now."},"decision_method":{"type":"string","enum":["v1","v2"],"description":"How the shown test decides a cell won, lost or level, as its loss Actions were decided. Only a rival install or built its own can win a loss, never no_provider, and unmatched-code guesses (coverage fallback_runs) are left out. v2: only when the 90% interval on the difference between the two pick rates excludes zero. v1: by raw counts, the own tool winning when it beat the rival or took a majority of completed runs and losing when the rival was picked at least as often without an own majority of the runs that were not guesses. A public test reports its edition’s method, and a project’s copy of it keeps that method. Missing means v1."},"own_tool_recorded":{"type":"boolean","description":"Whether the shown test recorded the own tool among its tools (its category tools or job tools). Where no run picked the own tool, a test that did not record it never measured it there, so the cell is no loss."},"completed_at":{"type":"string","format":"date-time"},"measured_pairs":{"type":"array","description":"When each agent and model pair was measured, present only when the shown test is a public board test that carried some scenarios over from an earlier edition instead of running them again. carried_scenarios counts the pair’s carried scenarios and fresh_scenarios the ones the shown test ran; a pair with both was partly carried over. measured_from is when its earliest carried scenario was measured, else the test’s completion; measured_to is the test’s completion when it ran any of the pair’s scenarios, else when its latest carried one was measured. Absent when the test ran every scenario itself.","items":{"type":"object","properties":{"agent":{"type":"string"},"model":{"type":"string"},"measured_from":{"type":"string","format":"date-time"},"measured_to":{"type":"string","format":"date-time"},"carried_scenarios":{"type":"integer","minimum":0},"fresh_scenarios":{"type":"integer","minimum":0}},"required":["agent","model","measured_from","measured_to","carried_scenarios","fresh_scenarios"]}}},"required":["id","scope","kind","directional","completed_at"]},"runs":{"type":"integer","minimum":0,"description":"Completed runs in the shown test."},"scenarios":{"type":"integer","minimum":0,"description":"Scenarios (test app, request, agent and model) with completed runs. Every rate averages its scenarios’ rates, each counted once."},"install_rate_hidden_reason":{"type":["string","null"],"enum":["no_installable_tool",null],"description":"no_installable_tool when none of the test’s tools has a package, config name or import pattern an agent could install (a hosted dashboard has only brand domains). Every install_rate, install_rate_low and install_rate_high is then null, never 0, and only the pick rate is shown. Null when install rates are shown."},"coverage":{"$ref":"#/components/schemas/AgentPickCoverage","description":"How many of the planned runs matching the filters completed. Every rate uses completed runs only."},"incomplete_cells":{"type":"array","description":"Every cell (test app, question, agent and model) where fewer runs completed than were planned, including cells where none did. Empty when every planned run completed.","items":{"allOf":[{"$ref":"#/components/schemas/AgentPickCoverage"},{"type":"object","properties":{"environment":{"type":"string","description":"The test app’s slug."},"intent":{"type":"string","enum":["add","recommend","hosted","improve"]},"agent":{"type":"string","enum":["claude_code","codex","cursor","pi","opencode","muse_code"]},"model":{"type":"string"}},"required":["environment","intent","agent","model"]}]}},"tools":{"type":"array","description":"Every tool picked, plus the project’s own tool and competitors, highest pick rate first: ties by more scenarios, then tool key, then built_its_own, then no_provider.","items":{"allOf":[{"$ref":"#/components/schemas/AgentPickRate"},{"type":"object","properties":{"display_name":{"type":["string","null"],"description":"The tool's name. Null when none is recorded."},"brand_domain":{"type":["string","null"],"description":"The tool's website domain. Null when none is recorded."},"is_own":{"type":"boolean","description":"True for the customer's own tool."},"is_competitor":{"description":"True for a competitor in the shown test, as competitor_tool_keys lists them.","type":"boolean"}},"required":["display_name","brand_domain","is_own","is_competitor"]}]}},"frameworks":{"description":"Rates by framework, across test apps.","type":"array","items":{"type":"object","properties":{"framework":{"type":["string","null"],"description":"Null for the empty folder, which has no framework."},"runs":{"type":"integer","minimum":0,"description":"Completed runs on the framework."},"scenarios":{"type":"integer","minimum":0,"description":"Scenarios with completed runs on the framework."},"top_picks":{"description":"Up to three of the tools with the highest pick rate on the framework.","type":"array","maxItems":3,"items":{"$ref":"#/components/schemas/AgentPickRate"}}},"required":["framework","runs","scenarios","top_picks"]}},"agents":{"description":"Runs by coding agent and model.","type":"array","items":{"type":"object","properties":{"agent":{"type":"string","enum":["claude_code","codex","cursor","pi","opencode","muse_code"]},"model":{"type":"string"},"runs":{"type":"integer","minimum":0,"description":"Completed runs."},"scenarios":{"type":"integer","minimum":0,"description":"Scenarios with completed runs."},"tools":{"type":"array","description":"The same tools as the top-level list, in its order.","items":{"$ref":"#/components/schemas/AgentPickRate"}}},"required":["agent","model","runs","scenarios","tools"]}},"environments":{"type":"array","description":"Each test app, most scenarios first and the empty folder last, then any test app where no run completed (runs 0). Every group lists the tools picked there plus the project’s own tool, highest pick rate first.","items":{"type":"object","properties":{"slug":{"type":"string"},"name":{"type":"string"},"framework":{"type":["string","null"],"description":"Null for the empty folder, which has no framework."},"runs":{"type":"integer","minimum":0,"description":"Completed runs in the test app."},"scenarios":{"type":"integer","minimum":0,"description":"Scenarios with completed runs in the test app."},"tools":{"description":"Rates in the test app, highest pick rate first.","type":"array","items":{"$ref":"#/components/schemas/AgentPickRate"}},"agents":{"type":"array","description":"Rates per agent, its models’ scenarios pooled, each counted once.","items":{"type":"object","properties":{"agent":{"type":"string","enum":["claude_code","codex","cursor","pi","opencode","muse_code"]},"runs":{"type":"integer","minimum":0,"description":"Its completed runs here."},"tools":{"description":"Rates for the agent here, highest pick rate first.","type":"array","items":{"$ref":"#/components/schemas/AgentPickRate"}}},"required":["agent","runs","tools"]}},"cells":{"type":"array","description":"One per question and agent, in question order (add, recommend, hosted). A cell where no run completed has runs 0 and no tools: it was not measured.","items":{"type":"object","properties":{"intent":{"type":"string","enum":["add","recommend","hosted","improve"]},"agent":{"type":"string","enum":["claude_code","codex","cursor","pi","opencode","muse_code"]},"runs":{"type":"integer","minimum":0,"description":"Completed runs in the cell. 0 means the cell was not measured."},"tools":{"description":"Rates in the cell, highest pick rate first; its models’ scenarios each count once.","type":"array","items":{"$ref":"#/components/schemas/AgentPickRate"}},"coverage":{"$ref":"#/components/schemas/AgentPickCoverage","description":"The cell’s coverage, its models summed."}},"required":["intent","agent","runs","tools","coverage"]}}},"required":["slug","name","framework","runs","tools","agents","cells"]}},"filters":{"type":"object","description":"The filters applied; null means all.","properties":{"agent":{"type":["string","null"],"description":"The agent filter. Null when not filtered."},"intent":{"type":["string","null"],"description":"The intent filter. Null when not filtered."}},"required":["agent","intent"]},"open_loss_actions":{"type":"integer","minimum":0,"description":"The project’s managed Agent Picks loss Actions that are still suggested or accepted, whichever managed test created them; self-run Actions are not counted. Filters and test_id do not change it."},"copied_from":{"type":["object","null"],"description":"The public category this setup was copied from. Null for a blank setup and for no setup.","required":["category_id","slug","name"],"properties":{"category_id":{"type":"string","format":"uuid"},"slug":{"type":"string"},"name":{"type":"string"}}},"edited":{"type":["object","null"],"description":"Whether the customer has edited this setup since it was created, as the setup stands now, whichever test is shown. Null for no setup and for a public board’s own setup.","required":["requests","recognition"],"properties":{"requests":{"type":"boolean","description":"A request, the request ending or a context was edited."},"recognition":{"type":"boolean","description":"How a tool is recognized was edited."}}}},"required":["status","category","job","copied_from","edited","open_loss_actions","own_tool_key","competitor_tool_keys","enabled_environment_ids","enabled_frameworks","enabled_agent_models","enabled_agents","test","filters","runs","install_rate_hidden_reason","coverage","incomplete_cells","tools","frameworks","agents","environments"]},"AccuracyWindow":{"type":"object","description":"The run days the figures cover.","properties":{"days":{"type":"integer","enum":[30,90]},"from":{"type":"string","format":"date","description":"First day of the window."},"to":{"type":"string","format":"date","description":"Last day of the window, included."}},"required":["days","from","to"]},"AccuracyTotals":{"type":"object","description":"Claim groups by outcome. facts is the number with an outcome, and the outcomes sum to it.","properties":{"facts":{"type":"integer","minimum":0,"description":"Claim groups with an outcome."},"match":{"type":"integer","minimum":0,"description":"Claims a page on the site states."},"contradict":{"type":"integer","minimum":0,"description":"Claims a page on the site says something different from."},"site_conflict":{"type":"integer","minimum":0,"description":"Claims one site page states and another contradicts."},"not_on_site":{"type":"integer","minimum":0,"description":"Claims DevTune did not find on the site’s crawled pages: missing, or false."},"stale":{"type":"integer","minimum":0,"description":"Claims the site supports only with dated evidence that may no longer hold."},"unjudged":{"type":"integer","minimum":0,"description":"Claims the check could not reach a confident verdict on."},"dismissed":{"type":"integer","minimum":0,"description":"Contradictions the team marked not a contradiction, and site conflicts it marked both right."},"match_rate":{"type":["number","null"],"minimum":0,"maximum":1,"description":"Share of facts your site agrees with, as a fraction from 0 to 1. Null when there are no facts."}},"required":["facts","match","contradict","site_conflict","not_on_site","stale","unjudged","dismissed","match_rate"]},"AccuracySitePage":{"type":"object","description":"The page on the brand’s site the claim was judged against.","properties":{"url":{"type":["string","null"],"description":"The page URL. Null when it was not recorded."},"quote":{"type":["string","null"],"description":"The page text the claim was judged against. Null when none was kept."},"page_type":{"type":["string","null"],"description":"The kind of page, such as docs, blog or changelog. Null when unknown."},"crawled_at":{"type":["string","null"],"format":"date-time","description":"When DevTune crawled the page. Null when unknown."},"locked":{"type":"boolean","description":"The customer has marked the page off-limits for suggested changes."}},"required":["url","quote","page_type","crawled_at","locked"]},"AccuracyOpenAction":{"type":"object","description":"The open action for the claim, created from Accuracy or attached to.","properties":{"id":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["open","in_progress","blocked"],"description":"Where work on the action stands."},"loop_state":{"type":["string","null"],"description":"The action’s status in Actions: accepted while it is open. Null when the action is hidden from the Actions feed."},"created_by":{"type":["string","null"],"format":"uuid","description":"The member who created the action. Null when no member is recorded."},"created_at":{"type":"string","format":"date-time"}},"required":["id","status","loop_state","created_by","created_at"]},"AccuracyOtherPage":{"type":"object","description":"Another page that states the claim: a conflicting_page makes a site conflict; a set_aside_page is a blog post or changelog that leaves the claim a contradiction.","properties":{"url":{"type":["string","null"],"description":"The page URL. Null when it was not recorded."},"quote":{"type":["string","null"],"description":"The page text that states the claim. Null when none was kept."},"page_type":{"type":["string","null"],"description":"The kind of page, such as docs, blog or changelog. Null when unknown."},"role":{"type":["string","null"],"enum":["conflicting_page","set_aside_page",null],"description":"conflicting_page or set_aside_page, as described above. Null when unknown."},"locked":{"type":"boolean","description":"The customer has marked the page off-limits for suggested changes."}},"required":["url","quote","page_type","role","locked"]},"AccuracyDecision":{"type":"object","description":"A decision the team made on a claim. Claim lists and the claim detail carry the active one.","properties":{"id":{"type":"string","format":"uuid"},"kind":{"type":"string","enum":["contradiction","site_conflict","not_on_site"],"description":"The kind of claim the decision is on."},"choice":{"type":"string","enum":["not_a_contradiction","both_right","site","other","true","true_no_action","false"],"description":"For a not-on-site claim: true when the team marked it true and created an \"Add to your site\" action, true_no_action when they marked it true without an action, false when they marked it false."},"chosen_url":{"type":["string","null"],"description":"The page a site-conflict answer kept. Null for other choices."},"changed_url":{"type":["string","null"],"description":"The page a site-conflict answer changes to agree with chosen_url. Null for other choices."},"action_id":{"type":["string","null"],"format":"uuid","description":"The action the decision created or joined. Null when it has none."},"action_role":{"type":["string","null"],"enum":["created","attached",null],"description":"Where the decision stands on its action now: created when the action is built from this decision, attached when the decision is added to an action built from another claim, null when it has none. Undoing the decision an action was built from rebuilds it from the next claim on it, whose role becomes created."},"no_action_reason":{"type":["string","null"],"enum":["dismissed","both_right","changed_page_locked","nothing_to_add",null],"description":"Why the decision has no action: dismissed for a contradiction marked not a contradiction, both_right for a site conflict where both pages are right, changed_page_locked when the page to change is locked, nothing_to_add for a not-on-site claim marked true without an action. Null when the decision calls for an action."},"label":{"description":"What the team saw when it decided, kept as it was: the claim text, excerpt, platform, pages and choice."},"decided_by":{"type":["string","null"],"format":"uuid","description":"The member who decided. Null when no member is recorded."},"decided_at":{"type":"string","format":"date-time","description":"When the decision was made."},"undo_available":{"type":"boolean","description":"False once the decision is undone, or when the action it created is live."}},"required":["id","kind","choice","chosen_url","changed_url","action_id","action_role","no_action_reason","label","decided_by","decided_at","undo_available"]},"AccuracyData":{"type":"object","properties":{"window":{"$ref":"#/components/schemas/AccuracyWindow"},"prior_window":{"type":"object","description":"The window of the same length that ends the day before window.from.","properties":{"from":{"type":"string","format":"date","description":"First day of the previous window."},"to":{"type":"string","format":"date","description":"Last day of the previous window, included."}},"required":["from","to"]},"totals":{"$ref":"#/components/schemas/AccuracyTotals"},"prior_status":{"type":"string","enum":["comparable","incomplete","method_changed"],"description":"incomplete when the series started after the previous window began or its checks are not finished; method_changed when question or sampling versions differ, or either window mixes sampling versions. Only comparable returns prior_totals and platform prior_match_rate, prior_facts and prior_match."},"prior_totals":{"description":"totals for prior_window. Null unless prior_status is comparable.","anyOf":[{"$ref":"#/components/schemas/AccuracyTotals"},{"type":"null"}]},"contradiction_rows":{"type":["integer","null"],"minimum":0,"description":"The site sentences active contradictions conflict with: the rows listAccuracyContradictions returns. totals.contradict counts the contradicted claim groups (AI wordings) instead."},"prior_contradiction_rows":{"type":["integer","null"],"minimum":0,"description":"contradiction_rows for the previous window. Null unless prior_status is comparable."},"unquoted_contradictions":{"type":["integer","null"],"minimum":0,"description":"Active contradictions (claim groups) whose contradicting claim quotes no sentence of a site page. No contradiction row lists them; they are counted in totals.contradict."},"weekly":{"type":"array","description":"Monday-UTC weeks clipped to the window. A week with sampled answers but no checked facts has facts 0 and a null match_rate.","items":{"type":"object","properties":{"week_start":{"type":"string","format":"date","description":"The Monday that starts the UTC week."},"sampling_versions":{"type":"array","items":{"type":"integer"},"description":"Sampling methods applied in this UTC week, including days before the clipped window start: 1 is one answer, 2 is up to three per prompt and platform. Mixed weeks retain both; empty means unknown."},"facts":{"type":"integer","minimum":0,"description":"Claim groups with an outcome that week."},"contradict":{"type":"integer","minimum":0,"description":"Claims that contradict the site that week."},"match_rate":{"type":["number","null"],"minimum":0,"maximum":1,"description":"Share of the week’s facts the site agrees with, from 0 to 1. Null when facts is 0."}},"required":["week_start","facts","contradict","match_rate","sampling_versions"]}},"platforms":{"type":"array","description":"Per platform. A claim seen on several platforms counts on each.","items":{"type":"object","properties":{"platform":{"type":"string"},"facts":{"type":"integer","minimum":0,"description":"Claim groups with an outcome on the platform."},"match":{"type":"integer","minimum":0,"description":"Of facts, claims the site states."},"contradict":{"type":"integer","minimum":0,"description":"Of facts, claims that contradict the site."},"site_conflict":{"type":"integer","minimum":0,"description":"Of facts, claims the site’s pages disagree on."},"not_on_site":{"type":"integer","minimum":0,"description":"Of facts, claims not found on the site."},"match_rate":{"type":["number","null"],"minimum":0,"maximum":1,"description":"match over facts, from 0 to 1. Null when facts is 0."},"prior_facts":{"type":["integer","null"],"minimum":0,"description":"Facts checked on this platform in the previous window. Null unless prior_status is comparable, and when the platform had no facts then."},"prior_match":{"type":["integer","null"],"minimum":0,"description":"Matching facts on this platform in the previous window. Null unless prior_status is comparable, and when the platform had no facts then."},"prior_match_rate":{"type":["number","null"],"minimum":0,"maximum":1,"description":"match_rate in the previous window. Null unless prior_status is comparable, and when the platform had no facts then."}},"required":["platform","facts","match","contradict","site_conflict","not_on_site","match_rate","prior_match_rate","prior_facts","prior_match"]}},"coverage":{"type":"object","description":"Answers eligible for a check and the sampled ones by status. Pending answers have not been checked yet.","properties":{"eligible_answers":{"type":"integer","minimum":0,"description":"AI answers that name the brand and can be checked."},"sampled":{"type":"integer","minimum":0,"description":"Of those, answers picked for a check: up to three per prompt, platform and week."},"checked":{"type":"integer","minimum":0,"description":"Sampled answers whose claims were checked."},"no_claims":{"type":"integer","minimum":0,"description":"Sampled answers that made no claim to check."},"failed":{"type":"integer","minimum":0,"description":"Sampled answers the check could not process."},"pending":{"type":"integer","minimum":0,"description":"Sampled answers not checked yet."}},"required":["eligible_answers","sampled","checked","no_claims","failed","pending"]},"series_started_on":{"type":["string","null"],"format":"date","description":"The first day an answer was sampled. Null when none has been."},"question_versions":{"type":"array","description":"Versions of the check’s questions used in the window. Windows on different versions are not compared.","items":{"type":"integer"}},"sampling_versions":{"type":"array","items":{"type":"integer"},"description":"Sampling methods in the window: 1 checks one answer per prompt/platform/week; 2 checks up to three. Mixed sampling windows are not compared."}},"required":["window","prior_window","totals","prior_status","prior_totals","contradiction_rows","prior_contradiction_rows","unquoted_contradictions","weekly","platforms","coverage","series_started_on","question_versions","sampling_versions"]},"AccuracyClaimsData":{"type":"object","properties":{"pagination":{"$ref":"#/components/schemas/ListPagination"},"window":{"$ref":"#/components/schemas/AccuracyWindow"},"outcome":{"type":"string","enum":["contradict","site_conflict","not_on_site","dismissed"],"description":"The list, as requested."},"items":{"type":"array","description":"Claim groups with this outcome in the window.","items":{"type":"object","properties":{"group_id":{"type":"string","format":"uuid"},"claim_text":{"type":"string"},"answers":{"type":"integer","minimum":0,"description":"AI answers that made the claim in the window."},"platforms":{"type":"array","description":"The AI platforms where the claim was made in the window.","items":{"type":"string"}},"last_seen_on":{"type":"string","format":"date","description":"The last run day the claim was made."},"last_seen_at":{"type":"string","format":"date-time","description":"When the claim was last made."},"execution_id":{"type":["string","null"],"format":"uuid","description":"The latest answer with the listed outcome. Null briefly after two claim groups are merged; read again later."},"claim_index":{"type":["integer","null"],"minimum":0,"description":"The claim within that answer. With execution_id it names the claim a decision on this item acts on. Null when execution_id is."},"prompt_id":{"description":"The tracked prompt behind that answer. Null when execution_id is.","type":["string","null"],"format":"uuid"},"prompt_text":{"type":["string","null"],"description":"Null once the prompt is deleted."},"excerpt":{"type":["string","null"],"description":"The passage of the AI answer that makes the claim. Null when execution_id is."},"site_page":{"description":"The site page the claim was judged against. Null when there is none, as for a not-on-site claim.","anyOf":[{"$ref":"#/components/schemas/AccuracySitePage"},{"type":"null"}]},"other_page":{"description":"Another site page that states the claim. Null when there is none.","anyOf":[{"$ref":"#/components/schemas/AccuracyOtherPage"},{"type":"null"}]},"decision":{"description":"The team’s active decision on the claim’s current outcome. Null when there is none, even if the team decided on the claim under another outcome.","anyOf":[{"$ref":"#/components/schemas/AccuracyDecision"},{"type":"null"}]},"answered":{"type":"boolean","description":"The team said whether a not-on-site claim is true or false."},"sentence_key":{"type":["string","null"],"pattern":"^[0-9a-f]{32}$","description":"For a contradiction, dismissed or not: the contradiction row it is listed under (getAccuracyContradiction). Null for other outcomes, and for a contradiction whose claim quotes no sentence of a site page, which no row lists."},"action":{"description":"The open action for the claim. Null when there is none.","anyOf":[{"$ref":"#/components/schemas/AccuracyOpenAction"},{"type":"null"}]}},"required":["group_id","claim_text","answers","platforms","last_seen_on","last_seen_at","execution_id","claim_index","prompt_id","prompt_text","excerpt","site_page","other_page","decision","answered","sentence_key","action"]}},"dismissed_count":{"type":"integer","minimum":0,"description":"Claims in the window the team dismissed, of both kinds."},"dismissed_contradiction_count":{"type":"integer","minimum":0,"description":"Dismissed claims that were contradictions. With dismissed_site_conflict_count, sums to dismissed_count."},"dismissed_site_conflict_count":{"type":"integer","minimum":0,"description":"Dismissed claims that were site conflicts the team marked both right."},"answered_count":{"type":"integer","minimum":0,"description":"Not-on-site claims in the window the team marked true or false."},"next_cursor":{"type":["string","null"],"description":"Pass verbatim as cursor for the next page, with the other parameters unchanged. Null on the last page."}},"required":["window","outcome","items","dismissed_count","dismissed_contradiction_count","dismissed_site_conflict_count","answered_count","next_cursor","pagination"]},"AccuracyClaimData":{"type":"object","properties":{"window":{"$ref":"#/components/schemas/AccuracyWindow"},"group_id":{"type":"string","format":"uuid"},"claim_text":{"type":"string"},"outcome":{"type":["string","null"],"enum":["contradict","site_conflict","stale","not_on_site","match","unjudged","dismissed",null],"description":"Null when the claim made no answer in the window and filters."},"platforms":{"type":"array","description":"The AI platforms where the claim was made in the window.","items":{"type":"string"}},"answers":{"type":"integer","minimum":0,"description":"AI answers that made the claim in the window."},"last_seen_on":{"type":["string","null"],"format":"date","description":"The last run day the claim was made. Null when it made no answer in the window and filters."},"intent_tag":{"type":["string","null"],"description":"The most common intent among its answers. Null when it made no answer in the window and filters."},"claim":{"type":["object","null"],"description":"An answer with the outcome the claim was given, the most recent if several have it. Without one, the most recent other answer, with matching answers last. Null when the claim made no answer in the window and filters.","properties":{"execution_id":{"type":"string","format":"uuid"},"claim_index":{"type":["integer","null"],"minimum":0,"description":"The claim within that answer. With execution_id it names the claim a decision on this group acts on."},"platform":{"type":"string"},"logical_run_date":{"type":"string","format":"date","description":"The run day of the answer."},"outcome":{"type":"string","enum":["contradict","site_conflict","stale","not_on_site","match","unjudged"]},"prompt_id":{"type":["string","null"],"format":"uuid","description":"The tracked prompt behind the answer."},"prompt_text":{"type":["string","null"],"description":"The prompt text. Null once the prompt is deleted."},"excerpt":{"type":"string","description":"The passage of the AI answer that makes the claim."},"site_page":{"description":"The site page the claim was judged against. Null when there is none, as for a not-on-site claim.","anyOf":[{"$ref":"#/components/schemas/AccuracySitePage"},{"type":"null"}]},"other_page":{"description":"Another site page that states the claim. Null when there is none.","anyOf":[{"$ref":"#/components/schemas/AccuracyOtherPage"},{"type":"null"}]},"site_crawled_at":{"type":["string","null"],"format":"date-time","description":"When DevTune crawled the site page. Null when there is no site page."},"recheck_version":{"type":["integer","null"],"description":"Version of the contradiction recheck run on this claim, whatever it found. Null when the first check found no contradiction to recheck."},"evidence":{"type":["array","null"],"description":"The site passages the claim was judged against. Null when none were kept.","items":{"type":"object","properties":{"id":{"type":"string"},"url":{"type":["string","null"],"description":"The page URL. Null when not recorded."},"pageId":{"type":["string","null"],"description":"The crawled page. Null when not recorded."},"pageType":{"type":["string","null"],"description":"The kind of page, such as docs or blog. Null when unknown."},"heading":{"type":["string","null"],"description":"The heading the passage sits under. Null when it has none."},"text":{"type":"string"}},"required":["id","url","pageId","pageType","heading","text"]}}},"required":["execution_id","claim_index","platform","logical_run_date","outcome","prompt_id","prompt_text","excerpt","site_page","other_page","site_crawled_at","recheck_version","evidence"]},"other_answers":{"type":"array","description":"Up to 20 other answers that made the claim.","items":{"type":"object","properties":{"execution_id":{"type":"string","format":"uuid"},"platform":{"type":"string"},"logical_run_date":{"type":"string","format":"date","description":"The run day of the answer."},"excerpt":{"type":"string","description":"The passage of the AI answer that makes the claim."}},"required":["execution_id","platform","logical_run_date","excerpt"]}},"cited_by":{"type":"array","description":"The brand’s own pages the answers cited, with how many answers cite each.","items":{"type":"object","properties":{"url":{"type":"string"},"answers":{"type":"integer","minimum":0,"description":"Answers that cite it."}},"required":["url","answers"]}},"decision":{"description":"The team’s active decision on the claim’s current outcome. Null when there is none, even if the team decided on the claim under another outcome.","anyOf":[{"$ref":"#/components/schemas/AccuracyDecision"},{"type":"null"}]},"sentence_key":{"type":["string","null"],"pattern":"^[0-9a-f]{32}$","description":"For a contradiction, dismissed or not: the contradiction row it is listed under (getAccuracyContradiction). Null for other outcomes, and for a contradiction whose claim quotes no sentence of a site page, which no row lists."},"action":{"description":"The open action for the claim. Null when there is none.","anyOf":[{"$ref":"#/components/schemas/AccuracyOpenAction"},{"type":"null"}]}},"required":["window","group_id","claim_text","outcome","platforms","answers","last_seen_on","intent_tag","claim","other_answers","cited_by","decision","sentence_key","action"]},"AccuracySentencesData":{"type":"object","properties":{"pagination":{"$ref":"#/components/schemas/ListPagination"},"window":{"$ref":"#/components/schemas/AccuracyWindow"},"items":{"type":"array","description":"Contradiction rows in the window.","items":{"type":"object","description":"A contradiction row: one sentence on the brand’s site that active contradictions conflict with, and the AI wordings (claim groups) that contradict it.","properties":{"sentence_key":{"type":"string","pattern":"^[0-9a-f]{32}$","description":"The row: its site page and its sentence as a reader sees it."},"site_page":{"description":"The site page the sentence is on.","anyOf":[{"$ref":"#/components/schemas/AccuracySitePage"},{"type":"null"}]},"other_page":{"description":"Another site page that states the claim. Null when there is none.","anyOf":[{"$ref":"#/components/schemas/AccuracyOtherPage"},{"type":"null"}]},"wording_count":{"type":"integer","minimum":0,"description":"Active AI wordings (claim groups) that contradict the sentence."},"dismissed_wording_count":{"type":"integer","minimum":0,"description":"Wordings the team dismissed."},"answers":{"type":"integer","minimum":0,"description":"The active wordings’ answers. An answer stating two wordings counts under each."},"platforms":{"type":"array","description":"The AI platforms of the active wordings’ answers.","items":{"type":"string"}},"last_seen_on":{"type":"string","format":"date","description":"The last run day an active wording was made."},"last_seen_at":{"type":"string","format":"date-time","description":"When an active wording was last made."},"example":{"type":"object","description":"The active wording with the most answers, by its latest contradicting answer. Its execution_id and claim_index name the claim a Create action on the row acts on.","properties":{"group_id":{"type":"string","format":"uuid"},"claim_text":{"type":"string"},"execution_id":{"type":["string","null"],"format":"uuid","description":"Its latest contradicting answer."},"claim_index":{"type":["integer","null"],"minimum":0,"description":"The claim within that answer. Null when execution_id is."},"prompt_id":{"description":"The tracked prompt behind that answer.","type":["string","null"],"format":"uuid"},"prompt_text":{"type":["string","null"],"description":"The prompt text. Null once the prompt is deleted."},"excerpt":{"type":["string","null"],"description":"The passage of the AI answer that makes the claim. Null when execution_id is."}},"required":["group_id","claim_text","execution_id","claim_index","prompt_id","prompt_text","excerpt"]},"group_ids":{"type":"array","description":"The active wordings, most answers first.","items":{"type":"string","format":"uuid"}},"action":{"description":"The open action for the claim. Null when there is none.","anyOf":[{"$ref":"#/components/schemas/AccuracyOpenAction"},{"type":"null"}]}},"required":["sentence_key","site_page","other_page","wording_count","dismissed_wording_count","answers","platforms","last_seen_on","last_seen_at","example","group_ids","action"]}},"next_cursor":{"type":["string","null"],"description":"Pass verbatim as cursor for the next page, with the other parameters unchanged. Null on the last page."}},"required":["window","items","next_cursor","pagination"]},"AccuracySentenceData":{"type":"object","properties":{"window":{"$ref":"#/components/schemas/AccuracyWindow"},"sentence_key":{"type":"string","pattern":"^[0-9a-f]{32}$","description":"The row, as requested."},"site_page":{"anyOf":[{"$ref":"#/components/schemas/AccuracySitePage"},{"type":"null"}],"description":"Null, with no wordings, when the row has none in the window and filters."},"other_page":{"description":"Another site page that states the claim. Null when there is none.","anyOf":[{"$ref":"#/components/schemas/AccuracyOtherPage"},{"type":"null"}]},"wording_count":{"type":"integer","minimum":0,"description":"Active AI wordings (claim groups) that contradict the sentence."},"dismissed_wording_count":{"type":"integer","minimum":0,"description":"Wordings the team dismissed."},"answers":{"type":"integer","minimum":0,"description":"The active wordings’ answers. An answer stating two wordings counts under each."},"platforms":{"type":"array","description":"The AI platforms of the active wordings’ answers.","items":{"type":"string"}},"last_seen_on":{"type":["string","null"],"format":"date","description":"The last run day an active wording was made. Null when the row has none in the window and filters."},"wordings":{"type":"array","description":"Every AI wording in the row, active first, then by most answers.","items":{"type":"object","properties":{"group_id":{"type":"string","format":"uuid"},"claim_text":{"type":"string"},"outcome":{"type":"string","enum":["contradict","dismissed"],"description":"contradict while active, dismissed once the team marked it not a contradiction."},"answers":{"type":"integer","minimum":0,"description":"AI answers that made the wording in the window."},"platforms":{"type":"array","description":"The AI platforms where it was made.","items":{"type":"string"}},"last_seen_on":{"type":"string","format":"date","description":"The last run day it was made."},"execution_id":{"type":["string","null"],"format":"uuid","description":"Its latest contradicting answer."},"claim_index":{"type":["integer","null"],"minimum":0,"description":"The claim within that answer. Null when execution_id is."},"platform":{"type":["string","null"],"description":"The AI platform of that answer. Null when execution_id is."},"logical_run_date":{"type":["string","null"],"format":"date","description":"The run day of that answer. Null when execution_id is."},"prompt_id":{"description":"The tracked prompt behind that answer.","type":["string","null"],"format":"uuid"},"prompt_text":{"type":["string","null"],"description":"The prompt text. Null once the prompt is deleted."},"excerpt":{"type":["string","null"],"description":"The passage of the AI answer that makes the claim. Null when execution_id is."},"recheck_version":{"type":["integer","null"],"description":"Version of the contradiction recheck run on this claim, whatever it found. Null when the first check found no contradiction to recheck."},"decision":{"anyOf":[{"$ref":"#/components/schemas/AccuracyDecision"},{"type":"null"}],"description":"The decision that dismissed a dismissed wording. Null for an active wording."}},"required":["group_id","claim_text","outcome","answers","platforms","last_seen_on","execution_id","claim_index","platform","logical_run_date","prompt_id","prompt_text","excerpt","recheck_version","decision"]}},"action":{"description":"The row’s open action. Null when there is none, or the row has no active wording.","anyOf":[{"$ref":"#/components/schemas/AccuracyOpenAction"},{"type":"null"}]}},"required":["window","sentence_key","site_page","other_page","wording_count","dismissed_wording_count","answers","platforms","last_seen_on","wordings","action"]},"AccuracyDecisionsData":{"type":"object","properties":{"pagination":{"$ref":"#/components/schemas/ListPagination"},"items":{"type":"array","description":"Every decision the team has made, newest first.","items":{"description":"A decision the team made, active or undone: the decision document listAccuracyClaims and getAccuracyClaim return, with its claim and status.","allOf":[{"$ref":"#/components/schemas/AccuracyDecision"},{"type":"object","properties":{"group_id":{"type":"string","format":"uuid"},"claim_text":{"type":"string","description":"One wording of the claim decided on."},"status":{"type":"string","enum":["active","undone"],"description":"active while the decision stands, undone once it was undone."},"undone_at":{"type":["string","null"],"format":"date-time","description":"When it was undone. Null while active."}},"required":["group_id","claim_text","status","undone_at"]}]}},"next_cursor":{"type":["string","null"],"description":"Pass verbatim as cursor for the next page. Null on the last page."}},"required":["items","next_cursor","pagination"]},"LookalikeSitesData":{"type":"object","properties":{"window":{"type":"object","description":"The run days the window covers, today (UTC) included.","properties":{"days":{"type":"integer","enum":[7,30,90]},"date_from":{"type":"string","format":"date"},"date_to":{"type":"string","format":"date"}},"required":["days","date_from","date_to"]},"executions_checked":{"type":"integer","description":"Answers checked for lookalike citations in the window, across every platform."},"coverage":{"type":"array","description":"Answers checked per platform in the window.","items":{"type":"object","properties":{"platform":{"type":"string"},"executions_checked":{"type":"integer"}},"required":["platform","executions_checked"]}},"hosts":{"type":"array","description":"Every lookalike host cited in the window, most cited first. Empty when none was cited.","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid","description":"Pass as hostId to listLookalikeSiteCitations."},"registrable_domain":{"type":"string"},"signal":{"type":"string","enum":["confusable","suffix_swap","one_edit","brand_affix"],"description":"How the domain imitates the owned domain: confusable characters, a swapped suffix, one edit, or the brand name with words added."},"owned_domain":{"type":"string","description":"The project domain it imitates."},"first_seen_at":{"type":"string","format":"date-time"},"last_seen_at":{"type":"string","format":"date-time"},"registered_at":{"type":["string","null"],"description":"When the domain was registered, from its public registration record. Null when unknown."},"registrar":{"type":["string","null"],"description":"The registrar, from its public registration record. Null when unknown."},"abuse_email":{"type":["string","null"],"description":"Where the registrar takes abuse reports, from its public registration record. Null when unknown."},"screen":{"type":["object","null"],"description":"Why DevTune archived the host’s action when it first saw the host, without alerting. Null when it did not.","properties":{"reason":{"type":"string","enum":["other_company"],"description":"other_company: the answers that cited it describe another company’s site."},"screened_at":{"type":"string","format":"date-time"}},"required":["reason","screened_at"]},"executions_citing":{"type":"integer","description":"Answers in the window that cited the host."},"citation_rate":{"type":["number","null"],"description":"executions_citing over executions_checked. Null when no answer was checked."},"platforms":{"type":"array","description":"The platforms whose answers cited the host.","items":{"type":"object","properties":{"platform":{"type":"string"},"executions_citing":{"type":"integer"},"executions_checked":{"type":"integer"},"citation_rate":{"type":["number","null"],"description":"executions_citing over the platform’s executions_checked. Null when none was checked."}},"required":["platform","executions_citing","executions_checked","citation_rate"]}}},"required":["id","registrable_domain","signal","owned_domain","first_seen_at","last_seen_at","registered_at","registrar","abuse_email","screen","executions_citing","citation_rate","platforms"]}}},"required":["window","executions_checked","coverage","hosts"]},"LookalikeSiteCitationsData":{"type":"object","properties":{"pagination":{"$ref":"#/components/schemas/ListPagination"},"citations":{"type":"array","description":"The answers that cited the host, newest run day first.","items":{"type":"object","properties":{"execution_id":{"type":"string","format":"uuid"},"platform":{"type":"string"},"prompt_id":{"type":"string","format":"uuid"},"prompt_text":{"type":["string","null"],"description":"Null when the prompt was deleted."},"logical_run_date":{"type":"string","format":"date"},"url":{"type":"string","description":"The cited URL."},"framing":{"type":["string","null"],"enum":["official","download","install","sign_in",null],"description":"How the answer presented the link, when it called it official or offered it to download, install or sign in. Null otherwise."},"excerpt":{"type":["string","null"],"description":"The answer text around the citation."}},"required":["execution_id","platform","prompt_id","prompt_text","logical_run_date","url","framing","excerpt"]}},"next_cursor":{"type":["string","null"],"description":"Pass verbatim as cursor for the next page. Null on the last page."}},"required":["citations","next_cursor","pagination"]},"PositioningStatementListData":{"type":"object","properties":{"competitors":{"type":"array","maxItems":5,"description":"Effective ordered positioning competitors, including those with no matches. Uses the saved choice, or the first five configured competitors when no eligible choice remains.","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"name":{"type":"string"}},"required":["id","name"]}},"statements":{"type":"array","description":"The project’s active and archived positioning statements.","items":{"$ref":"#/components/schemas/PositioningStatement"}}},"required":["statements","competitors"]},"PositioningAnswersData":{"type":"object","required":["window","statementId","assignee","answers","nextCursor","pagination"],"properties":{"pagination":{"$ref":"#/components/schemas/ListPagination"},"window":{"type":"integer","enum":[30,90]},"statementId":{"type":"string","format":"uuid"},"assignee":{"type":"string","description":"A brand id, or another_product, several or nobody, as requested."},"answers":{"type":"array","description":"The shown answers that gave the statement to assignee.","items":{"type":"object","required":["executionId","promptId","prompt","promptDeleted","platform","intent","date","confidence"],"properties":{"executionId":{"type":"string","format":"uuid"},"promptId":{"type":["string","null"],"format":"uuid","description":"The tracked prompt the answer came from."},"prompt":{"type":["string","null"],"description":"The prompt text. Null when the prompt is no longer recorded."},"promptDeleted":{"type":"boolean","description":"The prompt has since been removed."},"platform":{"type":"string"},"intent":{"type":"string","description":"The prompt’s intent, such as pricing_commercial."},"date":{"type":"string","format":"date","description":"The run day of the answer."},"confidence":{"type":"number","minimum":0,"maximum":1,"description":"How sure the check was of the verdict, from 0 to 1."}}}},"nextCursor":{"type":["string","null"],"description":"Pass verbatim as cursor for the next page, with the other parameters unchanged. Null on the last page."}}},"PositioningData":{"type":"object","properties":{"current_set_id":{"type":["string","null"],"format":"uuid","description":"Current competitor set covered by the totals, or null when no set is recorded."},"window":{"type":"object","description":"The run days the totals cover.","properties":{"days":{"type":"integer","enum":[30,90]},"from":{"type":"string","format":"date","description":"First day of the window."},"to":{"type":"string","format":"date","description":"Last day of the window, included."}},"required":["days","from","to"]},"prior_window":{"type":"object","description":"The window of the same length that ends the day before window.from. Each statement’s prior_status says whether it compares.","properties":{"from":{"type":"string","format":"date","description":"First day of the previous window."},"to":{"type":"string","format":"date","description":"Last day of the previous window, included."}},"required":["from","to"]},"series_started_on":{"type":"string","format":"date","description":"The first day in the window with results under the current competitor set. window.from when there are none."},"competitor_changes":{"type":"array","description":"Days in the window when the competitor set changed. Results across a change do not compare.","items":{"type":"string","format":"date"}},"statements":{"type":"array","description":"One total for each active or archived statement with results in the window, under the filters. A statement with none is left out.","items":{"$ref":"#/components/schemas/PositioningMatchCell"}},"daily":{"type":"array","description":"Each trend point identifies its competitor set. A day can contain multiple points for the same statement. Keep sets separate and show a break when competitors change, rather than a jump.","items":{"$ref":"#/components/schemas/PositioningMatchDailyCell"}}},"required":["current_set_id","window","series_started_on","competitor_changes","statements","daily"]},"AuditSummaryData":{"type":"object","properties":{"searchConsole":{"$ref":"#/components/schemas/GscWindowPeriods"},"domains":{"type":"array","description":"One entry per tracked domain, from its latest completed Audit. When that Audit covered only crawled pages or only the live site, the entry adds the other half from the Audit before it, and score is the lower of the two. Each entry has id, domain, targetUrl, completedAt, score (0 to 100), statusLabel, findingCount, pillars (each pillar's score, 0 to 100 or null when no check in it is scored), checksByPillar, crawlerAccess and retrievalAcknowledgements. A domain with no completed Audit is left out.","items":{"$ref":"#/components/schemas/FlexibleRollupItem"}},"trend":{"type":"array","description":"One point per day of the window, oldest first, each beside the matching day of the period before.","items":{"type":"object","description":"date is the UTC day for citations and answer fetches. clicksDate and priorClicksDate are Pacific Search Console days: clicksDate ends at searchConsole.current.window.dateTo and priorClicksDate is one period earlier. A point pairs days by their position in the period, not by calendar date.","properties":{"date":{"type":"string","format":"date","description":"The UTC day for aiCitations and answerFetches."},"clicksDate":{"type":"string","format":"date","description":"The Search Console day for clicks."},"priorClicksDate":{"type":"string","format":"date","description":"The Search Console day for priorClicks, one period before clicksDate."},"clicks":{"type":["number","null"],"description":"Search Console clicks on clicksDate. Null when the day is not verified."},"priorClicks":{"type":["number","null"],"description":"Search Console clicks on priorClicksDate. Null when the day is not verified."},"aiCitations":{"type":["number","null"],"description":"Citations of the project's domains in tracked AI answers on date. Null on a day without citation evidence."},"priorAiCitations":{"type":["number","null"],"description":"aiCitations for the same day one period earlier. Null on a day without citation evidence."},"answerFetches":{"type":["number","null"],"description":"Answer fetches of the project's pages on date. Null when none were recorded that day."},"priorAnswerFetches":{"type":["number","null"],"description":"answerFetches for the same day one period earlier. Null when none were recorded that day."}},"required":["date","clicksDate","priorClicksDate","clicks","priorClicks","aiCitations","priorAiCitations","answerFetches","priorAnswerFetches"]}},"windowDays":{"type":"integer","enum":[7,30,90]}},"required":["domains","trend","windowDays","searchConsole"]},"AuditExtractionData":{"type":"object","required":["status"],"properties":{"status":{"type":"string","enum":["available","unavailable","stale"],"description":"available when the excerpt is returned; the other fields are present only then. stale when the page evidence changed after the finding, the review is over 30 days old, or it used an older review method. unavailable when no matching excerpt was kept."},"sourceUrl":{"type":"string","format":"uri","description":"The page URL that was read."},"snapshotId":{"type":"string","format":"uuid"},"extractedAt":{"type":"string","format":"date-time","description":"When the page was read."},"assessedAt":{"type":"string","format":"date-time","description":"When the section was reviewed."},"publishedAt":{"type":"string","format":"date-time","description":"When Audit published the evidence."},"refreshId":{"type":"string","format":"uuid"},"contentHash":{"type":"string","description":"The contentHash passed, as the finding returned it."},"inputHash":{"type":"string","description":"The inputHash passed, as the finding returned it."},"section":{"type":"object","additionalProperties":true,"description":"Exact section from the section assessment, including heading, source and UTF-16 locator."},"findings":{"type":"array","maxItems":3,"description":"The review findings for this section.","items":{"type":"object","additionalProperties":true}},"context":{"type":"string","maxLength":6000,"description":"Exact nearby Markdown in extracted reading order."},"truncatedBefore":{"type":"boolean","description":"True when context leaves out page text before it."},"truncatedAfter":{"type":"boolean","description":"True when context leaves out page text after it."}}},"AuditFindingsData":{"type":"object","properties":{"sectionAssessments":{"type":"object","description":"Published 30-day Audit section evidence. Includes status (available, unavailable, stale), publication time, independent page pagination and per-page complete, partial, unavailable, stale or failed assessments. Assessments retain version, source content hash, input hash, assessment time, bounded selected sections with Markdown offsets and exact source excerpts, and at most three advisory findings. Locked-page editing remedies are replaced with guidance to leave the page unchanged and consider separate supporting content. No numerical score or citation-effect claim.","properties":{"status":{"type":"string","enum":["available","unavailable","stale"],"description":"available when Audit has published section evidence, unavailable when it has not, stale when the published evidence is out of date."},"publishedAt":{"type":["string","null"],"format":"date-time","description":"When Audit published the section evidence. Null unless status is available."},"refreshId":{"type":["string","null"],"description":"Identifies the published evidence. Null unless status is available."},"windowDays":{"type":"integer","enum":[30]},"items":{"type":"array","maxItems":100,"description":"This page of the pages in Audit's published 30-day evidence, ordered by domain and path. A page with no section review is listed with status unavailable and a null assessment.","items":{"type":"object","properties":{"domainCanonical":{"type":"string","description":"The tracked domain."},"pagePath":{"type":"string"},"status":{"type":"string","enum":["complete","partial","unavailable","stale","failed"],"description":"The review's status, as in CitabilitySectionAssessment. stale when the page content changed after the review, the review is over 30 days old, or it used an older review method; then no assessment is returned."},"assessment":{"description":"The review. Null when status is stale, or no usable review is kept.","anyOf":[{"$ref":"#/components/schemas/CitabilitySectionAssessment"},{"type":"null"}]}},"required":["domainCanonical","pagePath","status","assessment"]}},"pagination":{"type":"object","description":"Where this page sits among the section assessments alone.","properties":{"offset":{"type":"integer","minimum":0,"description":"Rows before this page."},"pageSize":{"type":"integer","minimum":1,"maximum":100,"description":"Rows per page."},"hasMore":{"type":"boolean","description":"More rows follow this page."}},"required":["offset","pageSize","hasMore"]}},"required":["status","publishedAt","refreshId","windowDays","items","pagination"]},"items":{"type":"array","description":"The findings Audit shows, with affectedUrls (a capped sample), affectedPageCount and affectedScope.","items":{"$ref":"#/components/schemas/FlexibleRollupItem"}},"pagination":{"$ref":"#/components/schemas/OffsetPagination"}},"required":["items","pagination","sectionAssessments"]},"GoogleEligibilityDimensions":{"type":"object","additionalProperties":false,"description":"Each indexing check, read from the latest inspection. pass and fail are measured; unmeasured means the inspection did not report enough to decide.","properties":{"canonical":{"type":"string","enum":["pass","fail","unmeasured"],"description":"pass when the canonical URL Google selected matches the one the page declares, fail when they differ, unmeasured when either is missing."},"crawlable":{"type":"string","enum":["pass","fail","unmeasured"],"description":"fail when robots.txt blocks Google or Google could not fetch the page. pass when robots.txt allows the page, the fetch succeeded and indexing was allowed or blocked only by noindex. unmeasured otherwise, such as when the inspection did not report all three."},"indexed":{"type":"string","enum":["pass","fail","unmeasured"],"description":"pass when Google indexed the page, fail when the verdict is any other outcome, unmeasured when the inspection failed or reported no detail."},"structuredData":{"type":"string","enum":["pass","fail","unmeasured"],"description":"pass when Google's rich results check passed. fail when it failed, passed only in part, or found no rich results. unmeasured when the inspection reported no detail."}},"required":["canonical","crawlable","indexed","structuredData"]},"GoogleEligibilityData":{"type":"object","additionalProperties":false,"properties":{"window":{"type":"object","additionalProperties":false,"description":"The reporting window, in UTC days, ending today.","properties":{"days":{"type":"integer","enum":[7,30,90]},"dateFrom":{"type":"string","format":"date","description":"First day of the window."},"dateTo":{"type":"string","format":"date","description":"Last day of the window, included."}},"required":["days","dateFrom","dateTo"]},"totals":{"type":"object","additionalProperties":false,"description":"Counts of pages, sitemaps and properties, not capped by evidenceLimit. A page counts when its latest successful URL Inspection ran inside the window and no more than 14 days ago, on a Search Console property that is connected and active for the project.","properties":{"inspectedPages":{"type":"integer","minimum":0,"description":"Pages counted, whatever their verdict."},"indexedPages":{"type":"integer","minimum":0,"description":"Pages whose verdict is indexed."},"inspectionPropertiesMeasured":{"type":"integer","minimum":0,"description":"Connected Search Console properties with at least one counted page. When 0, the inspection counts are unmeasured."},"nonIndexedPages":{"type":"integer","minimum":0,"description":"Pages whose verdict is anything other than indexed or error."},"inspectionErrors":{"type":"integer","minimum":0,"description":"Pages whose latest inspection failed (verdict error). They are counted because an earlier inspection in the window succeeded."},"pendingConfirmationPages":{"type":"integer","minimum":0,"description":"Pages with a pendingVerdict: a new verdict waiting for a second inspection to confirm it."},"confirmedTransitionPages":{"type":"integer","minimum":0,"description":"Pages whose confirmed verdict changed inside the window. The same pages as transitions, uncapped."},"pagesWithAssociatedSitemaps":{"type":"integer","minimum":0,"description":"Pages Google lists in at least one sitemap."},"sitemapPropertiesMeasured":{"type":"integer","minimum":0,"description":"Connected Search Console properties whose sitemap list DevTune read in the last 7 days. When 0, the sitemap counts are unmeasured."},"sitemapSubmissions":{"type":"integer","minimum":0,"description":"Sitemaps read inside the window from the properties in sitemapPropertiesMeasured."},"sitemapSubmissionsWithErrors":{"type":"integer","minimum":0,"description":"Of sitemapSubmissions, those Search Console reports at least one error for."},"sitemapSubmissionsWithWarnings":{"type":"integer","minimum":0,"description":"Of sitemapSubmissions, those Search Console reports at least one warning for."}},"required":["inspectedPages","indexedPages","inspectionPropertiesMeasured","nonIndexedPages","inspectionErrors","pendingConfirmationPages","confirmedTransitionPages","pagesWithAssociatedSitemaps","sitemapPropertiesMeasured","sitemapSubmissions","sitemapSubmissionsWithErrors","sitemapSubmissionsWithWarnings"]},"verdicts":{"type":"array","minItems":9,"maxItems":9,"description":"One row for every verdict, in a fixed order, counting the same pages as totals. A verdict no page has reports 0.","items":{"type":"object","additionalProperties":false,"properties":{"verdict":{"type":"string","enum":["indexed","crawled_not_indexed","discovered_not_crawled","duplicate_canonical","blocked_by_robots","blocked_by_noindex","fetch_error","excluded","error"],"description":"The latest inspection's outcome: indexed, or the reason Google has not indexed the page. error means the latest inspection request failed."},"totalCount":{"type":"integer","minimum":0,"description":"Pages with this verdict."}},"required":["verdict","totalCount"]}},"pages":{"type":"array","maxItems":25,"description":"Up to evidenceLimit of the counted pages, the ones needing attention first: not indexed, then failed inspections, then pending changes, then the most recently inspected.","items":{"type":"object","additionalProperties":false,"properties":{"domainCanonical":{"type":"string","maxLength":512,"description":"The tracked domain whose Search Console property covers the page."},"pagePath":{"type":"string","maxLength":2048,"description":"Path of the page on that domain. Starts with / and has no query or fragment."},"verdict":{"type":"string","enum":["indexed","crawled_not_indexed","discovered_not_crawled","duplicate_canonical","blocked_by_robots","blocked_by_noindex","fetch_error","excluded","error"],"description":"The latest inspection's outcome: indexed, or the reason Google has not indexed the page. error means the latest inspection request failed."},"confirmedVerdict":{"type":["string","null"],"enum":["indexed","crawled_not_indexed","discovered_not_crawled","duplicate_canonical","blocked_by_robots","blocked_by_noindex","fetch_error","excluded",null],"description":"The verdict DevTune treats as current. The first successful inspection sets it; after that, a different verdict must appear in two inspections in a row to replace it. Null when no verdict has been confirmed."},"pendingVerdict":{"type":["string","null"],"enum":["indexed","crawled_not_indexed","discovered_not_crawled","duplicate_canonical","blocked_by_robots","blocked_by_noindex","fetch_error","excluded",null],"description":"A verdict that differs from confirmedVerdict, seen in the latest successful inspection and waiting for the next one to confirm it. Null when nothing is waiting."},"coverageState":{"type":["string","null"],"maxLength":512,"description":"Google's coverage state for the page, worded as Search Console shows it, such as Submitted and indexed. Null when the latest inspection failed or reported none."},"googleCanonical":{"type":["string","null"],"maxLength":2048,"description":"The canonical URL Google selected for the page. Null when the inspection did not report one."},"userCanonical":{"type":["string","null"],"maxLength":2048,"description":"The canonical URL the page declares. Null when the inspection did not report one."},"robotsTxtState":{"type":["string","null"],"maxLength":256,"description":"Google's robots.txt result for the page, such as ALLOWED or DISALLOWED. Null when the inspection did not report one."},"indexingState":{"type":["string","null"],"maxLength":256,"description":"Google's indexing state, such as INDEXING_ALLOWED or BLOCKED_BY_META_TAG. Null when the inspection did not report one."},"pageFetchState":{"type":["string","null"],"maxLength":256,"description":"Google's fetch result, such as SUCCESSFUL, SOFT_404 or SERVER_ERROR. Null when the inspection did not report one."},"crawledAs":{"type":["string","null"],"maxLength":256,"description":"Which Googlebot crawled the page, such as MOBILE or DESKTOP. Null when the inspection did not report it."},"associatedSitemapCount":{"type":"integer","minimum":0,"description":"Sitemaps URL Inspection lists for the page, counting up to 20. associatedSitemaps shows the first 10."},"associatedSitemaps":{"type":"array","maxItems":10,"description":"The first 10 sitemap URLs Google lists for the page.","items":{"type":"string","maxLength":2048}},"referringUrlCount":{"type":"integer","minimum":0,"description":"URLs Google reports as linking to the page, counting up to 50. referringUrls shows the first 10."},"referringUrls":{"type":"array","maxItems":10,"description":"The first 10 URLs Google reports as linking to the page.","items":{"type":"string","maxLength":2048}},"richResults":{"type":["object","null"],"additionalProperties":true,"description":"Google's rich results result for the page, as Search Console returns it: an overall verdict and the items it detected. Null when the inspection reported none."},"dimensions":{"$ref":"#/components/schemas/GoogleEligibilityDimensions"},"observedAt":{"type":"string","format":"date-time","description":"When the latest successful inspection ran."},"lastCrawlTime":{"type":["string","null"],"format":"date-time","description":"When Google last crawled the page, as Search Console reports it. Null when it reports no crawl."}},"required":["domainCanonical","pagePath","verdict","confirmedVerdict","pendingVerdict","coverageState","googleCanonical","userCanonical","robotsTxtState","indexingState","pageFetchState","crawledAs","associatedSitemapCount","associatedSitemaps","referringUrlCount","referringUrls","richResults","dimensions","observedAt","lastCrawlTime"]}},"transitions":{"type":"array","maxItems":25,"description":"Up to evidenceLimit pages whose confirmed verdict changed inside the window, newest change first. A page whose latest inspection failed, or disagrees with the new verdict, is left out.","items":{"type":"object","additionalProperties":false,"properties":{"domainCanonical":{"type":"string","maxLength":512,"description":"The tracked domain whose Search Console property covers the page."},"pagePath":{"type":"string","maxLength":2048,"description":"Path of the page on that domain. Starts with / and has no query or fragment."},"previousConfirmedVerdict":{"type":"string","enum":["indexed","crawled_not_indexed","discovered_not_crawled","duplicate_canonical","blocked_by_robots","blocked_by_noindex","fetch_error","excluded"],"description":"The confirmed verdict before the change."},"confirmedVerdict":{"type":"string","enum":["indexed","crawled_not_indexed","discovered_not_crawled","duplicate_canonical","blocked_by_robots","blocked_by_noindex","fetch_error","excluded"],"description":"The confirmed verdict after the change, seen in two inspections in a row."},"coverageState":{"type":["string","null"],"maxLength":512,"description":"Google's coverage state for the page, worded as Search Console shows it, such as Submitted and indexed. Null when the latest inspection failed or reported none."},"dimensions":{"$ref":"#/components/schemas/GoogleEligibilityDimensions"},"observedAt":{"type":"string","format":"date-time","description":"When the latest successful inspection ran."},"transitionedAt":{"type":"string","format":"date-time","description":"When the inspection that confirmed the change ran."}},"required":["domainCanonical","pagePath","previousConfirmedVerdict","confirmedVerdict","coverageState","dimensions","observedAt","transitionedAt"]}},"sitemaps":{"type":"array","maxItems":25,"description":"Up to evidenceLimit of the sitemaps in sitemapSubmissions, the ones needing attention first: errors, then warnings, then pending, then the most recently read.","items":{"type":"object","additionalProperties":false,"properties":{"domainCanonical":{"description":"The tracked domain whose Search Console property lists the sitemap.","type":"string","maxLength":512},"sitemapUrl":{"type":"string","maxLength":2048,"description":"The sitemap URL: one submitted to Search Console, or one listed inside a submitted sitemap index."},"sitemapType":{"type":["string","null"],"maxLength":64,"description":"The sitemap type Search Console reports, such as sitemap or rssFeed. Null when it reports none."},"isPending":{"type":"boolean","description":"True while Search Console has not finished processing the sitemap."},"isSitemapsIndex":{"type":"boolean","description":"True when the file is a sitemap index that lists other sitemaps."},"lastSubmitted":{"type":["string","null"],"format":"date-time","description":"When the sitemap was last submitted. Null when Search Console reports no submission date."},"lastDownloaded":{"type":["string","null"],"format":"date-time","description":"When Google last downloaded the sitemap. Null when Google has never downloaded it."},"errorCount":{"type":["integer","null"],"minimum":0,"description":"Errors Search Console reports for the sitemap. Null when it reports no count."},"warningCount":{"type":["integer","null"],"minimum":0,"description":"Warnings Search Console reports for the sitemap. Null when it reports no count."},"submittedUrlCount":{"type":["integer","null"],"minimum":0,"description":"Web page URLs the sitemap submits; other content types, such as images and video, are not counted. Null when Search Console reports no web page count."},"fetchedAt":{"type":"string","format":"date-time","description":"When DevTune read this sitemap from Search Console."}},"required":["domainCanonical","sitemapUrl","sitemapType","isPending","isSitemapsIndex","lastSubmitted","lastDownloaded","errorCount","warningCount","submittedUrlCount","fetchedAt"]}}},"required":["window","totals","verdicts","pages","transitions","sitemaps"]},"DirectLiftData":{"type":"object","additionalProperties":false,"description":"GA4 Direct traffic and AI-correlated lift evidence. Correlation is not attribution, and inferred lift is never added to measured referral totals.","properties":{"window":{"type":"object","additionalProperties":false,"description":"The evaluation days the response covers, in UTC.","properties":{"days":{"type":"integer","enum":[7,30,90]},"dateFrom":{"type":"string","format":"date","description":"First evaluation day."},"dateTo":{"type":"string","format":"date","description":"Last evaluation day, included."},"evaluationWindowDays":{"type":"integer","enum":[7],"description":"Every classification compares one trailing seven-day evaluation window with its baseline and prior window."}},"required":["days","dateFrom","dateTo","evaluationWindowDays"]},"series":{"type":"array","maxItems":90,"description":"One point per evaluation day, oldest first.","items":{"type":"object","additionalProperties":false,"properties":{"date":{"type":"string","format":"date","description":"The evaluation day."},"fullDirectSessions":{"type":["integer","null"],"minimum":0,"description":"GA4's unfiltered project Direct-session total for the day. Null when no total is recorded for the day, or when directTotalsAvailability is unavailable."},"measuredActualDaily":{"type":["number","null"],"minimum":0,"description":"Daily equivalent of the measured-page seven-day actual, not an inferred traffic total. Null when no page was measured that day."},"measuredBaselineDaily":{"type":["number","null"],"minimum":0,"description":"Daily equivalent of the same measured-page population baseline. Null when no page was measured that day."},"pagesMeasured":{"type":"integer","minimum":0,"description":"Pages with a classification other than unmeasured that day."}},"required":["date","fullDirectSessions","measuredActualDaily","measuredBaselineDaily","pagesMeasured"]}},"totals":{"type":"object","additionalProperties":false,"description":"Counts over every page and evaluation day, not capped by pageLimit.","properties":{"classificationCounts":{"type":"object","additionalProperties":false,"description":"Page-windows (one page on one evaluation day) with each classification.","properties":{"ai_correlated_lift":{"type":"integer","minimum":0},"unexplained_lift":{"type":"integer","minimum":0},"explained":{"type":"integer","minimum":0},"baseline":{"type":"integer","minimum":0},"unmeasured":{"type":"integer","minimum":0}},"required":["ai_correlated_lift","unexplained_lift","explained","baseline","unmeasured"]},"directTotalsAvailability":{"type":"string","enum":["available","unavailable"],"description":"Whether series.fullDirectSessions can be read; directTotalsUnavailableReason says why not."},"directTotalsUnavailableReason":{"type":["string","null"],"description":"Why property Direct totals are unavailable, including posthog_property_total_unsupported and no_selected_analytics_provider. Null when they are available."},"classificationAvailability":{"type":"string","enum":["available","population_limit"],"description":"Whether page-window classifications were evaluated. Population limits preserve complete property Direct totals and leave lift evidence unavailable."},"pagePopulationSize":{"type":"integer","minimum":0,"description":"Landing pages with GA4 Direct data in the evaluation days, including pages beyond pageLimit. Counted even when classificationAvailability is population_limit."},"pageWindows":{"type":"integer","minimum":0,"description":"Page-windows evaluated: pages times evaluation days with an evaluation."},"pagesMeasured":{"type":"integer","minimum":0,"description":"series.pagesMeasured on the last evaluation day."}},"required":["classificationCounts","classificationAvailability","directTotalsAvailability","directTotalsUnavailableReason","pagePopulationSize","pageWindows","pagesMeasured"]},"pages":{"type":"array","maxItems":25,"description":"Bounded page list. Population sizes remain complete. When classificationAvailability is population_limit, no windows are classified and page details are empty.","items":{"type":"object","additionalProperties":false,"properties":{"domainCanonical":{"type":"string","maxLength":512,"description":"The tracked domain."},"pagePath":{"type":"string","maxLength":2048},"windows":{"type":"array","maxItems":90,"description":"One evaluation per day, oldest first.","items":{"type":"object","additionalProperties":false,"properties":{"evaluationDay":{"type":"string","format":"date","description":"The last day of the seven-day window evaluated."},"result":{"type":"object","additionalProperties":false,"description":"The classification and what decided it.","properties":{"classification":{"type":"string","enum":["ai_correlated_lift","unexplained_lift","explained","baseline","unmeasured"],"description":"baseline: no material lift. explained: material lift, and organic, paid or non-AI referral growth accounts for it. ai_correlated_lift: material lift that no channel explains, with qualifying AI evidence. unexplained_lift: the same without qualifying AI evidence. unmeasured: an input was missing; unmeasuredReasons says which. A correlation, never an attribution."},"aiEvidence":{"type":"string","enum":["citations","answer_fetches","absent","nonqualifying","unmeasured"],"description":"absent means covered families measured zero activity; nonqualifying means covered activity was observed but did not qualify."},"channelExplanation":{"type":["string","null"],"description":"The channel whose growth accounts for at least a quarter of the lift. Null unless classification is explained.","enum":["gsc_organic","paid","non_ai_referral",null]},"materialLift":{"type":["boolean","null"],"description":"True when Direct sessions rose by at least 10 over the baseline and to at least 125% of it. Null when unmeasured."},"unmeasuredReasons":{"type":"array","description":"What was missing: the GA4 connection or its coverage, the Search Console connection or its coverage, or AI evidence coverage. Empty unless classification is unmeasured.","items":{"type":"string","enum":["ga4_connection","ga4_coverage","gsc_connection","gsc_coverage","ai_evidence_coverage"]}}},"required":["classification","aiEvidence","channelExplanation","materialLift","unmeasuredReasons"]},"aiEvidenceAssessment":{"type":"object","additionalProperties":false,"description":"Each AI evidence family: qualifying (enough activity, not down more than 25% from the prior window), below_threshold, declined, or unmeasured.","properties":{"citations":{"type":"string","description":"Citations of the page; one in the current window qualifies.","enum":["qualifying","below_threshold","declined","unmeasured"]},"answerFetches":{"type":"string","description":"Answer fetches of the page; five in the current window qualify.","enum":["qualifying","below_threshold","declined","unmeasured"]}},"required":["citations","answerFetches"]},"facts":{"type":"object","additionalProperties":false,"description":"The measured values and coverage behind this window’s classification.","properties":{"evaluationWindow":{"$ref":"#/components/schemas/DirectLiftEvaluationWindowFacts"},"aiEvidence":{"$ref":"#/components/schemas/DirectLiftAiEvidenceFacts"},"channels":{"$ref":"#/components/schemas/DirectLiftChannelFacts"},"ga4Connection":{"$ref":"#/components/schemas/DirectLiftConnectionFacts"},"gscCoverage":{"$ref":"#/components/schemas/DirectLiftGscCoverageFacts"}},"required":["evaluationWindow","aiEvidence","channels","ga4Connection","gscCoverage"]}},"required":["evaluationDay","result","aiEvidenceAssessment","facts"]}}},"required":["domainCanonical","pagePath","windows"]}}},"required":["window","series","totals","pages"]},"DirectLiftEvaluationWindowFacts":{"type":"object","additionalProperties":false,"description":"GA4 Direct sessions for the page over the seven days ending on evaluationDay, against its baseline.","properties":{"actualDirectSessions":{"type":["number","null"],"minimum":0,"description":"Direct sessions in the current seven days. Null when not measured."},"baselineDirectSessions":{"type":["number","null"],"minimum":0,"description":"The median of the prior seven days and up to three seven-day periods before it. Null unless hasMeasurableBaseline."},"signedDirectLift":{"type":["number","null"],"description":"actualDirectSessions minus baselineDirectSessions; negative for a fall. Null unless hasMeasurableBaseline."},"priorDirectSessions":{"type":["number","null"],"minimum":0,"description":"Direct sessions in the seven days before. Null when not measured."},"currentWindowCoveredDays":{"type":"integer","minimum":0,"maximum":7,"description":"Days of the current seven GA4 covered."},"priorWindowCoveredDays":{"type":"integer","minimum":0,"maximum":7,"description":"Days of the prior seven GA4 covered."},"evaluableBaselineBucketCount":{"type":"integer","minimum":0,"maximum":4,"description":"Fully covered seven-day periods the baseline is the median of. At least 2 are needed."},"currentWindowEvaluable":{"type":"boolean","description":"True when GA4 covered all seven current days."},"priorWindowEvaluable":{"type":"boolean","description":"True when GA4 covered all seven prior days."},"hasMeasurableBaseline":{"type":"boolean","description":"True when both windows are evaluable and at least two baseline periods are."}},"required":["actualDirectSessions","baselineDirectSessions","signedDirectLift","priorDirectSessions","currentWindowCoveredDays","priorWindowCoveredDays","evaluableBaselineBucketCount","currentWindowEvaluable","priorWindowEvaluable","hasMeasurableBaseline"]},"DirectLiftAiEvidenceFacts":{"type":"object","additionalProperties":false,"description":"AI activity on the page in the current and prior seven days.","properties":{"citations":{"$ref":"#/components/schemas/DirectLiftEvidenceFamilyFacts","description":"Citations of the page in AI answers."},"answerFetches":{"$ref":"#/components/schemas/DirectLiftEvidenceFamilyFacts"}},"required":["citations","answerFetches"]},"DirectLiftEvidenceFamilyFacts":{"type":"object","additionalProperties":false,"properties":{"currentWindowCount":{"type":"integer","minimum":0,"description":"Count in the current seven days."},"priorWindowCount":{"type":"integer","minimum":0,"description":"Count in the prior seven days."},"currentWindowCovered":{"type":"boolean","description":"Whether the current seven days were measured."},"priorWindowCovered":{"type":"boolean","description":"Whether the prior seven days were measured."}},"required":["currentWindowCount","priorWindowCount","currentWindowCovered","priorWindowCovered"]},"DirectLiftChannelFacts":{"type":"object","additionalProperties":false,"description":"Other channels that could explain the lift: one does when it grew at least 10% and by at least a quarter of the lift.","properties":{"gscOrganic":{"$ref":"#/components/schemas/DirectLiftCountFacts","description":"Search Console clicks to the page."},"paid":{"$ref":"#/components/schemas/DirectLiftCountFacts","description":"Paid sessions on the page."},"nonAiReferral":{"$ref":"#/components/schemas/DirectLiftCountFacts","description":"Referral sessions not from AI answers."}},"required":["gscOrganic","paid","nonAiReferral"]},"DirectLiftCountFacts":{"type":"object","additionalProperties":false,"properties":{"currentWindowCount":{"type":"integer","minimum":0,"description":"Count in the current seven days."},"priorWindowCount":{"type":"integer","minimum":0,"description":"Count in the prior seven days."}},"required":["currentWindowCount","priorWindowCount"]},"DirectLiftConnectionFacts":{"type":"object","additionalProperties":false,"description":"The GA4 connection. It must be active and synced in the last 48 hours for the window to be measured.","properties":{"status":{"type":["string","null"],"description":"The connection status, such as active. Null when there is none."},"lastSyncedAt":{"type":["string","null"],"format":"date-time","description":"When it last synced. Null when it never has."}},"required":["status","lastSyncedAt"]},"DirectLiftGscCoverageFacts":{"type":"object","additionalProperties":false,"description":"The Search Console connection. It must be active, synced in the last 48 hours and cover both windows.","properties":{"status":{"type":["string","null"],"description":"The connection status, such as active. Null when there is none."},"lastSyncedAt":{"type":["string","null"],"format":"date-time","description":"When it last synced. Null when it never has."},"currentWindowCovered":{"type":"boolean","description":"Whether it covers the current seven days."},"priorWindowCovered":{"type":"boolean","description":"Whether it covers the prior seven days."}},"required":["status","lastSyncedAt","currentWindowCovered","priorWindowCovered"]},"PublishedPageTrackingData":{"type":"object","required":["actionId","crawlRefusal","activePages","pageLimit","limitSource","pages","allowanceStatus"],"properties":{"actionId":{"type":["string","null"],"format":"uuid","description":"The actionId filter, as requested. Null for the whole project."},"crawlRefusal":{"type":["string","null"],"enum":["account_paused","email_domain_mismatch",null],"description":"Why the project's site is not crawled. account_paused: DevTune support paused the account, so nothing is crawled. email_domain_mismatch: without a paid plan, a page marked crawl_refused is on a domain no member's confirmed work email matches; invite a teammate on that domain or choose a plan. Null otherwise."},"activePages":{"type":"integer","minimum":0,"description":"Pages the project tracks now."},"allowanceStatus":{"description":"Daily page tracking status. Null before the project is first measured. Demand counts are null when their source was not measured.","anyOf":[{"type":"null"},{"type":"object","required":["state","beyondPlanCount","demandPageCeiling","discoveredUntrackedCount","demandSources","demandUntrackedCount","citedUntrackedCount","citedUntrackedCitations","citedUntrackedSample","computedAt"],"properties":{"state":{"type":"string","enum":["within_limit","at_limit","beyond_plan"],"description":"at_limit: the plan allowance is full. beyond_plan: the demand overage tracks pages with AI demand past the plan allowance."},"beyondPlanCount":{"type":"integer","minimum":0,"description":"Tracked pages past the plan allowance."},"demandPageCeiling":{"type":["integer","null"],"description":"Most pages the demand overage may track; null without the overage."},"discoveredUntrackedCount":{"type":"integer","minimum":0,"description":"Pages found on the site that are not tracked."},"demandSources":{"type":["array","null"],"description":"The demand sources measured: AI citations, Search Console clicks and AI referrals. Null when none was measured.","items":{"type":"string","enum":["ai_citations","gsc_clicks","ai_referrals"]}},"demandUntrackedCount":{"type":["integer","null"],"description":"Untracked pages with demand from those sources. Null when no source was measured."},"citedUntrackedCount":{"type":["integer","null"],"description":"Untracked pages AI answers cite. Null when citations were not measured."},"citedUntrackedCitations":{"type":["integer","null"],"description":"Citations of those pages. Null when citations were not measured."},"citedUntrackedSample":{"type":"array","description":"Some of those pages, most cited first.","items":{"type":"object","required":["url","citations"],"properties":{"url":{"type":"string"},"citations":{"type":"integer","minimum":0}}}},"computedAt":{"type":"string","format":"date-time","description":"When the untracked-page and demand counts were last measured. state, beyondPlanCount and demandPageCeiling reflect the current plan and tracked pages."}}}]},"pageLimit":{"type":"integer","minimum":0,"description":"The page limit tracking is checked against: the plan's page allowance, plus pages with AI demand tracked past it when the demand overage applies, up to allowanceStatus.demandPageCeiling."},"limitSource":{"type":"string","enum":["plan"]},"pages":{"type":"array","description":"The action's published URLs. Empty without actionId.","items":{"type":"object","required":["url","status"],"properties":{"url":{"type":"string","format":"uri"},"status":{"type":"string","description":"tracked, plan_limit (the allowance is full), awaiting_crawl, crawl_disabled, crawl_refused (not crawled; crawlRefusal says why), page_unavailable or outside_project.","enum":["tracked","plan_limit","awaiting_crawl","crawl_disabled","crawl_refused","page_unavailable","outside_project"]}}}}}},"OutcomeUrlAttachmentData":{"type":"object","required":["status","candidates"],"properties":{"status":{"type":"string","enum":["saved"],"description":"saved when the URLs were attached."},"candidates":{"type":"array","description":"Always empty.","items":{"type":"object","required":["id","changedAt","kind"],"properties":{"id":{"type":"string","format":"uuid"},"changedAt":{"type":"string","format":"date-time","description":"When the change was detected."},"kind":{"type":"string","enum":["first_discovered","content_changed"],"description":"first_discovered when the page first appeared, content_changed when its content changed."}}}},"interventionId":{"type":["string","null"],"format":"uuid","description":"The measurement the URLs feed. Null until one exists."},"measurementStart":{"type":["string","null"],"format":"date-time","description":"When the before/after comparison starts. Null until set."},"measurementBasis":{"type":["string","null"],"description":"What measurementStart is anchored to. Null until set.","enum":["action_completion","first_discovered","content_changed",null]}}},"ContentChangeClaimData":{"type":"object","required":["actionId","interventionId"],"properties":{"actionId":{"type":"string","format":"uuid"},"interventionId":{"type":"string","format":"uuid"}}},"ActionStatusReceipt":{"type":"object","additionalProperties":false,"required":["actionId","actionIds","blocked","changed","fromStatus","toStatus","transition","transitionedAt"],"description":"A receipt of the transition as it applied, not a current read of the action. Read the action's current state from the actions list.","properties":{"actionId":{"type":"string","format":"uuid"},"actionIds":{"type":"array","minItems":1,"maxItems":40,"items":{"type":"string","format":"uuid"},"description":"Every action the transition moved: the named one and any suggestions merged into it. Just the named action when nothing changed."},"blocked":{"type":"boolean","description":"Whether the action is blocked after the transition."},"changed":{"type":"boolean","description":"False when the action was already where the transition leads; nothing was written or announced."},"fromStatus":{"$ref":"#/components/schemas/PublicActionStatus"},"toStatus":{"$ref":"#/components/schemas/PublicActionStatus"},"transition":{"type":"string","enum":["accept","unaccept","archive","restore","block","unblock","mark_live","reopen"],"description":"The transition, as requested."},"transitionedAt":{"type":["string","null"],"format":"date-time","description":"When the transition applied; null when nothing changed."}}},"ActionEventsData":{"type":"object","required":["actionId","items","hasMore","nextCursor","pagination"],"properties":{"pagination":{"$ref":"#/components/schemas/ListPagination"},"actionId":{"type":"string","format":"uuid"},"items":{"type":"array","description":"Edits to the action, newest first.","items":{"type":"object","required":["id","kind","from","to","actor","createdAt"],"properties":{"id":{"type":"string","format":"uuid"},"kind":{"type":"string","description":"What was edited.","enum":["renamed","owner_set","owner_cleared","channels_changed","description_changed","published_page_added","published_page_removed"]},"from":{"description":"The value before the edit: a title, owner user id, channel array, description or page URL; null when there was none."},"to":{"description":"The value after the edit, in the same form as from; null when it was cleared or removed."},"actor":{"type":"object","description":"Who made the edit.","required":["type"],"properties":{"type":{"type":"string","enum":["user","api_key","system"],"description":"A member, an API key, or DevTune itself."},"userId":{"type":["string","null"],"format":"uuid","description":"The member who made the edit; null once their user is deleted. Present for user edits."},"name":{"type":["string","null"],"description":"The member's name or email while they are still on the account; null once they leave. Present for user edits."},"apiKeyId":{"type":["string","null"],"format":"uuid","description":"The API key that made the edit; null once the key is deleted. Present for api_key edits."}}},"createdAt":{"type":"string","format":"date-time"}}}},"hasMore":{"type":"boolean","description":"More rows follow this page."},"nextCursor":{"type":["string","null"],"description":"Pass as cursor to read the next page. Null on the last page."}}},"ContentChangesData":{"type":"object","required":["items","totalCount","hasMore","nextCursor","summary","window","pagination"],"properties":{"pagination":{"$ref":"#/components/schemas/ListPagination"},"items":{"type":"array","description":"Content changes DevTune detected on tracked pages in the window.","items":{"type":"object","properties":{"changeEventId":{"type":"string","format":"uuid"},"changedAt":{"type":"string","format":"date-time","description":"When the change was detected."},"targetUrl":{"type":"string","description":"The changed page."},"actionId":{"type":"string","nullable":true,"description":"The action the change is claimed for. Null when unclaimed."},"actionTitle":{"type":"string","nullable":true,"description":"That action’s title. Null when unclaimed."},"interventionId":{"type":"string","nullable":true,"description":"The measurement the claim opened. Null when unclaimed."},"existingMeasurementId":{"type":"string","format":"uuid","nullable":true,"description":"Existing measurement on this page, completed within 14 days of the unclaimed change. Does not imply attribution; claim requests return 409 for this overlapping measurement. Null when there is none."},"claimToken":{"type":"string","nullable":true,"description":"Pass it with changeEventId to unclaim the change. Null when the change has no active claim."},"manualClaim":{"type":"boolean","description":"True when the change was claimed by a person or an API key."},"reclaim":{"type":"boolean","description":"True when the change was claimed before and that claim was undone."},"citationDelta":{"type":"number","nullable":true,"description":"Change in the page's citations per covered day, after minus before, over the 14-day comparison. Null until the after window reaches 14 days, or when either window has no covered day."},"presenceDelta":{"type":"number","nullable":true,"description":"Change in the share of the project's answers that cite the page, after minus before, as a fraction from -1 to 1, over the same comparison. Null until the after window reaches 14 days, or when either window has no answers."},"confidence":{"type":"string"},"measurementUpdating":{"type":"boolean","description":"Longer comparisons are being filled asynchronously after claim. Conservatively true for claimed changes when refresh.available is false."},"claimed":{"type":"boolean","description":"True when the change is claimed for an action."}}}},"refresh":{"type":"object","description":"Audit's refresh status. Each item's measurementUpdating says whether its measurement is still updating.","properties":{"auditRefreshedAt":{"type":"string","format":"date-time","nullable":true,"description":"When Audit last refreshed the pages. Null when unknown."},"available":{"type":"boolean","description":"False when refresh status could not be loaded; published outcome data is still returned."},"auditUpdating":{"type":"boolean","description":"True while Audit is refreshing the pages, and whenever refresh.available is false."}}},"totalCount":{"type":"integer","description":"Changes matching the filters, across every page."},"hasMore":{"type":"boolean","description":"More rows follow this page."},"nextCursor":{"type":"string","nullable":true,"description":"Pass as cursor to read the next page. Null on the last page."},"summary":{"type":"object","description":"Full-window totals, before path filtering.","properties":{"detectedCount":{"type":"integer","description":"Changes detected in the window."},"claimedCount":{"type":"integer","description":"Of those, changes claimed for an action."},"unclaimedCount":{"type":"integer","description":"Of those, changes not claimed."}}},"window":{"type":"object","description":"The UTC days the list covers.","properties":{"days":{"type":"integer"},"dateFrom":{"type":"string","format":"date","description":"First day of the window."},"dateTo":{"type":"string","format":"date","description":"Last day of the window, included."},"comparisonDays":{"type":"integer","enum":[14]},"summaryScope":{"type":"string","enum":["selected_window"]}}}}},"OutcomeOverlap":{"type":"object","additionalProperties":false,"properties":{"state":{"type":"string","enum":["available","unavailable"],"description":"available means the overlap check completed; unavailable means it failed, not that there were no other actions."},"targetUrl":{"type":"string","description":"The measured page."},"windowDays":{"type":"integer","description":"Length of each comparison window, in days."},"windowStartsAt":{"type":"string","format":"date-time","description":"Inclusive start of the before period."},"windowEndsAt":{"type":"string","format":"date-time","description":"Exclusive end of the after period."},"actions":{"type":"array","nullable":true,"description":"Matching completed actions when state is available; null when unavailable. An empty array means a successful check found no overlap.","items":{"type":"object","additionalProperties":false,"properties":{"actionId":{"type":"string","format":"uuid"},"title":{"type":"string","description":"The other action’s title."},"completedAt":{"type":"string","format":"date-time"}},"required":["actionId","title","completedAt"]}}},"required":["state","targetUrl","windowDays","windowStartsAt","windowEndsAt","actions"]},"OutcomeOverlapUnavailable":{"type":"object","additionalProperties":false,"description":"The requested measurement detail could not be loaded. Its page and comparison dates are unknown, so this entry has no page/window metadata.","properties":{"state":{"type":"string","enum":["unavailable"]},"actions":{"type":"array","nullable":true,"enum":[null],"items":{},"description":"Always null."}},"required":["state","actions"]},"OutcomesLedgerData":{"type":"object","properties":{"overlappingActions":{"type":"array","items":{"oneOf":[{"$ref":"#/components/schemas/OutcomeOverlap"},{"$ref":"#/components/schemas/OutcomeOverlapUnavailable"}]},"description":"Included with interventionId, for its 7, 14, 30 and 90 day comparisons. Other completed actions on each page; page movement is not attributable to one action alone. If measurement detail cannot be loaded, contains one unavailable entry without page/window metadata."},"impactWindows":{"type":"array","description":"Included when interventionId is supplied. Completion-based comparisons of citations, presence and answer fetches; unfinished or unsupported deltas are null. evidenceQuality grades citations and presence; answerFetchEvidenceQuality grades answer fetches by machine-sensor coverage.","items":{"type":"object","properties":{"windowDays":{"type":"integer","enum":[7,14,30,90],"description":"Length of each comparison window, in days."},"evidenceQuality":{"type":"string","description":"still_measuring until the after window closes; observed when both windows are fully covered; directional when measured with gaps; unavailable when not measurable.","enum":["still_measuring","observed","directional","unavailable"]},"citationDelta":{"type":"number","nullable":true,"description":"Change in the page's citations per covered day, after minus before. Null while still_measuring or unavailable, or when either window has no covered day."},"presenceDelta":{"type":"number","nullable":true,"description":"Change in the share of the project’s answers that cite the page, after minus before, as a fraction from -1 to 1: 0.1 is 10 percentage points. Null while still_measuring or unavailable, or when either window has no answers."},"answerFetchEvidenceQuality":{"type":"string","description":"The same grades for answer fetches, by machine-sensor coverage.","enum":["still_measuring","observed","directional","unavailable"]},"answerFetchDelta":{"type":"number","nullable":true,"description":"Change in answer fetches per sensor-covered day, after minus before. Null while still_measuring or unavailable."}},"required":["windowDays","evidenceQuality","citationDelta","presenceDelta","answerFetchEvidenceQuality","answerFetchDelta"]}},"attributedConversions":{"type":"array","description":"Conversions the analytics provider attributes to AI referrals, by window.","items":{"$ref":"#/components/schemas/OutcomesAttributedConversion"}},"items":{"type":"array","description":"Measured actions, ordered by measuredAt, newest first, then by the latest page change. Actions with no measuredAt come last.","items":{"$ref":"#/components/schemas/OutcomesLedgerItem"}},"pagination":{"$ref":"#/components/schemas/RollupCursorPagination"},"scoreboard":{"$ref":"#/components/schemas/OutcomesScoreboard"},"window":{"type":"object","description":"The UTC days that select items and scoreboard: actions completed on these days. attributedConversions carries its own dates.","properties":{"days":{"type":"integer"},"dateFrom":{"type":"string","format":"date","description":"First day of the window."},"dateTo":{"type":"string","format":"date","description":"Last day of the window, included."}},"required":["days","dateFrom","dateTo"]}},"required":["attributedConversions","items","pagination","scoreboard","window"]},"OutcomesLedgerItem":{"type":"object","additionalProperties":true,"description":"One measured action and its before/after window.","properties":{"overlappingActions":{"type":"array","items":{"$ref":"#/components/schemas/OutcomeOverlap"},"description":"Other completed actions sharing this page during its 14-day before/after comparison. Empty when there is no comparison."},"analyticsComparison":{"description":"Sessions and conversions before and after. Null when the action has no analytics comparison.","anyOf":[{"$ref":"#/components/schemas/AnalyticsComparison"},{"type":"null"}]},"interventionId":{"type":"string","format":"uuid"},"actionId":{"type":"string","format":"uuid"},"actionTitle":{"type":"string"},"method":{"type":["string","null"],"enum":["v1","v2",null],"description":"Results method used for a closed window with evidence. Null while open and for Agent Picks and lookalike rows. Closed windows keep their method. New closes use v2."},"result":{"type":["string","null"],"enum":["improved","mixed","no_clear_change","declined","still_measuring","unmeasured",null],"description":"The result: what the before/after window shows, never what caused it. A metric moves when its count changes further than chance would explain: a binomial test on the combined before and after count, weighted by each window's exposure (all probe answers for cited probe answers, window length for answer fetches in v1), comes in under 0.1 two-sided, the higher window holds at least 3 across at least 3 days, and the exposure-adjusted rate moves at least 20%. V2 requires full coverage for a conclusive metric, matches the live prompt/platform cohort across both citation windows, uses sensor-covered days for fetch exposure, and divides effective counts by the largest dispersion, with a floor of one: within-window daily counts for both metrics and, for citations, the spread across prompt/platform pairs. V2 citations also need at least 3 prompt/platform pairs in each window. Original count, active-day and relative-change floors still apply. V2 decisions are saved at close; v1 retains its derivation. evidence carries the counts, days, exposure and chance behind each metric. improved: a rise and no fall. declined: a fall and no rise. mixed: one metric rose and another fell. no_clear_change: a conclusive window where no metric moved; it is not a decline. Pending fetch comparisons stay still_measuring even when citations are complete; an established citation improvement remains conclusive, and becomes mixed if the answer-fetch comparison later closes with a material fall; a mixed result is dated at that fetch-window close. Open windows stay still_measuring; missing evidence is unmeasured. Null when the result cannot be read."},"measuredAt":{"type":["string","null"],"format":"date-time","description":"When the measurement was last recorded; it can be earlier than the end of the comparison window. Read resultAvailableAt for when a result is conclusive. Null until measured."},"resultAvailableAt":{"type":["string","null"],"format":"date-time","description":"Server-derived conclusive result date: the later of the complete daily comparison boundary and measuredAt, also including the recorded fetch-window close when citations did not independently establish an improvement. Null for non-conclusive results or an unavailable date. This describes the supporting comparison, not an audit timestamp of first observation."},"citationConfidence":{"type":["string","null"],"description":"observed (both windows fully covered), directional (measured with gaps), insufficient_window (the after window has not closed), not_observable, or not_applicable when the metric does not apply. Null after the after window ends until its result is recorded, and when the measurement changed after the ledger window's last day."},"answerFetchConfidence":{"type":["string","null"],"description":"The same grades for answer fetches. Null when the measurement changed after the ledger window's last day."},"citationsBefore":{"type":["number","null"],"description":"The page's citations per covered day in the before window. Null when no page change is linked, when citationConfidence is not observed or directional, or when the before window had no probe answers."},"citationsAfter":{"type":["number","null"],"description":"The page's citations per covered day in the after window. Null until the after window closes, and when citations were not compared: either window had no probe answers, citationConfidence is not observed or directional, or a v2 window was not fully covered."},"citationDelta":{"type":["number","null"],"description":"citationsAfter minus citationsBefore. Null when either is."},"answerFetchesBefore":{"type":["number","null"],"description":"Answer fetches of the page in the before window. Null when no before-window count was recorded, such as when no server-side traffic sensor covered the page."},"answerFetchesAfter":{"type":["number","null"],"description":"Answer fetches in the after window. Null unless answerFetchConfidence is observed or directional."},"answerFetchDelta":{"type":["number","null"],"description":"The change in answer fetches, after minus before. Null unless answerFetchConfidence is observed or directional."},"evidence":{"anyOf":[{"$ref":"#/components/schemas/OutcomeResultEvidence"},{"type":"null"}],"description":"What the result rests on, per metric. Null for rows without a comparison."},"lookalike":{"$ref":"#/components/schemas/OutcomeLookalike"}},"required":["overlappingActions","interventionId","actionId","actionTitle","result","method","resultAvailableAt"]},"OutcomeLookalikeRate":{"type":"object","additionalProperties":false,"description":"One window of a lookalike citation rate: answers checked, answers citing the lookalike site, and their ratio.","properties":{"checked":{"type":"integer","minimum":0,"description":"Probe answers the lookalike check read."},"citing":{"type":"integer","minimum":0,"description":"Probe answers that cited the lookalike site."},"rate":{"type":["number","null"],"description":"citing over checked. Null when no answer was checked: Unmeasured."}},"required":["checked","citing","rate"]},"OutcomeLookalike":{"type":"object","additionalProperties":false,"description":"Present only on a live lookalike action, an action to report a site imitating the customer. Its result compares the lookalike citation rate (answers citing the site over answers checked) in the 14 days before the go-live day with the 14 days from it, per platform and overall; overall sums the platforms checked in both windows. A fall is improved and a rise is declined; each comparison takes the same count test as other results, with answers checked as exposure. A platform with no answers checked in a window is unmeasured, and a window that starts before the 181 days of retained lookalike data counts as no answers checked. The overall result is still_measuring until 14 days after going live, unmeasured when no platform was checked in both windows, mixed when platforms moved in opposite directions, and otherwise improved, declined or no_clear_change as the overall counts moved. It shows what moved, not that the reports caused it. Omitted while the account does not have lookalike sites.","properties":{"hostId":{"type":"string","format":"uuid"},"registrableDomain":{"type":"string","description":"The lookalike site. Text, not a link."},"liveOn":{"type":"string","format":"date","description":"The UTC go-live day the windows compare around."},"beforeFrom":{"type":"string","format":"date"},"beforeTo":{"type":"string","format":"date"},"afterFrom":{"type":"string","format":"date"},"afterTo":{"type":"string","format":"date"},"result":{"type":"string","enum":["improved","mixed","no_clear_change","declined","still_measuring","unmeasured"]},"overall":{"type":"object","additionalProperties":false,"properties":{"before":{"$ref":"#/components/schemas/OutcomeLookalikeRate"},"after":{"$ref":"#/components/schemas/OutcomeLookalikeRate"}},"required":["before","after"]},"platforms":{"type":"array","items":{"type":"object","additionalProperties":false,"properties":{"platform":{"type":"string"},"before":{"$ref":"#/components/schemas/OutcomeLookalikeRate"},"after":{"$ref":"#/components/schemas/OutcomeLookalikeRate"},"result":{"type":"string","enum":["improved","no_clear_change","declined","still_measuring","unmeasured"]},"pValue":{"type":["number","null"],"description":"How often chance moves the citing count this far, two-sided. Null while measuring or when the platform was not compared."}},"required":["platform","before","after","result","pValue"]}},"rule":{"type":"object","additionalProperties":false,"properties":{"significance":{"type":"number"},"minCount":{"type":"number"},"minActiveDays":{"type":"number"},"minRelativeChange":{"type":"number"}},"required":["significance","minCount","minActiveDays","minRelativeChange"]}},"required":["hostId","registrableDomain","liveOn","beforeFrom","beforeTo","afterFrom","afterTo","result","overall","platforms","rule"]},"OutcomeMetricEvidence":{"type":"object","additionalProperties":false,"description":"One metric's counts in the before and after windows, and the test result.","properties":{"before":{"type":["number","null"],"description":"Before-window count: cited probe answers for citations, requests for answer fetches. Null when no before-window count was recorded."},"after":{"type":["number","null"],"description":"The same count for the after window. Null until it is measured."},"beforeActiveDays":{"type":["number","null"],"description":"Days in the window with at least one; null when the days cannot be counted."},"afterActiveDays":{"type":["number","null"],"description":"The same for the after window. Null when the days cannot be counted, or when the metric was not compared."},"beforeProbeAnswers":{"type":["number","null"],"description":"Probe answers in the window; v2 counts only prompts and platforms present in both windows. Citations only; null otherwise."},"afterProbeAnswers":{"type":["number","null"],"description":"The same for the after window. Citations only; null otherwise, and null when citations were not compared."},"dispersion":{"type":"number","minimum":1,"description":"V2 daily-count overdispersion factor."},"fullCoverage":{"type":"boolean","description":"V2 requires full-window coverage for a conclusive metric."},"beforeExposureDays":{"type":["number","null"],"description":"Sensor-covered days in the before window, for answer fetches under v2; omitted otherwise. Null when a connected sensor's coverage could not be counted."},"afterExposureDays":{"type":["number","null"],"description":"The same for the after window, and null in the same cases."},"direction":{"type":["number","null"],"enum":[-1,0,1,null],"description":"1 rose, -1 fell, 0 no change counted, null not compared for this window (see fullCoverage and the exposure days)."},"pValue":{"type":["number","null"],"description":"Two-sided: how often a shift at least this large, in either direction, happens by chance with no real change. Null until the window is measured."}}},"OutcomeRedirect404":{"type":"object","additionalProperties":false,"description":"404 requests to the missing path for a redirect-shipped action, separate from the overall result. Every requested host needs bot requests with response status codes on every UTC date touched by each window. Counts are null for uncovered windows or counters changed after capture. State depends on the existence of 404 rows at capture, independently of count stability. Saved suggested destinations identify redirects; requested URLs require observed 3xx traffic and no 2xx responses on the requested path across all requested hosts after the window closes. The browser snippet cannot measure 404s. Coverage-day counts are the minimum across the requested hosts.","properties":{"generation":{"type":"string","enum":["redirect-404-v1"]},"source":{"type":"string","enum":["llm_traffic_hourly"]},"requestedPath":{"type":"string","description":"The missing path the redirect serves."},"hosts":{"type":"array","description":"The hosts the path was requested on.","items":{"type":"string"}},"state":{"type":"string","enum":["stopped","still_happening","unmeasured"],"description":"stopped: 404 rows existed before capture in the before window and none in the after window, with both windows fully covered and closed. still_happening: 404 rows existed at capture in both fully covered windows. unmeasured: window_open, unknown_hosts, status_coverage_gap, or no_baseline_404s. counter_changed only nulls an affected count and preserves a known state. This never changes the overall action result."},"before":{"type":["integer","null"],"minimum":0,"description":"404 requests for the path in the before window. Null while the window is open, when the requested hosts are unknown, when the before window lacks full status-code coverage, or when its count changed after capture."},"after":{"type":["integer","null"],"minimum":0,"description":"404 requests in the after window. Null while the window is open, when the requested hosts are unknown, when the after window lacks full status-code coverage, or when its count changed after capture."},"beforeCoveredDays":{"type":"integer","minimum":0,"description":"The smallest number of before-window days with status-code coverage among the requested hosts, each host counted on its own."},"afterCoveredDays":{"type":"integer","minimum":0,"description":"The smallest number of after-window days with status-code coverage among the requested hosts, each host counted on its own."},"beforeRequiredDays":{"type":"integer","minimum":0,"description":"Days the before window needs covered."},"afterRequiredDays":{"type":"integer","minimum":0,"description":"Days the after window needs covered."},"beforeStart":{"type":["string","null"],"description":"Start of the before window.","format":"date-time"},"beforeEnd":{"type":["string","null"],"description":"End of the before window.","format":"date-time"},"afterStart":{"type":["string","null"],"description":"Start of the after window.","format":"date-time"},"afterEnd":{"type":["string","null"],"description":"End of the after window.","format":"date-time"},"reason":{"type":["string","null"],"description":"window_open, unknown_hosts, status_coverage_gap and no_baseline_404s explain unmeasured states. counter_changed explains an unavailable count in a known state. Null when neither applies.","enum":["window_open","unknown_hosts","status_coverage_gap","counter_changed","no_baseline_404s",null]}},"required":["generation","source","requestedPath","hosts","state","before","after","beforeCoveredDays","afterCoveredDays","beforeRequiredDays","afterRequiredDays","beforeStart","beforeEnd","afterStart","afterEnd","reason"]},"OutcomeAgentScope":{"type":"object","additionalProperties":false,"description":"Requests from the agents a shipped robots.txt unblock allowed, across every document path on the hosts of its drafted robots.txt files (robots.txt itself excluded), separate from the overall result. Each agent is counted only on the hosts whose own robots.txt file unblocked it (agents[].hosts); requests from a host that still blocks it never count. One entry per provider and bot class: answer fetchers count answer fetches and index bots count crawls, and neither is added to the other. Traffic records provider and class, not the user-agent token, so an entry merges every crawler of that provider and class (Anthropic answer fetches include Claude-User and Claude-Web). Counts are recorded when the comparison window closes and are read from that record, not recomputed from traffic. Counts are null unless a server-side sensor covered every exact host for both full windows, and coverage says how far they can be trusted: verified when a Cloudflare log pull on that hostname with whole-site scope covered every host; sensor_reported when edge middleware accepted events on a host every UTC day, which shows it was running there but not that it sees every path. Present sensor_reported counts as seen by the edge sensor, not as verified. A day with no accepted edge event leaves the window unmeasured. A www host and its bare domain are separate hosts, and coverage of one never counts for the other. The browser snippet cannot see these requests. Historical reads anchored before today (as-of) omit agentScope.","properties":{"generation":{"type":"string","enum":["robots-unblock-scope-v1"]},"source":{"type":"string","enum":["llm_traffic_hourly"]},"origins":{"type":"array","minItems":1,"items":{"type":"string"},"description":"Hosts whose robots.txt the action changed."},"state":{"type":"string","enum":["measured","unmeasured"],"description":"measured: both windows closed and counts available; see coverage for the coverage level. unmeasured: see reason; every count is null."},"coverage":{"type":["string","null"],"description":"Lowest level across hosts. verified: Cloudflare whole-site on every host. sensor_reported: edge middleware reported on at least one host every day without proof it sees every path; show these counts with that caveat. Null when unmeasured.","enum":["verified","sensor_reported",null]},"hosts":{"type":"array","minItems":1,"items":{"type":"object","additionalProperties":false,"properties":{"host":{"type":"string"},"coverage":{"type":["string","null"],"description":"This host's level over both windows; null when it was not covered.","enum":["verified","sensor_reported",null]}},"required":["host","coverage"]}},"reason":{"type":["string","null"],"description":"window_open: the comparison window has not closed. no_sensor: no Cloudflare log pull or edge middleware reported for any of the exact hosts. partial_coverage: a sensor reported for only part of a window or of the hosts. Null when measured.","enum":["window_open","no_sensor","partial_coverage",null]},"beforeStart":{"type":["string","null"],"description":"Start of the before window. Null until the comparison window closes.","format":"date-time"},"beforeEnd":{"type":["string","null"],"description":"End of the before window. Null until the comparison window closes.","format":"date-time"},"afterStart":{"type":["string","null"],"description":"Start of the after window. Null until the comparison window closes.","format":"date-time"},"afterEnd":{"type":["string","null"],"description":"End of the after window. Null until the comparison window closes.","format":"date-time"},"agents":{"type":"array","minItems":1,"description":"Each agent the unblock allowed, with its counts.","items":{"type":"object","additionalProperties":false,"properties":{"provider":{"type":"string","description":"AI provider, as in AI Traffic (for example anthropic or openai)."},"botClass":{"type":"string","enum":["answer_fetcher","index_bot"]},"metric":{"type":"string","enum":["answer_fetches","crawls"],"description":"answer_fetches for answer fetchers, crawls for index bots."},"hosts":{"type":"array","minItems":1,"items":{"type":"string"},"description":"The hosts whose robots.txt file unblocked this agent. Its counts sum these hosts only."},"before":{"type":["integer","null"],"minimum":0,"description":"Its requests in the before window. Null when unmeasured."},"after":{"type":["integer","null"],"minimum":0,"description":"Its requests in the after window. Null when unmeasured."},"coverage":{"type":["string","null"],"description":"The level these counts carry: the lowest across this agent's own hosts. Null when unmeasured.","enum":["verified","sensor_reported",null]}},"required":["provider","botClass","metric","hosts","before","after","coverage"]}}},"required":["generation","source","origins","state","reason","coverage","hosts","beforeStart","beforeEnd","afterStart","afterEnd","agents"]},"OutcomeResultEvidence":{"type":"object","additionalProperties":false,"description":"What a result rests on, per metric.","properties":{"redirect404":{"$ref":"#/components/schemas/OutcomeRedirect404","description":"Omitted for other action families and page creation."},"agentScope":{"$ref":"#/components/schemas/OutcomeAgentScope","description":"Present on one active measurement of a shipped robots.txt unblock Action, its earliest page still published; it moves with the page when that page is removed or restored. Omitted otherwise."},"citations":{"description":"Citation evidence. Null unless citationConfidence is observed or directional. When citations were not compared, direction is null.","anyOf":[{"$ref":"#/components/schemas/OutcomeMetricEvidence"},{"type":"null"}]},"answerFetches":{"description":"Answer-fetch evidence. Null when answer fetches were not compared, except under v2, where it is present once the window closes and direction is null when they were not compared.","anyOf":[{"$ref":"#/components/schemas/OutcomeMetricEvidence"},{"type":"null"}]},"rule":{"type":"object","additionalProperties":false,"description":"The thresholds a metric must pass to count as moved.","properties":{"significance":{"type":"number","description":"pValue must be below this."},"minCount":{"type":"number","description":"The fewest counted events the higher window must hold."},"minActiveDays":{"type":"number"},"minRelativeChange":{"type":"number","description":"The smallest relative change, as a fraction."}},"required":["significance","minCount","minActiveDays","minRelativeChange"]}},"required":["citations","answerFetches","rule"]},"OutcomesScoreboard":{"type":"object","additionalProperties":true,"description":"Totals across the measured actions that match the filters.","properties":{"conclusiveCount":{"type":"integer","minimum":0,"description":"Measured actions with a closed, conclusive result."},"improvedCount":{"type":"integer","minimum":0},"mixedCount":{"type":"integer","minimum":0,"description":"Results where metrics moved in different directions."},"noClearChangeCount":{"type":"integer","minimum":0,"description":"Results with no clear change."},"declinedCount":{"type":"integer","minimum":0},"stillMeasuringCount":{"type":"integer","minimum":0,"description":"Measured actions whose window is still open."},"unmeasuredCount":{"type":"integer","minimum":0,"description":"Measured actions with no usable evidence."},"improvementRate":{"type":["number","null"],"minimum":0,"maximum":1,"description":"Improved results divided by conclusive measurements in the same filtered population. Null when there are none."},"combinedCitationLift":{"type":"number","description":"Sum of citation deltas across citation improvements only, in citations/day."}},"required":["conclusiveCount","improvedCount","mixedCount","noClearChangeCount","declinedCount","stillMeasuringCount","unmeasuredCount","improvementRate","combinedCitationLift"]},"RecordedAnalyticsCounts":{"type":"object","description":"Counts observed in one window, which may be partial.","properties":{"sessions":{"type":["number","null"],"description":"Sessions observed. Null when none were recorded and the window is not fully covered."},"conversions":{"type":["number","null"],"description":"Conversions observed. Null when none were recorded and the window is not fully covered."}},"required":["sessions","conversions"]},"ScopedAnalyticsCoverage":{"type":"object","description":"Window coverage for the project, for each AI platform and for each listed landing page.","properties":{"project":{"$ref":"#/components/schemas/ReferralWindowCoverage"},"engines":{"type":"array","description":"Coverage for each AI platform.","items":{"type":"object","properties":{"platform":{"type":"string"},"coverage":{"$ref":"#/components/schemas/ReferralWindowCoverage"}},"required":["platform","coverage"]}},"pages":{"type":"array","description":"Coverage for each landing page on this page of results.","items":{"type":"object","properties":{"domain":{"type":"string","description":"The tracked domain."},"path":{"type":"string"},"coverage":{"$ref":"#/components/schemas/ReferralWindowCoverage"}},"required":["domain","path","coverage"]}}},"required":["project","engines","pages"]},"AnalyticsComparison":{"type":"object","description":"One provider and conversion definition across both measured-action windows. Completed evidence preserves its original provenance. Measured values remain null without full coverage. Optional recorded counts preserve partial observations without percentage eligibility.","properties":{"recorded":{"type":"object","description":"Observed qualified counts from one published identity. Not complete measurements; null is not zero.","properties":{"before":{"$ref":"#/components/schemas/RecordedAnalyticsCounts"},"after":{"$ref":"#/components/schemas/RecordedAnalyticsCounts"}},"required":["before","after"]},"provider":{"type":["string","null"],"description":"ga4 or posthog. Null when no connected analytics provider covers the page's domain, or while the comparison is being refreshed."},"connectionId":{"type":["string","null"],"description":"The analytics connection. Null when no connected analytics provider covers the page's domain, or while the comparison is being refreshed."},"generation":{"type":["integer","null"],"description":"Identifies the import the counts come from. Null when no connected analytics provider covers the page's domain, or while the comparison is being refreshed."},"definitionHash":{"type":["string","null"],"description":"Identifies the conversion definition counted. Null when no connected analytics provider covers the page's domain, while the comparison is being refreshed, or before a PostHog connection has reported."},"sessionsCovered":{"type":"boolean","description":"True when both windows have complete session coverage."},"conversionsCovered":{"type":"boolean","description":"True when both windows have complete conversion coverage."},"before":{"type":"object","description":"Measured counts for the before window.","properties":{"sessions":{"type":["number","null"],"description":"Sessions. Null unless sessionsCovered."},"conversions":{"type":["number","null"],"description":"Conversions. Null unless conversionsCovered."}}},"after":{"type":"object","description":"Measured counts for the after window.","properties":{"sessions":{"type":["number","null"],"description":"Sessions. Null unless sessionsCovered."},"conversions":{"type":["number","null"],"description":"Conversions. Null unless conversionsCovered."}}}},"required":["provider","connectionId","generation","definitionHash","sessionsCovered","conversionsCovered","before","after"]},"OutcomesAttributedConversion":{"type":"object","additionalProperties":false,"properties":{"attributionLabel":{"type":"string","enum":["GA4-attributed","PostHog-attributed","Analytics-attributed"],"description":"Whose attribution the count is."},"conversions":{"type":["number","null"],"minimum":0,"description":"Conversions attributed to AI referrals. Null unless state is measured."},"dateFrom":{"type":"string","format":"date","description":"First day of the window."},"dateTo":{"type":"string","format":"date","description":"Last day of the window, included."},"state":{"type":"string","enum":["measured","unmeasured"],"description":"unmeasured when no analytics connection has key events selected, the window is not fully covered, or the window has no AI-referral sessions."},"windowDays":{"type":"integer","enum":[30,90]}},"required":["attributionLabel","conversions","dateFrom","dateTo","state","windowDays"]},"RecentAnswerFetchesData":{"type":"object","additionalProperties":false,"properties":{"items":{"type":"array","maxItems":20,"description":"The latest answer-fetch records, newest hour first.","items":{"type":"object","additionalProperties":false,"properties":{"id":{"type":"string","format":"uuid"},"hour_bucket":{"type":"string","format":"date-time","description":"Start of the UTC hour containing these requests. Not an individual event timestamp."},"page_path":{"type":"string","description":"The fetched page path."},"domain":{"type":["string","null"],"description":"The fetched host. Null when the sensor did not report it."},"llm_platform":{"type":"string","description":"The AI Traffic provider key, such as openai."},"event_count":{"type":"integer","minimum":1,"description":"Requests in this stored record only; not a full page/hour or window total."},"verified_event_count":{"type":"integer","minimum":0,"description":"Requests in this record whose claimed operator was confirmed, by the peer address against the networks the operator publishes or by a Web Bot Auth signature."},"verified_by_signature_event_count":{"type":["integer","null"],"minimum":0,"description":"Of verified_event_count, requests verified by a Web Bot Auth signature; the rest were verified by their peer address. Null when the method was not recorded for some verified requests in this record. Requests stored before DevTune began recording the method have none."},"failed_event_count":{"type":"integer","minimum":0,"description":"Requests in this record whose claimed operator was contradicted by the peer address. Treat as spoofed."},"unknown_no_address_count":{"type":"integer","minimum":0,"description":"Unknown requests for which the sensor supplied no peer address."},"unknown_no_contract_count":{"type":"integer","minimum":0,"description":"Unknown requests whose claimed operator publishes no supported identity-checking contract."},"unknown_check_unavailable_count":{"type":"integer","minimum":0,"description":"Unknown requests for which an available operator check could not complete. The before-verification remainder is event_count minus verified_event_count, failed_event_count, and all three unknown-reason counts."},"status_class":{"type":["string","null"],"description":"The response status: 2xx, 3xx, 404, 4xx_other or 5xx. Null when the sensor did not report it."}},"required":["id","hour_bucket","page_path","domain","llm_platform","event_count","verified_event_count","verified_by_signature_event_count","failed_event_count","unknown_no_address_count","unknown_no_contract_count","unknown_check_unavailable_count","status_class"]}},"limit":{"type":"integer","minimum":1,"maximum":20,"description":"The most records returned, as requested."},"timeBasis":{"type":"string","enum":["hour_bucket"]},"window":{"type":"object","additionalProperties":false,"description":"The UTC days the records fall in.","properties":{"days":{"type":"integer","enum":[7,30,90]},"dateFrom":{"type":"string","format":"date","description":"First day of the window."},"dateTo":{"type":"string","format":"date","description":"Last day of the window, included."},"before":{"type":"string","format":"date-time","description":"Exclusive server-derived upper bound on hour_bucket."}},"required":["days","dateFrom","dateTo","before"]}},"required":["items","limit","timeBasis","window"]},"VisitsPerAnswerFetchData":{"type":"object","additionalProperties":false,"properties":{"items":{"type":"array","maxItems":100,"description":"Pages with answer fetches or AI-referred visits in the window.","items":{"type":"object","additionalProperties":false,"properties":{"domain":{"type":["string","null"],"description":"The page’s host. Null when it was not recorded."},"pagePath":{"type":"string"},"answerFetches":{"type":"integer","minimum":0,"description":"Requests from AI answer fetchers retrieving the page while building an answer. An answer fetch is not a citation."},"aiReferralVisits":{"type":["integer","null"],"minimum":0,"description":"Visits that arrived from an AI answer, counted by the source in referralSource. Null unless coverage is covered, never 0."},"visitsPerAnswerFetch":{"type":["number","null"],"minimum":0,"description":"aiReferralVisits divided by answerFetches, rounded to 4 decimal places. Null unless coverage is covered, and when answerFetches is 0."},"coverage":{"type":"string","enum":["covered","observed","no_referral_data"],"description":"covered when visits come from GA4 or PostHog and its sync covers the whole window, or from the traffic snippet and it reported on the page's host for every full UTC day of the window (running the day before it starts and the day after it ends). observed when a referral sensor recorded visits on the page but did not cover every day of the window: observedAiReferralVisits is then a lower bound. Otherwise no_referral_data: no complete referral data for this window."},"referralSource":{"type":"string","description":"Where visits were counted from: ga4, posthog, snippet (the traffic snippet), edge_middleware, vercel_drain, or mixed when the host used more than one sensor across the window. Read it only when coverage is covered or observed; a no_referral_data page names the source it would have used."},"observedAiReferralVisits":{"type":["integer","null"],"minimum":1,"description":"With observed coverage, the AI-referred visits a sensor recorded on the days it covered: a lower bound. Null otherwise."},"observedVisitsPerAnswerFetch":{"type":["number","null"],"minimum":0,"description":"observedAiReferralVisits divided by answerFetches, rounded to 4 decimal places: a lower bound. Null unless coverage is observed, and when answerFetches is 0."}},"required":["domain","pagePath","answerFetches","aiReferralVisits","visitsPerAnswerFetch","coverage","referralSource","observedAiReferralVisits","observedVisitsPerAnswerFetch"]}},"summary":{"description":"Project total over every page with activity, including pages past the 100-page list, returned only when every one is covered. Otherwise null, with summaryWithheldReason.","oneOf":[{"type":"object","additionalProperties":false,"properties":{"aiReferralVisits":{"type":"integer","minimum":0,"description":"Visits from AI answers across every page."},"answerFetches":{"type":"integer","minimum":0,"description":"Answer fetches across every page."},"referralSource":{"type":"string","enum":["ga4","posthog","snippet","edge_middleware","analytics","mixed"],"description":"ga4, posthog, snippet or edge_middleware when every page used that source; analytics for GA4 and PostHog; mixed when pages used both referral sensors, or a sensor and an analytics provider."},"visitsPerAnswerFetch":{"type":["number","null"],"minimum":0,"description":"Null when answerFetches is 0."}},"required":["aiReferralVisits","answerFetches","referralSource","visitsPerAnswerFetch"]},{"type":"null"}]},"summaryWithheldReason":{"type":["string","null"],"enum":["partial_coverage","page_limit","no_activity",null],"description":"Why summary is null: partial_coverage when some pages have no complete referral data; page_limit when the 100-page cap was reached and the total could not be checked for every page; no_activity when no page had answer fetches or visits. Null when summary is returned."},"excludedAnswerFetches":{"type":["integer","null"],"minimum":0,"description":"With partial_coverage, the answer fetches on pages without complete referral data, including pages past the 100-page list. Null otherwise."},"pageLimit":{"type":"integer","enum":[100],"description":"Most pages ranked per window."},"pageLimitReached":{"type":"boolean","description":"True when the list reached pageLimit pages, so lower-ranked pages may be omitted from items."},"pagination":{"type":"object","additionalProperties":false,"description":"Where this page sits in the ranked pages.","properties":{"hasMore":{"type":"boolean","description":"More rows follow this page."},"nextCursor":{"type":["string","null"],"description":"Pass as cursor to read the next page. Null on the last page."},"pageSize":{"type":"integer","minimum":1,"maximum":100,"description":"Rows per page."},"totalCount":{"type":"integer","minimum":0,"maximum":100,"description":"Ranked pages available to page through. A cursor from an earlier read is rejected with 400 once a late import changes the ranking."}},"required":["hasMore","nextCursor","pageSize","totalCount"]},"window":{"type":"object","additionalProperties":false,"description":"The completed UTC days the figures cover.","properties":{"dateFrom":{"type":"string","format":"date","description":"First day of the window."},"dateTo":{"type":"string","format":"date","description":"Last completed UTC day in the window."}},"required":["dateFrom","dateTo"]},"windowDays":{"type":"integer","enum":[7,30,90]}},"required":["items","summary","summaryWithheldReason","excludedAnswerFetches","pageLimit","pageLimitReached","pagination","window","windowDays"]},"MeasureStatus":{"type":"string","enum":["measured","lower_bound","unmeasured"],"description":"How complete the count beside it is. measured: the count covers the whole window. lower_bound: the window is only partly covered, so the true count is at least this number. unmeasured: the count is null, because nothing covered the window or the window is only partly covered and nothing was counted."},"ReferralCountTotal":{"type":"object","additionalProperties":false,"description":"A count for the current and previous windows, each with its status.","properties":{"current":{"type":["number","null"],"description":"The count in the current window. Null when currentStatus is unmeasured."},"currentStatus":{"$ref":"#/components/schemas/MeasureStatus"},"previous":{"type":["number","null"],"description":"The count in the previous window of the same length. Null when previousStatus is unmeasured."},"previousStatus":{"$ref":"#/components/schemas/MeasureStatus"}},"required":["current","currentStatus","previous","previousStatus"]},"ReferralCountMeasure":{"type":"object","additionalProperties":false,"description":"A count for the current and previous windows, each with its status, and whether they can be compared.","properties":{"current":{"type":["number","null"],"description":"The count in the current window. Null when currentStatus is unmeasured."},"currentStatus":{"$ref":"#/components/schemas/MeasureStatus"},"previous":{"type":["number","null"],"description":"The count in the previous window of the same length. Null when previousStatus is unmeasured."},"previousStatus":{"$ref":"#/components/schemas/MeasureStatus"},"comparisonAvailable":{"type":"boolean","description":"True only when currentStatus and previousStatus are both measured. Compare the two windows only when true."}},"required":["current","currentStatus","previous","previousStatus","comparisonAvailable"]},"ReferralOverviewMeasure":{"type":"object","additionalProperties":false,"description":"An overview count for the current and previous windows, each with its status.","properties":{"comparisonAvailable":{"type":"boolean","description":"True only when currentStatus and previousStatus are both measured. Compare the two windows only when true."},"current":{"type":["number","null"],"description":"The count in the current window. Null when currentStatus is unmeasured."},"currentStatus":{"$ref":"#/components/schemas/MeasureStatus"},"previous":{"type":["number","null"],"description":"The count in the previous window of the same length. Null when previousStatus is unmeasured."},"previousStatus":{"$ref":"#/components/schemas/MeasureStatus"},"sourceAvailable":{"type":"boolean","description":"Whether an analytics source that records this count is connected."}},"required":["comparisonAvailable","current","currentStatus","previous","previousStatus","sourceAvailable"]},"ReferralWindowCoverage":{"type":"object","additionalProperties":false,"description":"Coverage of session and conversion-event counts across the current and previous reporting windows. False means observed counts may be partial or unavailable. Compare periods only when both flags for that metric are true; these flags apply to overview, engine and landing-page counts.","properties":{"sessionsCurrent":{"type":"boolean","description":"Sessions are complete for the current window."},"sessionsPrevious":{"type":"boolean","description":"Sessions are complete for the previous window."},"conversionsCurrent":{"type":"boolean","description":"Conversion events are complete for the current window."},"conversionsPrevious":{"type":"boolean","description":"Conversion events are complete for the previous window."}},"required":["sessionsCurrent","sessionsPrevious","conversionsCurrent","conversionsPrevious"]},"AnalyticsQuality":{"type":"object","description":"Import continuity, observed tracking and attribution completeness are independent. Attributed partial counts are lower bounds; complete is required for comparisons and inferred zero.","properties":{"configured":{"type":"boolean","description":"Whether the provider is set up to count this measure; for conversions, whether a conversion event is selected."},"importedDays":{"type":"integer","description":"Days of the period imported."},"expectedDays":{"type":"integer","description":"Days in the period."},"importsComplete":{"type":"boolean","description":"True when every day of the period is imported and finished."},"trackingObserved":{"type":"boolean","description":"Whether the provider had recorded any session for the site by the period's first day. Always true for GA4."},"attributionGapOccurrences":{"type":"number","description":"Sessions or conversion events that could not be traced to the site, a landing page or a source."},"noSiteVisitOccurrences":{"type":"number","description":"Selected conversion events with no site visit, such as events sent from a server. They cannot be attributed to a landing page or AI referral, so they are not counted in attributionGapOccurrences and do not make conversions incomplete. Always 0 for sessions."},"attributionComplete":{"type":"boolean","description":"True when a day was imported and attributionGapOccurrences is 0."},"complete":{"type":"boolean","description":"True when configured, every day is imported, tracking is observed and attribution is complete."},"reasons":{"type":"array","description":"Why the period is not complete: no_selected_conversion, day_incomplete, day_not_imported, history_unavailable, attribution_incomplete or unconfigured. Empty when complete.","items":{"type":"string"}}},"required":["configured","importedDays","expectedDays","importsComplete","trackingObserved","attributionGapOccurrences","attributionComplete","complete","reasons"]},"AnalyticsReportingContext":{"type":"object","description":"Selected analytics provider provenance, full-window coverage, explicit daily gaps and AI-referred session metrics. PostHog duration and bounce are native metrics, not GA4 engagement.","properties":{"comparisonCoverage":{"$ref":"#/components/schemas/ReferralWindowCoverage"},"quality":{"type":"array","description":"For each connection and day of both windows, whether sessions and conversions are complete.","items":{"type":"object","properties":{"connectionId":{"type":"string"},"domainCanonical":{"type":"string","description":"Tracked site of the connection."},"day":{"type":"string","format":"date","description":"The day checked."},"sessions":{"$ref":"#/components/schemas/AnalyticsQuality"},"conversions":{"$ref":"#/components/schemas/AnalyticsQuality"}},"required":["connectionId","domainCanonical","day","sessions","conversions"]}},"comparisonQuality":{"type":"array","description":"The same daily check, as a comparison between the two windows applies it.","items":{"type":"object","properties":{"connectionId":{"type":"string"},"domainCanonical":{"type":"string","description":"Tracked site of the connection."},"day":{"type":"string","format":"date","description":"The day checked."},"sessions":{"$ref":"#/components/schemas/AnalyticsQuality"},"conversions":{"$ref":"#/components/schemas/AnalyticsQuality"}},"required":["connectionId","domainCanonical","day","sessions","conversions"]}},"window":{"type":"object","description":"The current window and the previous one of the same length.","properties":{"dateFrom":{"type":"string","format":"date","description":"First day of the current window."},"dateTo":{"type":"string","format":"date","description":"Last day of the current window, included."},"previousDateFrom":{"type":"string","format":"date","description":"First day of the previous window."},"previousDateTo":{"type":"string","format":"date","description":"Last day of the previous window, included."}},"required":["dateFrom","dateTo","previousDateFrom","previousDateTo"]},"providers":{"type":"array","description":"One entry per selected analytics connection.","items":{"type":"object","properties":{"provider":{"type":"string","enum":["ga4","posthog"]},"domainCanonical":{"type":"string","description":"Tracked site of the connection."},"connectionId":{"type":"string"},"definitionHash":{"type":["string","null"],"description":"Identifies the report definition being served. For PostHog, null before the first report."},"generation":{"type":"integer","description":"Identifies the import being served."},"status":{"type":"string"},"reportingTimezone":{"type":"string","description":"The timezone days are reported in: UTC for PostHog. For GA4 the value is source timezone, meaning the GA4 property's own timezone."},"reportSnapshot":{"type":["object","null"],"description":"Null for GA4, and for PostHog until its first collection starts. How current this provider report is. throughDate is the last day it includes and syncedAt is when it last synced. While state is replacing, the previous report is still served, identified by definitionHash and generation, and the pending fields describe the report being collected; it replaces the previous one once the last 180 days, and every earlier day of the past year that the previous report covered, are collected. Older history can still have explicit coverage gaps.","properties":{"state":{"type":"string","enum":["collecting","replacing","current"],"description":"collecting before the first report, replacing while a new report is collected, current otherwise."},"syncedAt":{"type":["string","null"],"description":"When the report last synced. Null before the first sync."},"throughDate":{"type":["string","null"],"description":"The last day the report includes. Null until history is imported."},"replacementWindowEnd":{"type":["string","null"],"description":"The day the replacement report’s 180-day collection window ends, not included. Null when no replacement is in progress."},"pendingDefinitionHash":{"type":["string","null"],"description":"The definition of the report being collected. Null when none is recorded."},"pendingGeneration":{"type":"integer","description":"The import being collected."}},"required":["state","syncedAt","throughDate","replacementWindowEnd","pendingDefinitionHash","pendingGeneration"]}},"required":["provider","domainCanonical","connectionId","definitionHash","generation","status","reportingTimezone"]}},"coverage":{"$ref":"#/components/schemas/ReferralWindowCoverage"},"gaps":{"type":"array","description":"One entry per connection, day and reason for each day of either window whose sessions, or conversions when a conversion event is selected, are not complete. Empty when there are none.","items":{"type":"object","properties":{"domainCanonical":{"type":"string","description":"Tracked site of the connection."},"day":{"type":"string","format":"date"},"reason":{"type":"string","description":"One of the AnalyticsQuality reasons."}},"required":["domainCanonical","day","reason"]}},"nativeMetrics":{"type":"array","description":"The provider’s own engagement measures for AI-referred sessions in the current window. Each is null when a day of the window is not imported, AI-referral tracking is not observed, a session that may be an AI referral is not confirmed to belong to the site, or there were no AI-referred sessions.","items":{"type":"object","properties":{"provider":{"type":"string","enum":["ga4","posthog"]},"domainCanonical":{"type":"string","description":"Tracked site of the connection."},"averageSessionDurationSeconds":{"type":["number","null"],"description":"PostHog only: average session length, in seconds. Null for GA4."},"bounceRate":{"type":["number","null"],"description":"PostHog only: bounced sessions over sessions, from 0 to 1. Null for GA4."},"engagementRate":{"type":["number","null"],"description":"GA4 only: engaged sessions over sessions, from 0 to 1. Null for PostHog."},"averageEngagementSeconds":{"type":["number","null"],"description":"GA4 only: average engagement time per session, in seconds. Null for PostHog."}},"required":["provider","domainCanonical","averageSessionDurationSeconds","bounceRate","engagementRate","averageEngagementSeconds"]}}},"required":["window","providers","coverage","gaps","nativeMetrics"]},"AiReferralsData":{"type":"object","properties":{"comparisonCoverage":{"$ref":"#/components/schemas/ScopedAnalyticsCoverage"},"analyticsReportingContext":{"$ref":"#/components/schemas/AnalyticsReportingContext"},"engines":{"type":"object","description":"Every AI platform the selected provider recorded, whatever page of landing pages you read. A null count is unmeasured, and a count whose status is lower_bound is at least that number; a null conversion rate or engagement time can also mean the platform sent no sessions in that window. Event-per-session ratios are not rates of people who converted.","properties":{"items":{"type":"array","description":"One row per AI platform.","items":{"$ref":"#/components/schemas/ConversionAttributionEngine"}},"hasConversionSource":{"type":"boolean","description":"Whether a conversion event is selected on any selected analytics connection."},"hasEngagementSource":{"type":"boolean","description":"True when at least one AI platform has an engagement time in the current window; false with PostHog, which does not record engagement time."},"totals":{"type":"object","additionalProperties":true,"description":"Sessions and conversion events across every platform. A total is measured only when every platform is; otherwise it is a lower bound when anything was counted, and null when nothing was.","properties":{"sessions":{"$ref":"#/components/schemas/ReferralCountTotal"},"conversions":{"$ref":"#/components/schemas/ReferralCountTotal"}},"required":["sessions","conversions"]}},"required":["items","hasConversionSource","hasEngagementSource","totals"]},"window":{"type":"object","description":"The fixed window.","properties":{"dateFrom":{"type":"string","format":"date","description":"First day of the window."},"dateTo":{"type":"string","format":"date","description":"Last day of the window, included."}},"required":["dateFrom","dateTo"]},"items":{"type":"array","items":{"$ref":"#/components/schemas/FlexibleRollupItem"},"description":"Landing pages ordered by current sessions descending, then domain and path, with sessions, conversion events, and nullable currentEngagementSeconds, priorEngagementSeconds, engagementRate and pageValue. Page value uses conversion totals across all pages."},"overview":{"type":"object","description":"Window totals and a daily series for the project's AI Traffic: AI-referred visits and conversions, answer fetches, training crawls and citations. referralCoverage says how complete the visit and conversion counts are.","additionalProperties":true,"properties":{"referralCoverage":{"$ref":"#/components/schemas/ReferralWindowCoverage"},"metrics":{"type":"object","additionalProperties":true,"description":"Window totals for the current and previous windows. The AI-referred visit and conversion totals carry a status for each window.","properties":{"humanReferralSessions":{"$ref":"#/components/schemas/ReferralOverviewMeasure","description":"Sessions referred by AI answers."},"humanConversions":{"$ref":"#/components/schemas/ReferralOverviewMeasure","description":"Selected conversion events from AI-referred sessions."}}}},"required":["referralCoverage"]},"pagination":{"$ref":"#/components/schemas/RollupCursorPagination"},"conversionDefinitions":{"type":"array","description":"Per selected PostHog event or action that occurred in the same fixed window (a selected definition with no occurrences is omitted): occurrences with no site visit (no linked session and no page host, such as server-sent events) and occurrences tied to a site visit (a linked session). A definition with no-site-visit occurrences and no site-visit occurrences in a complete window cannot be credited to an AI referral; choose a browser-side event. GA4 key events are always session-scoped, so they are omitted. Absent when the read fails; the rest of the response is still returned.","items":{"type":"object","properties":{"provider":{"type":"string","enum":["posthog"]},"domainCanonical":{"type":"string","description":"Tracked site of the PostHog connection."},"definition":{"type":"string","description":"Selected conversion definition: event:<name> or action:<id>."},"noSiteVisitOccurrences":{"type":"integer","minimum":0,"description":"Occurrences with no site visit."},"siteVisitOccurrences":{"type":"integer","minimum":0,"description":"Occurrences tied to a site visit."},"windowComplete":{"type":"boolean","description":"Every day of the window is published for the connection. When false, the counts are partial."}},"required":["provider","domainCanonical","definition","noSiteVisitOccurrences","siteVisitOccurrences","windowComplete"]}},"visitorShare":{"$ref":"#/components/schemas/AiVisitorShare"},"pageComparison":{"$ref":"#/components/schemas/AiPageComparison"},"windowDays":{"type":"integer","enum":[7,30,90]}},"required":["engines","items","overview","pagination","window","windowDays"]},"AiVisitorShare":{"type":"object","description":"Share of visitors sent by AI answers for the fixed window: AI-referral sessions over all sessions from the selected analytics provider (GA4 or PostHog), read in the same database snapshot as the referrals. Reported whenever the provider imported every day of the window and recorded sessions in it. Sessions that may have come from an AI answer but could not be traced, because their source or site is unknown, are not counted as AI; unattributedSessions counts them and partial is true when there are any. Otherwise share is null with coverage.reason no_source (no provider connected), coverage_incomplete (a day not imported yet), no_visits, or only_untraced (sessions were recorded but none could be traced, so there is no share to report; unattributedSessions counts them). It is never reported as zero for an unmeasured window.","additionalProperties":false,"properties":{"share":{"type":["number","null"],"description":"AI-referral sessions over all sessions, as a fraction from 0 to 1. Null when the window is not measured or had no sessions; coverage.reason says why."},"priorShare":{"type":["number","null"],"description":"The same ratio for the previous window of equal length. Null unless both windows have a share; priorPartial says whether it leaves out untraced sessions."},"partial":{"type":"boolean","description":"True when sessions that may have come from an AI answer could not be traced in the window; unattributedSessions gives how many."},"priorPartial":{"type":"boolean","description":"The same for the previous window."},"unattributedSessions":{"type":["integer","null"],"description":"Sessions in the window that may have come from an AI answer but could not be traced, because their source or site is unknown. The share does not count them as AI. Null when the window is not measured."},"priorUnattributedSessions":{"type":["integer","null"],"description":"The same count for the previous window. Null when that window is not measured."},"source":{"type":["string","null"],"enum":["analytics",null],"description":"analytics when a provider is connected; null when none is."},"providers":{"type":"array","items":{"type":"string","enum":["ga4","posthog"]},"description":"Selected analytics providers behind the share. Empty when none is connected."},"window":{"type":"object","additionalProperties":false,"description":"The current window and the previous one of the same length.","properties":{"windowDays":{"type":"integer","enum":[7,30,90]},"dateFrom":{"type":"string","format":"date","description":"First day of the current window."},"dateTo":{"type":"string","format":"date","description":"Last day of the current window, included."},"priorDateFrom":{"type":"string","format":"date","description":"First day of the previous window."},"priorDateTo":{"type":"string","format":"date","description":"Last day of the previous window, included."}},"required":["windowDays","dateFrom","dateTo","priorDateFrom","priorDateTo"]},"coverage":{"type":"object","additionalProperties":false,"description":"How complete each window is.","properties":{"current":{"type":"string","enum":["complete","partial","unavailable"],"description":"complete: every day imported and every session traced. partial: every day imported, with untraced sessions. unavailable: no provider, or a day not imported yet."},"prior":{"type":"string","enum":["complete","partial","unavailable"],"description":"The same coverage for the previous window."},"reason":{"type":["string","null"],"enum":["no_source","coverage_incomplete","no_visits","only_untraced",null],"description":"Why share is null: no_source (no provider connected), coverage_incomplete (a day not imported yet), no_visits (no sessions), only_untraced (every session was untraced). Null when share is set."}},"required":["current","prior","reason"]}},"required":["share","priorShare","partial","priorPartial","unattributedSessions","priorUnattributedSessions","source","providers","window","coverage"]},"AiPageComparison":{"type":"object","description":"Per landing page on this page of results: AI-referral sessions against every other session that landed on the same page, over the current fixed window, read in the same database snapshot as the referrals. Rates are null unless status is compared: a page is compared once its analytics provider imported every day of the window, no AI-referral session that could belong to it is untraced (unknown source, landing page or site), untraced sessions that could belong to its other traffic are at most 2% of that traffic's sessions, and both sides have at least minimumSessions sessions. Conversion rates also need selected key events, every AI-referral conversion traced, and the other side's untraced conversions at most 2% of its conversions; conversionReason says why they are missing. Conversion rates count only selected conversions credited to the landing page, so conversions with no site visit (such as server-sent events) are left out of both sides; noSiteVisitConversions counts them. Absent when the read fails; the rest of the response is still returned.","additionalProperties":false,"properties":{"window":{"type":"object","description":"The current fixed window.","properties":{"windowDays":{"type":"integer"},"dateFrom":{"type":"string","format":"date","description":"First day of the window."},"dateTo":{"type":"string","format":"date","description":"Last day of the window, included."}},"required":["windowDays","dateFrom","dateTo"]},"minimumSessions":{"type":"integer","description":"Sessions each side needs before its rates are compared."},"gap":{"type":["string","null"],"enum":["no_source","disconnected","needs_reauth","incomplete_import","no_key_events",null],"description":"The first coverage gap: no_source (no analytics provider connected), disconnected (connect it again), needs_reauth (reconnect it; data stops updating), incomplete_import (a day of the window not imported yet), no_key_events (select key events to compare conversion rates). Null when there is none."},"noSiteVisitConversions":{"type":"number","minimum":0,"description":"Selected conversions with no site visit in the window, left out of both conversion rates."},"pages":{"type":"array","description":"The landing pages on this page of results.","items":{"type":"object","additionalProperties":false,"properties":{"domainCanonical":{"type":"string","description":"The tracked domain."},"landingPagePath":{"type":"string"},"provider":{"type":["string","null"],"enum":["ga4","posthog",null],"description":"The analytics provider for the page’s site. Null when none is connected."},"status":{"type":"string","enum":["compared","too_few_sessions","unavailable"],"description":"compared when the data has no gap and both sides have minimumSessions; unavailable when the data has a gap, which reason names; too_few_sessions when there is no gap but either side has fewer."},"reason":{"type":["string","null"],"enum":["needs_reauth","incomplete_import","incomplete_attribution","too_few_ai_sessions","too_few_other_sessions",null],"description":"Why the page is not compared; null when it is."},"conversionReason":{"type":["string","null"],"enum":["no_key_events","incomplete_attribution",null],"description":"Why a compared page has no conversion rate on either side: no_key_events (select key events for the page's site) or incomplete_attribution (some conversions could not be traced). Null when both sides have one or the page is not compared."},"untracedShare":{"type":["object","null"],"additionalProperties":false,"description":"Untraced evidence that could belong to the page's other traffic, as a share of that traffic: sessions over its sessions, conversions over its conversions. Present on every compared page; null when the page is not compared. conversions is null when the conversion rate is not compared.","properties":{"sessions":{"type":"number","minimum":0,"description":"Untraced sessions over the other side’s sessions."},"conversions":{"type":["number","null"],"minimum":0,"description":"Untraced conversions over the other side’s conversions. Null when conversionReason is set."}},"required":["sessions","conversions"]},"ai":{"type":"object","additionalProperties":false,"description":"Sessions referred by AI answers.","properties":{"sessions":{"type":["integer","null"],"minimum":0,"description":"Sessions on the page in the window. Null when some could not be traced and none were credited."},"engagementRate":{"type":["number","null"],"description":"Engaged sessions over sessions, 0 to 1. GA4 only; null for PostHog, which does not record engaged sessions."},"bounceRate":{"type":["number","null"],"description":"Bounced sessions over sessions, 0 to 1. For GA4, sessions that were not engaged. Null unless status is compared and the provider records bounces or engagement."},"conversionRate":{"type":["number","null"],"description":"Selected conversions credited to the landing page over sessions (events per session, not converted people). Null whenever conversionReason is set."}},"required":["sessions","engagementRate","bounceRate","conversionRate"]},"other":{"type":"object","additionalProperties":false,"description":"Every other session that landed on the page.","properties":{"sessions":{"type":["integer","null"],"minimum":0,"description":"Sessions on the page in the window. Null when some could not be traced and none were credited."},"engagementRate":{"type":["number","null"],"description":"Engaged sessions over sessions, 0 to 1. GA4 only; null for PostHog, which does not record engaged sessions."},"bounceRate":{"type":["number","null"],"description":"Bounced sessions over sessions, 0 to 1. For GA4, sessions that were not engaged. Null unless status is compared and the provider records bounces or engagement."},"conversionRate":{"type":["number","null"],"description":"Selected conversions credited to the landing page over sessions (events per session, not converted people). Null whenever conversionReason is set."}},"required":["sessions","engagementRate","bounceRate","conversionRate"]}},"required":["domainCanonical","landingPagePath","provider","status","reason","conversionReason","untracedShare","ai","other"]}}},"required":["window","minimumSessions","gap","noSiteVisitConversions","pages"]},"ConversionAttributionMeasure":{"type":"object","description":"One measure for the current and previous windows.","additionalProperties":false,"properties":{"current":{"type":["number","null"],"description":"The value in the current window. Null when unmeasured; engagement time is also null when there were no sessions."},"previous":{"type":["number","null"],"description":"The value in the previous window of the same length. Null when unmeasured; engagement time is also null when there were no sessions."},"comparisonAvailable":{"type":"boolean","description":"Whether both reporting windows have complete coverage for this measure. Observed current and previous values may remain numeric when false."}},"required":["current","previous"]},"ConversionAttributionEngine":{"type":"object","additionalProperties":false,"properties":{"coverage":{"$ref":"#/components/schemas/ReferralWindowCoverage"},"conversionRate":{"type":["number","null"],"description":"Conversions over sessions in the current window: events per session, so it can exceed 1. Null when the provider records no conversions, when sessions or conversions are incomplete for the current window, or when there were no sessions."},"conversions":{"$ref":"#/components/schemas/ReferralCountMeasure","description":"Selected conversion events from these sessions."},"engagementSeconds":{"$ref":"#/components/schemas/ConversionAttributionMeasure","description":"Average engagement time per session, in seconds, to one decimal."},"platform":{"type":"string","description":"The AI platform that referred the sessions."},"sessions":{"$ref":"#/components/schemas/ReferralCountMeasure","description":"Sessions the platform referred."}},"required":["conversionRate","conversions","engagementSeconds","platform","sessions"]},"Traffic404Relevance":{"type":"object","description":"Inferred fit with the current project context, separate from measured request counts. Uncertain includes missing context or an unavailable assessment.","additionalProperties":false,"properties":{"category":{"type":"string","enum":["relevant","adjacent","uncertain","unrelated"],"description":"How well the missing page fits what the project is about, from relevant to unrelated."},"reason":{"type":["string","null"],"description":"Why it was judged so. Null when no reason was given, or when the project’s details changed after it was judged."}},"required":["category","reason"]},"Traffic404DemandCluster":{"type":"object","additionalProperties":false,"properties":{"clusterKey":{"type":"string","description":"Stays the same for the same group of paths across runs."},"gitbookHosts":{"type":"array","description":"Hosts whose GitBook-reported 404 page views fall on any of this cluster's paths, not only the sample paths, on a host the cluster recorded: its counts include those page views, which are unverified. Absent on clusters with no GitBook page views of their own.","items":{"type":"string"}},"hosts":{"type":["array","null"],"description":"Request hosts with request counts, limited to the 20 busiest known hosts across the project's 404 Demand window, then a null host entry counting requests whose host is unknown or outside those hosts. Null or empty means unknown.","items":{"type":"object","additionalProperties":false,"properties":{"host":{"type":["string","null"],"description":"The request host. Null for requests whose host is unknown or outside the 20 busiest."},"requestCount":{"type":"integer","minimum":0,"description":"The cluster’s 404 requests on the host."}},"required":["host","requestCount"]}},"relevance":{"$ref":"#/components/schemas/Traffic404Relevance"},"demandClassification":{"type":"string","enum":["content_intent","generic_probe","technical_asset","infrastructure_path"],"description":"content_intent: a page someone could write. generic_probe, technical_asset and infrastructure_path: scanner probes, asset files and infrastructure paths, listed as suspected noise."},"distinctBots":{"type":"integer","description":"Distinct bot user agents that requested the cluster's paths. Versions of one bot with different user agents count separately."},"id":{"type":"string","format":"uuid"},"priorRequestCount":{"type":["integer","null"],"description":"requestCount from the run before. Null when the run before did not publish the cluster."},"representativePath":{"type":"string","description":"One path that stands for the cluster."},"requestCount":{"type":"integer","description":"AI requests for the cluster’s paths that returned 404, in the window."},"samplePaths":{"type":"array","description":"Paths in the cluster, most requested first.","items":{"type":"string"}},"status":{"type":"string","enum":["new","acknowledged","dismissed"],"description":"The cluster's review status. Dismissed clusters are not listed, and the seven-day window always reports new."},"totalClusterCount":{"type":"integer","description":"Clusters in the same list, clusters or suspectedNoiseClusters, including those beyond the 25 returned."},"totalRequestCount":{"type":"integer","description":"requestCount summed over the same list."},"windowEnd":{"type":"string","format":"date","description":"Last day of the window the cluster was mined from."},"windowStart":{"type":"string","format":"date","description":"First day of that window."}},"required":["clusterKey","demandClassification","distinctBots","id","priorRequestCount","representativePath","requestCount","samplePaths","status","totalClusterCount","totalRequestCount","windowEnd","windowStart"]},"Traffic404DemandState":{"type":"object","additionalProperties":false,"description":"The latest run that looked for 404 Demand in the window.","properties":{"clusterCount":{"type":"integer","description":"Clusters the run published."},"completedAt":{"type":"string","format":"date-time"},"lastError":{"type":["string","null"],"description":"Why the run failed. Null unless status is failed."},"requestCount":{"type":"integer","description":"AI requests that returned 404 in the clusters the run published. With skipped_rollups_only, every such request in the daily totals."},"status":{"type":"string","enum":["completed","skipped_rollups_only","failed"],"description":"completed: clusters were published. skipped_rollups_only: only daily totals remain for the window, so no clusters were published; requestCount still counts the 404s. failed: the run failed."},"windowEnd":{"type":"string","format":"date","description":"Last day of the run’s window."},"windowStart":{"type":"string","format":"date","description":"First day of the run’s window."}},"required":["clusterCount","completedAt","lastError","requestCount","status","windowEnd","windowStart"]},"Traffic404DemandData":{"type":"object","additionalProperties":false,"properties":{"clusters":{"type":"array","description":"Paths AI systems requested that do not exist and someone could write, most relevant first. Empty when dataAvailability is degraded.","items":{"$ref":"#/components/schemas/Traffic404DemandCluster"}},"dataAvailability":{"type":"string","enum":["available","degraded","empty"],"description":"available when the window has clusters. empty when it was mined and has none. degraded when no clusters could be published (no run yet, a failed run, only daily totals left, or a run that does not match the window); the lists are then empty and do not mean the site had no 404s."},"dataScope":{"type":"string","enum":["project"]},"hasObservedDemand":{"type":"boolean","description":"True when either list has a cluster."},"suspectedNoiseClusters":{"type":"array","description":"Clusters that look like scanner probes, asset files or infrastructure paths. Empty when dataAvailability is degraded.","items":{"$ref":"#/components/schemas/Traffic404DemandCluster"}},"state":{"description":"The latest run. Null when none has run, or, for the seven-day window, when the latest run did not publish that window.","anyOf":[{"$ref":"#/components/schemas/Traffic404DemandState"},{"type":"null"}]},"window":{"type":["object","null"],"additionalProperties":false,"description":"The selected window as published by the latest run, with clusters or with daily totals only. Null when that window has not been published, including when the latest run failed: state still reports the failure, dataAvailability is degraded, and it is not a window without 404s.","properties":{"asOf":{"type":"string","format":"date-time","description":"When the run that mined this window finished. The clusters, counts, state and start and end dates are as of this time."},"coverage":{"type":"object","additionalProperties":false,"description":"Measurement coverage for the window's dates, read live from AI Traffic when you request it, not when the window was mined. Hours recorded after asOf, such as a late Cloudflare sync, can raise coveredDays while the clusters stay as mined, so do not read the two as one snapshot.","properties":{"coveredDays":{"type":"integer","minimum":0,"description":"UTC days in the window on which a crawler sensor that reports status codes (Cloudflare or the edge middleware) recorded machine traffic. Fewer than windowDays means part of the window is unmeasured, not free of 404s. The browser snippet cannot observe a 404 and never counts."},"observedAt":{"type":"string","format":"date-time","description":"When DevTune started reading the coverage; the read reflects no older data than this. Compare it with asOf to see how long after the mine coverage was read."}},"required":["coveredDays","observedAt"]},"end":{"type":"string","format":"date","description":"Last day of the window."},"start":{"type":"string","format":"date","description":"First day of the window."}},"required":["asOf","coverage","end","start"]},"windowDays":{"type":"integer","enum":[7,30]},"provenance":{"type":"object","additionalProperties":false,"description":"Present when DevTune could not check whether some counts come from a source that cannot verify requests: some counts may then be unverified. Absent when that was checked.","properties":{"state":{"type":"string","enum":["unavailable"]}},"required":["state"]},"gitbook":{"type":"object","additionalProperties":false,"description":"Present when the window includes 404s GitBook reported, or when GitBook is known to be in use but that could not be read. state available lists those hosts: GitBook documents a 404 only for page views on the site itself, so its 404s are page views only, never server-side or Markdown fetches, and they are unverified. state unavailable means GitBook is known to be in use but DevTune could not read which 404s came from it, so some clusters may include unverified page views.","properties":{"state":{"type":"string","enum":["available","unavailable"],"description":"available lists the GitBook hosts; unavailable means DevTune could not read which 404s came from GitBook."},"verification":{"type":"string","enum":["unverified"]},"notFoundDemand":{"type":"string","enum":["page_views_only"]},"hosts":{"type":"array","description":"GitBook hosts with 404 page views in the window.","items":{"type":"object","additionalProperties":false,"properties":{"hostname":{"type":"string"},"notFoundRequests":{"type":"integer","description":"GitBook 404 page views on the host."}},"required":["hostname","notFoundRequests"]}}},"required":["state"]},"trafficSensors":{"type":"object","additionalProperties":false,"description":"Which crawler sensors are measuring, read from the same derivation as `sensors` on the traffic summary. Only Cloudflare and the edge middleware can observe a 404, so those are the two providers reported here.","properties":{"connectedProviders":{"type":"array","items":{"type":"string"},"description":"Providers whose sensor state is `active` or `no_recent_traffic`. A healthy sensor that simply had no AI traffic inside its freshness window stays listed; a connection that has stopped syncing, or whose configuration has been superseded, does not."},"hasConnectedSensor":{"type":"boolean","description":"True when `connectedProviders` is non-empty. False means no crawler sensor is currently producing the evidence these clusters are built from."},"middlewareConnected":{"type":"boolean","description":"True when the edge-push sensor state is `active` or `no_recent_traffic` for the project's current traffic configuration."}},"required":["connectedProviders","hasConnectedSensor","middlewareConnected"]}},"required":["clusters","dataAvailability","dataScope","hasObservedDemand","suspectedNoiseClusters","state","trafficSensors","window","windowDays"]},"ProjectEvidenceCoverage":{"type":"object","description":"Project-wide evidence state at the moment the gallery was read, in the same vocabulary the per-agent verdicts use.","properties":{"asOf":{"type":"string","format":"date-time","description":"When the evidence state was read."},"sources":{"type":"array","description":"Each evidence source and its state.","items":{"type":"object","properties":{"source":{"$ref":"#/components/schemas/AgentEvidenceGap/properties/source"},"state":{"type":"string","enum":["available","missing","unconfirmed"],"description":"available when it delivers measurements or has delivered one in the last 90 days, missing when nothing is connected, unconfirmed when it is connected but has not delivered one, or its last one is more than 90 days old."}},"required":["source","state"]}}},"required":["asOf","sources"]},"AgentEvidenceGap":{"type":"object","properties":{"source":{"type":"string","enum":["analytics","answer_engine_audits","browser_machine_traffic","owned_content","search_console","search_probe_answers","search_probes","server_machine_traffic"],"description":"The evidence source the agent cannot read. The two machine-traffic sources are not interchangeable: only server_machine_traffic sees answer fetches and 404 demand."},"state":{"type":"string","enum":["missing","unconfirmed"],"description":"missing means nothing is connected; unconfirmed means it is connected but has never delivered a measurement."}},"required":["source","state"]},"AgentEvidence":{"type":"object","description":"Whether the evidence an agent reads is available.","properties":{"unavailable":{"type":"array","description":"Sources the agent reads that it cannot see. In the agent list this is the state now; in a run's evidenceBlock, the state recorded when the run was blocked.","items":{"$ref":"#/components/schemas/AgentEvidenceGap"}},"verdict":{"type":"string","enum":["blocked","partial","ready"],"description":"blocked means nothing this agent reads is available and a run would be refused before any credits are spent. partial means the run proceeds and reports which sources it could not see."}},"required":["unavailable","verdict"]},"AgentSchedule":{"description":"When scheduled runs start: daily, weekly on dayOfWeek, or monthly on dayOfMonth, at time in timezone.","oneOf":[{"$ref":"#/components/schemas/AgentScheduleDaily"},{"$ref":"#/components/schemas/AgentScheduleWeekly"},{"$ref":"#/components/schemas/AgentScheduleMonthly"}]},"AgentScheduleDaily":{"type":"object","additionalProperties":false,"required":["cadence","time","timezone"],"properties":{"cadence":{"type":"string","enum":["daily"]},"time":{"type":"string","pattern":"^([01]\\d|2[0-3]):[0-5]\\d$","description":"Run time as 24-hour HH:mm."},"timezone":{"type":"string","description":"IANA timezone, such as Europe/London."},"modelId":{"type":"string","minLength":1,"description":"Optional. The modelId sent beside the schedule is the one scheduled runs use."}}},"AgentScheduleWeekly":{"type":"object","additionalProperties":false,"required":["cadence","dayOfWeek","time","timezone"],"properties":{"cadence":{"type":"string","enum":["weekly"]},"dayOfWeek":{"type":"integer","minimum":0,"maximum":6,"description":"0 (Sunday) to 6 (Saturday)."},"time":{"type":"string","pattern":"^([01]\\d|2[0-3]):[0-5]\\d$","description":"Run time as 24-hour HH:mm."},"timezone":{"type":"string","description":"IANA timezone, such as Europe/London."},"modelId":{"type":"string","minLength":1,"description":"Optional. The modelId sent beside the schedule is the one scheduled runs use."}}},"AgentScheduleMonthly":{"type":"object","additionalProperties":false,"required":["cadence","dayOfMonth","time","timezone"],"properties":{"cadence":{"type":"string","enum":["monthly"]},"dayOfMonth":{"type":"integer","minimum":1,"maximum":28,"description":"The day of the month runs start on."},"time":{"type":"string","pattern":"^([01]\\d|2[0-3]):[0-5]\\d$","description":"Run time as 24-hour HH:mm."},"timezone":{"type":"string","description":"IANA timezone, such as Europe/London."},"modelId":{"type":"string","minLength":1,"description":"Optional. The modelId sent beside the schedule is the one scheduled runs use."}}},"AgentScheduleState":{"type":"object","description":"The agent's schedule: whether it is on, when it runs, and on which model.","additionalProperties":false,"required":["autonomyLevel","schedule","modelId","scheduledAt"],"properties":{"autonomyLevel":{"type":"string","enum":["manual","scheduled"],"description":"scheduled when the schedule is on; manual when it is off."},"schedule":{"nullable":true,"description":"When scheduled runs start. Null when the schedule is off.","allOf":[{"$ref":"#/components/schemas/AgentSchedule"}]},"modelId":{"type":"string","nullable":true,"description":"The model scheduled runs use. Null when none is set."},"scheduledAt":{"type":"string","format":"date-time","nullable":true,"description":"When the schedule was last turned on or changed. Null when the schedule is off."}}},"AgentScheduleWrite":{"type":"object","additionalProperties":false,"required":["agentKey","autonomyLevel","schedule","modelId","scheduledAt"],"properties":{"agentKey":{"type":"string","description":"The agent the schedule is for."},"autonomyLevel":{"type":"string","enum":["manual","scheduled"],"description":"scheduled when the schedule is on; manual when it is off."},"schedule":{"nullable":true,"description":"When scheduled runs start. Null when the schedule is off.","allOf":[{"$ref":"#/components/schemas/AgentSchedule"}]},"modelId":{"type":"string","nullable":true,"description":"The model scheduled runs use. Null when none is set."},"scheduledAt":{"type":"string","format":"date-time","nullable":true,"description":"When the schedule was last turned on or changed. Null when the schedule is off."}}},"CustomAgentInput":{"type":"object","additionalProperties":false,"required":["displayName","description","instructions","modelId","quickActions","toolAllowlist"],"properties":{"displayName":{"type":"string","minLength":1,"maxLength":80},"description":{"type":"string","minLength":1,"maxLength":240,"description":"One sentence saying what the agent does."},"instructions":{"type":"string","minLength":1,"maxLength":12000,"description":"What the agent does on every run."},"modelId":{"type":"string","minLength":1,"maxLength":120,"description":"A model available now; others are refused with 400."},"quickActions":{"type":"array","minItems":1,"maxItems":6,"items":{"type":"string","minLength":1,"maxLength":160},"description":"Suggested requests shown with the agent."},"toolAllowlist":{"type":"array","minItems":1,"uniqueItems":true,"items":{"type":"string"},"description":"Tools the agent may use, including exactly one draft output: create_draft_action, create_draft_brief or create_report_artifact."}}},"CustomAgent":{"type":"object","additionalProperties":false,"description":"A custom agent the team created.","required":["agentKey","displayName","description","instructions","modelId","quickActions","toolAllowlist","outputArtifactType","workflowKey","isCustom","scheduling"],"properties":{"agentKey":{"type":"string","description":"Identifies the agent. Pass it to run, schedule or archive it."},"displayName":{"type":"string"},"description":{"type":"string","description":"One sentence saying what the agent does."},"instructions":{"type":"string","description":"What the agent does on every run."},"modelId":{"type":"string"},"quickActions":{"type":"array","description":"Suggested requests shown with the agent.","items":{"type":"string"}},"toolAllowlist":{"type":"array","items":{"type":"string"},"description":"The tools the agent uses, including library search, which every agent gets."},"outputArtifactType":{"type":"string","enum":["action","brief","report"],"description":"What each run drafts for review."},"workflowKey":{"type":"string","description":"The key the agent’s runs carry as workflowKey."},"isCustom":{"type":"boolean","enum":[true]},"scheduling":{"$ref":"#/components/schemas/AgentScheduleState"}}},"CustomAgentWrite":{"type":"object","additionalProperties":false,"required":["agent"],"properties":{"agent":{"$ref":"#/components/schemas/CustomAgent"}}},"CustomAgentArchiveWrite":{"type":"object","additionalProperties":false,"required":["agent"],"properties":{"agent":{"type":"object","additionalProperties":false,"description":"The custom agent after the write.","required":["agentKey","displayName","description","instructions","modelId","quickActions","toolAllowlist","outputArtifactType","workflowKey","isCustom","scheduling","archivedAt"],"properties":{"agentKey":{"description":"Identifies the agent. Pass it to restore the agent once archived, or to run, schedule or archive it once restored.","type":"string"},"displayName":{"type":"string"},"description":{"type":"string","description":"One sentence saying what the agent does."},"instructions":{"type":"string","description":"What the agent does on every run."},"modelId":{"type":"string"},"quickActions":{"type":"array","description":"Suggested requests shown with the agent.","items":{"type":"string"}},"toolAllowlist":{"type":"array","description":"The tools the agent uses, including library search, which every agent gets.","items":{"type":"string"}},"outputArtifactType":{"type":"string","enum":["action","brief","report"],"description":"What each run drafts for review."},"workflowKey":{"type":"string","description":"The key the agent’s runs carry as workflowKey."},"isCustom":{"type":"boolean","enum":[true]},"scheduling":{"$ref":"#/components/schemas/AgentScheduleState"},"archivedAt":{"type":"string","format":"date-time","nullable":true,"description":"When the agent was archived; null once it is restored."}}}}},"AgentDefinition":{"type":"object","description":"A built-in or custom agent.","properties":{"agentKey":{"type":"string","description":"Identifies the agent. Pass it to run or schedule it. An archived custom agent must be restored first, and the Page Drafter and Section Editor start only from an action's content draft."},"archivedAt":{"type":"string","format":"date-time","description":"When the custom agent was archived. Present only in the archived list."},"description":{"type":"string","description":"What the agent does."},"displayName":{"type":"string"},"evidence":{"$ref":"#/components/schemas/AgentEvidence"},"isCustom":{"type":"boolean","description":"True for an agent the team created."},"outputArtifactType":{"type":"string","description":"What each run drafts for review, such as action, brief or report."},"quickActions":{"type":"array","description":"Suggested requests shown with the agent.","items":{"type":"string"}},"workflowKey":{"type":"string","description":"The key the agent’s runs carry as workflowKey."}},"required":["agentKey","description","displayName","evidence","isCustom","outputArtifactType","workflowKey"]},"AgentRun":{"type":"object","description":"One run of an agent.","properties":{"completedAt":{"type":["string","null"],"format":"date-time","description":"When the run ended. Null while it is queued or running."},"creditsUsed":{"type":["number","null"],"description":"Exact credits consumed by the settled run. Null when settlement evidence is not recorded."},"createdAt":{"type":"string","format":"date-time"},"definition":{"description":"The agent that ran. Null when its definition cannot be found.","anyOf":[{"$ref":"#/components/schemas/AgentDefinition"},{"type":"null"}]},"evidenceBlock":{"anyOf":[{"$ref":"#/components/schemas/AgentEvidence"},{"type":"null"}],"description":"Why a blocked run never started, when it was blocked for missing evidence. Null for every other run."},"id":{"type":"string","format":"uuid"},"scheduleOccurrenceKey":{"type":["string","null"],"description":"Identifies the scheduled slot the run filled. Null for a run started on demand."},"startedAt":{"type":["string","null"],"description":"When the run started. Null until it starts, and for a run that ended without starting.","format":"date-time"},"status":{"type":"string","description":"superseded when every unreviewed draft of the run was replaced by a newer draft; a run with some drafts still current stays draft_ready."},"supersededByRunId":{"type":["string","null"],"format":"uuid","description":"The newer run that replaced this one. Null unless status is superseded."},"usage":{"type":["object","null"],"additionalProperties":true,"description":"Usage counters the model provider reported. Null when none were reported; a zero is a measured zero."},"workflowKey":{"type":"string","description":"The agent the run belongs to."}},"required":["completedAt","creditsUsed","createdAt","definition","evidenceBlock","id","scheduleOccurrenceKey","startedAt","status","supersededByRunId","usage","workflowKey"]},"AgentRunDetail":{"allOf":[{"$ref":"#/components/schemas/AgentRun"},{"type":"object","properties":{"draftOutput":{"type":["object","null"],"additionalProperties":true,"description":"The complete stored action, brief or report artifact for a completed run. Markdown is not shortened on read. Null while a run is unfinished or its artifact type is unsupported, including content_draft."}},"required":["draftOutput"]}]},"AgentRunReviewResult":{"type":"object","properties":{"alreadyReviewed":{"type":"boolean","description":"true when the run already had this decision, so this request changed nothing."},"runId":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["approved","dismissed"],"description":"The run status after the decision."}},"required":["alreadyReviewed","runId","status"]},"ContentDraftFactCheck":{"type":"object","description":"The draft's mandatory fact check. A product fact (a specific price, number, limit, date, integration or capability) on the brief's facts list counts as supported by that fact's source. Every other product fact is flagged and searched on the customer's own site: unsupported or contradicted when the site does not state it or says otherwise, else not_in_brief, with siteJudgment saying whether the site supports it. A claim about another product that an external fact on the brief's facts list provably contradicts is flagged contradicted_by_source, with that fact as the brief words it (which may paraphrase the source it cites). A sentence linking to one of the project's configured competitors is flagged competitor_link, with the link in linkUrl.","properties":{"checkedAt":{"type":"string","format":"date-time"},"status":{"type":"string","enum":["checked","partial","unavailable"],"description":"checked: every sentence was judged. partial: some searches or checks failed. unavailable: nothing was checked, so the facts are unverified."},"flags":{"type":"array","description":"The sentences still flagged in the saved draft.","items":{"type":"object","properties":{"claim":{"type":"string","description":"The flagged sentence."},"reason":{"type":"string","enum":["unsupported","contradicted","not_in_brief","contradicted_by_source","competitor_link"],"description":"unsupported or contradicted by the customer's own site, not_in_brief: a product fact the brief's facts list does not state, whatever the site says, contradicted_by_source: a claim about another product that an external fact on the brief's facts list contradicts, proven against that fact's wording, or competitor_link: the sentence links to one of the project's configured competitors."},"linkUrl":{"type":["string","null"],"description":"For a competitor_link flag, the competitor URL the sentence links to. Null otherwise."},"siteJudgment":{"type":["string","null"],"enum":["supported","stale","unclear",null],"description":"For a not_in_brief flag, what the site check found: supported means the customer's site states it, so it can be added to the brief's facts; stale means only dated pages state it, which may no longer hold; unclear means the site was inconclusive. Null otherwise, or when the site check did not judge it."},"evidenceUrls":{"type":"array","items":{"type":"string"},"description":"The owned pages the sentence was judged against; for contradicted_by_source, the source the brief's fact cites, when it has a URL."},"quote":{"type":["object","null"],"description":"The passage that contradicts the sentence: quoted word for word from the customer's site, or, for contradicted_by_source, the brief's listed fact as the brief words it, which may paraphrase the source it cites (url). Null when nothing contradicts it.","properties":{"text":{"type":"string"},"url":{"type":["string","null"],"description":"Where it is. Null when there is no URL."}},"required":["text","url"]},"changeIndexes":{"type":["array","null"],"items":{"type":"integer","minimum":0},"description":"For section edits, the indexes of the changes whose proposed text states the sentence. Null for a page draft, or when no change could be matched to the sentence."}},"required":["claim","reason","siteJudgment","linkUrl","evidenceUrls","quote","changeIndexes"]}},"flaggedBeforeFix":{"type":["integer","null"],"minimum":0,"description":"Flags the first check returned to the agent before its one fix pass; null when it did not run."},"flaggedAfterFix":{"type":["integer","null"],"minimum":0,"description":"Flags on the saved draft; null when its check did not run and no link to a competitor was found."},"coverage":{"type":["object","null"],"description":"How much of the draft the check covered, in sentences. read counts the sentences the check reviewed as possible claims (questions and short fragments are not counted); the other counts split it. hasGap is false only when every sentence was judged and settled. Null when the check did not run or was saved before coverage was recorded.","properties":{"read":{"type":"integer","minimum":0,"description":"Sentences the check reviewed as possible claims, including those marked [Needs source]. Questions and short fragments are not counted."},"checked":{"type":"integer","minimum":0,"description":"Settled: restated from the brief's facts list, or judged against the customer's site. Includes third-party claims the brief's facts support."},"unsettled":{"type":"integer","minimum":0,"description":"Judged but not settled: unclear, or supported only by dated pages."},"notChecked":{"type":"integer","minimum":0,"description":"Not checked because classification, search or the check failed."},"notVerified":{"type":"integer","minimum":0,"description":"Claims about a third party (another product, company, standard or source) that were read but not verified. Always 0 on a check saved before claim kinds were recorded, whose claims about other products are counted in setAside."},"setAside":{"type":"integer","minimum":0,"description":"Set aside as guidance, an example, a heading or an opinion, with nothing to verify. On a check saved before claim kinds were recorded, also claims about other products."},"needsSource":{"type":"integer","minimum":0,"description":"Marked [Needs source] in the draft."},"hasGap":{"type":"boolean","description":"True when some sentence was not both judged and settled."}},"required":["read","checked","unsettled","notChecked","notVerified","setAside","needsSource","hasGap"]}},"required":["checkedAt","status","flags","flaggedBeforeFix","flaggedAfterFix","coverage"]},"ContentDraftVersion":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"draftKind":{"type":"string","enum":["page","section_edits"],"description":"page: a whole new page written by the Page Drafter. section_edits: a change list for an existing page written by the Section Editor."},"title":{"type":"string"},"contentMarkdown":{"type":["string","null"],"description":"The downloadable markdown: the page, or the change list rendered as markdown. Null when the version has no markdown."},"changeList":{"type":["object","null"],"description":"For section edits, the structured change list. Null for a page draft.","properties":{"page":{"type":"object","description":"The page the changes apply to.","properties":{"url":{"type":"string"},"crawledAt":{"type":["string","null"],"format":"date-time","description":"When DevTune crawled the page. Null when the text came from the repository file, or none was held."},"crawled":{"type":"boolean","description":"False when DevTune held no copy of the page's current text, so none could be quoted. The text comes from DevTune's crawl, or from the page's file in a connected content repository; crawledAt is null for the file."}},"required":["url","crawledAt","crawled"]},"lockedPagesAvoided":{"type":"array","items":{"type":"string"},"description":"Locked pages the action was steered away from; none of them is edited."},"changes":{"type":"array","items":{"type":"object","properties":{"heading":{"type":"string","description":"The section the change edits or adds."},"newSection":{"type":["object","null"],"description":"Set for a new section, with the existing heading it goes after (null for the end of the page).","properties":{"afterHeading":{"type":["string","null"],"description":"The existing heading it goes after. Null for the end of the page."}},"required":["afterHeading"]},"currentText":{"type":"string","description":"The text the change replaces, as the page had it when the draft was written."},"proposedText":{"type":"string"},"reason":{"type":"string","description":"Why the change helps."}},"required":["heading","newSection","currentText","proposedText","reason"]}}},"required":["page","lockedPagesAvoided","changes"]},"status":{"type":"string","enum":["current","superseded"],"description":"current for the draft the action shows now; every other version, including the other agent's last draft from before the action changed between create and refresh, is superseded."},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"briefId":{"type":["string","null"],"format":"uuid","description":"The brief the draft was written from. Null when not recorded."},"briefGeneratedAt":{"type":["string","null"],"format":"date-time","description":"When that brief was generated. Null when not recorded."},"runId":{"type":["string","null"],"format":"uuid","description":"The agent run that wrote the draft. Null when not recorded."},"factCheck":{"description":"The draft's fact check. Null when none was saved.","oneOf":[{"$ref":"#/components/schemas/ContentDraftFactCheck"},{"type":"null"}]}},"required":["id","draftKind","title","contentMarkdown","changeList","status","createdAt","updatedAt","briefId","briefGeneratedAt","runId","factCheck"]},"ActionContentDraft":{"type":"object","properties":{"actionId":{"type":"string","format":"uuid"},"draftKind":{"type":["string","null"],"enum":["page","section_edits",null],"description":"The draft the action takes now: page for a create action, section_edits for a refresh. Null for an action that takes no draft."},"agentKey":{"type":["string","null"],"enum":["page-drafter","section-editor",null],"description":"The agent that writes the draft: page-drafter for a page, section-editor for section edits. Null when the action takes no draft."},"briefState":{"type":["string","null"],"enum":["ready","missing","in_progress","not_ready","outdated",null],"description":"A draft is written from a ready brief only; not_ready covers a stale or failed brief, and outdated a ready brief written before the current brief format, which must be regenerated to draft. Null when the action takes no draft."},"targetPage":{"type":["object","null"],"description":"For section edits, the page edited. Null for a page draft or when the action takes no draft.","properties":{"url":{"type":["string","null"],"description":"The page. Null when state is no_target."},"state":{"type":"string","enum":["ready","locked","no_target"],"description":"ready to edit, locked (the page is locked, so it is not edited), or no_target (neither the action nor its ready brief names a page on the project's own domains)."},"briefRecommendedUrl":{"type":["string","null"],"description":"The page the ready brief recommends, when it differs from the action's own target, which is used. Null when they match, when the action has no target of its own (the brief's page is then url), or when the brief names no page on the project's own domains."}},"required":["url","state","briefRecommendedUrl"]},"models":{"type":"array","description":"Models a draft run can use, with the per-run credit estimate from recent runs (null without enough history).","items":{"type":"object","properties":{"id":{"type":"string"},"isDefault":{"type":"boolean","description":"True for the model a run uses unless you choose another."},"estimateCredits":{"type":["number","null"],"description":"Estimated credits for one draft run with this model. Null when there is no estimate."},"estimateBasis":{"type":["string","null"],"enum":["project_runs","account_runs","all_runs",null],"description":"Which past runs the estimate comes from: this project’s, the account’s, or every account’s. Null when there is no estimate."}},"required":["id","isDefault","estimateCredits","estimateBasis"]}},"run":{"type":["object","null"],"description":"The draft run in flight, or the last one that ended without a draft and was not followed by one. Null when there is neither.","properties":{"id":{"type":"string","format":"uuid"},"state":{"type":"string","enum":["running","failed"],"description":"running while it works, failed when it ended without a draft."},"status":{"type":"string"}},"required":["id","state","status"]},"current":{"description":"The draft the action shows now. Null when there is none.","oneOf":[{"$ref":"#/components/schemas/ContentDraftVersion"},{"type":"null"}]},"versions":{"type":"array","description":"Newest first: the current draft and up to 10 earlier versions, the same ones the action page lists.","items":{"$ref":"#/components/schemas/ContentDraftVersion"}},"pullRequest":{"oneOf":[{"$ref":"#/components/schemas/ActionPullRequest"},{"type":"null"}],"description":"The pull request opened from the draft on a content repo: the open one, else the latest attempt. Null when there is none."}},"required":["actionId","draftKind","agentKey","briefState","targetPage","models","run","current","versions","pullRequest"]},"ActionPullRequest":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"provider":{"type":"string","enum":["github","gitbook"],"description":"github for a pull request on a content repo, gitbook for a change request in a GitBook space."},"state":{"type":"string","enum":["opening","open","merged","closed","failed"],"description":"opening while DevTune writes it, open once GitHub or GitBook created it, merged or closed as DevTune last read it, failed when GitHub or GitBook refused the attempt or an automatic attempt was refused. A GitBook change request archived in GitBook is closed. DevTune never merges."},"number":{"type":["integer","null"],"description":"The pull request or change request number. Null until it was created."},"url":{"type":["string","null"],"format":"uri","description":"The pull request on GitHub, or the change request in GitBook. Null until it was created."},"repository":{"type":["string","null"],"description":"The repo, as owner/name. Null for GitBook."},"branch":{"type":["string","null"],"description":"DevTune's branch, devtune/action-<id>. Null for GitBook."},"space":{"description":"The GitBook space. Null for GitHub.","oneOf":[{"type":"object","additionalProperties":false,"required":["id","title"],"properties":{"id":{"type":"string"},"title":{"type":"string"}}},{"type":"null"}]},"filePath":{"type":["string","null"],"description":"The repo file the pull request edits or creates, or the GitBook page path the change request edits or creates; null for a failed automatic attempt that found no file."},"isNewFile":{"type":"boolean","description":"True when the pull request creates filePath rather than editing it."},"draftId":{"type":"string","format":"uuid","description":"The draft version the pull request carries."},"trigger":{"type":"string","enum":["app","api","mcp","auto"],"description":"auto when DevTune opened it for a new draft, because the content source has automatic pull requests on."},"failure":{"type":["object","null"],"description":"Why the attempt failed. An automatic attempt also records the refusals no one saw: source_needs_reconnect, file_not_found, file_exists and draft_does_not_match_source. A GitBook attempt can also fail with gitbook_error, change_request_diverged, page_not_preserved, or disconnected when GitBook was disconnected in DevTune. Null unless state is failed.","properties":{"code":{"type":"string","enum":["branch_diverged","github_error","source_needs_reconnect","file_not_found","file_exists","draft_does_not_match_source","gitbook_error","change_request_diverged","change_request_conflicts","page_not_preserved","disconnected"]},"detail":{"type":["string","null"],"description":"More about the failure. Null when there is none."}},"required":["code","detail"]},"createdAt":{"type":"string","format":"date-time"},"openedAt":{"type":["string","null"],"format":"date-time","description":"When GitHub created the pull request. Null until then."},"mergedAt":{"type":["string","null"],"format":"date-time","description":"When DevTune recorded the pull request merged in GitHub. Null unless merged."},"closedAt":{"type":["string","null"],"format":"date-time","description":"When DevTune recorded the pull request closed. Null unless it was closed; a pull request closed, then reopened and merged in GitHub, keeps this time."},"checkedAt":{"type":["string","null"],"format":"date-time","description":"When DevTune last read this GitBook change request in GitBook and GitBook answered. Null for a GitHub pull request and until the first read."},"updateFailure":{"description":"The latest update DevTune could not write to an open GitBook change request, while DevTune has not written to it since: change_request_diverged (someone else edited it), change_request_conflicts (its rebase left conflicts) or page_not_preserved (GitBook stored the page differently). The change request stays open. Null otherwise.","oneOf":[{"type":"object","additionalProperties":false,"required":["code","detail","draftId","createdAt"],"properties":{"code":{"type":"string","enum":["change_request_diverged","change_request_conflicts","page_not_preserved"]},"detail":{"type":["string","null"]},"draftId":{"type":"string","format":"uuid"},"createdAt":{"type":"string","format":"date-time"}}},{"type":"null"}]}},"required":["id","provider","state","number","url","repository","branch","space","filePath","isNewFile","draftId","trigger","failure","createdAt","openedAt","mergedAt","closedAt","checkedAt","updateFailure"]},"ActionPullRequestData":{"type":"object","properties":{"pullRequest":{"description":"The pull request.","oneOf":[{"$ref":"#/components/schemas/ActionPullRequest"},{"type":"null"}]}},"required":["pullRequest"]},"ContentDraftStartResult":{"type":"object","properties":{"runId":{"type":"string","format":"uuid","description":"The draft run. Follow it with GET /projects/{projectId}/actions/{actionId}/draft, whose run is this one while it is in flight and whose current version is its draft once it finishes. An account with content drafts can also read it from GET /projects/{projectId}/agents/runs/{runId}; for an account that drafts only Agent Picks Actions, that read returns 404."},"status":{"type":"string","enum":["queued","failed"],"description":"queued for a run that is queued, running or finished, including a replay of an earlier requestIdempotencyKey. failed only when this requestIdempotencyKey was already used for a run that has since failed or been canceled; that run is returned as it ended. A dispatch that fails during this request returns 500 and starts no run."}},"required":["runId","status"]},"AgentRunTriggerResult":{"type":"object","properties":{"attribution":{"type":"object","description":"Who the run is recorded as started by.","properties":{"actorType":{"type":"string","enum":["key_minting_user","service"],"description":"key_minting_user when the run acts as the member who created the API key; oauth_user when it acts as the member who authorized the OAuth connection."},"apiKeyId":{"type":"string","format":"uuid"},"createdByUserId":{"type":["string","null"],"format":"uuid","description":"The member who created the key, or for an OAuth request the signed-in member."},"source":{"type":"string","enum":["api_key"]}},"required":["actorType","apiKeyId","createdByUserId","source"]},"status":{"type":"string","enum":["already_queued","failed","queued"],"description":"Dispatch outcome. already_queued is returned only for an idempotent replay resolved through this request's idempotency key; an unrelated queued run returns 409."},"taskId":{"type":"string","format":"uuid"},"workflowId":{"type":"string","format":"uuid","description":"The agent version the run uses. Present when status is queued or failed; absent when a retry with the same request key returns already_queued."}},"required":["attribution","status","taskId"]},"DexConversationThread":{"type":"object","properties":{"creditTotal":{"type":"number","minimum":0,"description":"Credits the conversation has used: charged for settled runs, reserved for runs still settling."},"creditTotalIsFinal":{"type":"boolean","description":"False while a charge is still settling or when older history for this thread is not included; creditTotal may then be lower than the final total."},"currentTaskId":{"type":"string","format":"uuid","description":"The conversation's latest run."},"lastMessageAt":{"type":"string","format":"date-time","description":"When the key’s creator last sent a message."},"state":{"type":"string","enum":["active","closed","expired"],"description":"active: the conversation can take messages now. expired: it went idle; the next message continues it in a new run. closed: it has ended."},"taskIds":{"type":"array","description":"The conversation's runs; the last is currentTaskId.","items":{"type":"string","format":"uuid"}},"threadId":{"type":"string","format":"uuid","description":"The conversation. Pass it to the other Dex endpoints."},"title":{"type":["string","null"],"description":"The conversation title: from the first message, or as renamed. Null when it has none."},"updatedAt":{"type":"string","format":"date-time","description":"When the conversation last changed. Lists sort by it."}},"required":["creditTotal","creditTotalIsFinal","currentTaskId","lastMessageAt","state","taskIds","threadId","title","updatedAt"]},"DexConversationThreadPage":{"type":"object","properties":{"items":{"type":"array","description":"Conversations of the key’s creator in the project, most recently updated first.","items":{"$ref":"#/components/schemas/DexConversationThread"}},"truncated":{"type":"boolean","description":"True when older conversation history exists that this list does not include."}},"required":["items","truncated"]},"DexConversationStartResult":{"type":"object","properties":{"status":{"type":"string","enum":["started"]},"taskId":{"type":"string","format":"uuid","description":"The run handling the conversation."},"threadId":{"type":"string","format":"uuid","description":"The new conversation. Read its events and send further messages with it."}},"required":["status","taskId","threadId"]},"DexConversationMessageResult":{"type":"object","properties":{"status":{"type":"string","enum":["queued","resumed","steered"],"description":"steered: Dex was waiting for input and received the message. queued: Dex is busy and reads the message when its current turn ends. resumed: the conversation had gone idle or its run had ended, so a new run continues it with the message."},"taskId":{"type":["string","null"],"format":"uuid","description":"The run that receives the message: the new run when resumed."}},"required":["status","taskId"]},"DexConversationEvent":{"type":"object","properties":{"createdAt":{"type":"string","format":"date-time","description":"When the event was recorded."},"eventType":{"type":"string","description":"What happened, such as a message or a tool call. payload depends on it."},"id":{"type":"string","format":"uuid"},"insertionOrdinal":{"type":"integer","format":"int64","minimum":0,"description":"The event’s position in the conversation. Events are returned in this order."},"payload":{"type":"object","additionalProperties":true,"description":"The event content. Its fields depend on eventType."},"taskId":{"type":"string","format":"uuid","description":"The run that recorded the event."},"turnId":{"type":["string","null"],"description":"The turn the event belongs to. Null for an event outside a turn."}},"required":["createdAt","eventType","id","insertionOrdinal","payload","taskId","turnId"]},"DexConversationEventPage":{"type":"object","properties":{"items":{"type":"array","description":"Events in insertionOrdinal order, oldest first.","items":{"$ref":"#/components/schemas/DexConversationEvent"}},"nextAfter":{"type":["integer","null"],"format":"int64","minimum":0,"description":"Pass as after to read the next page. Null when no more events have been recorded yet."},"threadTruncated":{"type":"boolean","description":"True when this conversation has older history that these events do not include."}},"required":["items","nextAfter","threadTruncated"]},"DexConversationRenameResult":{"type":"object","properties":{"status":{"type":"string","enum":["renamed"]}},"required":["status"]},"DexConversationDeleteResult":{"type":"object","additionalProperties":false,"properties":{"status":{"type":"string","enum":["deleted"]}},"required":["status"]},"DexHandoff":{"type":"object","properties":{"agent":{"type":"string","description":"The specialist agent’s display name."},"agentKey":{"type":"string","description":"The specialist agent’s key."},"estimateCredits":{"type":["number","null"],"description":"Estimated managed-agent credits the run would spend. Null when there is no estimate."},"estimateBasis":{"type":["string","null"],"enum":["project_runs","account_runs","all_runs",null],"description":"Which past runs of the agent the estimate comes from: this project’s, the account’s, or every account’s. Null when there is no estimate."},"expiresAt":{"type":"string","format":"date-time","description":"When the handoff stops waiting for a decision."},"handoffId":{"type":"string","format":"uuid","description":"Pass it to confirm or cancel the handoff."},"instructions":{"type":"string","description":"What Dex asks the specialist to do, up to 4,000 characters."}},"required":["agent","agentKey","estimateCredits","estimateBasis","expiresAt","handoffId","instructions"]},"DexHandoffList":{"type":"array","description":"Handoffs in the conversation that wait for a decision.","items":{"$ref":"#/components/schemas/DexHandoff"}},"DexHandoffDecisionResult":{"type":"object","additionalProperties":true,"properties":{"status":{"type":"string","enum":["canceled","not_pending","not_started","queued"],"description":"queued: the specialist run started and spends credits. canceled: the handoff was cancelled. not_pending: no handoff with this id is waiting, because it was already decided or has expired. not_started: the run could not start; the handoff is still waiting, so confirm again later."},"taskId":{"type":"string","format":"uuid","description":"The specialist run. Present only when status is queued."}},"required":["status"]},"DexMemoryDecisionResult":{"type":"object","properties":{"itemId":{"type":"string","format":"uuid","description":"The saved memory. Present only when status is saved."},"status":{"type":"string","enum":["dismissed","saved"],"description":"saved when the offered memory was stored, dismissed when nothing was saved."}},"required":["status"]}},"responses":{"UnauthorizedError":{"description":"The request has no valid credential: the Authorization header is missing or malformed, or the API key or OAuth token is invalid, expired or revoked. Send `Authorization: Bearer <API key>` with an active key, or sign in again to get a new OAuth token. See [Errors](/api-reference/errors).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"ForbiddenError":{"description":"The credential is valid but can't make this request. The message says which cause applies: the key or sign-in lacks the scope this endpoint requires (named in the message), it belongs to another project, the plan doesn't include API access or this feature, the project is paused (a paused project keeps only read scopes and setup.write), or the member a write acts as no longer has the role it needs. Read `GET /projects/{projectId}/connection` for the credential's current scopes and features, then use a key that has the scope, sign in again and approve it, or upgrade the plan. See [Errors](/api-reference/errors).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"ConflictError":{"description":"The request conflicts with the resource's current state. The message, and reason where present, say what conflicts; the endpoint's description lists its reasons. Read the resource again, then change the request. When the body has retryable true, send the same request again. See [Errors](/api-reference/errors).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"RateLimitExceededError":{"description":"Too many requests from this credential in the current window. The request was refused before it took effect, so it is safe to retry: wait the seconds in Retry-After, then send it again. Endpoints that start agent runs have a stricter limit. See [Rate limits](/api-reference/rate-limits).","headers":{"Retry-After":{"description":"Seconds to wait before retrying a 429 or retryable 503 response.","schema":{"type":"integer","minimum":1}},"X-RateLimit-Limit":{"$ref":"#/components/headers/X-RateLimit-Limit"},"X-RateLimit-Remaining":{"$ref":"#/components/headers/X-RateLimit-Remaining"},"X-RateLimit-Reset":{"$ref":"#/components/headers/X-RateLimit-Reset"}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"BadRequestError":{"description":"The request is invalid: a parameter or body field is missing, has the wrong type or is out of range, or a cursor doesn't match this list. The message says which; reason, where present, is listed in the endpoint's description. Fix the request before retrying, and restart a list without its cursor. See [Errors](/api-reference/errors).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"LiftPopulationTooLargeError":{"description":"Legacy compatibility error for Direct-lift readers with the former 5,000-page population limit","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Error"},{"type":"object","properties":{"error":{"const":"lift_population_too_large"},"message":{"const":"Direct-lift evaluation supports a maximum measured-page population of 5,000 pages"},"status":{"const":422}}}]}}}},"NotFoundError":{"description":"The resource doesn't exist in this project, was deleted, or the credential can't see it. Check the IDs in the path, and that the projectId is the project the credential is bound to. See [Errors](/api-reference/errors).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"ServerError":{"description":"Internal server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"headers":{"X-RateLimit-Limit":{"description":"Maximum requests per window","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Requests remaining in current window","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Unix timestamp when the window resets","schema":{"type":"integer"}}}},"paths":{"/projects/{projectId}/prompt-set-analysis":{"get":{"summary":"Get prompt-set analysis","operationId":"getPromptSetAnalysis","description":"Read the latest or selected analysis, paginated assessments and proposals, latest completed run identity, and credit state. Reads never reserve or settle credits or apply proposals. Pass pagination.nextCursor as cursor for the next page; the cursor stays on the run the first page read. You can also review proposals in the app. Requires the `visibility.read` scope.","x-devtune-required-scope":"visibility.read","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}},{"name":"runId","in":"query","schema":{"type":"string","format":"uuid"}},{"name":"cursor","in":"query","schema":{"type":"string","maxLength":1024},"description":"pagination.nextCursor from the previous page, unchanged. Keep the other parameters the same: a cursor sent with different filters, or beside a deprecated position parameter, returns 400."},{"name":"offset","in":"query","description":"Deprecated: Pass cursor instead. Requires runId when greater than zero. Applies to assessments and proposals independently.","schema":{"type":"integer","minimum":0,"maximum":100000,"default":0},"deprecated":true},{"name":"pageSize","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":25}},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":25},"description":"The same setting as pageSize. Send either; both with different values returns 400."}],"responses":{"200":{"description":"Analysis and credit state; run is null when none exists.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"run":{"type":["object","null"],"description":"The analysis run this page reads: the one runId names, or the latest. assessments contains at most pageSize items. Null when the project has no analysis.","properties":{"id":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["queued","running","completed","failed"],"description":"completed and failed are final. A failed run charges nothing."},"assessments":{"type":"array","maxItems":100,"description":"This page of the run’s prompt assessments, at most pageSize.","items":{"type":"object"}},"estimated_credits":{"type":"number","description":"Credits the run was estimated to cost when it started, and reserved."},"prompt_count":{"type":"integer","description":"Prompts the run analyses."},"failure_message":{"type":["string","null"],"description":"Why the run failed, for a person to read. Null unless status is failed."},"created_at":{"type":"string","format":"date-time"},"completed_at":{"type":["string","null"],"format":"date-time","description":"When the run finished. Null while it is queued or running."}}},"proposals":{"type":"array","maxItems":100,"description":"This page of the run’s proposals, oldest first, at most pageSize. Review them before anything changes.","items":{"type":"object","description":"A proposed change to the prompt set, with id, run_id, proposal_type, state, source_prompt_id, old_text, proposed_text, topic_id and applied_prompt_id."}},"nextOffset":{"type":["integer","null"],"deprecated":true,"description":"Deprecated: pass pagination.nextCursor as cursor. Null when both assessment and proposal pages are exhausted."},"pagination":{"$ref":"#/components/schemas/ListPagination"},"latestCompletedAnalysis":{"type":["object","null"],"description":"The newest completed run. It differs from run when runId selects another run, or when the newest run is still in progress or failed. Null when no run has completed.","properties":{"id":{"type":"string","format":"uuid"},"completed_at":{"type":["string","null"],"format":"date-time","description":"When the run completed."}}},"creditState":{"type":"object","description":"What a new analysis would cost and what the account can spend, in credits.","properties":{"availableCredits":{"type":"number","description":"Spendable account-wide balance; shared across projects."},"meteredCredits":{"type":["number","null"],"description":"Persisted settlement for the selected run; null while unavailable."},"promptCount":{"type":"integer","description":"Active prompts in active topics: what a new analysis would analyse."},"estimatedCredits":{"type":"number","description":"Current estimate for active prompts in active topics; reservations are rechecked at start."}}}},"required":["run","proposals","nextOffset","pagination","latestCompletedAnalysis","creditState"]}},"required":["data"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Prompts and topics"]},"post":{"summary":"Start or retry prompt-set analysis","operationId":"startPromptSetAnalysis","description":"Spends credits under the actions.write scope. Obtain human approval of the current estimate before calling. Requires an API key whose creator still has project management permission; this endpoint does not accept OAuth tokens. Over MCP, devtune_start_prompt_set_analysis also works with an OAuth connection, as its signed-in member. Reserves estimated credits and settles completed usage; failed analyses release the reservation without charging. Reuses an analysis that is already queued or running instead of starting another. Does not apply proposals. Repeating after a terminal run starts a new billable analysis. Requires the `actions.write` scope.","x-devtune-required-scope":"actions.write","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["humanApproved","approvedEstimatedCredits"],"properties":{"approvedEstimatedCredits":{"type":"number","minimum":0,"description":"The exact creditState.estimatedCredits value approved by the human. A changed estimate returns 409 and requires renewed approval."},"humanApproved":{"type":"boolean","const":true,"description":"Attests to explicit approval from the human; agents must not supply this on their own authority."}}}}}},"responses":{"200":{"description":"Started or reused run.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"run":{"type":"object","description":"The run started, or the queued or running run reused instead, with the fields the analysis read returns."}}}}}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"402":{"description":"Insufficient agent credits."},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"409":{"description":"The approved credit estimate has changed. Read the current estimate and obtain human approval again."},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Prompts and topics"]}},"/projects/{projectId}/prompt-set-analysis/proposals":{"post":{"summary":"Review a prompt-set proposal","operationId":"reviewPromptSetProposal","description":"Show the proposal and obtain explicit human approval of apply, undo, dismiss or restore. Requires an API key whose creator still has project management permission. Uses the same quota, stale-state and concurrency checks as in-app review. No analysis credit charge. Restoration returns a dismissed proposal to review; it never applies it. Requires the `actions.write` scope.","x-devtune-required-scope":"actions.write","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["proposalId","operation","humanApproved"],"properties":{"proposalId":{"type":"string","format":"uuid"},"operation":{"type":"string","enum":["apply","undo","dismiss","restore"]},"humanApproved":{"type":"boolean","const":true}}}}}},"responses":{"200":{"description":"Current proposal state.","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"proposal":{"type":"object","description":"The proposal after the review, with the fields the analysis read lists."}}}}}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"409":{"description":"Proposal is stale, has a conflicting transition, duplicates an existing prompt or exceeds quota. Refresh and review before retrying."},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Prompts and topics"]}},"/projects/{projectId}/pages/metrics":{"get":{"summary":"List page metrics","description":"Returns per-page metrics for a fixed 7-, 30- or 90-day window. Page rows and summary cover content pages only; searchConsole holds the whole-project Search Console totals across all pages, with current and prior coverage and source dates. The last included Search Console day is three days before today in Pacific time. Treat Search Console columns as ranking evidence only when availability is measured or verified_zero. Partial coverage names the covered date ranges, and a delta needs both periods to be verified. Requires the `pages.read` scope.","operationId":"listUnifiedPageMetrics","x-devtune-required-scope":"pages.read","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}},{"name":"windowDays","in":"query","schema":{"type":"integer","default":30,"enum":[7,30,90]},"description":"Fixed rolling window in days. Supported values are 7, 30, and 90. Defaults to 30."},{"name":"cursor","in":"query","schema":{"type":"string","maxLength":12288},"description":"Opaque cursor from the previous response."},{"name":"pageSize","in":"query","schema":{"type":"integer","default":25,"minimum":1,"maximum":100},"description":"Results per page. Capped at 100."},{"name":"limit","in":"query","schema":{"type":"integer","default":25,"minimum":1,"maximum":100},"description":"The same setting as pageSize. Send either; both with different values returns 400."},{"name":"category","description":"Only pages with this diagnosis: lost_ground, fragile_win, unclaimed_demand, crawled_not_cited, blocked or healthy.","in":"query","schema":{"type":"string"}},{"name":"sortKey","description":"Sort by opportunity_score, page_path, category, gsc_clicks, ai_citations, citability_score or answer_fetches. Defaults to page_path.","in":"query","schema":{"type":"string"}},{"name":"sortDirection","description":"asc or desc. Defaults to asc.","in":"query","schema":{"type":"string","enum":["asc","desc"],"default":"asc"}}],"responses":{"200":{"description":"Unified page metrics","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/PageMetricsData"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Audit and pages"]}},"/projects/{projectId}/pages/guard-rails":{"get":{"summary":"List page guard-rail findings","description":"Returns the page guard-rail findings that Audit shows. Arbitrary date ranges are not accepted. Pass url to return only the rows page protection checks for that page; any of them with isUserLocked true means the page is locked. An empty filtered list means DevTune holds no guard-rail row for the page, including pages outside the tracked domains, and does not show the page is unlocked. Requires the `pages.read` scope.","operationId":"listPageGuardRails","x-devtune-required-scope":"pages.read","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}},{"name":"cursor","in":"query","schema":{"type":"string","maxLength":1024},"description":"pagination.nextCursor from the previous page, unchanged. Keep the other parameters the same: a cursor sent with different filters, or beside a deprecated position parameter, returns 400."},{"name":"offset","in":"query","schema":{"type":"integer","minimum":0,"default":0},"deprecated":true,"description":"Deprecated: Pass cursor instead."},{"name":"pageSize","in":"query","schema":{"type":"integer","default":25,"minimum":1,"maximum":100},"description":"Results per page. Capped at 100."},{"name":"limit","in":"query","schema":{"type":"integer","default":25,"minimum":1,"maximum":100},"description":"The same setting as pageSize. Send either; both with different values returns 400."},{"name":"url","in":"query","description":"An absolute http or https page URL, passed once. Matches rows on host without www., port, query or fragment, and on path with or without a trailing slash, under every tracked domain whose path prefix covers it. Items then start with the most specific tracked domain. Surrounding whitespace is ignored before the 2048-character URL limit; the normalized host is limited to 255 characters. Credentials, control characters anywhere in the original input and other schemes are refused.","schema":{"type":"string","minLength":1}}],"responses":{"200":{"description":"Page guard-rail findings","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/PageGuardRailsData"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"},"503":{"headers":{"Retry-After":{"description":"Seconds to wait before retrying a 429 or retryable 503 response.","schema":{"type":"integer","minimum":1}}},"description":"Filtering by url is temporarily unavailable; retry later. This response holds no guard-rail data and does not mean the page is unlocked. Requests without url are unaffected.","content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/Error"},{"type":"object","properties":{"error":{"const":"page_filter_unavailable"},"status":{"const":503}}}]}}}}},"tags":["Audit and pages"]},"post":{"summary":"Lock a page","description":"Locks a page on one of the project's own tracked domains, so actions, drafts and agents steer around it. The page does not need to be crawled yet. A domain that is not one of the project's own tracked domains returns 404. Locking a page that is already locked keeps it locked, so a retry is safe. Requires the `setup.write` scope.","operationId":"lockPage","x-devtune-required-scope":"setup.write","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["domainCanonical","pagePath"],"properties":{"domainCanonical":{"type":"string","minLength":1,"maxLength":512,"description":"One of the project's own tracked domains, as listDomains returns it."},"pagePath":{"type":"string","minLength":1,"maxLength":2048,"description":"The page path, such as /pricing, without a query string or fragment. /docs and /docs/ are the same page."}}}}}},"responses":{"200":{"description":"The locked page","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/PageLockWrite"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"409":{"$ref":"#/components/responses/ConflictError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Audit and pages"]},"delete":{"summary":"Unlock a page","description":"Unlocks a locked page, so suggestions may change it again. A page that is not locked returns 404. Requires the `setup.write` scope.","operationId":"unlockPage","x-devtune-required-scope":"setup.write","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["domainCanonical","pagePath"],"properties":{"domainCanonical":{"type":"string","minLength":1,"maxLength":512,"description":"One of the project's own tracked domains, as listDomains returns it."},"pagePath":{"type":"string","minLength":1,"maxLength":2048,"description":"The page path, such as /pricing, without a query string or fragment. /docs and /docs/ are the same page."}}}}}},"responses":{"200":{"description":"The unlocked page","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/PageLockWrite"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"409":{"$ref":"#/components/responses/ConflictError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Audit and pages"]}},"/projects/{projectId}/positioning":{"get":{"summary":"Get positioning match","description":"Returns positioning counts, resolved brand names, daily counts and share_of_statement. Share is the primary brand count divided by brand_counts plus another_product, excluding several and nobody. Below ten named-product assignments the share is null. Each statement also carries prior_status and, when that is comparable, the previous window’s totals, for a change against the window of the same length before it. prior_status is comparable when both windows measured the current competitor set and the previous one was scored on every day, platform and intent it had eligible answers for; otherwise it is competitor_changed or incomplete and prior is null. Competitor changes and the series start date identify the comparison period. Requires the `positioning.read` scope.","operationId":"getPositioning","x-devtune-required-scope":"positioning.read","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}},{"name":"window","in":"query","schema":{"type":"integer","enum":[30,90],"default":30}},{"name":"platform","in":"query","schema":{"type":"string","minLength":1,"maxLength":100}},{"name":"intent","in":"query","schema":{"type":"string","minLength":1,"maxLength":100}}],"responses":{"200":{"description":"Positioning","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/PositioningData"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Positioning"]}},"/projects/{projectId}/positioning/statements":{"get":{"summary":"List positioning statements","description":"Returns id, kind, statement, relevant_intents, status and created_at for active and archived positioning statements. Requires the `positioning.read` scope.","operationId":"listPositioningStatements","x-devtune-required-scope":"positioning.read","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Positioning","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/PositioningStatementListData"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Positioning"]}},"/projects/{projectId}/positioning/statements/{statementId}/answers":{"get":{"summary":"List positioning match answers","description":"Lists the AI answers behind one positioning match cell, newest first: accepted verdicts for the statement and assignee over the same window, filters and current competitor set as getPositioning, so the number of answers equals that cell. A statement that is a fact, proposed, dismissed or not in this project returns 404. Verdicts carry no quote; read the answer itself for the wording. Pass pagination.nextCursor as cursor while pagination.hasMore. A page of exactly 100 rows reports more even when it is the last, so the following page can be empty. Requires the `positioning.read` scope.","operationId":"listPositioningAnswers","x-devtune-required-scope":"positioning.read","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}},{"name":"statementId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"assignee","in":"query","required":true,"description":"Who the cell credits: a brand id, or another_product, several or nobody.","schema":{"type":"string"}},{"name":"window","in":"query","schema":{"type":"integer","enum":[30,90],"default":30}},{"name":"platform","in":"query","schema":{"type":"string","minLength":1,"maxLength":100}},{"name":"intent","in":"query","schema":{"type":"string","minLength":1,"maxLength":100}},{"name":"cursor","in":"query","description":"nextCursor from the previous page, verbatim.","schema":{"type":"string","maxLength":200}},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":25}},{"name":"pageSize","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":25},"description":"The same setting as limit. Send either; both with different values returns 400."}],"responses":{"200":{"description":"Positioning answers","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/PositioningAnswersData"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Positioning"]}},"/projects/{projectId}/accuracy":{"get":{"summary":"Get Accuracy","description":"Returns how the claims AI answers made about the product compare with its own site over a 30 or 90 day window: claim groups by outcome and the match rate, the previous window when it compares, the weekly trend, the per-platform split and check coverage. Each claim group has one outcome, the most severe any of its answers had: contradict, site_conflict, stale, not_on_site, match, then unjudged. A dismissed contradiction or site conflict counts as dismissed. Requires the `accuracy.read` scope.","operationId":"getAccuracy","x-devtune-required-scope":"accuracy.read","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}},{"name":"window","in":"query","description":"Fixed window in days: 30 or 90. Defaults to 30.","schema":{"type":"integer","enum":[30,90],"default":30}},{"name":"platform","in":"query","description":"Only answers from this AI platform, by its key, such as chatgpt-search.","schema":{"type":"string","minLength":1,"maxLength":100}},{"name":"intent","in":"query","description":"Only answers to prompts with this intent, such as pricing_commercial or comparison_evaluation.","schema":{"type":"string","minLength":1,"maxLength":100}}],"responses":{"200":{"description":"Accuracy","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AccuracyData"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Accuracy"]}},"/projects/{projectId}/accuracy/claims":{"get":{"summary":"List Accuracy claims","description":"Lists the claim groups with one outcome over the same window and filters as getAccuracy, most answers first, then most recently seen. Each item carries the latest answer with that outcome, the site page and any other page, and the team’s active decision. Requires the `accuracy.read` scope.","operationId":"listAccuracyClaims","x-devtune-required-scope":"accuracy.read","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}},{"name":"outcome","in":"query","required":true,"schema":{"type":"string","enum":["contradict","site_conflict","not_on_site","dismissed"]}},{"name":"window","in":"query","description":"Fixed window in days: 30 or 90. Defaults to 30.","schema":{"type":"integer","enum":[30,90],"default":30}},{"name":"platform","in":"query","description":"Only answers from this AI platform, by its key, such as chatgpt-search.","schema":{"type":"string","minLength":1,"maxLength":100}},{"name":"intent","in":"query","description":"Only answers to prompts with this intent, such as pricing_commercial or comparison_evaluation.","schema":{"type":"string","minLength":1,"maxLength":100}},{"name":"cursor","in":"query","description":"next_cursor from the previous page, verbatim.","schema":{"type":"string","maxLength":400}},{"name":"limit","in":"query","description":"Rows per page, 1 to 100. Defaults to 25.","schema":{"type":"integer","minimum":1,"maximum":100,"default":25}},{"name":"pageSize","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":25},"description":"The same setting as limit. Send either; both with different values returns 400."}],"responses":{"200":{"description":"Accuracy claims","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AccuracyClaimsData"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Accuracy"]}},"/projects/{projectId}/accuracy/claims/{groupId}":{"get":{"summary":"Get Accuracy claim","description":"Returns one claim group over the same window and filters as getAccuracy: its outcome, platforms and answers, the latest answer with its pages and the evidence it was judged against, up to 20 other answers, the brand’s own pages the answers cited, and the team’s active decision. A merged group opens as the group it was merged into. A group outside the project returns 400. Requires the `accuracy.read` scope.","operationId":"getAccuracyClaim","x-devtune-required-scope":"accuracy.read","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}},{"name":"groupId","in":"path","required":true,"description":"group_id from listAccuracyClaims.","schema":{"type":"string","format":"uuid"}},{"name":"window","in":"query","description":"Fixed window in days: 30 or 90. Defaults to 30.","schema":{"type":"integer","enum":[30,90],"default":30}},{"name":"platform","in":"query","description":"Only answers from this AI platform, by its key, such as chatgpt-search.","schema":{"type":"string","minLength":1,"maxLength":100}},{"name":"intent","in":"query","description":"Only answers to prompts with this intent, such as pricing_commercial or comparison_evaluation.","schema":{"type":"string","minLength":1,"maxLength":100}}],"responses":{"200":{"description":"Accuracy claim","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AccuracyClaimData"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Accuracy"]}},"/projects/{projectId}/accuracy/contradictions":{"get":{"summary":"List Accuracy contradictions","description":"Lists contradictions once per site sentence they conflict with, over the same window and filters as getAccuracy, most answers first, then most recently seen: the rows of the Accuracy tab’s contradictions table. Each row carries the sentence and its page, how many AI wordings (claim groups) contradict it and how many the team dismissed, their answers and platforms, an example wording and the row’s open action. A row whose wordings were all dismissed is not listed. Requires the `accuracy.read` scope.","operationId":"listAccuracyContradictions","x-devtune-required-scope":"accuracy.read","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}},{"name":"window","in":"query","description":"Fixed window in days: 30 or 90. Defaults to 30.","schema":{"type":"integer","enum":[30,90],"default":30}},{"name":"platform","in":"query","description":"Only answers from this AI platform, by its key, such as chatgpt-search.","schema":{"type":"string","minLength":1,"maxLength":100}},{"name":"intent","in":"query","description":"Only answers to prompts with this intent, such as pricing_commercial or comparison_evaluation.","schema":{"type":"string","minLength":1,"maxLength":100}},{"name":"cursor","in":"query","description":"next_cursor from the previous page, verbatim.","schema":{"type":"string","maxLength":400}},{"name":"limit","in":"query","description":"Rows per page, 1 to 100. Defaults to 25.","schema":{"type":"integer","minimum":1,"maximum":100,"default":25}},{"name":"pageSize","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":25},"description":"The same setting as limit. Send either; both with different values returns 400."}],"responses":{"200":{"description":"Accuracy contradictions","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AccuracySentencesData"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Accuracy"]}},"/projects/{projectId}/accuracy/contradictions/{sentenceKey}":{"get":{"summary":"Get Accuracy contradiction","description":"Returns one contradiction row over the same window and filters as getAccuracy: the site sentence and its page, the row’s counts, platforms and open action, and every AI wording that contradicts it, active first, with its latest contradicting answer and the decision that dismissed it. A row with no wording in the window returns no wordings. A malformed key returns 400. Requires the `accuracy.read` scope.","operationId":"getAccuracyContradiction","x-devtune-required-scope":"accuracy.read","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}},{"name":"sentenceKey","in":"path","required":true,"description":"sentence_key from listAccuracyContradictions, or from a contradiction in listAccuracyClaims or getAccuracyClaim.","schema":{"type":"string","pattern":"^[0-9a-f]{32}$"}},{"name":"window","in":"query","description":"Fixed window in days: 30 or 90. Defaults to 30.","schema":{"type":"integer","enum":[30,90],"default":30}},{"name":"platform","in":"query","description":"Only answers from this AI platform, by its key, such as chatgpt-search.","schema":{"type":"string","minLength":1,"maxLength":100}},{"name":"intent","in":"query","description":"Only answers to prompts with this intent, such as pricing_commercial or comparison_evaluation.","schema":{"type":"string","minLength":1,"maxLength":100}}],"responses":{"200":{"description":"Accuracy contradiction","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AccuracySentenceData"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Accuracy"]}},"/projects/{projectId}/accuracy/decisions":{"get":{"summary":"List Accuracy decisions","description":"Lists the decisions the team made on Accuracy claims, active and undone, newest first. Requires the `accuracy.read` scope.","operationId":"listAccuracyDecisions","x-devtune-required-scope":"accuracy.read","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}},{"name":"cursor","in":"query","description":"next_cursor from the previous page, verbatim.","schema":{"type":"string","maxLength":400}},{"name":"limit","in":"query","description":"Rows per page, 1 to 100. Defaults to 25.","schema":{"type":"integer","minimum":1,"maximum":100,"default":25}},{"name":"pageSize","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":25},"description":"The same setting as limit. Send either; both with different values returns 400."}],"responses":{"200":{"description":"Accuracy decisions","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AccuracyDecisionsData"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Accuracy"]}},"/projects/{projectId}/agent-picks/settings":{"patch":{"summary":"Update Agent Picks model settings","description":"Save ordered agent/model pairs for paid tests on an existing setup. Duplicate pairs, unpriced or mismatched models and empty selections are refused. Every Agent Picks plan includes every agent. The starter test keeps its fixed models. The member who created the API key needs project management permission and edit rights on the project; otherwise 403. This endpoint takes an API key, not an OAuth token; over MCP, an OAuth connection acts as its signed-in member. Requires Agent Picks access. Requires the `actions.write` scope.","operationId":"updateAgentPicksSettings","x-devtune-required-scope":"actions.write","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"enabled_agent_models":{"type":"array","minItems":1,"maxItems":7,"items":{"$ref":"#/components/schemas/AgentPicksAgentModel"}},"setup":{"type":"string","minLength":1,"maxLength":200,"description":"The Agent Picks setup to use: its id or its category slug. Optional while the project has one setup. With several, omitting it is refused with 400 and reason agent_picks_setup_required, and a setup the project does not have with 404 and reason agent_picks_setup_not_found; both list the project’s setups in details.setups (id, category_slug, category_name). The response echoes the setup used as setup."}},"required":["enabled_agent_models"]}}}},"responses":{"200":{"description":"Agent Picks model settings","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AgentPicksModelSettings"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Agent Picks"]}},"/projects/{projectId}/agent-picks/summary":{"get":{"summary":"Get Agent Picks summary","description":"Returns which tool coding agents picked when asked for one, with each pick rate’s install rate beside it, from the project’s latest completed non-directional own test, otherwise its latest completed directional own test, otherwise its category’s public test: the copy cloned into the project, else the latest public test (test.scope says which). Every rate averages each scenario’s picks over its completed runs, so each scenario counts once, with a Wilson 90% likely range over the scenarios (scenarios) and directional under 10 of them; outcome says whether a rate is a tool the agent installed, the agent building its own (built_its_own) or wiring in a hosted service without naming a provider (no_provider), and both of the last two have a null tool_key. Every scenario with completed runs is in every rate’s mean. Directional tests, including Quick, Custom subsets, cell re-runs and starter tests, are shown with directional=true only when no non-directional own result exists. They never replace full own test rates or feed loss Actions, re-test baselines or trends. Action re-tests are never shown. status is no_settings when no category or job is chosen and no_test when no test has finished. A project that tests its own job instead of a public category also returns job: its line, requests, building blocks and confirmed tools with their signals; a job has no public test. agent and intent narrow every number to the matching runs. test_id reads one of the project’s completed tests instead of the shown test, through the setup that now holds the category the test recorded, and setup echoes that setup; a removed setup’s test is read through the category it recorded, with the roles it recorded, and setup is null. A re-test, an unfinished test or another project’s test returns 404. Each rate also carries found, Found in over the same runs: how many reached the tool (searched for it, fetched its site or a package of it, or tried to install it), with runs that had no trail left out; it never feeds a pick rate, and it is null where the test has no Found in measurement. open_loss_actions counts the setup’s managed loss Actions still suggested or accepted, never self-run ones. Any other parameter is refused with 400. Needs the Agent Picks feature; without it the request is refused with 403. Requires the `visibility.read` scope.","operationId":"getAgentPicksSummary","x-devtune-required-scope":"visibility.read","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}},{"name":"agent","in":"query","schema":{"type":"string","enum":["claude_code","codex","cursor","pi","opencode","muse_code"]},"description":"Only runs by this coding agent."},{"name":"intent","in":"query","schema":{"type":"string","enum":["add","recommend","hosted","improve"]},"description":"Only runs of this question: add (build it), recommend (recommend one), hosted (hosted service) or improve (improve existing)."},{"name":"test_id","in":"query","required":false,"schema":{"type":"string","format":"uuid"},"description":"Read this completed test, from the history, instead of the shown one, through its own setup; a removed setup’s test is read too."},{"name":"setup","in":"query","required":false,"schema":{"type":"string","minLength":1,"maxLength":200},"description":"The Agent Picks setup to use: its id or its category slug. Optional while the project has one setup. With several, omitting it is refused with 400 and reason agent_picks_setup_required, and a setup the project does not have with 404 and reason agent_picks_setup_not_found; both list the project’s setups in details.setups (id, category_slug, category_name). The response echoes the setup used as setup."}],"responses":{"200":{"description":"Agent Picks summary","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AgentPicksSummaryData"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Agent Picks"]}},"/projects/{projectId}/agent-picks/runs":{"get":{"summary":"List Agent Picks runs","operationId":"listAgentPicksRuns","description":"List the setup’s run history, newest first by created_at then id, or the runs of the test test_id names, whichever setup it belongs to. Includes starter and paid runs. Public tests are not included. This lists runs, not rates; use the summary for rates. Requires Agent Picks. limit defaults to 50, maximum 100. Pass the returned next_cursor verbatim as cursor with the same filters until has_more is false. Detail pagination is inside evidence; list pagination is at the top level. Invalid parameters or a cursor for another scope return 400. Reads cannot start, cancel or change a run. Requires the `visibility.read` scope.","x-devtune-required-scope":"visibility.read","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}},{"name":"test_id","in":"query","required":false,"schema":{"type":"string","format":"uuid"},"description":"Narrow to an owned test."},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":50}},{"name":"pageSize","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":50},"description":"The same setting as limit. Send either; both with different values returns 400."},{"name":"cursor","in":"query","schema":{"type":"string","maxLength":2000}},{"name":"setup","in":"query","required":false,"schema":{"type":"string","minLength":1,"maxLength":200},"description":"The Agent Picks setup to use: its id or its category slug. Optional while the project has one setup. With several, omitting it is refused with 400 and reason agent_picks_setup_required, and a setup the project does not have with 404 and reason agent_picks_setup_not_found; both list the project’s setups in details.setups (id, category_slug, category_name). The response echoes the setup used as setup."}],"responses":{"200":{"description":"Agent Picks operational evidence","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AgentPicksRunList"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Agent Picks"]}},"/projects/{projectId}/agent-picks/tests/{testId}/runs/{runId}":{"get":{"summary":"Get Agent Picks run evidence","operationId":"getAgentPicksRun","description":"Inspect a run owned by the account, project and test together. Includes queued, running, failed, cancelled and completed runs. Request and job text are withheld; request_withheld and job_withheld distinguish withheld snapshots from absent ones; pick is null without a classification. Events are recorded observations ordered by seq then id, not inferred causes or proof of a working integration. not_retained means there are no retained events, including before a running run records them. Raw commands, error text, provider prompts and artifacts are withheld. URLs show only their HTTP origin. Search terms, file paths and unknown runtime tool names are withheld with explicit indicators. Packages must match the configured tool catalog. Requires Agent Picks. limit defaults to 50, maximum 100. Pass the returned next_cursor verbatim as cursor with the same filters until has_more is false. Detail pagination is inside evidence; list pagination is at the top level. Invalid parameters or a cursor for another scope return 400. Reads cannot start, cancel or change a run. Requires the `visibility.read` scope.","x-devtune-required-scope":"visibility.read","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}},{"name":"testId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"runId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":50}},{"name":"pageSize","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":50},"description":"The same setting as limit. Send either; both with different values returns 400."},{"name":"cursor","in":"query","schema":{"type":"string","maxLength":2000}}],"responses":{"200":{"description":"Agent Picks operational evidence","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AgentPicksRunDetail"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Agent Picks"]}},"/projects/{projectId}/agent-picks/runs/{runId}/trace":{"get":{"summary":"Get Agent Picks run trace","operationId":"getAgentPicksRunTrace","description":"Read one page of a project-owned run's console trace: redacted prompt, setup, narration, tool calls and results, file edits and errors, ordered by attempt then seq. An attempt's lines are available once that attempt finishes, so a running attempt has none yet; an attempt whose transcript could not be read has one error line, Trace unavailable for this attempt. availability says whether the run has stored lines, has none yet (pending: it is still running or finished within the last hour), or has none and none is expected (not_retained: it finished over an hour ago without stored lines, as older runs did), and attempts marks each attempt stored or not. When present, attempts.recovery is pending while trace recovery continues, stored after it completes, or unavailable after it gives up. Recovery can continue beyond the initial hour. A model_call item follows the first line of its attempt carrying its provider response id, else one of its tool-call ids, with its token counts. Calls that match no line are in unplaced, 200 at a time in finish order: the first page (cursor 0, 0) carries the first 200 with unplaced_total, and passing unplaced_pagination.after_id as after_unplaced returns the next 200; other pages carry none. Each has a reason: failed, no_ids, pending while the run is running, or unmatched. Codex reports no ids, so its calls are never placed. totals counts every model call the run made. Each line carries preview, its first 65,536 characters, with preview_truncated when the stored text is longer; full=true with limit=1 returns one line's whole stored text (up to 1 MiB, detail.truncated beyond). Pass pagination.after_attempt and after_seq back until has_more is false. limit defaults to 50, maximum 100; a page also stops once 256 KiB of lines are included and always holds at least one line. Requires Agent Picks. Requires the `visibility.read` scope.","x-devtune-required-scope":"visibility.read","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}},{"name":"runId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"after_attempt","in":"query","schema":{"type":"integer","minimum":0,"maximum":2147483647,"default":0}},{"name":"after_seq","in":"query","schema":{"type":"integer","minimum":0,"maximum":2147483647,"default":0}},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":50}},{"name":"full","in":"query","description":"Adds each line's whole stored text as text. Only with limit=1: pass the line's attempt and its seq minus one as the cursor.","schema":{"type":"boolean","default":false}},{"name":"after_unplaced","in":"query","description":"The unplaced_pagination.after_id of the previous page; returns the next 200 unplaced calls.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Agent Picks run trace","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AgentPicksRunTrace"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Agent Picks"]}},"/projects/{projectId}/agent-picks/tests/quote":{"post":{"summary":"Quote an Agent Picks test","operationId":"quoteAgentPicksTest","description":"Quote a paid test, an Action re-test or one run of a schedule in credits, without spending anything. Pass test options (preset quick or cell, runs_per_cell, scope) for a test; action_id alone for a re-test of a completed or live managed Action (only those; at most 40 runs; a self-run Action is refused as baseline_unavailable); or schedule alone (daily, weekly or monthly) for a schedule. Returns the estimate in credits, the agents the plan leaves out, a quote_token signed for this API key and project, and expires_at, 15 minutes later. A test quote also returns test_id, the id the start reserves under. A quote token works once: start another test by quoting again. Refused with 409 test_in_progress while another test is queued or running, and with 400 for a setup that cannot quote, with reason no_settings, job_incomplete, names_tool, rule_matches_starter, building_block_tool, invalid_pattern, runner_disabled, customer_requests_off, action_not_shipped, baseline_unavailable, baseline_changed, invalid_test_options, scope_not_enabled, invalid_quick_shape, empty_test_scope, invalid_cell_shape or unpriced_model; 403 agent_not_in_plan when the plan includes none of the saved agents; 403 hosted_tests_not_in_plan when the account’s Agent Picks plan runs tests on its own machines (Local Runner); hosted tests, re-tests and schedules need Agent Picks Cloud. Requires Agent Picks. A body that is not valid JSON, or an empty body, is refused with 400 bad_request and no reason. Requires the `visibility.read` scope.","x-devtune-required-scope":"visibility.read","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"action_id":{"type":"string","format":"uuid","description":"Quote a re-test of this completed or live Action. Send it alone."},"schedule":{"type":"string","enum":["daily","weekly","monthly"],"description":"Quote one run of a schedule at this cadence. Send it alone."},"preset":{"type":"string","enum":["quick","cell"],"description":"quick for a Quick test, cell for a re-run of one cell. Omit it for a full test."},"runs_per_cell":{"type":"integer","enum":[1,3,8],"description":"Runs in each cell. A test with fewer than 3 is directional. At 8 without a preset (8 is the default), a cell stops after 4 runs that agree; unneeded runs are not charged."},"scope":{"type":"object","additionalProperties":false,"properties":{"environment_ids":{"type":"array","items":{"type":"string"}},"request_ids":{"type":"array","items":{"type":"string"}},"intents":{"type":"array","items":{"type":"string","enum":["add","recommend","hosted","improve"]}},"agent_models":{"type":"array","minItems":1,"maxItems":7,"items":{"$ref":"#/components/schemas/AgentPicksAgentModel"}}}},"setup":{"type":"string","minLength":1,"maxLength":200,"description":"The Agent Picks setup to use: its id or its category slug. Optional while the project has one setup. With several, omitting it is refused with 400 and reason agent_picks_setup_required, and a setup the project does not have with 404 and reason agent_picks_setup_not_found; both list the project’s setups in details.setups (id, category_slug, category_name). The response echoes the setup used as setup."}},"required":[]}}}},"responses":{"200":{"description":"Agent Picks quote","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AgentPicksQuoteData"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"409":{"$ref":"#/components/responses/ConflictError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Agent Picks"]}},"/projects/{projectId}/agent-picks/tests":{"post":{"summary":"Start an Agent Picks test","operationId":"startAgentPicksTest","description":"Start a quoted paid test or re-test. Pass quote_token from the quote and max_credits, the most this test may reserve, in whole credits. The start re-quotes and reserves the fresh high estimate; settlement never charges more, and unused credits return when the test ends. A retry with the same quote token returns status existing and reserves nothing more. Refusals carry a reason: 400 quote_token_invalid (malformed, or quoted for another credential, project or purpose), quote_token_expired, quote_token_setup_mismatch (quoted for another setup than the one this start uses; pass the setup it was quoted for), unpriced_model (a saved model has lost its price since the quote; choose models again), and the quote refusals; 402 insufficient_credits with details.shortfall_credits; 403 forbidden, paused (DevTune support has paused the account), agent_not_in_plan or hosted_tests_not_in_plan; 409 estimate_changed (the setup or baseline changed since the quote; quote again), over_max_credits, test_in_progress (another test is active), test_finished (this quote already ran to an end), test_exists or project_deleting. Each start shares the agent-run rate limit. The member who created the API key needs project management permission and edit rights on the project; otherwise 403. This endpoint takes an API key, not an OAuth token; over MCP, an OAuth connection acts as its signed-in member. Requires Agent Picks. Not available in MCP code mode. A body that is not valid JSON, or an empty body, is refused with 400 bad_request and no reason. Requires the `agent_picks.run` scope.","x-devtune-required-scope":"agent_picks.run","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"quote_token":{"type":"string","maxLength":4096,"description":"The token from the quote. It works once and expires after 15 minutes."},"max_credits":{"type":"integer","minimum":1,"description":"The most credits a person approved one test, or one scheduled run, to reserve. A higher fresh estimate is refused with over_max_credits."},"setup":{"type":"string","minLength":1,"maxLength":200,"description":"The Agent Picks setup to use: its id or its category slug. Optional while the project has one setup. With several, omitting it is refused with 400 and reason agent_picks_setup_required, and a setup the project does not have with 404 and reason agent_picks_setup_not_found; both list the project’s setups in details.setups (id, category_slug, category_name). The response echoes the setup used as setup."}},"required":["quote_token","max_credits"]}}}},"responses":{"200":{"description":"Started test","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AgentPicksStartedTestData"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"402":{"description":"Not enough credits; details.shortfall_credits says how many more.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"409":{"$ref":"#/components/responses/ConflictError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Agent Picks"]}},"/projects/{projectId}/agent-picks/tests/{testId}":{"delete":{"summary":"Cancel an Agent Picks test","operationId":"cancelAgentPicksTest","description":"Cancel a queued or running paid test or re-test. Queued runs are cancelled at once; running runs keep their reservation until they settle, so credits may return later: refunded_micros and released_at on the test read show when. A repeat returns status cancelled again. Starter tests cannot be cancelled and return 404 not_found, as does another project’s test; an ended test is 409 test_finished. Works while the account is paused. The member who created the API key needs project management permission and edit rights on the project; otherwise 403. This endpoint takes an API key, not an OAuth token; over MCP, an OAuth connection acts as its signed-in member. Requires Agent Picks. Requires the `actions.write` scope.","x-devtune-required-scope":"actions.write","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}},{"name":"testId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Cancelled test","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AgentPicksCancelledTestData"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"409":{"$ref":"#/components/responses/ConflictError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Agent Picks"]}},"/projects/{projectId}/agent-picks/uploads":{"post":{"summary":"Create an Agent Picks self-run upload","operationId":"createAgentPicksUpload","description":"Open an upload for a bundle the agentpicks CLI wrote on the customer's own machine. The body is the bundle's manifest.json, which declares each artifact's name, bytes and sha256. The response gives one signed Storage address per declared artifact: PUT the artifact's bytes to its url within two hours, then complete the upload. A url can't replace an artifact already stored: Storage refuses the PUT, and a retry returns a null url for each artifact already stored, so it PUTs only the rest. The upload expires 24 hours after it is created unless completed. A retry with the same runId returns the same paths with fresh addresses and restarts the 24 hours; a retry for a completed upload returns it with no addresses. Refusals carry a reason: 400 invalid_manifest (the bundle format rejects it), repository_remote_missing (a repository run whose repository has no remote URL; add an origin remote and run it again), duplicate_artifact, artifact_too_large (diff.patch over 800,000 bytes, source-files.json over 1,048,576, any other artifact over 50 MiB; details name the artifact, its bytes and the limit), unknown_category, unknown_request, prompt_mismatch (the prompt isn't exactly what the agentpicks CLI composes for its stack, or none for a repository, and one of the category's active requests of its intent, then the default request ending, the category's own ending or none) or unknown_stack; 403 plan_excludes_uploads (the account's Agent Picks plan is not Local Runner, Cloud or Enterprise), project_uploads_off (on Local Runner, uploads are off for this project; a member who manages billing and is on the project turns them on in the project's Agent Picks settings), analysis_cap_reached (the project has recorded as many self-runs this UTC calendar month as its plan analyses: 500 on Local Runner, no cap on Cloud or Enterprise; deleting a run doesn't give one back; details name the limit and resets_at, the start of the next UTC month) or paused; 409 run_exists (another project or a hosted test uses this run id), upload_expired (the upload expired and its artifacts are still being removed; create it again once a cleanup sweep, which runs every 15 minutes, has removed its artifacts, no sooner than 2 hours 15 minutes after it was last created or retried) or upload_deleted (the upload was deleted and its artifacts are still being removed; create it again once a cleanup sweep, which runs every 15 minutes, has removed its artifacts, no sooner than 2 hours 15 minutes after it was last created or retried). The member who created the API key, or for an OAuth token the signed-in member, needs project management permission and edit rights on the project; otherwise, or without Agent Picks, 403 forbidden with no reason. The MCP tool devtune_create_agent_picks_upload returns the same fields; devtune_get_agent_picks_upload reads an upload's status. Requires Agent Picks. A body that is not valid JSON, or an empty body, is refused with 400 bad_request and no reason. Requires the `agent_picks.run` scope.","security":[{"bearerAuth":[]},{"oauthBearer":[]}],"x-devtune-required-scope":"agent_picks.run","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentPicksBundleManifest"}}}},"responses":{"200":{"description":"Opened upload","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AgentPicksCreatedUploadData"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"409":{"$ref":"#/components/responses/ConflictError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Agent Picks"]}},"/projects/{projectId}/agent-picks/uploads/{runId}":{"get":{"summary":"Get an Agent Picks self-run upload","operationId":"getAgentPicksUpload","description":"Read one self-run upload: open, completed or expired, and once completed the recorded run with its status and its analysis: pending, analysed, or failed with why. An open upload past its 24 hours reads as expired, as does one that held an artifact of the wrong size. A deleted upload, or another project's, is 404 not_found. Requires Agent Picks. The MCP tool devtune_get_agent_picks_upload returns the same fields. Requires the `visibility.read` scope.","security":[{"bearerAuth":[]},{"oauthBearer":[]}],"x-devtune-required-scope":"visibility.read","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}},{"name":"runId","in":"path","required":true,"description":"The run id in the bundle manifest.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Upload","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AgentPicksUploadData"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Agent Picks"]},"delete":{"summary":"Delete an Agent Picks self-run upload","operationId":"deleteAgentPicksUpload","description":"Delete an upload and its artifacts. Its artifacts are removed by a cleanup sweep that runs every 15 minutes, no sooner than 2 hours 15 minutes after the upload was last created or retried, when its addresses have lapsed; until then the run id can't be created again. For a completed upload this also deletes its recorded run, and the self-run test's results are recomputed without it. No upload, a deleted one or another project's is 404 not_found. Works while the account is paused. The member who created the API key, or for an OAuth token the signed-in member, needs project management permission and edit rights on the project; otherwise 403. The MCP tool devtune_delete_agent_picks_upload does the same. Requires Agent Picks. Requires the `actions.write` scope.","security":[{"bearerAuth":[]},{"oauthBearer":[]}],"x-devtune-required-scope":"actions.write","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}},{"name":"runId","in":"path","required":true,"description":"The run id in the bundle manifest.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Deleted upload","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AgentPicksDeletedUploadData"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Agent Picks"]}},"/projects/{projectId}/agent-picks/uploads/{runId}/complete":{"post":{"summary":"Complete an Agent Picks self-run upload","operationId":"completeAgentPicksUpload","description":"Complete an open upload once every artifact its manifest declares is stored at its declared bytes. The run is recorded as a self-run, completed and awaiting analysis, in its series' self-run test for the month it started, and its analysis is requested; run.analysis reads analysed once it has an outcome. A retry returns the completed upload without recording the run again. Refusals carry a reason: 404 not_found (no upload, a deleted one or another project's); 409 artifact_missing (details name the artifact and its declared bytes; upload it, then complete) or artifact_size_mismatch (details name the artifact, its declared bytes and the stored bytes; the bundle changed or was damaged after its manifest was written, and since a stored artifact can't be replaced the upload expires: create it again once a cleanup sweep, which runs every 15 minutes, has removed its artifacts, no sooner than 2 hours 15 minutes after it was last created or retried, once its artifacts are removed, and upload every artifact), upload_expired (not completed within 24 hours of being created or last retried, or an artifact was stored at the wrong size; create it again once a cleanup sweep, which runs every 15 minutes, has removed its artifacts, no sooner than 2 hours 15 minutes after it was last created or retried, once its artifacts are removed), series_busy (retry); 403 plan_excludes_uploads, project_uploads_off (as for create), analysis_cap_reached (as for create; the upload stays open until its 24 hours end, so complete it again after resets_at or once the plan allows more), paused or unavailable (Agent Picks access ended while completing); 400 unknown_category, unknown_request, prompt_mismatch (as for create: the category no longer holds the request the run asked) or unknown_stack. The member who created the API key, or for an OAuth token the signed-in member, needs project management permission and edit rights on the project; otherwise, or without Agent Picks, 403 forbidden with no reason. The MCP tool devtune_complete_agent_picks_upload returns the same fields. Requires Agent Picks. Takes no body. Requires the `agent_picks.run` scope.","security":[{"bearerAuth":[]},{"oauthBearer":[]}],"x-devtune-required-scope":"agent_picks.run","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}},{"name":"runId","in":"path","required":true,"description":"The run id in the bundle manifest.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Completed upload","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AgentPicksUploadData"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"409":{"$ref":"#/components/responses/ConflictError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Agent Picks"]}},"/projects/{projectId}/agent-picks/schedule":{"get":{"summary":"Get Agent Picks schedule","operationId":"getAgentPicksSchedule","description":"Read cadence, next UTC run, the newest skipped period with its reason and recovery action, and the current paid-test estimate per run and for the next calendar month against included monthly-grant credits from active subscriptions only, zero while trialing. Quote failures return estimate null. last_skipped is null when off or when the newest period was not skipped. agent_not_in_plan means the Agent Picks plan of the account includes none of the saved agents, and hosted_tests_not_in_plan that it runs tests on its own machines (Local Runner); both have action view_plans, which links to billing. test_in_flight and unavailable skips have null action and action_href; nothing needs fixing. Requires Agent Picks. Takes only setup. Requires the `visibility.read` scope.","x-devtune-required-scope":"visibility.read","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}},{"name":"setup","in":"query","required":false,"schema":{"type":"string","minLength":1,"maxLength":200},"description":"The Agent Picks setup to use: its id or its category slug. Optional while the project has one setup. With several, omitting it is refused with 400 and reason agent_picks_setup_required, and a setup the project does not have with 404 and reason agent_picks_setup_not_found; both list the project’s setups in details.setups (id, category_slug, category_name). The response echoes the setup used as setup."}],"responses":{"200":{"description":"Schedule and credit estimates","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AgentPicksScheduleData"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Agent Picks"]},"put":{"summary":"Enable Agent Picks schedule","operationId":"enableAgentPicksSchedule","description":"Enable or change the cadence: daily at midnight UTC, weekly on Monday, or monthly on the first. Quote with schedule set to the same cadence first, then pass cadence, its quote_token and max_credits, the most one scheduled run may reserve. A quote_token works once: reusing it is refused as 409 quote_token_used, also after the schedule is turned off or the setup is cleared and recreated. Refusals carry a reason: 400 quote_token_invalid (another credential, project, purpose or cadence), quote_token_expired, quote_token_setup_mismatch (quoted for another setup), unpriced_model (a saved model has lost its price since the quote; choose models again), setup_required and the quote refusals; 403 forbidden, paused or hosted_tests_not_in_plan (the plan runs tests on its own machines; schedules need Agent Picks Cloud); 409 estimate_changed, over_max_credits or quote_token_used. The confirmed high estimate is saved. Each run reserves its fresh quote at or below it, or skips as estimate_changed until re-confirmed. Insufficient credits skip that period and email the owner. Never auto-charges. When enabled, periods more than one cadence interval overdue advance without a catch-up test. The member who created the API key needs project management permission and edit rights on the project; otherwise 403. This endpoint takes an API key, not an OAuth token; over MCP, an OAuth connection acts as its signed-in member. Requires Agent Picks. Not available in MCP code mode. A body that is not valid JSON, or an empty body, is refused with 400 bad_request and no reason. Requires the `agent_picks.run` scope.","x-devtune-required-scope":"agent_picks.run","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"cadence":{"type":"string","enum":["daily","weekly","monthly"]},"quote_token":{"type":"string","maxLength":4096,"description":"The token from the quote. It works once and expires after 15 minutes."},"max_credits":{"type":"integer","minimum":1,"description":"The most credits a person approved one test, or one scheduled run, to reserve. A higher fresh estimate is refused with over_max_credits."},"setup":{"type":"string","minLength":1,"maxLength":200,"description":"The Agent Picks setup to use: its id or its category slug. Optional while the project has one setup. With several, omitting it is refused with 400 and reason agent_picks_setup_required, and a setup the project does not have with 404 and reason agent_picks_setup_not_found; both list the project’s setups in details.setups (id, category_slug, category_name). The response echoes the setup used as setup."}},"required":["cadence","quote_token","max_credits"]}}}},"responses":{"200":{"description":"Saved schedule","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AgentPicksSavedScheduleData"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"409":{"$ref":"#/components/responses/ConflictError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Agent Picks"]},"delete":{"summary":"Turn off Agent Picks schedule","operationId":"disableAgentPicksSchedule","description":"Turn the schedule off. Future scheduled tests stop; a test already queued or running continues until cancelled. Works while the account is paused. The member who created the API key needs project management permission and edit rights on the project; otherwise 403. This endpoint takes an API key, not an OAuth token; over MCP, an OAuth connection acts as its signed-in member. Requires Agent Picks. Requires the `actions.write` scope.","x-devtune-required-scope":"actions.write","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}},{"name":"setup","in":"query","required":false,"schema":{"type":"string","minLength":1,"maxLength":200},"description":"The Agent Picks setup to use: its id or its category slug. Optional while the project has one setup. With several, omitting it is refused with 400 and reason agent_picks_setup_required, and a setup the project does not have with 404 and reason agent_picks_setup_not_found; both list the project’s setups in details.setups (id, category_slug, category_name). The response echoes the setup used as setup."}],"responses":{"200":{"description":"Saved schedule","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AgentPicksSavedScheduleData"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Agent Picks"]}},"/projects/{projectId}/agent-picks/cells":{"get":{"summary":"Get Agent Picks test cells","operationId":"getAgentPicksCells","description":"Read per-model cells for up to eight distinct project-owned tests from history, with coverage, shared loss verdicts, fallback and outcome counts, and per-tool rates and Wilson ranges. Each test includes status; non-completed tests retain coverage only, with all tools unmeasured and null measurement counts, rates, ranges and verdicts. Each test includes judging_key and an opaque comparison_key; compare tests only when their keys are equal and break history lines when keys change, and keep managed and self-run tests on separate lines, and each self-run test on the line of its series.key. Each test includes origin, managed or self_run, and a self-run test’s series; a self-run test’s key never equals a managed test’s or another series’. Each test includes outcome_counts_available; the three outcome run counts are null when completed run detail is unavailable. Each test and cell includes runs_without_trail, the completed runs that recorded no trail event (null where trail coverage cannot be determined, as for a cloned public test). Each cell includes discovery: of its completed runs, how many reached your tool (searched for, fetched or installed it) before choosing, with the rate and its Wilson range over runs with a trail to decide from; null when the test has no discovery measurement, never 0. Model-side web search and fetch bypass the sandbox, so not reached means not seen in the sandbox. Displayed coverage includes fallback guesses; loss decisions exclude them. Unmeasured tools have measured false and null rates. Each test also includes integration: per installed tool, and per agent when the test ran more than one, how often install runs had an observed package version, matched the pinned registry latest, used a deprecated version, requested vendor docs and ran the package’s commands, each with its numerator, denominator and unknown runs. Each tool also includes found, Found in for the cell: how many completed runs reached the tool (searched for it, fetched its site or a package of it, or tried to install it), with runs that had no trail left out; null for built_its_own, no_provider, a tool the test did not record, and a test with no Found in measurement. It never feeds a pick rate or verdict. Requires Agent Picks. Requires the `visibility.read` scope.","x-devtune-required-scope":"visibility.read","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}},{"name":"test_ids","in":"query","required":true,"description":"Comma-separated project-owned test UUIDs, limited to 1 to 8 distinct IDs. Duplicates are returned once, in first-requested order.","schema":{"type":"string","minLength":36,"maxLength":2048}}],"responses":{"200":{"description":"Per-model test cells","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AgentPicksCellsData"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Agent Picks"]}},"/projects/{projectId}/agent-picks/history":{"get":{"summary":"Get Agent Picks test history","operationId":"getAgentPicksHistory","description":"Read a page of the setup’s paid and self-run tests newest first (limit or pageSize, default and maximum 100), or the one test test_id names, whichever setup it belongs to. Rates for the current project tool use completed cell rollups and Wilson 90% likely ranges. Unfinished tests have no rates. Each test says how it was run (type), what it has charged (settled_micros), whether it can be a baseline under the latest judging (baseline_eligible), whether it belongs on the full-test trend (trend_eligible), its judging_key and judging_changed boundary, and how many of its loss Actions are open. Compare equal non-null judging keys only and break lines when judging changes or is unavailable; also keep managed and self-run tests on separate lines, and each self-run test on the line of its series.key, since two series can share a judging key. Pass pagination.nextCursor as cursor while pagination.hasMore. A page of exactly 100 rows reports more even when it is the last, so the following page can be empty. Requires Agent Picks. Requires the `visibility.read` scope.","x-devtune-required-scope":"visibility.read","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}},{"name":"cursor","in":"query","schema":{"type":"string","maxLength":1024},"description":"pagination.nextCursor from the previous page, unchanged. Keep the other parameters the same: a cursor sent with different filters, or beside a deprecated position parameter, returns 400."},{"name":"offset","in":"query","schema":{"type":"integer","minimum":0,"default":0},"deprecated":true,"description":"Deprecated: Pass cursor instead."},{"name":"limit","in":"query","schema":{"type":"integer","default":100,"minimum":1,"maximum":100},"description":"Results per page. Capped at 100."},{"name":"pageSize","in":"query","schema":{"type":"integer","default":100,"minimum":1,"maximum":100},"description":"The same setting as limit. Send either; both with different values returns 400."},{"name":"test_id","in":"query","required":false,"schema":{"type":"string","format":"uuid"},"description":"Read only this paid test. An id that is not one of the project’s paid tests returns no tests; 404 while this read is unavailable."},{"name":"setup","in":"query","required":false,"schema":{"type":"string","minLength":1,"maxLength":200},"description":"The Agent Picks setup to use: its id or its category slug. Optional while the project has one setup. With several, omitting it is refused with 400 and reason agent_picks_setup_required, and a setup the project does not have with 404 and reason agent_picks_setup_not_found; both list the project’s setups in details.setups (id, category_slug, category_name). The response echoes the setup used as setup."}],"responses":{"200":{"description":"Paid-test history","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AgentPicksHistoryData"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Agent Picks"]}},"/projects/{projectId}/agent-picks/actions":{"get":{"summary":"Get Agent Picks Actions and re-test results","operationId":"getAgentPicksActions","description":"Read the setup’s loss Actions, or the one actionId names whichever setup it belongs to, with creation evidence, baseline cell pick rates and Wilson 90% ranges, latest paid re-test status, credits and measured Outcomes results, whose ranges are at the confidence their method decided the verdict at: 95% for method v1, 90% for v2. With actions.read, an Action with a current drafted fix carries agent_prompt, the fix as a prompt for a coding agent. Reads a page of up to 100 Actions (limit or pageSize, default 100); pass pagination.nextCursor as cursor while pagination.hasMore. A page of exactly 100 rows reports more even when it is the last, so the following page can be empty. Incomplete coverage is explicit. Rates come from completed test rollups and stored measurements. Requires Agent Picks. Requires the `visibility.read` scope.","x-devtune-required-scope":"visibility.read","parameters":[{"name":"actionId","in":"query","schema":{"type":"string","format":"uuid"},"description":"Read one Action and its re-test result."},{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}},{"name":"cursor","in":"query","schema":{"type":"string","maxLength":1024},"description":"pagination.nextCursor from the previous page, unchanged. Keep the other parameters the same: a cursor sent with different filters, or beside a deprecated position parameter, returns 400."},{"name":"offset","in":"query","schema":{"type":"integer","minimum":0,"default":0},"deprecated":true,"description":"Deprecated: Pass cursor instead."},{"name":"limit","in":"query","schema":{"type":"integer","default":100,"minimum":1,"maximum":100},"description":"Results per page. Capped at 100."},{"name":"pageSize","in":"query","schema":{"type":"integer","default":100,"minimum":1,"maximum":100},"description":"The same setting as limit. Send either; both with different values returns 400."},{"name":"setup","in":"query","required":false,"schema":{"type":"string","minLength":1,"maxLength":200},"description":"The Agent Picks setup to use: its id or its category slug. Optional while the project has one setup. With several, omitting it is refused with 400 and reason agent_picks_setup_required, and a setup the project does not have with 404 and reason agent_picks_setup_not_found; both list the project’s setups in details.setups (id, category_slug, category_name). The response echoes the setup used as setup."}],"responses":{"200":{"description":"Agent Picks Actions and results","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AgentPicksActionsData"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Agent Picks"]}},"/projects/{projectId}/agent-picks/test":{"get":{"summary":"Get paid Agent Picks test","operationId":"getAgentPicksPaidTest","description":"Read the latest paid project test, or the one test_id names, its estimate in credits, reservation, settlement and refund in credit micros, and every run in it. Failed runs and runs cancelled before they start are not charged; a run cancelled after it starts pays its recorded spend. Unknown spend on a completed run or a run cancelled after it starts charges the greater of recorded spend and the run reservation share, capped by the remaining reservation. One credit is 1000000 micros. Use the summary for rates from the latest completed non-directional own test, else the latest completed directional own test, else public results. test_id reads that paid test or re-test instead, so a caller can follow one test through cancellation and settlement after another starts; one that is not the project’s paid test returns 404. Any other parameter is refused with 400. Start tests with the start endpoint. Requires the Agent Picks feature. Requires the `visibility.read` scope.","x-devtune-required-scope":"visibility.read","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}},{"name":"test_id","in":"query","required":false,"schema":{"type":"string","format":"uuid"},"description":"Read this paid test or re-test, from a quote or start, instead of the latest."},{"name":"setup","in":"query","required":false,"schema":{"type":"string","minLength":1,"maxLength":200},"description":"The Agent Picks setup to use: its id or its category slug. Optional while the project has one setup. With several, omitting it is refused with 400 and reason agent_picks_setup_required, and a setup the project does not have with 404 and reason agent_picks_setup_not_found; both list the project’s setups in details.setups (id, category_slug, category_name). The response echoes the setup used as setup."}],"responses":{"200":{"description":"Paid Agent Picks test","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AgentPicksPaidTestData"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Agent Picks"]}},"/projects/{projectId}/agent-picks/starter-test":{"get":{"summary":"Get Agent Picks starter test","description":"Returns the project’s latest Agent Picks starter test as it runs: its status (queued, running, completed, failed or cancelled), when it was created and completed, planned vs completed runs, and each run’s request, agent and model, status, recorded sandbox attempts and, once classified, its pick (a tool_key, or built_its_own and the other outcomes). A failed or cancelled test keeps its runs. A project with no starter test returns test null and empty runs. These are per-run results, not rates; the summary shows the latest completed non-directional own test first, otherwise the latest completed directional own test, including a starter. A starter never displaces a full own result or feeds loss Actions, baselines or trends. Any parameter is refused with 400. Needs the Agent Picks feature; without it the request is refused with 403. Requires the `visibility.read` scope.","operationId":"getAgentPicksStarterTest","x-devtune-required-scope":"visibility.read","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Agent Picks starter test","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AgentPicksStarterTestData"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Agent Picks"]},"post":{"summary":"Start Agent Picks starter test","operationId":"startAgentPicksStarterTest","description":"Start the project’s free starter test on its saved setup. Each confirmed account-owner work domain gets one; queueing uses it up, and a failed or cancelled starter test does not give it back. Starter tests cannot be cancelled. Repository inputs are not supported; the body is empty or names the setup. Refusals carry a reason: 400 no_settings, category_unpublished, job_incomplete, customer_requests_off, runner_disabled, unverified_domain or not_work_domain; 403 forbidden, paused or hosted_tests_not_in_plan (the plan runs tests on its own machines; hosted tests need Agent Picks Cloud; the starter test is not used up); 409 already_used. Shares the agent-run rate limit. The member who created the API key needs project management permission and edit rights on the project; otherwise 403. This endpoint takes an API key, not an OAuth token; over MCP, an OAuth connection acts as its signed-in member. Requires Agent Picks. An empty body is accepted; one that is not valid JSON is refused with 400 bad_request and no reason. Requires the `actions.write` scope.","x-devtune-required-scope":"actions.write","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"setup":{"type":"string","minLength":1,"maxLength":200,"description":"The Agent Picks setup to use: its id or its category slug. Optional while the project has one setup. With several, omitting it is refused with 400 and reason agent_picks_setup_required, and a setup the project does not have with 404 and reason agent_picks_setup_not_found; both list the project’s setups in details.setups (id, category_slug, category_name). The response echoes the setup used as setup."}}}}}},"responses":{"200":{"description":"Started starter test","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AgentPicksStarterTestStartedData"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"409":{"$ref":"#/components/responses/ConflictError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Agent Picks"]}},"/projects/{projectId}/agent-picks/setup":{"get":{"summary":"List Agent Picks setups","operationId":"listAgentPicksSetups","description":"List the project’s Agent Picks setups with what each tests, oldest first, and the catalogue ids a save names. kind is job (the project’s own job), copy (its copy of a public category) or category (a setup saved on a public category itself). snapshot_digest is what saveAgentPicksJob and saveAgentPicksSetup take as snapshot_digest, so a setup changed since it was read is refused with 409 setup_changed. A category setup’s tool signals and building blocks are DevTune’s and come back null. The agentpicks CLI reads this to list, pull, push and run setups. The API key, or an OAuth token for the project, needs visibility.read. Requires Agent Picks. Requires the `visibility.read` scope.","x-devtune-required-scope":"visibility.read","security":[{"bearerAuth":[]},{"oauthBearer":[]}],"parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The project’s setups and the catalogue","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AgentPicksSetupListData"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Agent Picks"]},"put":{"summary":"Save Agent Picks setup","operationId":"saveAgentPicksSetup","description":"Save a setup for a published public category. A project has one setup per category and can add as many as it needs. Without setup, this updates the project’s setup for category_id, or adds one when the project has none for it: the project’s own copy of DevTune’s setup for the category, with its requests and how it recognizes each tool. With setup, it updates that setup, whose category is fixed: saving it with a category another of the project’s setups uses is refused with 409 category_in_use, and with any other category_id with 409 category_fixed. The body: category_id, the project’s own tool_key (or null), up to six competitor_tool_keys from the category’s tools (DevTune’s own public boards are exempt), not including tool_key, environment_ids and the ordered agent_models to test. An id outside the published catalogue is refused with 400 invalid_setup; getAgentPicksCatalogue lists the valid ids. A copy or custom job setup may also change its content, each part unchanged when absent: requests by intent (the text, or \"omitted\" to leave a request out; saving a left-out request’s text asks it again; at least one request stays active, and improve is taken only by a setup that has had one), request_ending (empty sends requests as written) and recognition (signals per tool the setup already measures, and building_blocks). A public board’s setup takes none of them: 400 invalid_setup. A request or the ending that names a measured tool by name, alias or domain is refused with 400 names_tool; recognition a selected test app’s starter files already match, so every run there would count as a pick, with 400 rule_matches_starter; a building-block package that a tool’s package also matches, exact names and prefixes alike, with 400 building_block_tool; any pattern the setup keeps, copied or written, that RE2 can’t compile or that runs long with 400 invalid_pattern. A save checks these on the setup as it read it: when another save changed it first, the save is refused with 409 setup_changed and writes nothing; read the setup again and save. While tests of customer-written requests are switched off, a save that changes the requests or ending is refused with 403 customer_requests_off. A save that sends the content as it stands changes nothing, so a copy saved unchanged stays unedited. For a custom job, save the job instead. Starts no test. The member who created the API key, or for an OAuth token the signed-in member, needs project management permission and edit rights on the project; otherwise 403. Requires Agent Picks. A body that is not valid JSON, or an empty body, is refused with 400 bad_request and no reason. Requires the `actions.write` scope.","security":[{"bearerAuth":[]},{"oauthBearer":[]}],"x-devtune-required-scope":"actions.write","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"category_id":{"type":"string","format":"uuid"},"tool_key":{"type":["string","null"],"maxLength":63},"competitor_tool_keys":{"type":"array","maxItems":200,"description":"Up to six of the category’s tools; DevTune’s own public boards are exempt.","items":{"type":"string","maxLength":63}},"environment_ids":{"type":"array","minItems":1,"maxItems":100,"items":{"type":"string","format":"uuid"}},"agent_models":{"type":"array","minItems":1,"maxItems":7,"items":{"$ref":"#/components/schemas/AgentPicksAgentModel"}},"requests":{"type":"object","additionalProperties":false,"description":"Requests by intent: the text, or \"omitted\" to leave it out, as long as one request stays active. An intent not named is unchanged.","properties":{"add":{"oneOf":[{"type":"string","enum":["omitted"]},{"type":"string","minLength":8,"maxLength":300}]},"recommend":{"oneOf":[{"type":"string","enum":["omitted"]},{"type":"string","minLength":8,"maxLength":300}]},"hosted":{"oneOf":[{"type":"string","enum":["omitted"]},{"type":"string","minLength":8,"maxLength":300}]},"improve":{"oneOf":[{"type":"string","enum":["omitted"]},{"type":"string","minLength":8,"maxLength":300}]}}},"request_ending":{"type":"string","description":"The text appended to each request, at most 300 characters once trimmed, stored after one space. Empty sends requests as written. The value a save returns can be sent back as it is."},"recognition":{"type":"object","additionalProperties":false,"description":"How the setup recognises its tools. Each part not named is unchanged, and so is a tool not listed.","properties":{"tools":{"type":"array","maxItems":35,"items":{"type":"object","additionalProperties":false,"properties":{"tool_key":{"type":"string","maxLength":63},"signals":{"type":"object","additionalProperties":true,"description":"The tool’s signals, replaced whole, in the shape saveAgentPicksJob takes."}},"required":["tool_key","signals"]}},"building_blocks":{"type":"object","additionalProperties":true}}},"snapshot_digest":{"type":["string","null"],"pattern":"^[a-f0-9]{64}$","description":"The setup’s snapshot_digest from listAgentPicksSetups when it was last read, or null when none was read. A save that finds the setup changed since, gone, or there when null was sent is refused with 409 setup_changed: read it again and save. Omit it to save without the check."},"setup":{"type":"string","minLength":1,"maxLength":200,"description":"The Agent Picks setup to use: its id or its category slug. Optional while the project has one setup. With several, omitting it is refused with 400 and reason agent_picks_setup_required, and a setup the project does not have with 404 and reason agent_picks_setup_not_found; both list the project’s setups in details.setups (id, category_slug, category_name). The response echoes the setup used as setup."}},"required":["category_id","tool_key","environment_ids","agent_models"]}}}},"responses":{"200":{"description":"Saved setup","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AgentPicksSetupData"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"409":{"$ref":"#/components/responses/ConflictError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Agent Picks"]},"delete":{"summary":"Clear Agent Picks setup","operationId":"clearAgentPicksSetup","description":"Remove a setup. Its tests and results stay readable, its schedule stops, and its suggested Actions that nobody has accepted are removed. With several setups, pass setup; without it the call is refused with 400 agent_picks_setup_required. Removing the last setup leaves the project with none until one is saved again. Removing a setup that is already gone is a no-op. The member who created the API key needs project management permission and edit rights on the project; otherwise 403. This endpoint takes an API key, not an OAuth token; over MCP, an OAuth connection acts as its signed-in member. Requires Agent Picks. Requires the `actions.write` scope.","x-devtune-required-scope":"actions.write","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}},{"name":"setup","in":"query","required":false,"schema":{"type":"string","minLength":1,"maxLength":200},"description":"The Agent Picks setup to use: its id or its category slug. Optional while the project has one setup. With several, omitting it is refused with 400 and reason agent_picks_setup_required, and a setup the project does not have with 404 and reason agent_picks_setup_not_found; both list the project’s setups in details.setups (id, category_slug, category_name). The response echoes the setup used as setup."}],"responses":{"200":{"description":"Cleared setup","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AgentPicksSetupClearedData"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Agent Picks"]}},"/projects/{projectId}/agent-picks/job/draft":{"post":{"summary":"Draft Agent Picks job","operationId":"draftAgentPicksJob","description":"Draft a custom job from the project’s product: the requests agents are given (add, recommend, hosted), the building blocks that decide built_its_own and no_provider, and competitor suggestions, plus the job line when job is null. A draft is a proposal; nothing reaches a test until the job is saved. Drafts share the project’s allowance of 60 an hour with the app, drafts still running included: 409 rate_limited when spent. Refused with 403 paused while DevTune support has paused the account. Shares the agent-run rate limit. The member who created the API key, or for an OAuth token the signed-in member, needs project management permission and edit rights on the project; otherwise 403. Requires Agent Picks. A body that is not valid JSON, or an empty body, is refused with 400 bad_request and no reason. Requires the `actions.write` scope.","security":[{"bearerAuth":[]},{"oauthBearer":[]}],"x-devtune-required-scope":"actions.write","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"job":{"type":["string","null"],"minLength":1,"maxLength":300}},"required":[]}}}},"responses":{"200":{"description":"Job draft","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AgentPicksJobDraftData"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"409":{"$ref":"#/components/responses/ConflictError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Agent Picks"]}},"/projects/{projectId}/agent-picks/tools/draft":{"post":{"summary":"Draft Agent Picks tool signals","operationId":"draftAgentPicksTool","description":"Draft one tool’s signals for a custom job from package registries and its own site: the packages, config names, import patterns, config files, config patterns and domains that name it in a run. Pass other_tools, the job’s other tools, to have the draft propose which of them run inside one another; each proposal in relationships cites the sentence and page it was read from. Shares the project’s 60-an-hour draft allowance with the app: 409 rate_limited when spent. Refused with 403 paused while DevTune support has paused the account. Shares the agent-run rate limit. The member who created the API key, or for an OAuth token the signed-in member, needs project management permission and edit rights on the project; otherwise 403. Requires Agent Picks. A body that is not valid JSON, or an empty body, is refused with 400 bad_request and no reason. Requires the `actions.write` scope.","security":[{"bearerAuth":[]},{"oauthBearer":[]}],"x-devtune-required-scope":"actions.write","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"tool_key":{"type":"string","maxLength":63},"name":{"type":"string","minLength":1,"maxLength":100},"domain":{"type":["string","null"],"maxLength":253},"job":{"type":"string","minLength":1,"maxLength":300},"other_tools":{"type":"array","maxItems":35,"description":"The job’s other tools, which a proposed relationship can name. Omitted, the draft proposes none.","items":{"type":"object","additionalProperties":false,"properties":{"tool_key":{"type":"string","maxLength":63},"name":{"type":"string","minLength":1,"maxLength":100}},"required":["tool_key","name"]}}},"required":["tool_key","name","job"]}}}},"responses":{"200":{"description":"Tool draft","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AgentPicksToolDraftData"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"409":{"$ref":"#/components/responses/ConflictError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Agent Picks"]}},"/projects/{projectId}/agent-picks/job":{"put":{"summary":"Save Agent Picks job","operationId":"saveAgentPicksJob","description":"Save a custom job as confirmed, with the setup that tests it. A project can have several custom jobs, each its own setup. Without setup, this updates the project’s custom job with the same job line (ignoring case and surrounding spaces), or adds a setup for a new job; a job without a source category never matches a copy by its line. A job with a source_category_id is the project’s copy of that category: without setup, the save updates the project’s copy of it whatever the job line, or adds one, and a project holds one copy per category; with setup, a source_category_id another setup’s copy holds is refused with 409 category_in_use, and changing or clearing the source of the setup’s own job with 409 category_fixed. With setup, it updates that setup. A copy may hold its copied tools plus seven and keep the name it was copied with; a job without a source category holds up to seven tools and a job line of at least 8 characters. The body: the job line, the public category it started from (or null), requests (add, recommend and hosted or null), building_blocks and tools with their signals, as the drafts return them, own_tool_key, competitor_tool_keys among the tools, environment_ids and agent_models. A tool’s signals.yieldsTo lists the job’s other tools that run inside it, or stand in for part of it: when a run uses both, it counts as the listed tool (GitHub Actions yieldsTo Depot, so a workflow on a Depot runner counts as Depot). Without it such a run is ambiguous. A yieldsTo loop of any length is refused with 400. signals.dismissedYieldsTo lists drafted suggestions of this tool yielding that the customer dismissed or removed, so they aren’t suggested again until either tool is redrafted; it never affects classification. A test app or source category outside the published catalogue is refused with 400 invalid_job; getAgentPicksCatalogue lists the valid ones; 403 agent_not_in_plan. With snapshot_digest, a setup changed since it was read is refused with 409 setup_changed. Starts no test. The member who created the API key, or for an OAuth token the signed-in member, needs project management permission and edit rights on the project; otherwise 403. Requires Agent Picks. A body that is not valid JSON, or an empty body, is refused with 400 bad_request and no reason. Requires the `actions.write` scope.","security":[{"bearerAuth":[]},{"oauthBearer":[]}],"x-devtune-required-scope":"actions.write","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"job":{"type":"string","minLength":1,"maxLength":300},"source_category_id":{"type":["string","null"],"format":"uuid"},"requests":{"type":"object","additionalProperties":false,"properties":{"add":{"type":"string","minLength":8,"maxLength":300},"recommend":{"type":"string","minLength":8,"maxLength":300},"hosted":{"type":["string","null"],"minLength":8,"maxLength":300}},"required":["add","recommend"]},"building_blocks":{"type":"object","additionalProperties":true},"tools":{"type":"array","minItems":1,"maxItems":35,"items":{"type":"object","additionalProperties":false,"properties":{"tool_key":{"type":"string","maxLength":63},"display_name":{"type":"string","minLength":1,"maxLength":100},"brand_domain":{"type":["string","null"],"maxLength":253},"signals":{"type":"object","additionalProperties":true},"draft_id":{"type":["string","null"],"format":"uuid"}},"required":["tool_key","display_name","signals"]}},"own_tool_key":{"type":["string","null"],"maxLength":63},"competitor_tool_keys":{"type":"array","maxItems":6,"items":{"type":"string","maxLength":63}},"environment_ids":{"type":"array","minItems":1,"maxItems":100,"items":{"type":"string","format":"uuid"}},"agent_models":{"type":"array","minItems":1,"maxItems":7,"items":{"$ref":"#/components/schemas/AgentPicksAgentModel"}},"snapshot_digest":{"type":["string","null"],"pattern":"^[a-f0-9]{64}$","description":"The setup’s snapshot_digest from listAgentPicksSetups when it was last read, or null when none was read. A save that finds the setup changed since, gone, or there when null was sent is refused with 409 setup_changed: read it again and save. Omit it to save without the check."},"setup":{"type":"string","minLength":1,"maxLength":200,"description":"The Agent Picks setup to use: its id or its category slug. Optional while the project has one setup. With several, omitting it is refused with 400 and reason agent_picks_setup_required, and a setup the project does not have with 404 and reason agent_picks_setup_not_found; both list the project’s setups in details.setups (id, category_slug, category_name). The response echoes the setup used as setup."}},"required":["job","requests","building_blocks","tools","own_tool_key","environment_ids","agent_models"]}}}},"responses":{"200":{"description":"Saved job","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AgentPicksJobSavedData"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"409":{"$ref":"#/components/responses/ConflictError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Agent Picks"]}},"/projects/{projectId}/audit/summary":{"get":{"summary":"Get Audit summary","description":"Returns completed Audit summaries and independently bounded trend calendars. date belongs to UTC citations and answer fetches; clicksDate and priorClicksDate belong to Pacific GSC source windows ending at D-3. Read GSC coverage and bounds from searchConsole. Unverified days are null. Requires the `audit.read` scope.","operationId":"getAuditSummary","x-devtune-required-scope":"audit.read","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}},{"name":"windowDays","in":"query","schema":{"type":"integer","default":30,"enum":[7,30,90]},"description":"Fixed rolling window in days. Supported values are 7, 30, and 90. Defaults to 30."}],"responses":{"200":{"description":"Audit summary","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AuditSummaryData"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Audit and pages"]}},"/projects/{projectId}/audit/extraction":{"get":{"summary":"Get the extracted section for an Audit finding","description":"Returns the page excerpt a section finding was based on: the section and up to 6,000 characters of nearby Markdown, with extractedAt for when the page was read. Pass domain, pagePath, contentHash, inputHash and locator exactly as the finding returned them. status is available when the excerpt is returned, stale when DevTune's evidence for the page has changed since the finding, and unavailable when no matching excerpt was kept. A newer crawl is never substituted. Requires the `audit.read` scope.","operationId":"getAuditExtraction","x-devtune-required-scope":"audit.read","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}},{"name":"domain","in":"query","required":true,"schema":{"type":"string"}},{"name":"pagePath","in":"query","required":true,"schema":{"type":"string"}},{"name":"contentHash","in":"query","required":true,"schema":{"type":"string"}},{"name":"inputHash","in":"query","required":true,"schema":{"type":"string"}},{"name":"locator","in":"query","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Stored section extraction","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AuditExtractionData"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Audit and pages"]}},"/projects/{projectId}/audit/retrieval-intent":{"post":{"summary":"Acknowledge or reconsider a retrieval restriction","description":"Records explicit customer intent for exact URLs displayed in the stored audit finding. Preserves restrictions, evidence and score. Requires project management permission. Requires the `actions.write` scope.","operationId":"changeRetrievalIntent","x-devtune-required-scope":"actions.write","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["auditRunId","findingId","urls","operation","confirmedIntent"],"properties":{"auditRunId":{"type":"string","format":"uuid"},"findingId":{"type":"string","minLength":1,"maxLength":200},"urls":{"type":"array","minItems":1,"maxItems":4,"items":{"type":"string","format":"uri","maxLength":2048}},"operation":{"type":"string","enum":["acknowledge","reconsider"]},"confirmedIntent":{"type":"boolean","enum":[true]}}}}}},"responses":{"200":{"description":"Intent updated without changing the audit score or evidence"},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"409":{"$ref":"#/components/responses/ConflictError","description":"With reason intent_saved_refresh_pending, the intent was saved and is in effect; the linked action has not refreshed yet and will refresh the next time it is reconciled. Do not resend the request. Any robots.txt draft that conflicts with the saved intent stays hidden meanwhile."},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Audit and pages"]}},"/projects/{projectId}/audit/findings":{"get":{"summary":"List Audit findings","description":"Returns the findings that Audit shows. Arbitrary date ranges are not accepted. affectedUrls is a capped sample; affectedPageCount reports the uncapped population the audit measured, in the unit named by affectedScope, and is null when a finding covers no countable population. Findings and sectionAssessments page together from one shared offset: pagination.hasMore stays true while either has rows left, and the one that has run out returns an empty array on later pages. sectionAssessments.pagination still describes the section assessments alone. Requires the `audit.read` scope.","operationId":"listAuditFindings","x-devtune-required-scope":"audit.read","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}},{"name":"includeDismissed","in":"query","schema":{"type":"boolean","default":false}},{"name":"cursor","in":"query","schema":{"type":"string","maxLength":1024},"description":"pagination.nextCursor from the previous page, unchanged. Keep the other parameters the same: a cursor sent with different filters, or beside a deprecated position parameter, returns 400."},{"name":"offset","in":"query","schema":{"type":"integer","minimum":0,"default":0},"deprecated":true,"description":"Deprecated: Pass cursor instead."},{"name":"pageSize","in":"query","schema":{"type":"integer","default":25,"minimum":1,"maximum":100},"description":"Results per page. Capped at 100."},{"name":"limit","in":"query","schema":{"type":"integer","default":25,"minimum":1,"maximum":100},"description":"The same setting as pageSize. Send either; both with different values returns 400."}],"responses":{"200":{"description":"Audit findings","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AuditFindingsData"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Audit and pages"]}},"/projects/{projectId}/audit/findings/dismissal":{"post":{"summary":"Dismiss or restore an Audit finding","description":"Dismisses one finding of a completed audit run in this project, or restores a dismissed one to open, as Dismiss and Restore on the Audit page do. Take auditRunId and findingId from List Audit findings; pass includeDismissed=true there to find dismissed ones. The change is made as the member who created the API key or holds the OAuth grant; they must be a project member with project management permission (403 otherwise). A dismissal holds for the same finding in later audits of the same site until it is restored. Dismissing a dismissed finding or restoring an open one changes nothing, and the response is the same. An audit run that is not in this project or has not completed, or a finding id the run does not hold, is a 404; a stored finding DevTune cannot read is a 409. The score, evidence and any action created from the finding are unchanged, and no webhook is sent. Requires the `actions.write` scope.","operationId":"changeAuditFindingDismissal","security":[{"bearerAuth":[]},{"oauthBearer":[]}],"x-devtune-required-scope":"actions.write","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["auditRunId","findingId","operation"],"properties":{"auditRunId":{"type":"string","format":"uuid"},"findingId":{"type":"string","minLength":1,"maxLength":200},"operation":{"type":"string","enum":["dismiss","restore"]}}}}}},"responses":{"200":{"description":"The finding status the request left in place","content":{"application/json":{"schema":{"type":"object","required":["data","meta"],"properties":{"data":{"type":"object","required":["auditRunId","findingId","operation","status"],"properties":{"auditRunId":{"type":"string","format":"uuid"},"findingId":{"type":"string"},"operation":{"type":"string","enum":["dismiss","restore"]},"status":{"type":"string","enum":["dismissed","open"]}}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"409":{"$ref":"#/components/responses/ConflictError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Audit and pages"]}},"/projects/{projectId}/audit/eligibility":{"get":{"summary":"Get Google eligibility evidence","description":"Returns whether Google can index your pages, from Search Console URL Inspection: each eligibility check, status changes confirmed by two inspections in a row, and sitemap submission state, for a fixed 7, 30, or 90 day window. Evidence is dated from successful measurements. Detail lists are capped while totals are not. Requires the `audit.read` scope.","operationId":"getGoogleEligibility","x-devtune-required-scope":"audit.read","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}},{"name":"windowDays","in":"query","schema":{"type":"integer","default":30,"enum":[7,30,90]},"description":"Fixed rolling window in days. Supported values are 7, 30, and 90. Defaults to 30."},{"name":"evidenceLimit","in":"query","description":"Maximum page, transition, and sitemap evidence rows returned per list. Aggregate totals are not capped.","schema":{"type":"integer","minimum":1,"maximum":25,"default":10}}],"responses":{"200":{"description":"Google eligibility","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/GoogleEligibilityData"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Audit and pages"]}},"/projects/{projectId}/outcomes/changes":{"get":{"summary":"List detected content changes","operationId":"listContentChanges","description":"Lists unclaimed and claimed changes with measured-action IDs and current manual claim tokens. Deltas compare fixed 14-day windows. Path filtering applies to list totals; summary covers the entire selected window. Missing evidence remains null. Requires the `outcomes.read` scope.","x-devtune-required-scope":"outcomes.read","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}},{"name":"windowDays","in":"query","schema":{"type":"integer","default":30,"enum":[7,30,90,365]},"description":"Fixed reporting window: 7, 30, 90 or 365 days."},{"name":"cursor","in":"query","schema":{"type":"string","maxLength":12288},"description":"Opaque cursor from the previous response; cursors expire after ~48 hours."},{"name":"pageSize","in":"query","schema":{"type":"integer","default":25,"minimum":1,"maximum":100},"description":"Results per page. Capped at 100."},{"name":"limit","in":"query","schema":{"type":"integer","default":25,"minimum":1,"maximum":100},"description":"The same setting as pageSize. Send either; both with different values returns 400."},{"name":"path","in":"query","schema":{"type":"string","maxLength":512}}],"responses":{"200":{"description":"Content changes","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ContentChangesData"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Results"]}},"/projects/{projectId}/outcomes/page-tracking":{"get":{"summary":"Get published-page tracking status","operationId":"getPublishedPageTracking","description":"Read-only operational status for saved URLs. Omit actionId for project-wide capacity and allowance status. At a plan limit the remedy is to upgrade; the subscription plan determines the allowance. Report these states as unmeasured. Requires the `outcomes.read` scope.","x-devtune-required-scope":"outcomes.read","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}},{"name":"actionId","in":"query","required":false,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Published page tracking","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/PublishedPageTrackingData"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Results"]}},"/projects/{projectId}/outcomes/changes/attach":{"post":{"summary":"Attach a published URL to a completed action","operationId":"attachOutcomeUrl","description":"Measurement starts at the action completion date, which is the declared went-live date. Crawling and page-tracking allowance do not gate measurement. Send every published URL the action should keep in publishedUrls: any you leave out is removed. Existing anchored measurements are preserved; changeEventId supports explicit historical event attachment. Requires the `actions.write` scope.","x-devtune-required-scope":"actions.write","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["actionId","url","publishedUrls"],"properties":{"actionId":{"type":"string","format":"uuid"},"url":{"type":"string","format":"uri","maxLength":2048,"pattern":"^[hH][tT][tT][pP][sS]?://","description":"The published URL to measure. It must also be in publishedUrls."},"publishedUrls":{"type":"array","description":"The action's full list of published URLs, including url. Any published URL you leave out is removed from the action.","minItems":1,"maxItems":20,"items":{"type":"string","format":"uri","maxLength":2048,"pattern":"^[hH][tT][tT][pP][sS]?://"}},"changeEventId":{"type":"string","format":"uuid"}}}}}},"responses":{"200":{"description":"URL saved or recorded dates requiring selection","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/OutcomeUrlAttachmentData"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"409":{"description":"The recorded event or existing measurement cannot be changed"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Results"]}},"/projects/{projectId}/outcomes/changes/claim":{"post":{"summary":"Claim a content change","operationId":"claimContentChange","description":"Creates a completed measured action, or restores the same archived action and measurement on re-claim. Repeated calls return the original IDs and do not overwrite action copy. The claim returns at once; the change list shows measurementUpdating while the measurement catches up. Returns 409 if the page has another measurement completed within 14 days of this change; the change list exposes it as existingMeasurementId. Requires the `actions.write` scope.","x-devtune-required-scope":"actions.write","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["changeEventId","title","description"],"properties":{"changeEventId":{"type":"string","format":"uuid"},"title":{"type":"string","minLength":1,"maxLength":500},"description":{"type":"string","minLength":1,"maxLength":10000}}}}}},"responses":{"200":{"description":"Claim identifiers","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ContentChangeClaimData"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"409":{"description":"The claim cannot safely be changed"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Results"]}},"/projects/{projectId}/outcomes/changes/unclaim":{"post":{"summary":"Unclaim a content change","operationId":"unclaimContentChange","description":"Reverses only an unchanged manual claim using its current claimToken. Archives the action and excludes the retained measurement from totals. Preserves history. Returns 409 for stale tokens, edited or linked work, automatic attribution, or claims without reversible history. Requires the `actions.write` scope.","x-devtune-required-scope":"actions.write","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["changeEventId","claimToken"],"properties":{"changeEventId":{"type":"string","format":"uuid"},"claimToken":{"type":"string","format":"uuid"}}}}}},"responses":{"200":{"description":"Claim identifiers","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ContentChangeClaimData"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"409":{"description":"The claim cannot safely be changed"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Results"]}},"/projects/{projectId}/outcomes/ledger":{"get":{"summary":"List measured results","description":"Returns the Results ledger and scoreboard, plus analytics-attributed conversions for fixed 30 and 90 day windows. An unmeasured conversion window returns null. Only the fixed 7, 30, 90, and 365 day ledger windows are accepted. Requires the `outcomes.read` scope.","operationId":"listOutcomesLedger","security":[{"bearerAuth":[]},{"oauthBearer":[]}],"x-devtune-required-scope":"outcomes.read","parameters":[{"name":"source","in":"query","schema":{"type":"string","enum":["ai_search","agent_picks","both"],"default":"both"},"description":"Filter the queue and results by source before counts and pagination."},{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}},{"name":"interventionId","in":"query","description":"Include fixed 7, 14, 30 and 90 day impact windows (citations, presence and answer fetches) for this completion-based measurement.","schema":{"type":"string","format":"uuid"}},{"name":"windowDays","in":"query","schema":{"type":"integer","default":30,"enum":[7,30,90,365]},"description":"Fixed rolling window in days. Supported values are 7, 30, 90, and 365. Defaults to 30."},{"name":"cursor","in":"query","schema":{"type":"string","maxLength":12288},"description":"Opaque cursor from the previous response; cursors expire after ~48 hours."},{"name":"pageSize","in":"query","schema":{"type":"integer","default":25,"minimum":1,"maximum":100},"description":"Results per page. Capped at 100."},{"name":"limit","in":"query","schema":{"type":"integer","default":25,"minimum":1,"maximum":100},"description":"The same setting as pageSize. Send either; both with different values returns 400."},{"name":"status","in":"query","schema":{"type":"string","enum":["measured","in_flight","blind","unmatched_url","sensor_gap","improved","mixed","no_clear_change","declined","still_measuring","unmeasured","conclusive"]}}],"responses":{"200":{"description":"Results ledger","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/OutcomesLedgerData"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Results"]}},"/projects/{projectId}/traffic/recent-answer-fetches":{"get":{"summary":"List recent answer fetches","description":"Returns up to 20 answer-fetch hourly records, newest hour first with descending id as the tie-break. The selected UTC calendar window includes today up to the server request time. Each item is one hourly record, not an individual request or a window total. An empty list means no records were found; it does not prove sensor coverage. Unavailable reads return an error. Requires the `traffic.read` scope.","operationId":"listRecentAnswerFetches","x-devtune-required-scope":"traffic.read","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}},{"name":"windowDays","in":"query","schema":{"type":"integer","default":30,"enum":[7,30,90]},"description":"Fixed UTC window in days, including today. Supported values: 7, 30, 90. Defaults to 30."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":20,"default":20},"description":"Maximum hourly records returned. No whole-window total is computed."}],"responses":{"200":{"description":"Recent answer-fetch hourly records","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/RecentAnswerFetchesData"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["AI Traffic"]}},"/projects/{projectId}/traffic/visits-per-answer-fetch":{"get":{"summary":"Get visits per answer fetch","description":"Returns AI-referred visits per answer fetch for each page and for the whole project, over completed UTC days ending the day before yesterday by default. An answer fetch is an AI system retrieving the page while building an answer; it is not a citation. A page is covered when its visits come from GA4 or PostHog and that provider has synced the whole window, or from the traffic snippet and it reported on the page's host for every full UTC day of the window (running the day before it starts and the day after it ends). A page is observed when a referral sensor recorded visits on it but did not cover every day of the window: observedAiReferralVisits and observedVisitsPerAnswerFetch are then lower bounds. Other pages are no_referral_data. Unless a page is covered, visits and the ratio are null, never 0. The project total covers every page with activity except uncovered pages on hosts excluded in Traffic settings or reported only by the tracking snippet, and is returned only when all of those pages are covered; otherwise summary is null and summaryWithheldReason says why. Covered pages are ranked by visits per answer fetch, highest first, with pages that have no ratio after them; observed and no_referral_data pages follow, by answer fetches. Up to 100 pages; a cursor from an earlier read is rejected with 400 once a late import changes the ranking. Requires the `traffic.read` scope.","operationId":"getVisitsPerAnswerFetch","x-devtune-required-scope":"traffic.read","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}},{"name":"windowDays","in":"query","schema":{"type":"integer","default":30,"enum":[7,30,90]},"description":"Fixed window of completed UTC days ending the day before yesterday. Supported values: 7, 30, 90. Defaults to 30."},{"name":"cursor","in":"query","schema":{"type":"string","maxLength":1024},"description":"Opaque cursor from the previous response; cursors expire after ~48 hours."},{"name":"pageSize","in":"query","schema":{"type":"integer","default":25,"minimum":1,"maximum":100},"description":"Results per page. Capped at 100."},{"name":"limit","in":"query","schema":{"type":"integer","default":25,"minimum":1,"maximum":100},"description":"The same setting as pageSize. Send either; both with different values returns 400."}],"responses":{"200":{"description":"Visits per answer fetch","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/VisitsPerAnswerFetchData"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["AI Traffic"]}},"/projects/{projectId}/traffic/referrals":{"get":{"summary":"List AI referrals","description":"Returns paginated AI-referral landing pages, overview and the complete selected-provider engine breakdown for the same fixed reporting dates. Sessions are not people; conversion events use the selected provider and conversion definitions. Nullable provider measurements remain unavailable. Known counts can be partial: overview.referralCoverage reports current and previous coverage for sessions and conversion events, and each engine, total and overview count carries currentStatus and previousStatus. A lower_bound count is at least that number; an unmeasured count is null, including a zero in a partly covered window. Compare a metric across periods only when comparisonAvailable is true. visitorShare gives the share of all sessions that AI answers sent over the same window, from the selected analytics provider and the same database snapshot as the referrals, with the sessions it could not trace; it is absent when that read fails, so the rest of the response is still returned. conversionDefinitions lists each selected PostHog conversion that occurred in the same window, with its occurrences with and without a site visit. Requires the `referrals.read` scope.","operationId":"listAiReferrals","x-devtune-required-scope":"referrals.read","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}},{"name":"windowDays","in":"query","schema":{"type":"integer","default":30,"enum":[7,30,90]},"description":"Fixed rolling window in days. Supported values are 7, 30, and 90. Defaults to 30."},{"name":"cursor","in":"query","schema":{"type":"string","maxLength":12288},"description":"Opaque cursor from the previous response; cursors expire after ~48 hours."},{"name":"pageSize","in":"query","schema":{"type":"integer","default":25,"minimum":1,"maximum":100},"description":"Results per page. Capped at 100."},{"name":"limit","in":"query","schema":{"type":"integer","default":25,"minimum":1,"maximum":100},"description":"The same setting as pageSize. Send either; both with different values returns 400."}],"responses":{"200":{"description":"AI referral demand","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AiReferralsData"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["AI Traffic"]}},"/projects/{projectId}/traffic/coding-agents":{"get":{"summary":"Get coding-agent answer fetches","description":"Returns answer fetches by coding agents over a fixed rolling window, per agent and platform since countedFrom: the coding-agent counts of the summary's codingAgents, without its totals of all answer fetches. Every Agent Picks plan can read it, with 404 Demand; the other traffic reads need an AI Search plan and return 403 without one. pathPrefix requires domain. Requires the `traffic.read` scope.","operationId":"getTrafficCodingAgents","security":[{"bearerAuth":[]},{"oauthBearer":[]}],"x-devtune-required-scope":"traffic.read","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}},{"name":"windowDays","in":"query","schema":{"type":"integer","default":30,"enum":[30,90]},"description":"Fixed rolling window in days. Supported values are 30 and 90. Defaults to 30."},{"name":"domain","description":"Only traffic on this host, such as docs.example.com.","in":"query","schema":{"type":"string"}},{"name":"pathPrefix","in":"query","description":"Count only this path and its descendants. Requires domain.","schema":{"type":"string","pattern":"^/"}}],"responses":{"200":{"description":"Coding-agent answer fetches","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"codingAgents":{"type":"object","description":"Answer fetches made by coding agents (Aider, Claude Code, Crush, Gemini CLI, OpenCode and Qwen Code) since countedFrom, under the domain and pathPrefix filters, as edge middleware, a Vercel log drain and the tracking snippet attribute them. Coding-agent counts only: no total of all answer fetches. coverage is partial when some answer fetches in the span could not be attributed, so totalEvents leaves them out. state is not_yet_observed, with a null totalEvents, until the project records a coding-agent fetch.","properties":{"state":{"type":"string","enum":["observed","not_yet_observed"],"description":"not_yet_observed until the project records its first coding-agent fetch."},"coverage":{"type":"string","enum":["complete","partial"],"description":"complete when every answer fetch in the span was attributed or ruled out; partial when some could not be attributed to a coding agent, for example Cloudflare pulls, so totalEvents is a lower bound."},"countedFrom":{"type":"string","format":"date-time","description":"Start of the counted span: the later of the window start and the first hour DevTune counted coding agents."},"totalEvents":{"type":["integer","null"],"description":"Coding-agent answer fetches attributed since countedFrom. Null while state is not_yet_observed."},"agents":{"type":"array","description":"Totals per coding agent, largest first.","items":{"type":"object","properties":{"label":{"type":"string","description":"The coding agent, such as OpenCode or Aider."},"llmPlatform":{"type":"string","description":"The AI Traffic provider key the agent belongs to, such as anthropic."},"events":{"type":"integer","description":"Its answer fetches since countedFrom."}},"required":["label","llmPlatform","events"]}},"platforms":{"type":"array","description":"Totals per AI platform, largest first.","items":{"type":"object","properties":{"llmPlatform":{"type":"string","description":"The AI Traffic provider key."},"events":{"type":"integer","description":"Coding-agent answer fetches for the provider since countedFrom."}},"required":["llmPlatform","events"]}}},"required":["state","coverage","countedFrom","totalEvents","agents","platforms"]},"window":{"type":"object","description":"The fixed window the figures cover, in UTC days.","properties":{"days":{"type":"integer"},"dateFrom":{"type":"string","format":"date","description":"First day of the window."},"dateTo":{"type":"string","format":"date","description":"Last day of the window, included."}},"required":["days","dateFrom","dateTo"]}},"required":["codingAgents","window"]},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["AI Traffic"]}},"/projects/{projectId}/traffic/referral-hosts":{"get":{"summary":"List AI referral hosts","description":"Lists the project's AI referral hosts in hostname order: its owned domains and every host a server-side sensor (edge middleware, Cloudflare or a Vercel log drain) has reported, plus any excluded host outside that set. A host only the tracking snippet reports is not listed. Project-wide referrals count as measured on a day only when a referral sensor reported on every listed host that isn't excluded. Referrals on excluded and unlisted hosts still count. Exclusions are changed in Traffic settings; this endpoint is read-only. Requires the `traffic.read` scope.","operationId":"listTrafficReferralHosts","security":[{"bearerAuth":[]},{"oauthBearer":[]}],"x-devtune-required-scope":"traffic.read","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"AI referral hosts","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"hosts":{"type":"array","items":{"type":"object","properties":{"hostname":{"type":"string","description":"The host in lower case, such as docs.example.com."},"excluded":{"type":"boolean","description":"True when the host is left out of the project-wide referral coverage check."}},"required":["hostname","excluded"]}}},"required":["hosts"]},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["AI Traffic"]}},"/projects/{projectId}/traffic/404-demand":{"get":{"summary":"Get 404 Demand","description":"Returns project-scoped content-intent and suspected-noise 404 clusters for one published window, including explicit degraded state, machine-traffic sensor coverage and the days the window was measured. The 30-day window, the default, is the one opportunities and actions use. The 7-day window is mined in the same run from the last seven days, so its clusters, counts and ranking are its own rather than a slice of the 30-day list. Any other windowDays returns 400. Requires the `traffic.read` scope.","operationId":"getTraffic404Demand","x-devtune-required-scope":"traffic.read","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}},{"name":"windowDays","in":"query","schema":{"type":"integer","default":30,"enum":[7,30]},"description":"Published 404 Demand window in days. Supported values are 7 and 30. Defaults to 30."}],"responses":{"200":{"description":"404 Demand","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/Traffic404DemandData"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["AI Traffic"]}},"/projects/{projectId}/traffic/direct-lift":{"get":{"summary":"Get AI-correlated Direct lift","description":"Returns the project's Direct traffic totals from GA4, each measured page's actual and baseline Direct traffic per day, and how each page-window is classified, for a fixed 7, 30, or 90 day request. AI-correlated lift is correlated evidence, never attributed traffic, and is never summed with measured referrals. Property totals and population sizes remain complete. Up to 250,000 page-windows are classified per request; larger requests return classificationAvailability=population_limit, zero evaluated windows, empty page details, and unavailable lift comparisons. Requires the `referrals.read` scope.","operationId":"getDirectLift","x-devtune-required-scope":"referrals.read","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}},{"name":"windowDays","in":"query","schema":{"type":"integer","default":30,"enum":[7,30,90]},"description":"Fixed rolling window in days. Supported values are 7, 30, and 90. Defaults to 30."},{"name":"pageLimit","in":"query","description":"Maximum pages returned with their classification windows. Property totals remain complete; page details are empty when classificationAvailability is population_limit.","schema":{"type":"integer","minimum":1,"maximum":25,"default":10}}],"responses":{"200":{"description":"AI-correlated Direct lift","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/DirectLiftData"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"422":{"$ref":"#/components/responses/LiftPopulationTooLargeError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["AI Traffic"]}},"/projects/{projectId}/agents":{"get":{"summary":"List managed agents","description":"Returns roster and active custom agents with display fields and, for each, the evidence verdict a run would start from. With archived=true, returns only archived custom agents instead, each with archivedAt, so you can find the agentKey to pass to restoreCustomAgent. Requires the `agents.read` scope.","operationId":"listManagedAgents","security":[{"bearerAuth":[]},{"oauthBearer":[]}],"x-devtune-required-scope":"agents.read","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}},{"name":"archived","in":"query","schema":{"type":"boolean","default":false},"description":"true lists only archived custom agents; false or leaving it out lists the agents that can run. Any other value returns 400."}],"responses":{"200":{"description":"Managed-agent gallery","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"agents":{"type":"array","description":"The agents the project can run: built-in agents and active custom agents. With archived=true, only archived custom agents, which must be restored before they run.","items":{"$ref":"#/components/schemas/AgentDefinition"}},"evidenceCoverage":{"$ref":"#/components/schemas/ProjectEvidenceCoverage"},"trial":{"type":"object","nullable":true,"description":"Trial policy, or null outside a trial. The allowance is one-time and shared across the account, not a remaining balance.","properties":{"creditAllowance":{"type":"integer","minimum":0,"description":"Credits the account was granted when its trial started."},"scheduledAgentsEnabled":{"type":"boolean","enum":[false]}},"required":["creditAllowance","scheduledAgentsEnabled"]}},"required":["agents","evidenceCoverage","trial"]},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Agents"]},"post":{"summary":"Create a custom agent","description":"Creates a custom agent, as Create agent does on the Agents page, with the same validation; a model that is not available now is refused with 400. The agent starts with no schedule. The response carries its agentKey. A retry creates a second agent. The OAuth member, or the member who created the API key, must belong to the project and hold project management permission; otherwise 403. Requires the `agents.write` scope.","operationId":"createCustomAgent","security":[{"bearerAuth":[]},{"oauthBearer":[]}],"x-devtune-required-scope":"agents.write","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CustomAgentInput"}}}},"responses":{"201":{"description":"The custom agent","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/CustomAgentWrite"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Agents"]}},"/projects/{projectId}/agents/runs":{"get":{"summary":"List managed-agent runs","description":"Returns project-scoped managed-agent run status and definition display fields. Draft output is available only from the single-run endpoint after completion. Requires the `agents.read` scope.","operationId":"listManagedAgentRuns","x-devtune-required-scope":"agents.read","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}},{"name":"agentKey","in":"query","schema":{"type":"string"}},{"name":"cursor","in":"query","schema":{"type":"string","maxLength":12288},"description":"Opaque cursor from the previous response."},{"name":"pageSize","in":"query","schema":{"type":"integer","default":25,"minimum":1,"maximum":100},"description":"Results per page. Capped at 100."},{"name":"limit","in":"query","schema":{"type":"integer","default":25,"minimum":1,"maximum":100},"description":"The same setting as pageSize. Send either; both with different values returns 400."}],"responses":{"200":{"description":"Managed-agent runs","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"items":{"type":"array","description":"Agent runs, newest first.","items":{"$ref":"#/components/schemas/AgentRun"}},"pagination":{"type":"object","description":"Where this page sits in the runs.","properties":{"hasMore":{"type":"boolean","description":"More rows follow this page."},"nextCursor":{"type":["string","null"],"description":"Pass as cursor to read the next page. Null on the last page."},"pageSize":{"type":"integer","minimum":1,"maximum":100,"description":"Rows per page."}},"required":["hasMore","nextCursor","pageSize"]}},"required":["items","pagination"]},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Agents"]}},"/projects/{projectId}/agents/runs/{runId}":{"get":{"summary":"Get a managed-agent run","description":"Returns run status and the complete action, brief or report output only after the run reaches a completed review state. Content-draft output remains null. Requires the `agents.read` scope.","operationId":"getManagedAgentRun","security":[{"bearerAuth":[]},{"oauthBearer":[]}],"x-devtune-required-scope":"agents.read","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}},{"name":"runId","in":"path","required":true,"description":"Managed-agent run identifier.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Managed-agent run","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AgentRunDetail"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Agents"]}},"/projects/{projectId}/agents/runs/{runId}/review":{"post":{"summary":"Review a managed-agent draft","description":"Approves or dismisses the draft of a run awaiting review (status draft_ready), as Approve and Dismiss do in the app. Approving an action or brief draft adds it to Actions, and approving a report keeps it. Dismissing discards the draft and any hidden action it created. A Company Context Refresher proposal is approved in DevTune only; approving it here is refused, and dismissing it works. The review is made as the member who created the API key or authorized the OAuth connection, and is refused with 403 unless that member belongs to the project and holds the manage permission. It is recorded against that member, with the API as its channel. Repeating the decision a run already has returns 200 with alreadyReviewed true. Refusals are 409 with a reason: already_reviewed when the run already has the other decision, not_awaiting_review when it has no draft awaiting review, sibling_approved when another draft for the same page was approved, action_archived when the brief's action is archived (dismissing still works), review_in_app when approving a Company Context Refresher run that has a draft awaiting review or is still running, and concurrent_review with retryable true when another review of the project was in progress. A run outside this project returns 404. A decision cannot be undone. Needs the agents.review scope, which agents.run does not include; a key or connection created before agents.review existed lacks it, so edit or replace the key, or revoke the client's OAuth authorization and connect again to approve it. Requires the `agents.review` scope.","operationId":"reviewManagedAgentRun","security":[{"bearerAuth":[]},{"oauthBearer":[]}],"x-devtune-required-scope":"agents.review","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}},{"name":"runId","in":"path","required":true,"description":"Managed-agent run identifier.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"decision":{"type":"string","enum":["approve","dismiss"],"description":"approve keeps the draft; dismiss discards it."}},"required":["decision"]}}}},"responses":{"200":{"description":"Reviewed run","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AgentRunReviewResult"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"409":{"$ref":"#/components/responses/ConflictError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Agents"]}},"/projects/{projectId}/agents/runs/{runId}/follow-ups":{"post":{"summary":"Send a managed-agent run a follow-up","description":"Queues a follow-up of a completed run (status draft_ready, approved or dismissed), as Run follow-up does in the app: a new metered run of the same agent version, grounded in the prior draft and the instructions. Credits are reserved and settled as for triggerManagedAgentRun, and with too few credits the request returns 402. Repeating the request with the same requestIdempotencyKey returns that run as it stands, including a blocked or failed outcome, unless it is still queued without a credit reservation; only then is the retry checked like a new request, and it can be refused. A run that is not complete, an approved brief, and a reused requestIdempotencyKey with different input return 409; a draft that predates the current agent runtime returns 409 with reason runtime_outdated; a run of an archived custom agent returns 409 with reason agent_archived. A run started for a target page while target pages are turned off returns 400 with error target_page_refused. The follow-up is started as the member who created the API key or authorized the OAuth connection; it is refused with 403 unless that member belongs to the project and holds the manage permission. A run outside this project returns 404. This endpoint has a stricter rate limit than read endpoints. Requires the `agents.run` scope.","operationId":"sendManagedAgentFollowUp","security":[{"bearerAuth":[]},{"oauthBearer":[]}],"x-devtune-required-scope":"agents.run","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}},{"name":"runId","in":"path","required":true,"description":"The completed run to follow up.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"instructions":{"type":"string","minLength":1,"maxLength":4000,"description":"What to change or do next."},"requestIdempotencyKey":{"type":"string","format":"uuid"}},"required":["instructions","requestIdempotencyKey"]}}}},"responses":{"200":{"description":"Queued or reused follow-up run","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AgentRunTriggerResult"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"402":{"description":"Insufficient included or purchased credits","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"409":{"$ref":"#/components/responses/ConflictError"},"422":{"description":"The project has no evidence this agent can read. The follow-up was refused before any credits were spent.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Agents"]}},"/projects/{projectId}/agents/dex/threads":{"get":{"summary":"List Dex conversation threads","description":"Lists your private Dex conversations in this project from recent history. Only the user attributed to the credential sees their conversations. truncated is true when older history is not included. creditTotalIsFinal is false while a charge is still settling or when older history for that thread is not included, so creditTotal may be lower than the final total. Requires the `agents.read` scope.","operationId":"listDexConversationThreads","x-devtune-required-scope":"agents.read","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Bounded Dex conversation history","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/DexConversationThreadPage"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"401":{"$ref":"#/components/responses/UnauthorizedError"},"402":{"description":"Dex conversations are not available for this account","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Dex"]},"post":{"summary":"Start a Dex conversation","description":"Starts a Dex conversation with your first message. Repeating the request with the same messageId returns the conversation it started instead of starting another. Dex runs spend managed-agent credits; with too few credits the request returns 402. The conversation is private to the member who created the API key, and that member must still be able to manage the project. Requires the `agents.run` scope.","operationId":"startDexConversation","x-devtune-required-scope":"agents.run","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"message":{"type":"string","minLength":1,"maxLength":4000},"messageId":{"type":"string","format":"uuid","description":"A UUID you choose for this message. Retrying with the same messageId returns the conversation it already started instead of starting another."}},"required":["message","messageId"]}}}},"responses":{"200":{"description":"Started or reused Dex conversation","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/DexConversationStartResult"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"402":{"description":"Dex access or managed-agent credits are required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"409":{"$ref":"#/components/responses/ConflictError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Dex"]}},"/projects/{projectId}/agents/dex/threads/{threadId}":{"delete":{"summary":"Delete your private Dex chat","description":"Stops live work and removes the chat from its owner’s history. Billing and audit records are retained. The API key creator must own the chat. Requires the `agents.run` scope.","operationId":"deleteDexConversationThread","x-devtune-required-scope":"agents.run","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}},{"name":"threadId","in":"path","required":true,"description":"Dex conversation thread identifier.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Deleted Dex conversation thread","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/DexConversationDeleteResult"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Dex"]},"patch":{"summary":"Rename a Dex conversation thread","description":"Renames one of your Dex conversations. Conversations are private to the member who created the API key, so another member's conversation returns 404. Renaming spends no credits, and sending the same title again changes nothing. Returns 402 when the account doesn't include Dex. Requires the `agents.run` scope.","operationId":"renameDexConversationThread","x-devtune-required-scope":"agents.run","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}},{"name":"threadId","in":"path","required":true,"description":"Dex conversation thread identifier.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"title":{"type":"string","minLength":1,"maxLength":80}},"required":["title"]}}}},"responses":{"200":{"description":"Renamed Dex conversation thread","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/DexConversationRenameResult"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"402":{"description":"Dex conversations are not available for this account","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Dex"]}},"/projects/{projectId}/agents/dex/threads/{threadId}/messages":{"get":{"summary":"List Dex conversation events","description":"Returns one page of a Dex conversation: its replies and status events, oldest first, from recent history. Pass nextAfter from the previous page as after to read the next page; threadTruncated is true when older history is not included. Requires the `agents.read` scope.","operationId":"getDexConversationEvents","x-devtune-required-scope":"agents.read","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}},{"name":"threadId","in":"path","required":true,"description":"Dex conversation thread identifier.","schema":{"type":"string","format":"uuid"}},{"name":"after","in":"query","required":false,"description":"nextAfter from the previous page.","schema":{"type":"integer","format":"int64","minimum":0}},{"name":"pageSize","in":"query","required":false,"schema":{"type":"integer","minimum":1,"maximum":100,"default":50}}],"responses":{"200":{"description":"Ordered Dex conversation events","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/DexConversationEventPage"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"402":{"description":"Dex conversations are not available for this account","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Dex"]},"post":{"summary":"Send a Dex conversation message","description":"Sends a message in one of your Dex conversations: it steers the task Dex is working on, or resumes a conversation that has gone idle. Resuming starts a Dex run, which spends managed-agent credits; with too few credits the request returns 402. This endpoint has the stricter rate limit that agent runs use. Requires the `agents.run` scope.","operationId":"sendDexConversationMessage","x-devtune-required-scope":"agents.run","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}},{"name":"threadId","in":"path","required":true,"description":"Dex conversation thread identifier.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"message":{"type":"string","minLength":1,"maxLength":4000},"messageId":{"type":"string","format":"uuid"}},"required":["message","messageId"]}}}},"responses":{"200":{"description":"Dex conversation message outcome","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/DexConversationMessageResult"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"402":{"description":"Dex access or managed-agent credits are required","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"409":{"$ref":"#/components/responses/ConflictError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Dex"]}},"/projects/{projectId}/agents/dex/threads/{threadId}/handoffs":{"get":{"summary":"List pending Dex specialist handoffs","description":"Lists the specialist agent runs Dex has proposed in one of your conversations that are waiting for your decision and have not expired. estimateCredits is the estimated managed-agent credits the run would spend, or null when there is no estimate; expiresAt is when the proposal lapses. Confirm or cancel one with Confirm or cancel a Dex specialist handoff. Reading spends nothing. Requires the `agents.read` scope.","operationId":"listDexHandoffs","x-devtune-required-scope":"agents.read","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}},{"name":"threadId","in":"path","required":true,"description":"Dex conversation thread identifier.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Pending Dex handoffs","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/DexHandoffList"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"402":{"description":"The account does not have access or available credits","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Dex"]},"post":{"summary":"Confirm or cancel a Dex specialist handoff","description":"Confirms or cancels a specialist agent run Dex proposed. confirm starts the run as the member who created the API key, and the run spends managed-agent credits; the handoff's estimateCredits is the estimate. The result status is queued, with the run's taskId, when the run started; not_started when it could not start, for example for lack of credits, and the handoff stays pending so you can confirm it again; canceled after cancel; or not_pending when the handoff was already decided, has expired or is unknown. A handoff starts at most one run, so repeating a decision is safe. A 409 means an earlier Dex conversation is still being recovered; retry shortly. Requires the `agents.run` scope.","operationId":"decideDexHandoff","x-devtune-required-scope":"agents.run","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}},{"name":"threadId","in":"path","required":true,"description":"Dex conversation thread identifier.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"decision":{"type":"string","enum":["confirm","cancel"]},"handoffId":{"type":"string","format":"uuid"}},"required":["decision","handoffId"]}}}},"responses":{"200":{"description":"Dex handoff decision","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/DexHandoffDecisionResult"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"402":{"description":"The account does not have access or available credits","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Dex"]}},"/projects/{projectId}/agents/dex/threads/{threadId}/memory":{"post":{"summary":"Save or dismiss a Dex memory offer","description":"Saves or dismisses a memory Dex offered in one of your conversations. To save, send offeredText exactly as Dex offered it, with its offerId and taskId. Different text, or an unknown offerId, returns 404. A saved memory becomes a Library item that agents can use. Requires the `agents.run` scope.","operationId":"decideDexMemory","x-devtune-required-scope":"agents.run","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}},{"name":"threadId","in":"path","required":true,"description":"Dex conversation thread identifier.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"decision":{"type":"string","enum":["save","dismiss"]},"offerId":{"type":"string","format":"uuid"},"offeredText":{"type":"string","minLength":1,"maxLength":4000},"taskId":{"type":"string","format":"uuid"}},"required":["decision","offerId","offeredText","taskId"]}}}},"responses":{"200":{"description":"Dex memory decision","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/DexMemoryDecisionResult"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"402":{"description":"The account does not have access or available credits","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Dex"]}},"/projects/{projectId}/agents/{agentKey}":{"patch":{"summary":"Update a custom agent","description":"Changes a custom agent's name, description, instructions, model, quick actions or tools. Send only the fields to change; the result must still be a valid agent by the Agents page's rules. If the agent's model is no longer available, the request is refused with 400 and reason model_unavailable until you also send modelId with an available model. The schedule is kept, and runs already started keep the definition they started with. If the agent changes between DevTune reading it and saving your change, the request is refused with 409 and reason revision_changed, and nothing is saved; read the agent again and retry. An archived agent is refused with 409 and reason agent_archived until it is restored. Built-in agents cannot be edited (400); a custom agent outside this project returns 404. The OAuth member, or the member who created the API key, must belong to the project and hold project management permission; otherwise 403. Requires the `agents.write` scope.","operationId":"updateCustomAgent","security":[{"bearerAuth":[]},{"oauthBearer":[]}],"x-devtune-required-scope":"agents.write","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}},{"name":"agentKey","in":"path","required":true,"description":"A custom agent key from listManagedAgents.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"minProperties":1,"properties":{"displayName":{"type":"string","minLength":1,"maxLength":80},"description":{"type":"string","minLength":1,"maxLength":240},"instructions":{"type":"string","minLength":1,"maxLength":12000},"modelId":{"type":"string","minLength":1,"maxLength":120},"quickActions":{"type":"array","minItems":1,"maxItems":6,"items":{"type":"string","minLength":1,"maxLength":160}},"toolAllowlist":{"type":"array","minItems":1,"uniqueItems":true,"items":{"type":"string"}}}}}}},"responses":{"200":{"description":"The custom agent","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/CustomAgentWrite"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"409":{"$ref":"#/components/responses/ConflictError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Agents"]},"delete":{"summary":"Archive a custom agent","description":"Archives a custom agent, as Archive does on the Agents page. The agent leaves listManagedAgents, its schedule turns off, and runs, edits and schedules for it are refused with 409 and reason agent_archived until it is restored. A run already started finishes. Its past runs, drafts and briefs stay readable through the run endpoints. Archiving an archived agent changes nothing. Built-in agents cannot be archived (400, reason built_in_agent): turn their schedule off instead. A custom agent outside this project returns 404. The OAuth member, or the member who created the API key, must belong to the project and hold project management permission; otherwise 403. Requires the `agents.write` scope.","operationId":"archiveCustomAgent","security":[{"bearerAuth":[]},{"oauthBearer":[]}],"x-devtune-required-scope":"agents.write","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}},{"name":"agentKey","in":"path","required":true,"description":"A custom agent key from listManagedAgents.","schema":{"type":"string"}}],"responses":{"200":{"description":"The archived agent","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/CustomAgentArchiveWrite"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Agents"]}},"/projects/{projectId}/agents/{agentKey}/restore":{"post":{"summary":"Restore an archived custom agent","description":"Restores an archived custom agent, as Restore does on the Agents page. It returns to listManagedAgents with its schedule off; turn the schedule back on with setAgentSchedule. Restoring an agent that is not archived changes nothing. Built-in agents are never archived (400, reason built_in_agent). A custom agent outside this project returns 404. The OAuth member, or the member who created the API key, must belong to the project and hold project management permission; otherwise 403. Requires the `agents.write` scope.","operationId":"restoreCustomAgent","security":[{"bearerAuth":[]},{"oauthBearer":[]}],"x-devtune-required-scope":"agents.write","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}},{"name":"agentKey","in":"path","required":true,"description":"A custom agent key that was archived.","schema":{"type":"string"}}],"responses":{"200":{"description":"The restored agent","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/CustomAgentArchiveWrite"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Agents"]}},"/projects/{projectId}/agents/{agentKey}/schedule":{"put":{"summary":"Set an agent schedule","description":"Turns an agent's schedule on or changes it (autonomyLevel scheduled, with schedule and modelId), or turns it off (autonomyLevel manual, without a schedule), as Schedule does on the Agents page, for built-in and custom agents. Other refusals, including invalid schedule inputs and unavailable models, return 400 with the same message as the Agents page. Each scheduled run is a metered run that uses agent credits. Refused with 403 and reason trialing while the account is on a trial. The change records which API key or OAuth connection made it. Turning on an archived agent's schedule is refused with 409, reason agent_archived and message: Restore this agent before scheduling it. Other inactive agents are refused with 409, reason agent_inactive and message: This agent is inactive, so its schedule can't be changed. Contact DevTune support to reactivate it. This refusal applies to both scheduling and turning off; their saved schedule is kept. An agent outside this project returns 404. The OAuth member, or the member who created the API key, must belong to the project and hold project management permission; otherwise 403. Requires the `agents.write` scope.","operationId":"setAgentSchedule","security":[{"bearerAuth":[]},{"oauthBearer":[]}],"x-devtune-required-scope":"agents.write","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}},{"name":"agentKey","in":"path","required":true,"description":"Roster or custom agent key.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["autonomyLevel"],"properties":{"autonomyLevel":{"type":"string","enum":["scheduled","manual"]},"modelId":{"type":"string","minLength":1,"description":"Required with scheduled."},"schedule":{"nullable":true,"allOf":[{"$ref":"#/components/schemas/AgentSchedule"}],"description":"Required with scheduled; leave it out with manual."}}}}}},"responses":{"200":{"description":"The agent schedule","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AgentScheduleWrite"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"409":{"$ref":"#/components/responses/ConflictError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Agents"]},"delete":{"summary":"Turn off an agent schedule","description":"Turns an agent's schedule off. Future scheduled runs stop; a run already started continues. An agent with no current revision is already off. An archived agent can be turned off. Other inactive agents are refused with 409, reason agent_inactive and message: This agent is inactive, so its schedule can't be changed. Contact DevTune support to reactivate it. Their saved schedule is kept. An agent outside this project returns 404. The OAuth member, or the member who created the API key, must belong to the project and hold project management permission; otherwise 403. Requires the `agents.write` scope.","operationId":"turnOffAgentSchedule","security":[{"bearerAuth":[]},{"oauthBearer":[]}],"x-devtune-required-scope":"agents.write","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}},{"name":"agentKey","in":"path","required":true,"description":"Roster or custom agent key.","schema":{"type":"string"}}],"responses":{"200":{"description":"The agent schedule","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AgentScheduleWrite"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"409":{"$ref":"#/components/responses/ConflictError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Agents"]}},"/projects/{projectId}/agents/{agentKey}/runs":{"post":{"summary":"Trigger a managed-agent run","description":"Queues a manual run of the agent, which spends managed-agent credits. Repeating the request with the same requestIdempotencyKey returns that run as it stands, including a blocked or failed outcome, unless it is still queued without a credit reservation; only then is the retry checked like a new request, and it can be refused. A project with nothing this agent can read is refused with 422 before any credits are spent. An agent that briefs one page (citability-editor) takes an optional targetPage, an absolute URL or a path on a tracked domain; the run briefs that page and no other, and ignores pages the instructions name. A targetPage is refused before any credits are spent, with error target_page_refused and a reason: 400 agent_takes_no_target_page, target_page_unavailable (target pages are turned off for agent runs) or target_page_invalid; 422 target_page_unowned, target_page_not_found (not an active page in the latest crawl), target_page_ambiguous (a path crawled on more than one tracked domain) or target_page_ineligible (neither a failing factor in its current Citability score nor findings from a section review, a path another crawled page on the same host shares, a page the customer locked, a removal candidate, or a page that already has a brief on an open action). A retry with the same requestIdempotencyKey and another targetPage is a 409. An archived custom agent is refused with 409 and reason agent_archived until it is restored. The run is started as the member who created the API key or authorized the OAuth connection; it is refused with 403 unless that member belongs to the project and holds the manage permission. This endpoint has a stricter rate limit than read endpoints. Requires the `agents.run` scope.","operationId":"triggerManagedAgentRun","security":[{"bearerAuth":[]},{"oauthBearer":[]}],"x-devtune-required-scope":"agents.run","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}},{"name":"agentKey","in":"path","required":true,"description":"Roster or custom agent key.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"instructions":{"type":"string","minLength":1,"maxLength":4000},"modelId":{"type":"string","minLength":1,"maxLength":120},"requestIdempotencyKey":{"type":"string","format":"uuid"},"targetPage":{"type":"string","minLength":1,"maxLength":2048,"description":"Page the run works on, for agents that brief one page: an absolute URL, or a path such as /pricing on a tracked domain."}},"required":["requestIdempotencyKey"]}}}},"responses":{"200":{"description":"Queued or reused managed-agent run","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AgentRunTriggerResult"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"402":{"description":"Insufficient included or purchased credits","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"409":{"$ref":"#/components/responses/ConflictError"},"422":{"description":"The project has no evidence this agent can read, or targetPage names no page this agent can work on. The run was refused before any credits were spent.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Agents"]}},"/projects/{projectId}/visibility/response-history":{"get":{"summary":"List AI response history","description":"Lists the AI answer runs DevTune still holds, with every status by default, newest first, in a fixed 30- or 90-day window ending when the first page is read. Each item is metadata only, without answer text or counts; read one run with getVisibilityResponse for its answer evidence. Use this list to follow runs, not to compute visibility rates. Enterprise platforms are included only for accounts whose plan has them. Keep the filters and window unchanged when passing the cursor back. A cursor expires after 90 days and can stop working sooner; when it is refused with 400, start again without one. Page size may change. Requires the `visibility.read` scope.","operationId":"listVisibilityResponseHistory","security":[{"bearerAuth":[]},{"oauthBearer":[]}],"x-devtune-required-scope":"visibility.read","parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"windowDays","description":"Fixed window: 30 or 90 days, ending when the first page is read. Defaults to 30.","in":"query","schema":{"type":"integer","enum":[30,90],"default":30}},{"name":"pageSize","description":"Results per page, 1 to 100. Defaults to 50.","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":50}},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":50},"description":"The same setting as pageSize. Send either; both with different values returns 400."},{"name":"cursor","description":"nextCursor from the previous page, with the other parameters unchanged.","in":"query","schema":{"type":"string","minLength":1,"maxLength":4096}},{"name":"promptId","description":"Only executions of this tracked prompt.","in":"query","schema":{"type":"string","format":"uuid"}},{"name":"platform","description":"Only executions on this AI platform, by its key, such as chatgpt-search.","in":"query","schema":{"type":"string","minLength":1,"maxLength":100}},{"name":"status","description":"Only executions in this status. Omit it for every status.","in":"query","schema":{"type":"string","enum":["pending","running","success","failed","rate_limited"]}}],"responses":{"200":{"description":"One execution history page","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/VisibilityResponseHistory"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Visibility"]}},"/projects/{projectId}/visibility/topics":{"get":{"summary":"List topics","description":"Pages the project's topics with their names and descriptions. Pass a topic id as topicId to listPrompts, getVisibilitySummary or getCompetitivePosition. Only active topics are listed unless includeInactive is true; deleted topics are never listed. Ordered by id. Pass nextCursor back as cursor with includeInactive unchanged. This is a live listing, not a snapshot: topics added or changed while you page can be missed or appear. Requires the `visibility.read` scope.","operationId":"listTopics","security":[{"bearerAuth":[]},{"oauthBearer":[]}],"x-devtune-required-scope":"visibility.read","parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"includeInactive","in":"query","schema":{"type":"boolean","default":false}},{"name":"pageSize","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":50}},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":50},"description":"The same setting as pageSize. Send either; both with different values returns 400."},{"name":"cursor","in":"query","schema":{"type":"string","minLength":1,"maxLength":512}}],"responses":{"200":{"description":"One page of topics","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/TrackedTopicsPage"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Prompts and topics"]},"post":{"summary":"Create a topic","description":"Creates a topic in this project to group prompts. A topic with the same name returns 409, so a retry after a lost response returns 409 once the first call succeeded: list the topics to find it. Requires the `prompts.write` scope.","operationId":"createTopic","x-devtune-required-scope":"prompts.write","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["name"],"properties":{"name":{"type":"string","minLength":1,"maxLength":200},"description":{"type":"string","maxLength":2000}}}}}},"responses":{"201":{"description":"The topic","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/TrackedTopicWrite"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"409":{"$ref":"#/components/responses/ConflictError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Prompts and topics"]}},"/projects/{projectId}/visibility/topics/{topicId}":{"patch":{"summary":"Update a topic","description":"Renames a topic, changes its description, or switches it on or off. A switched-off topic's prompts stop running and keep their own on/off state. A topic outside this project returns 404. Requires the `prompts.write` scope.","operationId":"updateTopic","x-devtune-required-scope":"prompts.write","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}},{"name":"topicId","in":"path","required":true,"description":"A topic id from listTopics.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"minProperties":1,"properties":{"name":{"type":"string","minLength":1,"maxLength":200},"description":{"type":"string","maxLength":2000},"isActive":{"type":"boolean","description":"Switch the topic on or off."}}}}}},"responses":{"200":{"description":"The updated topic","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/TrackedTopicWrite"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"409":{"$ref":"#/components/responses/ConflictError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Prompts and topics"]},"delete":{"summary":"Delete a topic","description":"Deletes a topic and every prompt in it. They stop running and free their allowance slots at once; history is kept, and restoring one of its prompts with restorePrompt brings the topic back. A topic that is already deleted, or outside this project, returns 404. Requires the `prompts.write` scope.","operationId":"deleteTopic","x-devtune-required-scope":"prompts.write","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}},{"name":"topicId","in":"path","required":true,"description":"A topic id from listTopics.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The deleted topic","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/TrackedTopicDeletion"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Prompts and topics"]}},"/projects/{projectId}/visibility/prompts":{"get":{"summary":"List tracked prompts","description":"Pages the project's tracked prompts with their exact text, topic and lifecycle. Pass a prompt id as promptId to createPromptAction, listPromptActions or listVisibilityResponseHistory. By default only prompts that are on and whose topic is on are listed; includeInactive adds switched-off prompts and prompts in switched-off topics. Deleted prompts are never listed. The plan's prompt allowance can still hold back an active prompt; getProjectUsage reports active prompts. Ordered by id. Pass nextCursor back as cursor with topicId and includeInactive unchanged. This is a live listing, not a snapshot: prompts added or changed while you page can be missed or appear. Requires the `visibility.read` scope.","operationId":"listPrompts","x-devtune-required-scope":"visibility.read","parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"topicId","in":"query","description":"Only prompts in this topic, from listTopics.","schema":{"type":"string","format":"uuid"}},{"name":"includeInactive","in":"query","schema":{"type":"boolean","default":false}},{"name":"pageSize","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":50}},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":50},"description":"The same setting as pageSize. Send either; both with different values returns 400."},{"name":"cursor","in":"query","schema":{"type":"string","minLength":1,"maxLength":512}}],"responses":{"200":{"description":"One page of tracked prompts","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/TrackedPromptsPage"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Prompts and topics"]},"post":{"summary":"Create a tracked prompt","description":"Tracks a new prompt in a topic of this project, switched on. It counts against the prompt allowance: over the allowance the request returns 409 with reason prompt_limit_reached and the allowance in the message. Text already tracked returns 409; a highly similar prompt returns 409 with reason similar_prompt unless force is true. The prompt is classified and its search volume estimated in the background. Requires the `prompts.write` scope.","operationId":"createPrompt","security":[{"bearerAuth":[]},{"oauthBearer":[]}],"x-devtune-required-scope":"prompts.write","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["topicId","promptText"],"properties":{"topicId":{"type":"string","format":"uuid","description":"A topic id from listTopics."},"promptText":{"type":"string","minLength":10,"maxLength":1000,"description":"The exact question to track."},"force":{"type":"boolean","default":false,"description":"Track the prompt even though a highly similar one is already tracked."}}}}}},"responses":{"201":{"description":"The tracked prompt","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/TrackedPromptWrite"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"409":{"$ref":"#/components/responses/ConflictError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Prompts and topics"]}},"/projects/{projectId}/visibility/prompts/{promptId}":{"patch":{"summary":"Update a tracked prompt","description":"Changes a prompt's text, moves it to another topic in this project, or switches it on or off. Switching it on counts against the prompt allowance and returns 409 with reason prompt_limit_reached when it is full; send isActive true on its own, without promptText or topicId. Pass expectedActive to refuse the change with 409 and reason prompt_changed if the prompt was switched since you read it. A prompt or topic outside this project returns 404. Requires the `prompts.write` scope.","operationId":"updatePrompt","x-devtune-required-scope":"prompts.write","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}},{"name":"promptId","in":"path","required":true,"description":"A prompt id from listPrompts.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"minProperties":1,"properties":{"promptText":{"type":"string","minLength":10,"maxLength":1000,"description":"The exact question to track."},"topicId":{"type":"string","format":"uuid","description":"Move the prompt to this topic."},"isActive":{"type":"boolean","description":"Switch the prompt on or off."},"expectedActive":{"type":"boolean","description":"Refuse the change unless the prompt is still in this on/off state."},"force":{"type":"boolean","default":false,"description":"Track the prompt even though a highly similar one is already tracked."}}}}}},"responses":{"200":{"description":"The updated prompt","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/TrackedPromptWrite"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"409":{"$ref":"#/components/responses/ConflictError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Prompts and topics"]},"delete":{"summary":"Delete a tracked prompt","description":"Deletes a prompt. It stops running and frees its allowance slot at once; its history is kept and restorePrompt brings it back. Pass your own requestId to retry safely: the same requestId returns the recorded result. A prompt that is already deleted, or outside this project, returns 404. Requires the `prompts.write` scope.","operationId":"deletePrompt","x-devtune-required-scope":"prompts.write","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}},{"name":"promptId","in":"path","required":true,"description":"A prompt id from listPrompts.","schema":{"type":"string","format":"uuid"}},{"name":"requestId","in":"query","description":"Your id for this request. Sending it again returns the recorded result.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The deleted prompt","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/TrackedPromptDeletion"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"409":{"$ref":"#/components/responses/ConflictError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Prompts and topics"]}},"/projects/{projectId}/visibility/prompts/{promptId}/restore":{"post":{"summary":"Restore a deleted prompt","description":"Brings back a deleted prompt, switched off; switch it on with updatePrompt. If its topic was deleted with it, the topic comes back too. Restoring a prompt that is not deleted changes nothing and returns alreadyRestored true. Returns 409 when the same text is tracked again or its topic was deleted on its own, and 404 for a prompt outside this project. The body is optional. Requires the `prompts.write` scope.","operationId":"restorePrompt","x-devtune-required-scope":"prompts.write","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}},{"name":"promptId","in":"path","required":true,"description":"A deleted prompt id.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"requestId":{"type":"string","format":"uuid","description":"Your id for this request. Sending it again returns the recorded result."}}}}}},"responses":{"200":{"description":"The restored prompt","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/TrackedPromptRestore"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"409":{"$ref":"#/components/responses/ConflictError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Prompts and topics"]}},"/projects/{projectId}/visibility/responses":{"get":{"summary":"List recent AI responses","description":"Lists the latest 20 successful executions, optionally filtered by prompt or platform. Use an executionId with the response detail endpoint to inspect retained evidence. This operational list is not a measurement sample; use visibility summary for rates. Requires the `visibility.read` scope.","operationId":"listVisibilityResponses","x-devtune-required-scope":"visibility.read","parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"promptId","in":"query","schema":{"type":"string","format":"uuid"}},{"name":"platform","in":"query","schema":{"type":"string","minLength":1,"maxLength":100}}],"responses":{"200":{"description":"Recent successful execution identifiers","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","maxItems":20,"items":{"type":"object","properties":{"executionId":{"type":"string","format":"uuid"},"promptId":{"type":"string","format":"uuid"},"platform":{"type":"string"},"startedAt":{"type":"string","format":"date-time"},"completedAt":{"type":["string","null"],"format":"date-time","description":"When the run finished. Null when no finish time was recorded."}}}}}}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Visibility"]}},"/projects/{projectId}/visibility/responses/{executionId}":{"get":{"summary":"Get a retained AI response","description":"Reads one execution as evidence, including captured sponsored ads. Get executionId from the response history or recent responses listing. This is an entity detail read, not a measurement query. Use visibility summary for advertising rates. retained=false means no nonempty public answer or ad evidence remains; evidence may never have been captured or may have expired. Empty raw defaults do not establish retention. Requires the `visibility.read` scope.","operationId":"getVisibilityResponse","security":[{"bearerAuth":[]},{"oauthBearer":[]}],"x-devtune-required-scope":"visibility.read","parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"executionId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Retained response evidence","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","required":["executionId","promptId","platform","answerText","searchQueries","completedAt","retained","adCaptureMeasured","ads"],"properties":{"executionId":{"type":"string","format":"uuid"},"promptId":{"type":"string","format":"uuid"},"platform":{"type":"string"},"answerText":{"type":["string","null"],"description":"The answer the platform gave. Null when no answer text is kept."},"searchQueries":{"type":["array","null"],"items":{"type":"string"},"description":"Web searches the AI platform reported running for this answer, in the order it ran them. `null` when the platform doesn't report its searches; empty when it reported none."},"completedAt":{"type":["string","null"],"format":"date-time","description":"When the run finished. Null when it has not finished."},"retained":{"type":"boolean","description":"True when answer text or ad content is kept for this run."},"adCaptureMeasured":{"type":"boolean","description":"Whether this successful capture included explicit supported ad data. When false, ads were not measured for this capture."},"ads":{"type":"array","description":"Sponsored ads the platform showed with the answer, as captured. Each field is present only when the platform showed it.","items":{"type":"object","properties":{"title":{"type":"string"},"description":{"type":"string","description":"The ad’s short description."},"domain":{"type":"string"},"name":{"type":"string"},"body":{"type":"string","description":"The ad text."},"url":{"type":"string","description":"Where the ad links."}}}}}},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"description":"Invalid execution ID"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"description":"Response not found in this project"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Visibility"]}},"/projects/{projectId}/visibility/summary":{"get":{"summary":"Get visibility summary metrics","description":"Returns share of voice, presence rate, brand mentions, and citation statistics. The default scope includes only platforms currently active for the project. Requires the `visibility.read` scope.","operationId":"getVisibilitySummary","x-devtune-required-scope":"visibility.read","parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"windowDays","in":"query","schema":{"type":"integer","default":30,"enum":[30,90]},"description":"Fixed rolling window in days. Supported values are 30 and 90. Defaults to 30."},{"name":"platform","in":"query","schema":{"type":"string","minLength":1,"maxLength":100}},{"name":"topicId","in":"query","schema":{"type":"string","format":"uuid"}},{"name":"promptScope","in":"query","description":"Which topics to count: all (default), branded (only topics whose every live prompt names the project's own brand) or unbranded (every other topic, mixed ones included). Applies together with topicId.","schema":{"type":"string","enum":["all","branded","unbranded"],"default":"all"}},{"name":"includeHistoricalPlatforms","in":"query","description":"Allow a requested inactive platform to return historical data. Defaults to false.","schema":{"type":"boolean","default":false}}],"responses":{"200":{"description":"Visibility summary","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"totalExecutions":{"type":"integer","description":"AI answers measured in the window: one per prompt, platform and run."},"shareOfVoice":{"type":["number","null"],"minimum":0,"maximum":100,"description":"Percent share of primary plus tracked competitor citations attributed to the primary brand. Null when no response has usable citation data or neither was cited."},"shareOfVoiceRange":{"type":"object","description":"90% range for shareOfVoice, in percent. successes and n count primary and competitor citations. value, low and high are null when n is 0.","properties":{"value":{"type":["number","null"],"description":"The rate, in the unit of the field this range sits beside. Null when n is 0."},"successes":{"type":"integer","minimum":0,"description":"Samples that count toward the rate."},"n":{"type":"integer","minimum":0,"description":"Samples the rate is measured over."},"nEffective":{"type":"number","minimum":0,"description":"Effective sample size behind the range. Equal to n when method is wilson."},"low":{"type":["number","null"],"description":"Lower end of the 90% range. Null when n is 0."},"high":{"type":["number","null"],"description":"Upper end of the 90% range. Null when n is 0."},"confidence":{"type":"number","enum":[0.9]},"directional":{"type":"boolean","description":"True when the rate rests on fewer than 10 effective samples: too few to rely on."},"method":{"type":"string","enum":["wilson","cluster"],"description":"wilson is a Wilson score interval on n. cluster discounts correlated samples first."}},"required":["value","successes","n","nEffective","low","high","confidence","directional","method"]},"citationPresence":{"type":["number","null"],"minimum":0,"maximum":100,"description":"Percent of citation-eligible AI answers in the window that cite the primary brand or its tracked URLs. Null when no response has usable citation data."},"citationPresenceRange":{"type":"object","description":"90% range for citationPresence, in percent. successes counts citation-eligible answers that cite the primary brand and n the citation-eligible answers. value, low and high are null when n is 0.","properties":{"value":{"type":["number","null"],"description":"The rate, in the unit of the field this range sits beside. Null when n is 0."},"successes":{"type":"integer","minimum":0,"description":"Samples that count toward the rate."},"n":{"type":"integer","minimum":0,"description":"Samples the rate is measured over."},"nEffective":{"type":"number","minimum":0,"description":"Effective sample size behind the range. Equal to n when method is wilson."},"low":{"type":["number","null"],"description":"Lower end of the 90% range. Null when n is 0."},"high":{"type":["number","null"],"description":"Upper end of the 90% range. Null when n is 0."},"confidence":{"type":"number","enum":[0.9]},"directional":{"type":"boolean","description":"True when the rate rests on fewer than 10 effective samples: too few to rely on."},"method":{"type":"string","enum":["wilson","cluster"],"description":"wilson is a Wilson score interval on n. cluster discounts correlated samples first."}},"required":["value","successes","n","nEffective","low","high","confidence","directional","method"]},"brandAwareness":{"type":["number","null"],"minimum":0,"maximum":100,"description":"Percent of AI answers in the window that mention the primary brand, on a 0-100 scale. Null when no answer was measured in the window."},"brandAwarenessRange":{"type":"object","description":"90% range for brandAwareness, in percent. successes counts answers that mention the primary brand and n the answers measured. value, low and high are null when n is 0.","properties":{"value":{"type":["number","null"],"description":"The rate, in the unit of the field this range sits beside. Null when n is 0."},"successes":{"type":"integer","minimum":0,"description":"Samples that count toward the rate."},"n":{"type":"integer","minimum":0,"description":"Samples the rate is measured over."},"nEffective":{"type":"number","minimum":0,"description":"Effective sample size behind the range. Equal to n when method is wilson."},"low":{"type":["number","null"],"description":"Lower end of the 90% range. Null when n is 0."},"high":{"type":["number","null"],"description":"Upper end of the 90% range. Null when n is 0."},"confidence":{"type":"number","enum":[0.9]},"directional":{"type":"boolean","description":"True when the rate rests on fewer than 10 effective samples: too few to rely on."},"method":{"type":"string","enum":["wilson","cluster"],"description":"wilson is a Wilson score interval on n. cluster discounts correlated samples first."}},"required":["value","successes","n","nEffective","low","high","confidence","directional","method"]},"avgPrimaryPosition":{"type":["number","null"],"description":"Average position of the primary brand's citations in the answers' source lists; 1 is the first source. Null when the primary brand was not cited."},"primaryCitations":{"type":"integer","description":"Citations of the primary brand's domains in AI answers."},"competitorCitations":{"type":"integer","description":"Citations of tracked competitors' domains."},"advertising":{"type":"object","description":"Forward-only ads detected in captured responses. Missing fields and unsupported platforms are unmeasured. Rates use measuredResponses as the denominator, independently of organic Presence.","properties":{"responses":{"type":"integer","description":"Successful AI answers in the window."},"measuredResponses":{"type":"integer","description":"Of responses, the answers whose ads were captured and checked."},"primaryResponses":{"type":"integer","description":"Measured answers with an ad for the primary brand."},"competitorResponses":{"type":"integer","description":"Measured answers with an ad for a tracked competitor."},"adResponses":{"type":"integer","description":"Measured answers with any ad."},"primaryRate":{"type":["number","null"],"minimum":0,"maximum":1,"description":"Share of measured responses with an ad for the primary brand, as a fraction from 0 to 1. Null when measuredResponses is 0."},"primaryRateRange":{"type":"object","description":"90% range for primaryRate, as a 0-1 fraction. n is measuredResponses. value, low and high are null when n is 0.","properties":{"value":{"type":["number","null"],"description":"The rate, in the unit of the field this range sits beside. Null when n is 0."},"successes":{"type":"integer","minimum":0,"description":"Samples that count toward the rate."},"n":{"type":"integer","minimum":0,"description":"Samples the rate is measured over."},"nEffective":{"type":"number","minimum":0,"description":"Effective sample size behind the range. Equal to n when method is wilson."},"low":{"type":["number","null"],"description":"Lower end of the 90% range. Null when n is 0."},"high":{"type":["number","null"],"description":"Upper end of the 90% range. Null when n is 0."},"confidence":{"type":"number","enum":[0.9]},"directional":{"type":"boolean","description":"True when the rate rests on fewer than 10 effective samples: too few to rely on."},"method":{"type":"string","enum":["wilson","cluster"],"description":"wilson is a Wilson score interval on n. cluster discounts correlated samples first."}},"required":["value","successes","n","nEffective","low","high","confidence","directional","method"]},"competitorRate":{"type":["number","null"],"minimum":0,"maximum":1,"description":"Share of measured responses with an ad for a tracked competitor, as a fraction from 0 to 1. Null when measuredResponses is 0."},"competitorRateRange":{"type":"object","description":"90% range for competitorRate, as a 0-1 fraction. n is measuredResponses. value, low and high are null when n is 0.","properties":{"value":{"type":["number","null"],"description":"The rate, in the unit of the field this range sits beside. Null when n is 0."},"successes":{"type":"integer","minimum":0,"description":"Samples that count toward the rate."},"n":{"type":"integer","minimum":0,"description":"Samples the rate is measured over."},"nEffective":{"type":"number","minimum":0,"description":"Effective sample size behind the range. Equal to n when method is wilson."},"low":{"type":["number","null"],"description":"Lower end of the 90% range. Null when n is 0."},"high":{"type":["number","null"],"description":"Upper end of the 90% range. Null when n is 0."},"confidence":{"type":"number","enum":[0.9]},"directional":{"type":"boolean","description":"True when the rate rests on fewer than 10 effective samples: too few to rely on."},"method":{"type":"string","enum":["wilson","cluster"],"description":"wilson is a Wilson score interval on n. cluster discounts correlated samples first."}},"required":["value","successes","n","nEffective","low","high","confidence","directional","method"]}}},"measurement":{"type":"object","description":"Which platforms the figures cover, and whether they were measured.","properties":{"scope":{"type":"string","enum":["current_active","requested"],"description":"current_active: the project's enabled platforms. requested: the platform the request named."},"requestedPlatform":{"type":["string","null"],"description":"The platform the request named. Null when it named none."},"configurationStatus":{"type":"string","enum":["active_set","active","inactive","not_configured","unknown"],"description":"active_set when no platform was requested. For a requested platform: active (enabled for the project), inactive (in the project's platform settings but turned off) or not_configured (not in them). unknown when it could not be read."},"dataStatus":{"type":"string","description":"Measurement state for the requested scope. no_data means the platform was not measured in the window.","enum":["measured","no_data","historical","historical_available"]},"historicalDataAvailable":{"type":"boolean","description":"True when dataStatus is historical or historical_available: the requested platform has measured answers in the window, but it is not enabled now or the topic filters leave all of them out."},"activePlatforms":{"type":"array","description":"Platforms currently enabled for this project. Use this list for current per-platform reporting.","items":{"type":"string"}},"measuredPlatforms":{"type":"array","description":"Platforms with execution data in the window, including inactive historical platforms.","items":{"type":"string"}},"reportablePlatforms":{"type":"array","description":"Currently enabled platforms with measured execution data in the window. Use this list for current per-platform reporting; configured onboarding platforms without measurements are excluded.","items":{"type":"string"}}}},"platformCadences":{"type":["array","null"],"description":"Cadence of each platform in scope. Platforms with inDailySeries false are checked less often than daily: window totals include them, and timeSeries leaves them out unless they are the only platforms in scope. Null when cadence could not be read.","items":{"type":"object","properties":{"platform":{"type":"string"},"cadenceDays":{"type":"integer","minimum":1,"description":"Days between scheduled checks."},"inDailySeries":{"type":"boolean","description":"Whether the platform joins daily cross-platform totals."},"lastCheckedOn":{"type":["string","null"],"format":"date","description":"Last date with responses, for platforms outside the daily series. Null for daily platforms, and for weekly platforms not checked in the last year."}},"required":["platform","cadenceDays","inDailySeries","lastCheckedOn"]}}}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Visibility"]}},"/projects/{projectId}/visibility/sentiment":{"get":{"summary":"Get brand sentiment","description":"Returns the sentiment of AI answers towards the project's primary brand over a fixed window of completed UTC days ending yesterday: overall and for each day, across every platform. Each answer that mentions the primary brand is scored from -1 to 1 for its stance towards that brand. A language model scores each answer. The score is about the primary brand only, never competitors, and answers that do not mention it are not scored. Each answer counts once, including answers whose brand has since stopped being primary. There is no per-platform breakdown. Days before 20 September 2026 are withheld. overall leaves withheld days out, window.dataFrom is the first day it counts and overall.coverage.daysWithheld how many days it left out; a withheld trend day has every figure null. averageScore and label are null when no answer was scored, never 0. coverage says how many answers were in the window and how many were scored. Requires the `visibility.read` scope.","operationId":"getBrandSentiment","security":[{"bearerAuth":[]},{"oauthBearer":[]}],"x-devtune-required-scope":"visibility.read","parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"windowDays","in":"query","schema":{"type":"integer","default":30,"enum":[30,90]},"description":"Fixed rolling window in days. Supported values are 30 and 90. Defaults to 30."}],"responses":{"200":{"description":"Brand sentiment","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"window":{"type":"object","description":"The window of completed UTC days.","properties":{"windowDays":{"type":"integer","enum":[30,90]},"dateFrom":{"type":"string","format":"date","description":"First UTC day of the window."},"dateTo":{"type":"string","format":"date","description":"Last UTC day of the window: yesterday."},"dataFrom":{"type":"string","format":"date","description":"First day overall counts: the later of dateFrom and 2026-09-20."}},"required":["windowDays","dateFrom","dateTo","dataFrom"]},"overall":{"allOf":[{"$ref":"#/components/schemas/BrandSentiment"},{"type":"object","properties":{"coverage":{"type":"object","description":"How many answers there were and how many were scored, plus the days left out.","properties":{"daysWithheld":{"type":"integer","minimum":0,"description":"Days in the window before 2026-09-20, left out of overall."}},"required":["daysWithheld"]}}}]},"trend":{"type":"array","description":"One row per day in the window.","items":{"allOf":[{"$ref":"#/components/schemas/BrandSentiment"},{"type":"object","properties":{"date":{"type":"string","format":"date","description":"The UTC day."}},"required":["date"]}]}}},"required":["window","overall","trend"]},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Visibility"]}},"/projects/{projectId}/timeline/events":{"get":{"summary":"Get project timeline events","description":"Returns project changes and sourced global facts as context for interpreting Visibility trends. Global facts are limited to all-platform events or platforms enabled for the project. Events do not imply a causal link to metric movements. Requires the `visibility.read` scope.","operationId":"getTimelineEvents","x-devtune-required-scope":"visibility.read","parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"windowDays","in":"query","schema":{"type":"integer","default":30,"enum":[30,90]},"description":"Fixed rolling window in days. Supported values are 30 and 90. Defaults to 30."}],"responses":{"200":{"description":"Project timeline events","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"window":{"type":"object","description":"The days the events fall in.","properties":{"days":{"type":"integer","enum":[30,90]},"from":{"type":"string","format":"date","description":"First day of the window."},"to":{"type":"string","format":"date","description":"Last day of the window, included."}},"required":["days","from","to"]},"events":{"type":"array","maxItems":500,"description":"Events in the window, newest first. They give context; none implies a cause of a metric movement.","items":{"$ref":"#/components/schemas/TimelineEvent"}},"coverage":{"type":"object","description":"Whether the response reached the event cap. A truncated response omits the oldest events in the window.","properties":{"limit":{"type":"integer","example":500,"description":"The most events one response returns."},"truncated":{"type":"boolean","example":false,"description":"True when the response holds limit events, so older events in the window may be left out."}},"required":["limit","truncated"]}},"required":["window","coverage","events"]},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Visibility"]}},"/projects/{projectId}/citations/stats":{"get":{"summary":"Get aggregate citation statistics","description":"Returns aggregate citation totals for a fixed 30- or 90-day window. Coverage tells data that is not ready yet apart from a measured zero. Use top-pages and top-domains for citation lists. Requires the `citations.read` scope.","operationId":"getCitationStats","security":[{"bearerAuth":[]},{"oauthBearer":[]}],"x-devtune-required-scope":"citations.read","parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"windowDays","in":"query","schema":{"type":"integer","default":30,"enum":[30,90]},"description":"Fixed rolling window in days. Supported values are 30 and 90. Defaults to 30."}],"responses":{"200":{"description":"Aggregate citation statistics","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"stats":{"type":["object","null"],"description":"Aggregate citation metrics, or null when coverage.status is unavailable.","properties":{"totalCitations":{"type":"integer","description":"Citations in AI answers in the window."},"uniquePages":{"type":"integer","description":"Distinct pages cited."},"uniqueDomains":{"type":"integer","description":"Distinct domains cited."},"avgPosition":{"type":["number","null"],"description":"Null when no citation in the window has a position."},"top3Count":{"type":"integer","description":"Citations in the first three positions of an answer’s sources."},"primaryCitations":{"type":"integer","description":"Citations of the primary brand's domains in AI answers."},"competitorCitations":{"type":"integer","description":"Citations of tracked competitors' domains."},"otherCitations":{"type":"integer","description":"Citations of every other source, such as forums, news and documentation sites."}},"required":["totalCitations","uniquePages","uniqueDomains","avgPosition","top3Count","primaryCitations","competitorCitations","otherCitations"]},"coverage":{"type":"object","description":"Whether the window has been measured. While it is unavailable, report the window as unmeasured, including when the list is empty.","properties":{"status":{"type":"string","enum":["available","unavailable"],"description":"available when the window has been measured, unavailable when it has not yet."},"windowDays":{"type":"integer"}},"required":["status","windowDays"]},"window":{"type":"object","description":"The fixed window the figures cover, in UTC days.","properties":{"days":{"type":"integer"},"dateFrom":{"type":"string","format":"date","description":"First day of the window."},"dateTo":{"type":"string","format":"date","description":"Last day of the window, included."}},"required":["days","dateFrom","dateTo"]}},"required":["stats","coverage","window"]},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Citations"]}},"/projects/{projectId}/citations/top-domains":{"get":{"summary":"Get top citation domains","description":"Returns top citation domains for a fixed 30- or 90-day window. Coverage tells data that is not ready yet apart from a measured zero. Pass topicId, promptId or platform to count only the citations from that topic, tracked prompt or AI platform; filters combine and apply before ranking and paging. A filtered list reports its own coverage, which can be unavailable while the unfiltered list is available: filtered data is ready only when every run in the window was recorded with its prompt, topic and platform, and its figures agree with the unfiltered list for the window. Report an unavailable list as unmeasured. An unknown, deleted or other-project promptId or topicId returns 404. Continue a list only with the window, search and filters unchanged; a cursor used with any of them changed, or a filtered list's cursor after the window's data was updated, returns 400, so start again without a cursor. Filtered lists page through their first 100,000 rows; a page with rows beyond that returns 400. A filtered list classifies each domain, including its sourceVertical, as of the window's last refresh, and its window is the one that refresh covered, so its dateTo can be earlier than today. Requires the `citations.read` scope.","operationId":"getTopCitationDomains","x-devtune-required-scope":"citations.read","parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"windowDays","in":"query","schema":{"type":"integer","default":30,"enum":[30,90]},"description":"Fixed rolling window in days. Supported values are 30 and 90. Defaults to 30."},{"name":"cursor","in":"query","schema":{"type":"string"},"description":"Opaque cursor from the previous response."},{"name":"pageSize","in":"query","schema":{"type":"integer","default":25,"minimum":1,"maximum":100},"description":"Results per page. Capped at 100."},{"name":"limit","in":"query","schema":{"type":"integer","default":25,"minimum":1,"maximum":100},"description":"The same setting as pageSize. Send either; both with different values returns 400."},{"name":"search","in":"query","schema":{"type":"string"},"description":"Optional prefix search within canonical domains."},{"name":"sourceVertical","in":"query","schema":{"type":"string","enum":["community","social","editorial","reference","government","education","commercial","video","marketplace","nonprofit","other"]},"description":"Optional source vertical scope. `community` covers Reddit, Hacker News, GitHub, Stack Overflow, Stack Exchange and Dev.to, including their subdomains."},{"name":"topicId","in":"query","schema":{"type":"string","format":"uuid"},"description":"Only citations recorded under this topic, by the id the topics list returns. A prompt moved to another topic keeps its earlier citations under the earlier topic."},{"name":"promptId","in":"query","schema":{"type":"string","format":"uuid"},"description":"Only citations from this tracked prompt, by the id the prompts list returns."},{"name":"platform","in":"query","schema":{"type":"string","minLength":1,"maxLength":100},"description":"Only citations from this AI platform: `chatgpt-search`, `perplexity`, `google-ai`, `google-ai-mode` or `bing-copilot-search`, plus `claude` and `xai-search` where the account tracks them."}],"responses":{"200":{"description":"Top citation domains","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"domains":{"type":"array","maxItems":100,"description":"Cited domains, most cited first.","items":{"type":"object","description":"One cited domain and its citations in the window.","properties":{"domain":{"type":"string"},"classification":{"type":["string","null"],"description":"primary for a domain of the primary brand, competitor for a tracked competitor’s, other for any other source, mixed when the domain’s citations in the window fall in more than one of these."},"citationCount":{"type":"integer","minimum":0,"description":"Citations of the domain in the window."},"uniqueUrls":{"type":"integer","minimum":0,"description":"Distinct URLs on the domain that were cited."},"avgPosition":{"type":["number","null"],"minimum":0,"description":"Average position in the answers' source lists, to one decimal; 1 is the first source. Null when no citation has a position."},"top3Count":{"type":"integer","minimum":0,"description":"Citations in the first three positions of an answer’s sources."},"primaryCount":{"type":"integer","minimum":0,"description":"Of citationCount, citations classified primary."},"competitorCount":{"type":"integer","minimum":0,"description":"Of citationCount, citations classified competitor."},"otherCount":{"type":"integer","minimum":0,"description":"Of citationCount, citations classified other."},"sourceVertical":{"type":["string","null"],"description":"The kind of source, such as community."}},"required":["domain","classification","citationCount","uniqueUrls","avgPosition","top3Count","primaryCount","competitorCount","otherCount","sourceVertical"]}},"coverage":{"type":"object","description":"Whether the window has been measured. While it is unavailable, report the window as unmeasured, including when the list is empty.","properties":{"status":{"type":"string","enum":["available","unavailable"],"description":"available when the window has been measured and, with a prompt, topic or platform filter, published for that filter; unavailable otherwise."},"windowDays":{"type":"integer"}},"required":["status","windowDays"]},"pagination":{"$ref":"#/components/schemas/CursorPagination"},"window":{"type":"object","description":"The fixed window the figures cover, in UTC days.","properties":{"days":{"type":"integer"},"dateFrom":{"type":"string","format":"date","description":"First day of the window."},"dateTo":{"type":"string","format":"date","description":"Last day of the window, included."}},"required":["days","dateFrom","dateTo"]}},"required":["domains","coverage","pagination","window"]},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Citations"]}},"/projects/{projectId}/citations/top-pages":{"get":{"summary":"Get top citation pages","description":"Returns top citation pages for a fixed 30- or 90-day window. Coverage tells data that is not ready yet apart from a measured zero. Pass topicId, promptId or platform to count only the citations from that topic, tracked prompt or AI platform; filters combine and apply before ranking and paging. A filtered list reports its own coverage, which can be unavailable while the unfiltered list is available: filtered data is ready only when every run in the window was recorded with its prompt, topic and platform, and its figures agree with the unfiltered list for the window. Report an unavailable list as unmeasured. An unknown, deleted or other-project promptId or topicId returns 404. Continue a list only with the window, search and filters unchanged; a cursor used with any of them changed, or a filtered list's cursor after the window's data was updated, returns 400, so start again without a cursor. Filtered lists page through their first 100,000 rows; a page with rows beyond that returns 400. A filtered list classifies each domain, including its sourceVertical, as of the window's last refresh, and its window is the one that refresh covered, so its dateTo can be earlier than today. Requires the `citations.read` scope.","operationId":"getTopCitationPages","x-devtune-required-scope":"citations.read","parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"windowDays","in":"query","schema":{"type":"integer","default":30,"enum":[30,90]},"description":"Fixed rolling window in days. Supported values are 30 and 90. Defaults to 30."},{"name":"cursor","in":"query","schema":{"type":"string"},"description":"Opaque cursor from the previous response."},{"name":"pageSize","in":"query","schema":{"type":"integer","default":25,"minimum":1,"maximum":100},"description":"Results per page. Capped at 100."},{"name":"limit","in":"query","schema":{"type":"integer","default":25,"minimum":1,"maximum":100},"description":"The same setting as pageSize. Send either; both with different values returns 400."},{"name":"search","in":"query","schema":{"type":"string"},"description":"Optional prefix search within citation URLs and canonical domains."},{"name":"sourceVertical","in":"query","schema":{"type":"string","enum":["community","social","editorial","reference","government","education","commercial","video","marketplace","nonprofit","other"]},"description":"Optional source vertical scope. `community` covers Reddit, Hacker News, GitHub, Stack Overflow, Stack Exchange and Dev.to, including their subdomains."},{"name":"topicId","in":"query","schema":{"type":"string","format":"uuid"},"description":"Only citations recorded under this topic, by the id the topics list returns. A prompt moved to another topic keeps its earlier citations under the earlier topic."},{"name":"promptId","in":"query","schema":{"type":"string","format":"uuid"},"description":"Only citations from this tracked prompt, by the id the prompts list returns."},{"name":"platform","in":"query","schema":{"type":"string","minLength":1,"maxLength":100},"description":"Only citations from this AI platform: `chatgpt-search`, `perplexity`, `google-ai`, `google-ai-mode` or `bing-copilot-search`, plus `claude` and `xai-search` where the account tracks them."}],"responses":{"200":{"description":"Top citation pages","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"pages":{"type":"array","maxItems":100,"description":"Cited pages, most cited first.","items":{"type":"object","description":"One cited page and its citations in the window.","properties":{"url":{"type":"string"},"domain":{"type":["string","null"],"description":"The URL’s domain."},"title":{"type":["string","null"],"description":"The page title. Null when none was captured."},"classification":{"type":["string","null"],"description":"primary for a domain of the primary brand, competitor for a tracked competitor’s, other for any other source."},"sourceVertical":{"type":["string","null"],"description":"The kind of source, such as community."},"contentType":{"type":["string","null"],"description":"The kind of page, such as documentation, blog_post, comparison or discussion. Null when not classified."},"count":{"type":"integer","minimum":0,"description":"Citations of the page in the window."},"avgPosition":{"type":["number","null"],"minimum":0,"description":"Average position in the answers' source lists, to one decimal; 1 is the first source. Null when no citation has a position."},"top3Count":{"type":"integer","minimum":0,"description":"Citations in the first three positions of an answer’s sources."}},"required":["url","domain","title","classification","sourceVertical","contentType","count","avgPosition","top3Count"]}},"coverage":{"type":"object","description":"Whether the window has been measured. While it is unavailable, report the window as unmeasured, including when the list is empty.","properties":{"status":{"type":"string","enum":["available","unavailable"],"description":"available when the window has been measured and, with a prompt, topic or platform filter, published for that filter; unavailable otherwise."},"windowDays":{"type":"integer"}},"required":["status","windowDays"]},"pagination":{"$ref":"#/components/schemas/CursorPagination"},"window":{"type":"object","description":"The fixed window the figures cover, in UTC days.","properties":{"days":{"type":"integer"},"dateFrom":{"type":"string","format":"date","description":"First day of the window."},"dateTo":{"type":"string","format":"date","description":"Last day of the window, included."}},"required":["days","dateFrom","dateTo"]}},"required":["pages","coverage","pagination","window"]},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Citations"]}},"/projects/{projectId}/lookalike-sites":{"get":{"summary":"List Lookalike sites","description":"Lists the hosts AI answers cited in a 7, 30 or 90 day window whose domain imitates one of the project’s own domains, most cited first, with how many answers cited each out of the answers checked, overall and per platform. Requires the `citations.read` scope.","operationId":"listLookalikeSites","x-devtune-required-scope":"citations.read","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}},{"name":"window","in":"query","description":"Fixed window in days: 7, 30 or 90. Defaults to 30.","schema":{"type":"integer","enum":[7,30,90],"default":30}}],"responses":{"200":{"description":"Lookalike sites","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/LookalikeSitesData"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Citations"]}},"/projects/{projectId}/lookalike-sites/{hostId}/citations":{"get":{"summary":"List Lookalike site citations","description":"Lists the answers that cited one lookalike host, newest run day first, with the cited URL, how the answer framed the link and the text around it. Returns 404 for a host outside the project. Requires the `citations.read` scope.","operationId":"listLookalikeSiteCitations","x-devtune-required-scope":"citations.read","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}},{"name":"hostId","in":"path","required":true,"description":"id from listLookalikeSites.","schema":{"type":"string","format":"uuid"}},{"name":"window","in":"query","description":"Only answers from the last 7, 30 or 90 run days, matching listLookalikeSites. Omit for every retained answer.","schema":{"type":"integer","enum":[7,30,90]}},{"name":"cursor","in":"query","description":"next_cursor from the previous page, verbatim.","schema":{"type":"string","minLength":1,"maxLength":2000}},{"name":"limit","in":"query","description":"Rows per page, 1 to 100. Defaults to 25.","schema":{"type":"integer","minimum":1,"maximum":100,"default":25}},{"name":"pageSize","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":25},"description":"The same setting as limit. Send either; both with different values returns 400."}],"responses":{"200":{"description":"Lookalike site citations","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/LookalikeSiteCitationsData"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Citations"]}},"/projects/{projectId}/visibility/diff":{"get":{"summary":"Get visibility diff","description":"Compares the current visibility window with the previous same-length window. comparisons says whether each share moved clearly; report a change only when comparison is up or down. Requires the `visibility.read` scope.","operationId":"getVisibilityDiff","x-devtune-required-scope":"visibility.read","parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"windowDays","in":"query","schema":{"type":"integer","default":30,"enum":[30,90]},"description":"Fixed rolling window in days. Supported values are 30 and 90. Defaults to 30."},{"name":"platform","in":"query","schema":{"type":"string"}},{"name":"topicId","in":"query","schema":{"type":"string","format":"uuid"}},{"name":"promptScope","in":"query","description":"Which topics to count: all (default), branded (only topics whose every live prompt names the project's own brand) or unbranded (every other topic, mixed ones included). Applies together with topicId.","schema":{"type":"string","enum":["all","branded","unbranded"],"default":"all"}}],"responses":{"200":{"description":"Visibility diff","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"current":{"type":"object","description":"One period of the comparison, in UTC days.","properties":{"dateFrom":{"type":"string","format":"date","description":"First day of the period."},"dateTo":{"type":"string","format":"date","description":"Last day of the period, included."},"totals":{"type":"object","description":"Citation counts and shares for the period.","properties":{"primaryCitations":{"type":"integer","minimum":0,"description":"Citations of the primary brand's domains in AI answers."},"competitorCitations":{"type":"integer","minimum":0,"description":"Citations of tracked competitors' domains."},"otherCitations":{"type":"integer","minimum":0,"description":"Citations of every other source, such as forums, news and documentation sites."},"total":{"type":"integer","minimum":0,"description":"primaryCitations plus competitorCitations plus otherCitations."},"primaryShare":{"type":["number","null"],"minimum":0,"maximum":100,"description":"Primary brand share of all citations in the period, as a percentage from 0 to 100: primary, competitor and other citations. Null when the period has no citations."},"primaryShareRange":{"type":"object","description":"90% range for the share, in percent. successes and n count citations. value, low and high are null when n is 0.","properties":{"value":{"type":["number","null"],"description":"The rate, in the unit of the field this range sits beside. Null when n is 0."},"successes":{"type":"integer","minimum":0,"description":"Samples that count toward the rate."},"n":{"type":"integer","minimum":0,"description":"Samples the rate is measured over."},"nEffective":{"type":"number","minimum":0,"description":"Effective sample size behind the range. Equal to n when method is wilson."},"low":{"type":["number","null"],"description":"Lower end of the 90% range. Null when n is 0."},"high":{"type":["number","null"],"description":"Upper end of the 90% range. Null when n is 0."},"confidence":{"type":"number","enum":[0.9]},"directional":{"type":"boolean","description":"True when the rate rests on fewer than 10 effective samples: too few to rely on."},"method":{"type":"string","enum":["wilson","cluster"],"description":"wilson is a Wilson score interval on n. cluster discounts correlated samples first."}},"required":["value","successes","n","nEffective","low","high","confidence","directional","method"]},"primaryShareOfAllCitations":{"type":["number","null"],"minimum":0,"maximum":100,"description":"The same value as primaryShare; use primaryShare. Null when the period has no citations."},"primaryShareOfAllCitationsRange":{"type":"object","description":"90% range for the share, in percent. successes and n count citations. value, low and high are null when n is 0.","properties":{"value":{"type":["number","null"],"description":"The rate, in the unit of the field this range sits beside. Null when n is 0."},"successes":{"type":"integer","minimum":0,"description":"Samples that count toward the rate."},"n":{"type":"integer","minimum":0,"description":"Samples the rate is measured over."},"nEffective":{"type":"number","minimum":0,"description":"Effective sample size behind the range. Equal to n when method is wilson."},"low":{"type":["number","null"],"description":"Lower end of the 90% range. Null when n is 0."},"high":{"type":["number","null"],"description":"Upper end of the 90% range. Null when n is 0."},"confidence":{"type":"number","enum":[0.9]},"directional":{"type":"boolean","description":"True when the rate rests on fewer than 10 effective samples: too few to rely on."},"method":{"type":"string","enum":["wilson","cluster"],"description":"wilson is a Wilson score interval on n. cluster discounts correlated samples first."}},"required":["value","successes","n","nEffective","low","high","confidence","directional","method"]},"shareOfVoice":{"type":["number","null"],"minimum":0,"maximum":100,"description":"Primary brand share of citations attributed to tracked primary and competitor brands. Null when neither was cited."},"shareOfVoiceRange":{"type":"object","description":"90% range for the share, in percent. successes and n count citations. value, low and high are null when n is 0.","properties":{"value":{"type":["number","null"],"description":"The rate, in the unit of the field this range sits beside. Null when n is 0."},"successes":{"type":"integer","minimum":0,"description":"Samples that count toward the rate."},"n":{"type":"integer","minimum":0,"description":"Samples the rate is measured over."},"nEffective":{"type":"number","minimum":0,"description":"Effective sample size behind the range. Equal to n when method is wilson."},"low":{"type":["number","null"],"description":"Lower end of the 90% range. Null when n is 0."},"high":{"type":["number","null"],"description":"Upper end of the 90% range. Null when n is 0."},"confidence":{"type":"number","enum":[0.9]},"directional":{"type":"boolean","description":"True when the rate rests on fewer than 10 effective samples: too few to rely on."},"method":{"type":"string","enum":["wilson","cluster"],"description":"wilson is a Wilson score interval on n. cluster discounts correlated samples first."}},"required":["value","successes","n","nEffective","low","high","confidence","directional","method"]}},"required":["primaryCitations","competitorCitations","otherCitations","total","primaryShare","primaryShareRange","primaryShareOfAllCitations","primaryShareOfAllCitationsRange","shareOfVoice","shareOfVoiceRange"]}},"required":["dateFrom","dateTo","totals"]},"previous":{"type":"object","description":"One period of the comparison, in UTC days.","properties":{"dateFrom":{"type":"string","format":"date","description":"First day of the period."},"dateTo":{"type":"string","format":"date","description":"Last day of the period, included."},"totals":{"type":"object","description":"Citation counts and shares for the period.","properties":{"primaryCitations":{"type":"integer","minimum":0,"description":"Citations of the primary brand's domains in AI answers."},"competitorCitations":{"type":"integer","minimum":0,"description":"Citations of tracked competitors' domains."},"otherCitations":{"type":"integer","minimum":0,"description":"Citations of every other source, such as forums, news and documentation sites."},"total":{"type":"integer","minimum":0,"description":"primaryCitations plus competitorCitations plus otherCitations."},"primaryShare":{"type":["number","null"],"minimum":0,"maximum":100,"description":"Primary brand share of all citations in the period, as a percentage from 0 to 100: primary, competitor and other citations. Null when the period has no citations."},"primaryShareRange":{"type":"object","description":"90% range for the share, in percent. successes and n count citations. value, low and high are null when n is 0.","properties":{"value":{"type":["number","null"],"description":"The rate, in the unit of the field this range sits beside. Null when n is 0."},"successes":{"type":"integer","minimum":0,"description":"Samples that count toward the rate."},"n":{"type":"integer","minimum":0,"description":"Samples the rate is measured over."},"nEffective":{"type":"number","minimum":0,"description":"Effective sample size behind the range. Equal to n when method is wilson."},"low":{"type":["number","null"],"description":"Lower end of the 90% range. Null when n is 0."},"high":{"type":["number","null"],"description":"Upper end of the 90% range. Null when n is 0."},"confidence":{"type":"number","enum":[0.9]},"directional":{"type":"boolean","description":"True when the rate rests on fewer than 10 effective samples: too few to rely on."},"method":{"type":"string","enum":["wilson","cluster"],"description":"wilson is a Wilson score interval on n. cluster discounts correlated samples first."}},"required":["value","successes","n","nEffective","low","high","confidence","directional","method"]},"primaryShareOfAllCitations":{"type":["number","null"],"minimum":0,"maximum":100,"description":"The same value as primaryShare; use primaryShare. Null when the period has no citations."},"primaryShareOfAllCitationsRange":{"type":"object","description":"90% range for the share, in percent. successes and n count citations. value, low and high are null when n is 0.","properties":{"value":{"type":["number","null"],"description":"The rate, in the unit of the field this range sits beside. Null when n is 0."},"successes":{"type":"integer","minimum":0,"description":"Samples that count toward the rate."},"n":{"type":"integer","minimum":0,"description":"Samples the rate is measured over."},"nEffective":{"type":"number","minimum":0,"description":"Effective sample size behind the range. Equal to n when method is wilson."},"low":{"type":["number","null"],"description":"Lower end of the 90% range. Null when n is 0."},"high":{"type":["number","null"],"description":"Upper end of the 90% range. Null when n is 0."},"confidence":{"type":"number","enum":[0.9]},"directional":{"type":"boolean","description":"True when the rate rests on fewer than 10 effective samples: too few to rely on."},"method":{"type":"string","enum":["wilson","cluster"],"description":"wilson is a Wilson score interval on n. cluster discounts correlated samples first."}},"required":["value","successes","n","nEffective","low","high","confidence","directional","method"]},"shareOfVoice":{"type":["number","null"],"minimum":0,"maximum":100,"description":"Primary brand share of citations attributed to tracked primary and competitor brands. Null when neither was cited."},"shareOfVoiceRange":{"type":"object","description":"90% range for the share, in percent. successes and n count citations. value, low and high are null when n is 0.","properties":{"value":{"type":["number","null"],"description":"The rate, in the unit of the field this range sits beside. Null when n is 0."},"successes":{"type":"integer","minimum":0,"description":"Samples that count toward the rate."},"n":{"type":"integer","minimum":0,"description":"Samples the rate is measured over."},"nEffective":{"type":"number","minimum":0,"description":"Effective sample size behind the range. Equal to n when method is wilson."},"low":{"type":["number","null"],"description":"Lower end of the 90% range. Null when n is 0."},"high":{"type":["number","null"],"description":"Upper end of the 90% range. Null when n is 0."},"confidence":{"type":"number","enum":[0.9]},"directional":{"type":"boolean","description":"True when the rate rests on fewer than 10 effective samples: too few to rely on."},"method":{"type":"string","enum":["wilson","cluster"],"description":"wilson is a Wilson score interval on n. cluster discounts correlated samples first."}},"required":["value","successes","n","nEffective","low","high","confidence","directional","method"]}},"required":["primaryCitations","competitorCitations","otherCitations","total","primaryShare","primaryShareRange","primaryShareOfAllCitations","primaryShareOfAllCitationsRange","shareOfVoice","shareOfVoiceRange"]}},"required":["dateFrom","dateTo","totals"]},"delta":{"type":"object","description":"Current period minus previous period.","properties":{"primaryCitations":{"type":"integer","description":"Change in primary brand citations."},"competitorCitations":{"type":"integer","description":"Change in tracked competitor citations."},"otherCitations":{"type":"integer","description":"Change in citations of other sources."},"primaryShare":{"type":["number","null"],"description":"Percentage-point change in primary citation share. Null when either window has no citations. Read comparisons.primaryShare before reporting it as a change."},"shareOfVoice":{"type":["number","null"],"description":"Percentage-point change in share of voice. Null when either window has no primary or competitor citations. Read comparisons.shareOfVoice before reporting it as a change."}},"required":["primaryCitations","competitorCitations","otherCitations","primaryShare","shareOfVoice"]},"comparisons":{"type":"object","description":"Whether each share moved clearly between the windows, judged on a 90% interval on the difference.","properties":{"primaryShare":{"type":"object","description":"Whether the rate moved clearly between the previous and current window. Report a change only when comparison is up or down. difference, low and high are the change and its 90% interval in percentage points, null when not_comparable.","properties":{"comparison":{"type":"string","enum":["up","down","no_clear_change","not_comparable"],"description":"up or down when the 90% interval on the difference excludes zero; no_clear_change when it includes zero; not_comparable when a window cannot be compared."},"notComparableReason":{"type":["string","null"],"enum":["unmeasured",null],"description":"unmeasured when either window has no citations to compare. Null when comparison is not not_comparable."},"difference":{"type":["number","null"],"description":"Current rate minus previous rate, in percentage points. Null when comparison is not_comparable."},"low":{"type":["number","null"],"description":"Lower end of the 90% interval on difference. Null when comparison is not_comparable."},"high":{"type":["number","null"],"description":"Upper end of the 90% interval on difference. Null when comparison is not_comparable."},"confidence":{"type":"number","enum":[0.9]}},"required":["comparison","notComparableReason","difference","low","high","confidence"]},"shareOfVoice":{"type":"object","description":"Whether the rate moved clearly between the previous and current window. Report a change only when comparison is up or down. difference, low and high are the change and its 90% interval in percentage points, null when not_comparable.","properties":{"comparison":{"type":"string","enum":["up","down","no_clear_change","not_comparable"],"description":"up or down when the 90% interval on the difference excludes zero; no_clear_change when it includes zero; not_comparable when a window cannot be compared."},"notComparableReason":{"type":["string","null"],"enum":["unmeasured",null],"description":"unmeasured when either window has no citations to compare. Null when comparison is not not_comparable."},"difference":{"type":["number","null"],"description":"Current rate minus previous rate, in percentage points. Null when comparison is not_comparable."},"low":{"type":["number","null"],"description":"Lower end of the 90% interval on difference. Null when comparison is not_comparable."},"high":{"type":["number","null"],"description":"Upper end of the 90% interval on difference. Null when comparison is not_comparable."},"confidence":{"type":"number","enum":[0.9]}},"required":["comparison","notComparableReason","difference","low","high","confidence"]}},"required":["primaryShare","shareOfVoice"]},"timeSeries":{"type":"array","description":"Daily points for platforms in the daily series. Period totals include every platform.","items":{"type":"object","description":"Citation counts for one UTC day.","properties":{"date":{"type":"string","format":"date","description":"The UTC day."},"primaryCitations":{"type":"integer","minimum":0,"description":"Citations of the primary brand's domains in AI answers."},"competitorCitations":{"type":"integer","minimum":0,"description":"Citations of tracked competitors' domains."},"otherCitations":{"type":"integer","minimum":0,"description":"Citations of every other source, such as forums, news and documentation sites."}},"required":["date","primaryCitations","competitorCitations","otherCitations"]}},"platformCadences":{"type":["array","null"],"description":"Cadence of each platform in scope. Platforms with inDailySeries false are checked less often than daily: window totals include them, and timeSeries leaves them out unless they are the only platforms in scope. Null when cadence could not be read.","items":{"type":"object","properties":{"platform":{"type":"string"},"cadenceDays":{"type":"integer","minimum":1,"description":"Days between scheduled checks."},"inDailySeries":{"type":"boolean","description":"Whether the platform joins daily cross-platform totals."},"lastCheckedOn":{"type":["string","null"],"format":"date","description":"Last date with responses, for platforms outside the daily series. Null for daily platforms, and for weekly platforms not checked in the last year."}},"required":["platform","cadenceDays","inDailySeries","lastCheckedOn"]}},"window":{"type":"object","description":"The fixed window the figures cover, in UTC days.","properties":{"days":{"type":"integer"},"dateFrom":{"type":"string","format":"date","description":"First day of the window."},"dateTo":{"type":"string","format":"date","description":"Last day of the window, included."}},"required":["days","dateFrom","dateTo"]}},"required":["current","previous","delta","comparisons","timeSeries","platformCadences","window"]},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Visibility"]}},"/projects/{projectId}/content-gaps/list":{"get":{"summary":"List content gaps","description":"Lists topic, source and brand content gaps over a fixed 30- or 90-day window: targets where competitors are cited, with citations split into primary and competitor, average position and your share. Your own sources and brand are excluded, as are topics you hold at least half the citations on, because those are presence rather than a gap; excluded targets are absent from the list and from totalCount. Every count is a citation; this endpoint reports no mention counts. Unavailable coverage means the window has not been measured yet, not that there are no gaps. An available window still reports availableDays and isPartial: read counts against availableDays, not windowDays. A cursor stays on the data its first page read, and any republish changes that data, even when the days and total stay the same. If the data is republished mid-read, the next page returns 409 with reason snapshot_changed: start again without cursor and send Cache-Control: no-cache, because a cached first page can still show the earlier data. The deprecated offset always reads the current data and never returns 409. Requires the `intelligence.read` scope.","operationId":"listContentGaps","x-devtune-required-scope":"intelligence.read","parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"windowDays","in":"query","schema":{"type":"integer","default":30,"enum":[30,90]},"description":"Fixed rolling window in days. Supported values are 30 and 90. Defaults to 30."},{"name":"gapType","in":"query","schema":{"type":"string","enum":["topic","source","brand"]},"description":"Filter by gap type."},{"name":"search","in":"query","schema":{"type":"string","minLength":1,"maxLength":200},"description":"Case-insensitive search on the display label."},{"name":"cursor","in":"query","schema":{"type":"string","maxLength":1024},"description":"pagination.nextCursor from the previous page, unchanged. Keep the other parameters the same: a cursor sent with different filters, or beside a deprecated position parameter, returns 400."},{"name":"offset","in":"query","schema":{"type":"integer","default":0,"minimum":0},"deprecated":true,"description":"Deprecated: Pass cursor instead."},{"name":"limit","in":"query","schema":{"type":"integer","default":25,"minimum":1,"maximum":100}},{"name":"pageSize","in":"query","schema":{"type":"integer","default":25,"minimum":1,"maximum":100},"description":"The same setting as limit. Send either; both with different values returns 400."}],"responses":{"200":{"description":"Content gaps","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"contentGaps":{"type":"array","maxItems":100,"description":"Content gaps in the window. Read counts against coverage.availableDays.","items":{"type":"object","properties":{"gapType":{"type":"string","enum":["topic","source","brand"],"description":"topic: a tracked topic where the primary brand or a competitor is cited and the primary brand has less than half the citations. source: a cited page that is not the primary brand's. brand: a cited domain that is not the primary brand's."},"targetKey":{"type":"string","description":"Identifies the target within its gapType. Pass it with gapType to read the tracked prompts and cited pages behind the gap."},"targetLabel":{"type":"string","description":"A readable name for the target: the topic name, the page title (the URL when no title was captured) or the domain."},"windowDays":{"type":"integer","enum":[30,90]},"asOfDate":{"type":"string","format":"date","description":"The last day the window covers."},"citationCount":{"type":"integer","minimum":0,"description":"Citations recorded for this target in the window. Content gaps count citations only, never mentions."},"primaryCitations":{"type":"integer","minimum":0,"description":"Citations of the primary brand's domains in AI answers."},"competitorCitations":{"type":"integer","minimum":0,"description":"Citations of tracked competitors' domains."},"averagePosition":{"type":["number","null"],"description":"Average position of the target's citations in the answers' source lists; 1 is the first source."},"share":{"type":["number","null"],"minimum":0,"maximum":1,"description":"Primary citations divided by primary plus competitor citations, as a fraction from 0 to 1. Null when the denominator is zero."},"estimated":{"type":"boolean","const":false,"description":"Always false."}},"required":["gapType","targetKey","targetLabel","windowDays","asOfDate","citationCount","primaryCitations","competitorCitations","averagePosition","share","estimated"]}},"coverage":{"type":"object","description":"Whether the window has been measured, and how many of its days are covered. An unavailable window has no measurement yet, which is not the same as a measured zero. A partial window covers fewer days than requested, so its totals describe only the days in availableDays. asOfDate is the last day the window includes; it is reported here as well as on each item, so a measured but empty page still identifies its data.","properties":{"status":{"type":"string","enum":["available","unavailable"],"description":"available when the window has been measured, unavailable when it has not yet."},"windowDays":{"type":"integer","enum":[30,90]},"availableDays":{"type":"integer","minimum":0,"description":"Days of the window that are measured."},"isPartial":{"type":"boolean","description":"True when availableDays is less than windowDays."},"asOfDate":{"type":["string","null"],"format":"date","description":"The last day the window covers. Null when coverage is unavailable."}},"required":["status","windowDays","availableDays","isPartial","asOfDate"]},"pagination":{"type":"object","description":"Where this page sits in the list.","properties":{"hasMore":{"type":"boolean","description":"More rows follow this page."},"nextCursor":{"type":["string","null"],"description":"Pass as cursor to read the next page. Null on the last page."},"offset":{"type":"integer","minimum":0,"description":"Rows before this page."},"limit":{"type":"integer","minimum":1,"maximum":100,"description":"Rows per page."},"totalCount":{"type":"integer","minimum":0,"description":"Content gaps in the window, across every page."}},"required":["hasMore","nextCursor","offset","limit","totalCount"]},"window":{"type":"object","description":"The fixed window the figures cover, in UTC days.","properties":{"days":{"type":"integer"},"dateFrom":{"type":"string","format":"date","description":"First day of the window."},"dateTo":{"type":"string","format":"date","description":"Last day of the window, included."}},"required":["days","dateFrom","dateTo"]}},"required":["contentGaps","coverage","pagination","window"]},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"409":{"$ref":"#/components/responses/ConflictError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Content gaps"]}},"/projects/{projectId}/content-gaps/evidence":{"get":{"summary":"Get content gap evidence","description":"Returns the tracked prompts and cited pages behind one content gap from List content gaps. Pass its gapType and targetKey with the same windowDays. Evidence is published with the gap, so it covers the same fixed window and asOfDate. A source gap matches its URL, a brand gap the pages cited on its domain, and a topic gap every page cited in answers recorded under that topic. Each item groups one prompt, recorded topic, cited URL, recorded domain and sourceType. The same prompt and URL can appear in several items when those recorded attributes differ; pagination counts those groups, ordered by the prompt's citations of the gap, then by the page's citations. Counts are the gap's own citations split by the tracked prompt that produced them, so attribution.attributedCitations falls short of gap.citationCount only by citations no tracked prompt produced. Prompt text and topic names are current values; promptStatus says whether the prompt is still tracked. Coverage is unavailable with reason window_not_measured before the window is rolled up, and evidence_not_published when the window was published without evidence, because it includes days measured before evidence was recorded, holds more evidence than one publish includes, or had its gaps last recalculated from current topic and competitor settings rather than from the recorded citations; neither is a measured zero. Pass pagination.nextCursor as cursor on later pages: it carries coverage.snapshotId, and if the window is republished meanwhile the request returns 409 with reason snapshot_changed rather than mixing two windows. A gap that is not in the published window returns 404. Requires the `intelligence.read` scope.","operationId":"getContentGapEvidence","x-devtune-required-scope":"intelligence.read","parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"gapType","in":"query","required":true,"schema":{"type":"string","enum":["topic","source","brand"]},"description":"gapType of the listed content gap."},{"name":"targetKey","in":"query","required":true,"schema":{"type":"string","minLength":1,"maxLength":8192,"pattern":"^[^\\u0000]+$"},"description":"targetKey of the listed content gap, exactly as listed: a topic id, a cited URL or a domain."},{"name":"windowDays","in":"query","schema":{"type":"integer","default":30,"enum":[30,90]},"description":"Fixed rolling window in days. Supported values are 30 and 90. Defaults to 30."},{"name":"cursor","in":"query","schema":{"type":"string","maxLength":1024},"description":"pagination.nextCursor from the previous page, unchanged. Keep the other parameters the same: a cursor sent with different filters, or beside a deprecated position parameter, returns 400."},{"name":"offset","in":"query","schema":{"type":"integer","default":0,"minimum":0,"maximum":2147483647},"deprecated":true,"description":"Deprecated: Pass cursor instead."},{"name":"limit","in":"query","schema":{"type":"integer","default":25,"minimum":1,"maximum":100}},{"name":"pageSize","in":"query","schema":{"type":"integer","default":25,"minimum":1,"maximum":100},"description":"The same setting as limit. Send either; both with different values returns 400."},{"name":"snapshotId","in":"query","schema":{"type":"string","format":"uuid"},"description":"Deprecated: cursor carries the snapshot. coverage.snapshotId from the first page. The request returns 409 if the window was republished since.","deprecated":true}],"responses":{"200":{"description":"Content gap evidence","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"gap":{"type":["object","null"],"description":"The gap as published in this window. Null when coverage is unavailable.","properties":{"gapType":{"type":"string","enum":["topic","source","brand"],"description":"topic: a tracked topic where the primary brand or a competitor is cited and the primary brand has less than half the citations. source: a cited page that is not the primary brand's. brand: a cited domain that is not the primary brand's."},"targetKey":{"type":"string","description":"Identifies the target within its gapType. Pass it with gapType to read the tracked prompts and cited pages behind the gap."},"targetLabel":{"type":"string","description":"A readable name for the target: the topic name, the page title (the URL when no title was captured) or the domain."},"windowDays":{"type":"integer","enum":[30,90]},"asOfDate":{"type":"string","format":"date","description":"The last day the window covers."},"citationCount":{"type":"integer","minimum":0,"description":"Citations recorded for this target in the window."},"primaryCitations":{"type":"integer","minimum":0,"description":"Citations of the primary brand's domains in AI answers."},"competitorCitations":{"type":"integer","minimum":0,"description":"Citations of tracked competitors' domains."},"averagePosition":{"type":["number","null"],"description":"Average position of the target's citations in the answers' source lists; 1 is the first source."},"share":{"type":["number","null"],"minimum":0,"maximum":1,"description":"Primary citations divided by primary plus competitor citations, as a fraction from 0 to 1. Null when the denominator is zero."}},"required":["gapType","targetKey","targetLabel","windowDays","asOfDate","citationCount","primaryCitations","competitorCitations","averagePosition","share"]},"evidence":{"type":"array","maxItems":100,"description":"This page of the gap's evidence: one item per prompt, recorded topic, cited URL, domain and sourceType. Empty when coverage is unavailable.","items":{"type":"object","properties":{"promptId":{"type":"string","format":"uuid"},"promptText":{"type":["string","null"],"description":"Current prompt text. Null when the prompt no longer exists."},"promptStatus":{"type":"string","enum":["active","paused","deleted","removed"],"description":"Whether the prompt is tracked now. Its citations in the window count whatever its status."},"promptCitationCount":{"type":"integer","minimum":1,"description":"Citations this prompt contributed to the gap across every page."},"topicId":{"type":["string","null"],"format":"uuid","description":"Topic the citations were recorded under, the topic the gap counts them under."},"topicName":{"type":["string","null"],"description":"Current topic name, also for a deleted topic. Null when the topic cannot be found."},"url":{"type":"string"},"domain":{"type":["string","null"],"description":"The domain the citation was recorded under."},"sourceType":{"type":"string","enum":["primary","competitor","other"],"description":"primary for a domain of the primary brand, competitor for a tracked competitor’s, other for any other source."},"title":{"type":["string","null"],"description":"The page title. Null when none was captured."},"platforms":{"type":"array","description":"The AI platforms that cited the page.","items":{"type":"string"}},"citationCount":{"type":"integer","minimum":1,"description":"This prompt's citations of the page that count toward the gap, under this item's recorded topic, domain and sourceType."},"averagePosition":{"type":["number","null"],"description":"Average position of the target's citations in the answers' source lists; 1 is the first source."}},"required":["promptId","promptText","promptStatus","promptCitationCount","topicId","topicName","url","domain","sourceType","title","platforms","citationCount","averagePosition"]}},"attribution":{"type":["object","null"],"description":"Totals across every evidence item for the gap, not just this page. Null when coverage is unavailable.","properties":{"attributedCitations":{"type":"integer","minimum":0,"description":"The gap's citations a tracked prompt produced. Falls short of gap.citationCount only by citations no tracked prompt produced."},"promptCount":{"type":"integer","minimum":0,"description":"Prompts in the evidence."},"pageCount":{"type":"integer","minimum":0,"description":"Distinct cited URLs in the evidence."}},"required":["attributedCitations","promptCount","pageCount"]},"coverage":{"type":"object","description":"Whether the window has been measured, and how many of its days are covered. An unavailable window has no measurement yet, which is not the same as a measured zero. A partial window covers fewer days than requested, so its totals describe only the days in availableDays. asOfDate is the last day the window includes; it is reported here as well as on each item, so a measured but empty page still identifies its data.","properties":{"status":{"type":"string","enum":["available","unavailable"],"description":"available when the window's evidence is published. unavailable when the window has not been measured yet or was published without evidence; reason says which."},"windowDays":{"type":"integer","enum":[30,90]},"availableDays":{"type":"integer","minimum":0,"description":"Days of the window that are measured."},"isPartial":{"type":"boolean","description":"True when availableDays is less than windowDays."},"asOfDate":{"type":["string","null"],"format":"date","description":"The last day the window covers. Null when coverage is unavailable."},"reason":{"type":["string","null"],"enum":["window_not_measured","evidence_not_published",null],"description":"Why coverage is unavailable. Null when available."},"snapshotId":{"type":["string","null"],"format":"uuid","description":"Identifies this publish of the window; pagination.nextCursor carries it. Null when coverage is unavailable."}},"required":["status","windowDays","availableDays","isPartial","asOfDate","reason","snapshotId"]},"pagination":{"type":"object","description":"Where this page sits in the list.","properties":{"hasMore":{"type":"boolean","description":"More rows follow this page."},"nextCursor":{"type":["string","null"],"description":"Pass as cursor to read the next page. Null on the last page."},"offset":{"type":"integer","minimum":0,"description":"Rows before this page."},"limit":{"type":"integer","minimum":1,"maximum":100,"description":"Rows per page."},"totalCount":{"type":["integer","null"],"minimum":0,"description":"Evidence groups for the gap, each identified by promptId, recorded topicId, URL, domain and sourceType. Null when coverage is unavailable."}},"required":["hasMore","nextCursor","offset","limit","totalCount"]},"window":{"type":"object","description":"The fixed window the figures cover, in UTC days.","properties":{"days":{"type":"integer"},"dateFrom":{"type":"string","format":"date","description":"First day of the window."},"dateTo":{"type":"string","format":"date","description":"Last day of the window, included."}},"required":["days","dateFrom","dateTo"]}},"required":["gap","evidence","attribution","coverage","pagination","window"]},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"409":{"$ref":"#/components/responses/ConflictError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Content gaps"]}},"/projects/{projectId}/competitive/position":{"get":{"summary":"Get competitive positioning data","description":"Returns citation comparison between primary, competitor, and other sources over time for a fixed rolling window. The default includes only currently active project platforms. Requires the `competitive.read` scope.","operationId":"getCompetitivePosition","x-devtune-required-scope":"competitive.read","parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"windowDays","in":"query","schema":{"type":"integer","default":30,"enum":[30,90]},"description":"Fixed rolling window in days. Supported values are 30 and 90. Defaults to 30."},{"name":"platform","in":"query","schema":{"type":"string","minLength":1,"maxLength":100}},{"name":"topicId","in":"query","schema":{"type":"string","format":"uuid"}},{"name":"grouping","in":"query","schema":{"type":"string","enum":["daily","weekly","monthly"],"default":"daily"}},{"name":"includeHistoricalPlatforms","in":"query","description":"Allow a requested inactive platform to return historical data. Defaults to false.","schema":{"type":"boolean","default":false}}],"responses":{"200":{"description":"Competitive positioning data","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"timeSeries":{"type":"array","description":"Points for platforms in the daily series, grouped by the requested period. A request for one weekly platform returns a point per run.","items":{"type":"object","properties":{"date":{"type":"string","format":"date","description":"The UTC day the point covers. With weekly or monthly grouping, the Monday of the week or the first day of the month."},"primaryCitations":{"type":"integer","description":"Citations of the primary brand's domains in AI answers."},"competitorCitations":{"type":"integer","description":"Citations of tracked competitors' domains."},"otherCitations":{"type":"integer","description":"Citations of every other source, such as forums, news and documentation sites."}}}},"platformCadences":{"type":["array","null"],"description":"Cadence of each platform in scope. Platforms with inDailySeries false are checked less often than daily: window totals include them, and timeSeries leaves them out unless they are the only platforms in scope. Null when cadence could not be read.","items":{"type":"object","properties":{"platform":{"type":"string"},"cadenceDays":{"type":"integer","minimum":1,"description":"Days between scheduled checks."},"inDailySeries":{"type":"boolean","description":"Whether the platform joins daily cross-platform totals."},"lastCheckedOn":{"type":["string","null"],"format":"date","description":"Last date with responses, for platforms outside the daily series. Null for daily platforms, and for weekly platforms not checked in the last year."}},"required":["platform","cadenceDays","inDailySeries","lastCheckedOn"]}},"totals":{"type":"object","description":"Window totals across every platform in scope, including weekly ones.","properties":{"primaryCitations":{"type":"integer","minimum":0,"description":"Citations of the primary brand's domains in AI answers."},"competitorCitations":{"type":"integer","minimum":0,"description":"Citations of tracked competitors' domains."},"otherCitations":{"type":"integer","minimum":0,"description":"Citations of every other source, such as forums, news and documentation sites."},"total":{"type":"integer","minimum":0,"description":"primaryCitations plus competitorCitations plus otherCitations."},"primaryShare":{"type":["number","null"],"minimum":0,"maximum":100,"description":"Primary brand share of all citations in the period, as a percentage from 0 to 100: primary, competitor and other citations. Null when the period has no citations."},"primaryShareRange":{"type":"object","description":"90% range for the share, in percent. successes and n count citations. value, low and high are null when n is 0.","properties":{"value":{"type":["number","null"],"description":"The rate, in the unit of the field this range sits beside. Null when n is 0."},"successes":{"type":"integer","minimum":0,"description":"Samples that count toward the rate."},"n":{"type":"integer","minimum":0,"description":"Samples the rate is measured over."},"nEffective":{"type":"number","minimum":0,"description":"Effective sample size behind the range. Equal to n when method is wilson."},"low":{"type":["number","null"],"description":"Lower end of the 90% range. Null when n is 0."},"high":{"type":["number","null"],"description":"Upper end of the 90% range. Null when n is 0."},"confidence":{"type":"number","enum":[0.9]},"directional":{"type":"boolean","description":"True when the rate rests on fewer than 10 effective samples: too few to rely on."},"method":{"type":"string","enum":["wilson","cluster"],"description":"wilson is a Wilson score interval on n. cluster discounts correlated samples first."}},"required":["value","successes","n","nEffective","low","high","confidence","directional","method"]},"primaryShareOfAllCitations":{"type":["number","null"],"minimum":0,"maximum":100,"description":"The same value as primaryShare; use primaryShare. Null when the period has no citations."},"primaryShareOfAllCitationsRange":{"type":"object","description":"90% range for the share, in percent. successes and n count citations. value, low and high are null when n is 0.","properties":{"value":{"type":["number","null"],"description":"The rate, in the unit of the field this range sits beside. Null when n is 0."},"successes":{"type":"integer","minimum":0,"description":"Samples that count toward the rate."},"n":{"type":"integer","minimum":0,"description":"Samples the rate is measured over."},"nEffective":{"type":"number","minimum":0,"description":"Effective sample size behind the range. Equal to n when method is wilson."},"low":{"type":["number","null"],"description":"Lower end of the 90% range. Null when n is 0."},"high":{"type":["number","null"],"description":"Upper end of the 90% range. Null when n is 0."},"confidence":{"type":"number","enum":[0.9]},"directional":{"type":"boolean","description":"True when the rate rests on fewer than 10 effective samples: too few to rely on."},"method":{"type":"string","enum":["wilson","cluster"],"description":"wilson is a Wilson score interval on n. cluster discounts correlated samples first."}},"required":["value","successes","n","nEffective","low","high","confidence","directional","method"]},"shareOfVoice":{"type":["number","null"],"minimum":0,"maximum":100,"description":"Primary brand share of citations attributed to tracked primary and competitor brands. Null when neither was cited."},"shareOfVoiceRange":{"type":"object","description":"90% range for the share, in percent. successes and n count citations. value, low and high are null when n is 0.","properties":{"value":{"type":["number","null"],"description":"The rate, in the unit of the field this range sits beside. Null when n is 0."},"successes":{"type":"integer","minimum":0,"description":"Samples that count toward the rate."},"n":{"type":"integer","minimum":0,"description":"Samples the rate is measured over."},"nEffective":{"type":"number","minimum":0,"description":"Effective sample size behind the range. Equal to n when method is wilson."},"low":{"type":["number","null"],"description":"Lower end of the 90% range. Null when n is 0."},"high":{"type":["number","null"],"description":"Upper end of the 90% range. Null when n is 0."},"confidence":{"type":"number","enum":[0.9]},"directional":{"type":"boolean","description":"True when the rate rests on fewer than 10 effective samples: too few to rely on."},"method":{"type":"string","enum":["wilson","cluster"],"description":"wilson is a Wilson score interval on n. cluster discounts correlated samples first."}},"required":["value","successes","n","nEffective","low","high","confidence","directional","method"]}},"required":["primaryCitations","competitorCitations","otherCitations","total","primaryShare","primaryShareRange","primaryShareOfAllCitations","primaryShareOfAllCitationsRange","shareOfVoice","shareOfVoiceRange"]}}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Visibility"]}},"/projects/{projectId}/adoption/metrics":{"get":{"summary":"Get adoption metrics","description":"Returns npm downloads and GitHub stars trends for project domains. Requires the `adoption.read` scope.","operationId":"getAdoptionMetrics","x-devtune-required-scope":"adoption.read","parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"windowDays","in":"query","schema":{"type":"integer","default":30,"enum":[30,90]},"description":"Fixed rolling window in days. Supported values are 30 and 90. Defaults to 30."},{"name":"metric","description":"Only this metric: github_stars or weekly_downloads. Omit it for both.","in":"query","schema":{"type":"string","enum":["github_stars","weekly_downloads"]}}],"responses":{"200":{"description":"Adoption metrics data","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"metrics":{"type":"array","description":"One row per tracked domain and metric with a reading in the window.","items":{"type":"object","properties":{"domain":{"type":["string","null"],"description":"The tracked domain the metric belongs to."},"isPrimary":{"type":"boolean","description":"True for the primary brand's domains, false for every other."},"metric":{"type":"string","description":"What is counted: weekly_downloads, total_downloads or github_stars."},"source":{"type":"string","description":"The registry or site the reading comes from, such as npm, pypi or github."},"value":{"type":"number","description":"The latest reading in the window."},"capturedAt":{"type":"string","format":"date-time","description":"When the latest reading was taken."}}}},"domains":{"type":"array","description":"The domains that appear in metrics.","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"domain":{"type":"string"},"isPrimary":{"type":"boolean","description":"True for the primary brand's domains, false for every other."}}}}}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Adoption"]}},"/projects/{projectId}/actions/from-prompt":{"get":{"summary":"List actions for a prompt","description":"Returns up to 100 existing and suggested actions explicitly linked by stored prompt evidence, with status and brief readiness. hasMore indicates truncated results. Requires the `actions.read` scope.","operationId":"listPromptActions","x-devtune-required-scope":"actions.read","parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"promptId","in":"query","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Related actions","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"hasMore":{"type":"boolean","description":"True when a lookup reached its limit, so some linked actions may be left out. Can be true with fewer than 100 actions returned."},"actions":{"type":"array","description":"Up to 100 actions whose evidence names the prompt.","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"title":{"type":"string"},"status":{"type":"string","description":"The work status: open, in_progress, blocked, completed, dismissed or archived."},"suggestionStatus":{"type":["string","null"],"description":"Where the suggestion stands: suggested, sleeping (snoozed), archived or accepted. Null when not recorded."},"hidden":{"type":"boolean","description":"True when the action is hidden from the queue."},"briefStatus":{"type":["string","null"],"description":"The action brief's status, such as ready. Null when no brief has been requested."},"canGenerateBrief":{"type":"boolean","description":"True for an action DevTune generated, including one created from a prompt or an Audit finding, while it is not hidden from the queue; Generate action brief accepts it."},"canRestore":{"type":"boolean","description":"True for a hidden suggestion DevTune ranked that can be restored to the queue."},"requiresHighStakesAcknowledgement":{"type":"boolean","description":"True when the target page is high-stakes: DevTune marked it locked without a person locking it."},"relationship":{"type":"string","enum":["evidence"]},"supportingPromptCount":{"type":"integer","description":"Prompts in the action’s evidence, at least 1."}}}}}}}}}}},"400":{"description":"Invalid prompt ID"},"404":{"description":"Prompt not found in this project"}},"tags":["Actions"]},"post":{"summary":"Create an action from a prompt","description":"Creates an action for the prompt: with targetUrl, to refresh that page; without it, to create new content under title. An existing action for the same page, or for new content with the same title, is returned instead of a second one. Saves the prompt's evidence from the last 30 days. generateBrief defaults to false; when true, it also generates a brief, which spends credits under the actions.write scope: it reserves 2 credits and settles at 1–2, as Generate action brief does. A failed brief request leaves the action saved and returns briefStatus failed. No content is published. Requires the `actions.write` scope.","operationId":"createPromptAction","x-devtune-required-scope":"actions.write","parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["promptId","title","description"],"properties":{"promptId":{"type":"string","format":"uuid"},"title":{"type":"string","minLength":1,"maxLength":500},"description":{"type":"string","minLength":1,"maxLength":10000},"targetUrl":{"type":"string","format":"uri","maxLength":2048,"description":"Optional HTTP(S) URL of the existing page to improve. Omit for new content."},"acknowledgedHighStakes":{"type":"boolean","default":false,"description":"Explicit customer acknowledgement required for a high-stakes target page."},"generateBrief":{"type":"boolean","default":false,"description":"Also generate a brief, which reserves 2 credits and settles at 1–2."}}}}}},"responses":{"200":{"description":"Saved action and optional brief state","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"actionId":{"type":"string","format":"uuid"},"reused":{"type":"boolean","description":"True when an existing action for the same page or title was returned instead of a new one."},"briefStatus":{"type":["string","null"],"description":"queued, generating, ready or failed when generateBrief was true. Null otherwise."},"suggestionStatus":{"type":["string","null"],"description":"Where the action stands: suggested, sleeping or accepted. Null when not recorded."}}}}}}}},"400":{"description":"Invalid input"},"404":{"description":"Prompt not found in this project"},"409":{"description":"Target page is locked or matching work must be restored"}},"tags":["Actions"]}},"/projects/{projectId}/actions/list":{"get":{"summary":"List actions","description":"Returns the project's actions. Each is suggested (DevTune proposed it and nobody has decided), accepted (your team wants it done), live (the page is published; its measurement is running or complete) or archived (taken off the queue). Filter with status; without it, every state is returned, suggestions first by portfolioRank ascending, nulls last. blocked is true on accepted work your team has marked blocked. detailLevel=context adds bounded why-now context, scores, metrics, top evidence, brief readiness and follow-up links. Live suggestions expose opportunity percentiles, components and measured facts; percentileScope signal_family compares like signals, and eligible signal families are interleaved. Requires the `actions.read` scope.","operationId":"listActions","security":[{"bearerAuth":[]},{"oauthBearer":[]}],"x-devtune-required-scope":"actions.read","parameters":[{"name":"pageUrl","in":"query","schema":{"type":"string","format":"uri","maxLength":4096},"description":"Filter by the HTTP(S) target page URL before counts and pagination. Matches host and path, ignoring scheme, www, query, fragment and trailing slash."},{"name":"source","in":"query","schema":{"type":"string","enum":["ai_search","agent_picks","both"],"default":"both"},"description":"Filter the queue and results by source before counts and pagination."},{"name":"projectId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"detailLevel","in":"query","schema":{"type":"string","enum":["summary","context"],"default":"summary"},"description":"summary is default. context adds bounded whyNow, opportunity evidence, metrics, top evidence with structured Google eligibility when available, brief readiness, and follow-up links."},{"name":"status","in":"query","schema":{"type":"string","enum":["suggested","accepted","live","archived"]},"description":"Filter by action status. Without it the list returns every status, suggested actions first in portfolio rank order."},{"name":"priority","description":"Only actions with this legacy priority label: low, medium, high or urgent.","in":"query","schema":{"type":"string"}},{"name":"channel","description":"Only actions recommended for this channel: docs, content, devrel, pr, engineering, product or other.","in":"query","schema":{"type":"string"}},{"name":"cursor","in":"query","schema":{"type":"string","maxLength":1024},"description":"pagination.nextCursor from the previous page, unchanged. Keep the other parameters the same: a cursor sent with different filters, or beside a deprecated position parameter, returns 400."},{"name":"page","in":"query","schema":{"type":"integer","default":1,"minimum":1},"deprecated":true,"description":"Deprecated: Pass cursor instead."},{"name":"pageSize","in":"query","schema":{"type":"integer","default":25,"minimum":1,"maximum":100},"description":"Results per page. A cursor continues only at the page size it was issued for."},{"name":"limit","in":"query","schema":{"type":"integer","default":25,"minimum":1,"maximum":100},"description":"The same setting as pageSize. Send either; both with different values returns 400."}],"responses":{"200":{"description":"Actions list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"actions":{"type":"array","description":"Suggested actions first, in portfolioRank order with unranked ones last; then every other action, newest first.","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"title":{"type":"string"},"description":{"type":["string","null"],"description":"What the action asks for."},"channels":{"type":"array","description":"The teams the work belongs to, such as docs, content or devrel.","items":{"type":"string"}},"priority":{"type":["string","null"],"description":"A workflow label: low, medium, high or urgent. It does not set recommendation order; use portfolioRank."},"status":{"$ref":"#/components/schemas/PublicActionStatus"},"blocked":{"type":"boolean","description":"True when the team has marked accepted work as blocked. The status stays accepted."},"assignedTo":{"type":["string","null"],"format":"uuid","description":"User id of the member who owns the action. Null when nobody owns it. Suggested actions have no owner."},"ownerName":{"type":["string","null"],"description":"The owner's display name, or their email when they have no name. Null when unassigned, and also when the owner has left the account: use assignedTo to tell the two apart."},"opportunity":{"type":"object","description":"Present on suggested actions ranked on the opportunity-percentile contract. Other statuses omit it, because only suggested actions are re-ranked each refresh. The raw ranking product is never exposed.","required":["family","estimated","percentile","components","measuredFacts"],"properties":{"family":{"type":"string","description":"prompt_gap: a shortfall on tracked prompts, such as competitors cited more or demand nobody is cited on yet. owned_page_audit: an owned page falls short in Audit. missing_page_demand: AI systems request a page that does not exist. citation_defence: citations of an owned page are falling.","enum":["prompt_gap","owned_page_audit","missing_page_demand","citation_defence"]},"estimated":{"type":"boolean","description":"True when reach is an estimate (prompt_gap); false when it is measured."},"percentile":{"type":["number","null"],"minimum":0,"maximum":100,"description":"Higher-is-better rank after dedupe and before selection. When percentileScope is signal_family, comparison is only with the same opportunity family; older records use the whole project. Null with percentileStatus insufficient_reach when reach is null, sensor coverage is absent, or estimated reach is zero. Null with percentileStatus no_measured_gap when every product in the family is zero and reach is non-null and not estimated-zero."},"percentileStatus":{"type":"string","enum":["insufficient_reach","no_measured_gap"],"description":"Present when percentile is null; omitted for numeric percentiles and older records. insufficient_reach means reach is null, sensor coverage is absent, or estimated reach is zero. no_measured_gap means every product in the family is zero and reach is non-null and not estimated-zero."},"percentilePoolSize":{"type":"integer","minimum":0,"description":"Number of rankable candidates in the full signal-family percentile pool before selection or pagination. A size of one means the only ranked opportunity of this type, not Top 1%. Absent on older records; do not infer a singleton from percentile 100 or the visible result count."},"percentileScope":{"type":"string","enum":["signal_family"],"description":"Compares only opportunities of the same signal family. Families are interleaved; the percentile does not compare fetches with estimated searches."},"components":{"type":"object","description":"The factors the opportunity is ranked on: reach, gap and confidence, and for missing_page_demand also relevance.","required":["reach","gap","confidence"],"properties":{"reach":{"type":"object","description":"How much demand the opportunity touches.","required":["value","unit","estimated"],"properties":{"value":{"type":["number","null"],"minimum":0,"description":"Null only when an owned page has no AI Traffic sensor coverage."},"unit":{"type":"string","description":"searches_per_month (estimated), demand_events_per_month or requests_per_month.","enum":["searches_per_month","demand_events_per_month","requests_per_month"]},"estimated":{"type":"boolean","description":"True when value is an estimate."}}},"gap":{"type":"object","description":"How far the customer falls short, from 0 to 1.","required":["value","unit","normalization"],"properties":{"value":{"type":"number","minimum":0,"maximum":1,"description":"The shortfall, from 0 to 1, worked out as normalization says."},"unit":{"type":"string","description":"What the shortfall is measured in.","enum":["citation_share","citability_shortfall","section_finding_share","unmet_demand","citation_rate_loss"]},"normalization":{"type":"string","description":"Names how value is worked out. For prompt_gap, prompts where the brand is absent, and demand nobody is cited on yet, count as a full gap, so value can be higher than the formula alone gives.","enum":["max(0, topCompetitorCitations - primaryCitations) / max(topCompetitorCitations, 1)","(100 - citabilityScore) / 100","sections with findings / reviewed sections","missing page demand is fully unmet","matched_cohort_executions","window_executions"]}}},"confidence":{"type":"object","description":"How much evidence backs the opportunity, from 0 to 1.","required":["value","unit","volumeConfidencePenalty","saturation"],"properties":{"value":{"type":"number","minimum":0,"maximum":1,"description":"Evidence so far against saturation, multiplied by volumeConfidencePenalty, from 0 to 1."},"unit":{"type":"string","enum":["ratio"]},"volumeConfidencePenalty":{"type":"number","minimum":0,"maximum":1,"description":"For prompt_gap, from 0.5 to 1 by how sure the search-volume estimates are: each prompt's 0.5, 0.75 or 1, averaged across the prompts and weighted by volume. 1 for other families."},"saturation":{"type":"object","description":"The evidence counts at which confidence stops rising.","required":["signalDays"],"properties":{"eventEvidence":{"type":"integer","minimum":1,"description":"Measured events."},"runPlatformObservations":{"type":"integer","minimum":1,"description":"Prompt runs observed across platforms."},"signalDays":{"type":"integer","minimum":1,"description":"Days with tracked data."}}}}}}},"measuredFacts":{"type":"array","description":"The measured facts behind the opportunity, for a person to read.","items":{"type":"string"}},"measurements":{"type":"object","additionalProperties":false,"description":"Citation-defence comparison facts. matched_cohort_executions uses prompt/platform cells present in both windows. window_executions uses successful prompt executions over each full page window when the matched cohort is not published yet.","required":["asOfDate","compositionChange","currentCitationCount","currentExecutions","currentProbeDays","matchedCohortCoverage","priorCitationCount","priorExecutions","priorProbeDays"],"properties":{"asOfDate":{"type":"string","format":"date","description":"The last day the comparison covers."},"compositionChange":{"type":"object","description":"Prompt and platform cells present in only one of the two windows.","required":["onlyPrior","onlyCurrent"],"properties":{"onlyPrior":{"$ref":"#/components/schemas/CitationDefenceCompositionSide"},"onlyCurrent":{"$ref":"#/components/schemas/CitationDefenceCompositionSide"}}},"currentCitationCount":{"type":"integer","minimum":0,"description":"The page's citations in the current window. With matched_cohort_executions, only prompt and platform cells probed in both windows count."},"currentExecutions":{"type":"integer","minimum":0,"description":"Successful AI answers in the current window. With matched_cohort_executions, only prompt and platform cells probed in both windows count."},"currentProbeDays":{"type":"integer","minimum":0,"description":"Days with measured answers in the current window."},"matchedCohortCoverage":{"type":"number","nullable":true,"minimum":0,"maximum":1,"description":"Matched prior citation volume divided by total prior citation volume. Null when normalization is window_executions; matched-cohort loss claims require at least 0.6."},"priorCitationCount":{"type":"integer","minimum":0,"description":"The page's citations in the prior window. With matched_cohort_executions, only prompt and platform cells probed in both windows count."},"priorExecutions":{"type":"integer","minimum":0,"description":"Successful AI answers in the prior window. With matched_cohort_executions, only prompt and platform cells probed in both windows count."},"priorProbeDays":{"type":"integer","minimum":0,"description":"Days with measured answers in the prior window."}}}}},"effortScore":{"type":["number","null"],"description":"Relative effort: 25 to update an existing page, 50 to create content or improve a source. Null when not estimated."},"portfolioCategory":{"type":["string","null"]},"portfolioRank":{"type":["integer","null"],"description":"One-based position in the project recommendation queue; null when the action has no place in it. An action that leaves the suggestions keeps the rank it last had, and the list orders only suggested actions by rank. Sort suggested actions ascending, nulls last, and preserve the returned order for ties. Do not recompute rank from opportunity evidence or priority. Recommendations with percentileScope signal_family interleave eligible families, ranked within each family by Reach × Gap × Confidence, times relevance for 404 demand. Recommendations whose product is zero come after those above zero, larger Gap first; audits of pages that already score well and have no trigger prompts come last.","minimum":1},"steeredFromPageUrl":{"type":["string","null"],"format":"uri","description":"The locked owned page this new-asset recommendation steers around. Present only on suggested actions that steer around one; absent, never null, otherwise."},"followUpOf":{"type":["object","null"],"description":"The earlier action that shipped this page; read its result from the outcomes ledger. Null when the action is not a follow-up.","required":["actionId","title","shippedAt"],"properties":{"actionId":{"type":"string","format":"uuid"},"title":{"type":"string","description":"The earlier action’s title."},"shippedAt":{"type":"string","format":"date-time","description":"When the earlier action shipped."}}},"createdAt":{"type":"string","format":"date-time"},"updatedAt":{"type":"string","format":"date-time"},"expectedImpact":{"type":["string","null"],"description":"A short summary of the expected impact. Null when there is none."},"isCommandCenter":{"type":"boolean","description":"True for an action DevTune generated, including one a person created from a prompt or an Audit finding; false for one made with Create Action."},"primaryCtaKind":{"type":["string","null"],"description":"The main next step: start_brief, open_action, review_evidence, inspect_asset or investigate. Null when there is none."},"targetLabel":{"type":["string","null"],"description":"What the action targets, such as a page or topic, as shown to people. Null when there is none."},"whatChanged":{"type":["string","null"],"description":"The change that prompted the action, in plain language. Null when there is none."},"whyNow":{"type":["string","null"],"description":"Why the action is worth doing now. Null when there is none."},"context":{"type":"object","description":"Present when detailLevel=context. Evidence and source-reference arrays are bounded per action.","properties":{"metrics":{"type":["object","null"],"additionalProperties":true,"description":"Stored evidence for this action. Page checks describe a bounded shortlist, never an exhaustive site search. Null when the action has none.","properties":{"ownedPageMatchCount":{"type":"integer","minimum":0,"description":"Total retained prompt checks; only the first forty are returned in ownedPageMatches."},"ownedPageMatches":{"type":"array","maxItems":40,"description":"Completed no_match means no suitable page was identified within the at-most-twelve-page shortlist. Coverage limits do not establish that no page exists elsewhere. Incomplete checks do not establish a no-match; omitted entries remain unreported.","items":{"type":"object","required":["promptId","status","url","reason","scope","coverageIssues"],"properties":{"promptId":{"type":["string","null"],"description":"The tracked prompt checked. Null when it was not recorded."},"status":{"type":"string","enum":["matched","no_match","incomplete"],"description":"matched when a page on the shortlist answers the prompt, no_match when none does, incomplete when the check did not finish."},"url":{"type":["string","null"],"description":"The matching page. Null unless status is matched."},"reason":{"type":"string","description":"Stored decision or availability code, not model prose."},"scope":{"type":"string","const":"observed_eligible_page_shortlist"},"coverageIssues":{"type":"array","items":{"type":"string"},"description":"shortlist_limit, sitemap_limit and search_console_limit describe bounded coverage; *_unavailable denotes a failed evidence source."},"checkedAt":{"type":"string","format":"date-time","description":"When the check ran. Omitted when no check time is known, such as an attempt that has not started."}}}}}},"interventionExplanation":{"type":["object","null"],"description":"The page decision recorded when the action was created, as the app shows it. When a brief chooses a different approach, the brief takes precedence. Null on actions you created and on some older actions. This is a recommendation requiring human review, not executed work or a fresh page review. This evidence does not establish a changed target question, same-intent overlap or a retained destination; do not infer retargeting or consolidation from absent citations or multiple ranking pages.","required":["version","selectedMove","buyerQuestions","targetUrl","source","auditFindings","alternative","limitations","protectedPageUrls","approval"],"properties":{"version":{"type":"integer","const":1},"selectedMove":{"type":"string","enum":["create","refresh","refactor","enrich","distribute","review"],"description":"Refactor reorganizes an existing answer; enrich adds missing or outdated detail. Both keep the refresh storage mode. Review means the retained evidence does not support choosing a page move. This does not change opportunity ranking or lifecycle."},"buyerQuestions":{"type":"array","maxItems":20,"description":"Up to 20 tracked prompts behind the action, as buyers ask them.","items":{"type":"object","required":["promptId","question"],"properties":{"promptId":{"type":"string"},"question":{"type":["string","null"],"description":"The prompt text, as the action recorded it. Null when the action did not record it.","maxLength":1000}}}},"targetUrl":{"type":["string","null"],"description":"The page the action targets. Null when it targets no existing page."},"source":{"type":["object","null"],"description":"What prompted the action. Null when that was not recorded.","required":["trigger","summary","windowDays"],"properties":{"trigger":{"type":"string","maxLength":128,"description":"The kind of signal, such as owned_page_gap or missing_page_demand."},"summary":{"type":"string","maxLength":2048,"description":"Why the action exists, for a person to read."},"windowDays":{"type":["number","null"],"description":"The window the signal was measured over, in days. Null when not recorded."}}},"auditFindings":{"type":"array","maxItems":30,"description":"Up to 30 failing Audit checks on the target page.","items":{"type":"object","required":["key","detail","assessedAt"],"properties":{"key":{"type":"string","description":"The Audit check that failed."},"detail":{"type":"string","maxLength":2048,"description":"What the check found."},"assessedAt":{"type":["string","null"],"description":"When the page was assessed. Null when not recorded."}}}},"alternative":{"type":"object","description":"The other page move, and why it was not chosen.","required":["move","reason"],"properties":{"move":{"type":"string","enum":["create","refresh"],"description":"The move not chosen."},"reason":{"type":"string","enum":["existing_page_selected","creation_selected","page_change_unproven","decision_evidence_unavailable","protected_target","existing_page_protected","source_surface_selected"],"description":"existing_page_selected: the stored package targets an existing page. creation_selected: the stored decision chose creation; whether an existing page could serve the question is not established. page_change_unproven: review missing evidence or protection before refreshing. source_surface_selected: the stored decision selected source work, not page creation. decision_evidence_unavailable: review the missing action evidence before choosing any approach. protected_target: the selected target matches a protected page."}}},"limitations":{"type":"array","items":{"type":"string","enum":["stored_snapshot","question_evidence_absent","question_text_unavailable","audit_evidence_unavailable","buyer_questions_truncated","audit_findings_truncated","source_evidence_absent","page_problem_unproven","protected_page"]},"description":"Always reports stored_snapshot: current content, questions and fingerprints have not been revalidated. Missing questions, recorded IDs without retained text, unavailable Audit reads and missing supporting sources are explicit. Lists explicitly report buyer_questions_truncated or audit_findings_truncated when more recorded items exist. Explanatory text is bounded and clipped text ends with an ellipsis. protected_page means keep the listed locked or retiring pages unchanged."},"protectedPageUrls":{"type":"array","description":"Locked or retiring pages the action steers around, including its own target when that page is locked or retiring.","items":{"type":"string"}},"approval":{"type":"string","const":"human_review_required","description":"No pages, redirects, links or drafts were changed by this explanation."}}},"topEvidence":{"type":"array","maxItems":3,"description":"Up to three evidence blocks behind the action.","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"title":{"type":["string","null"],"description":"The evidence title."},"summary":{"type":"string","description":"What the evidence shows."},"type":{"type":"string","description":"The kind of evidence, such as citation_shift, content_change, guard_rail or crawl_or_referral_change."},"redirectTargets":{"type":"array","maxItems":3,"description":"Existing same-site pages to check as redirect targets for missing-page demand. Candidates are advisory and do not affect action rank; an empty list is valid.","items":{"type":"object","properties":{"url":{"type":"string","format":"uri","description":"The existing page."},"title":{"type":["string","null"],"description":"Its title. Null when none was captured."}},"required":["url","title"]}},"audit":{"type":"object","description":"Assessment snapshot that justified this page action, with bounded factor evidence and explicit prompt-coverage limits.","properties":{"sectionAssessment":{"description":"Bounded advisory section evidence from the assessment snapshot that justified this action; null when unavailable or stale.","anyOf":[{"$ref":"#/components/schemas/CitabilitySectionAssessment"},{"type":"null"}]},"assessedAt":{"type":"string","format":"date-time"},"pageUrl":{"type":["string","null"],"description":"The page assessed. Null when not recorded."},"citationScope":{"type":["string","null"],"description":"Which citations the assessment counted. Null when not recorded."},"checks":{"type":"array","maxItems":9,"description":"Up to nine of the page's Audit checks.","items":{"type":"object","properties":{"label":{"type":"string"},"detail":{"type":"string"},"score":{"type":"number","minimum":0,"maximum":100,"description":"The check score, from 0 to 100."},"failing":{"type":"boolean","description":"True when score is below 60."}},"required":["label","detail","score","failing"]}}},"required":["assessedAt","checks"]},"citationLoss":{"type":"object","description":"Present on citation-loss evidence: the page's citations on each prompt and platform that cited it in either 30-day window, earlier then current; the cells add up to the loss totals. The loss is a change in citations per execution, so each cell carries its executions.","properties":{"pageLabel":{"type":["string","null"],"description":"The page, as shown to people. Null when not recorded."},"priorCitationCount":{"type":"number","minimum":0,"description":"The page's citations in the earlier of the two 30-day windows. When the comparison uses cells probed in both windows, only those cells count."},"currentCitationCount":{"type":"number","minimum":0,"description":"The page's citations in the later of the two 30-day windows, which ends on the day the evidence was measured. When the comparison uses cells probed in both windows, only those cells count."},"priorExecutions":{"type":["number","null"],"description":"AI answers measured in the earlier 30 days. Null when not recorded."},"currentExecutions":{"type":["number","null"],"description":"AI answers measured in the current 30 days. Null when not recorded."},"cellCount":{"type":"integer","minimum":0,"description":"Every valid cell the loss has; cells holds the 50 largest losses when this is higher."},"cells":{"type":"array","maxItems":50,"description":"One per prompt and platform, largest loss first.","items":{"type":"object","properties":{"promptId":{"type":"string"},"promptText":{"type":["string","null"],"description":"The prompt text. Null when it is no longer kept."},"platform":{"type":"string"},"priorCitationCount":{"type":"number","minimum":0,"description":"Citations in the earlier 30 days."},"currentCitationCount":{"type":"number","minimum":0,"description":"Citations in the later of the two 30-day windows, which ends on the day the evidence was measured."},"priorExecutions":{"type":["number","null"],"description":"Answers measured in the earlier 30 days. Null when not recorded."},"currentExecutions":{"type":["number","null"],"description":"Answers measured in the current 30 days. Null when not recorded."}},"required":["promptId","platform","priorCitationCount","currentCitationCount"]}}},"required":["priorCitationCount","currentCitationCount","cellCount","cells"]},"googleEligibility":{"type":"object","description":"Present only for entitled guard-rail evidence backed by a live exact-domain GSC source and a fresh observation.","properties":{"coverageState":{"type":["string","null"],"maxLength":1024,"description":"Google's coverage state for the page, worded as Search Console shows it, such as Submitted and indexed. Null when the latest inspection failed or reported none."},"dimensions":{"type":"object","description":"Each indexing check, as in GoogleEligibilityDimensions.","properties":{"canonical":{"type":"string","enum":["pass","fail","unmeasured"],"description":"pass when the canonical URL Google selected matches the one the page declares, fail when they differ, unmeasured when either is missing."},"crawlable":{"description":"fail when robots.txt blocks Google or Google could not fetch the page. pass when robots.txt allows the page, the fetch succeeded and indexing was allowed or blocked only by noindex. unmeasured otherwise, such as when the inspection did not report all three.","type":"string","enum":["pass","fail","unmeasured"]},"indexed":{"type":"string","enum":["pass","fail","unmeasured"],"description":"pass when Google indexed the page, fail when the verdict is any other outcome, unmeasured when the inspection failed or reported no detail."},"structuredData":{"type":"string","enum":["pass","fail","unmeasured"],"description":"pass when Google's rich results check passed. fail when it failed, passed only in part, or found no rich results. unmeasured when the inspection reported no detail."}},"required":["canonical","crawlable","indexed","structuredData"]},"observedAt":{"type":["string","null"],"format":"date-time","maxLength":64,"description":"When the latest successful inspection ran. Null when not recorded."}},"required":["coverageState","dimensions","observedAt"]}},"required":["id","title","summary","type"]}},"winningPages":{"type":"array","nullable":true,"maxItems":5,"description":"External pages winning this action's prompts over the last 30 UTC days, competitor and third-party alike, in the order briefs study them: each prompt's top page first, then by answers citing the page weighted by 1 / average position; one page per site. Empty when no external page is cited or the action is not a command-center recommendation; null when the pages could not be loaded.","items":{"type":"object","properties":{"rank":{"type":"integer","minimum":1,"description":"Its place in that order, from 1."},"url":{"type":"string","format":"uri","description":"The winning page."},"title":{"type":["string","null"],"description":"Its title. Null when none was captured."},"domain":{"type":["string","null"],"description":"Its domain. Null when not recorded."},"assetType":{"type":"string","enum":["competitor","third_party"],"description":"competitor for a tracked competitor’s page, third_party for any other site."},"citationCount":{"type":"integer","minimum":0,"description":"Its citations on the action's prompts."},"citedAnswers":{"type":"integer","minimum":0,"description":"AI answers that cited the page."},"promptCount":{"type":"integer","minimum":1,"description":"The action's prompts it was cited on."},"averagePosition":{"type":["number","null"],"description":"Its average position in the answers' sources; 1 is the first. Null when no citation has a position."}},"required":["rank","url","title","domain","assetType","citationCount","citedAnswers","promptCount","averagePosition"]}},"robotsTxtDraftSummary":{"type":["object","null"],"description":"Whether an Audit-finding action has a drafted robots.txt edit, the crawler tokens it unblocks, and when the audit read the file. Read the edited file and diff from the action's brief. Null when the action has no robots.txt draft.","required":["drafted","tokens","observedAt"],"properties":{"drafted":{"type":"boolean","description":"True when at least one file has a drafted edit."},"tokens":{"type":"array","description":"The crawler tokens the drafts unblock.","items":{"type":"string"}},"observedAt":{"type":["string","null"],"format":"date-time","description":"When the audit first read a file. Null when there are no files."}}},"mergedSuggestions":{"type":"array","nullable":true,"description":"Reworded suggestions DevTune's duplicate check merged into this action because it judged them the same fix, oldest first. A merged suggestion stays hidden from the queue while this action holds its decision. Split one from the action's page in the app to bring it back as its own suggestion after the next refresh. Empty when nothing was merged; null when the merged list could not be read.","items":{"type":"object","properties":{"label":{"type":"string","description":"The merged suggestion’s wording."},"mergedAt":{"type":"string","format":"date-time"},"confidence":{"type":["number","null"],"minimum":0,"maximum":1,"description":"The duplicate check confidence in the match; null when the merge stored no judgment."}},"required":["label","mergedAt","confidence"]}}},"required":["topEvidence"],"additionalProperties":true}}}},"pagination":{"$ref":"#/components/schemas/Pagination"}},"required":["actions","pagination"]},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Actions"]}},"/projects/{projectId}/actions/{actionId}":{"patch":{"summary":"Update an action","description":"Changes the title, owner, channels or description of an accepted or live action. Restore an archived action first. DevTune's suggestions, including dismissed and snoozed ones, keep their details and have no owner; accept one before changing it. assignedTo is the user id or sign-in email of a current member of the account that owns the project; null unassigns. channels is the full set of channels that own the work and replaces the current set; the same set in another order changes nothing. description changes only on an accepted action the team created: DevTune's actions keep the description their evidence wrote (409), and a live action's description is fixed until the action is reopened (409). A change queues an action.updated webhook whose changedFields lists what changed: title, assigned_to, description or channels. The owner is not notified and the brief is not marked out of date. Requires the `actions.write` scope.","operationId":"updateAction","x-devtune-required-scope":"actions.write","parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"actionId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"minProperties":1,"properties":{"title":{"type":"string","minLength":1,"maxLength":500,"description":"New title. Surrounding whitespace is trimmed; a blank title is refused."},"assignedTo":{"type":["string","null"],"description":"The new owner as a user id or the email they sign in with, or null to unassign. The owner must be a current member of the account."},"description":{"type":"string","minLength":1,"maxLength":10000,"description":"New description. Only for an accepted action the team created. Surrounding whitespace is trimmed; a blank description is refused."},"channels":{"type":"array","minItems":1,"uniqueItems":true,"items":{"type":"string","enum":["docs","content","devrel","pr","engineering","product","other"]},"description":"Every channel that owns the work. Replaces the action's channels; order does not matter."}}}}}},"responses":{"200":{"description":"The updated action","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"title":{"type":"string","description":"The title after the update."},"description":{"type":"string","description":"The description after the update."},"channels":{"type":"array","description":"The channels after the update.","items":{"type":"string"}},"status":{"$ref":"#/components/schemas/PublicActionStatus"},"assignedTo":{"type":["string","null"],"format":"uuid","description":"User id of the owner. Null when unassigned."},"ownerName":{"type":["string","null"],"description":"The owner's name, or email when they have none. Null when unassigned, or when the owner is no longer on the account."},"updatedAt":{"type":"string","format":"date-time"}}}}}}}},"400":{"description":"Invalid input, or assignedTo is not a member of the account"},"404":{"description":"Action not found in this project"},"409":{"description":"The action is suggested or archived; a description edit targets DevTune's action or a live action; or the action changed during the update"}},"tags":["Actions"]}},"/projects/{projectId}/actions/{actionId}/status":{"post":{"summary":"Change an action status","description":"Moves an action through its lifecycle, as the member who created the API key; they must be a project member with project management permission (403 otherwise, including for a key with no creator). Transitions come in pairs. accept moves a suggestion to accepted and unaccept returns it to the suggestions; unaccept is refused with not_reversible once the action has an owner, is in progress or blocked, has ever gone live or has a measurement, or has a brief, draft, edit, Library item, retest, prompt request added from another prompt, or dismissal note attached, and for some older accepted actions; an action someone created, such as a prompt action, is refused with invalid_transition; and a suggestion accepted as already covered by an existing page is not undone. archive takes a suggestion or accepted or live work off the queue; restore returns an archived suggestion to the suggestions, or archived work to the status it had, live work keeping its go-live date. Work archived while blocked returns to blocked, except some older work, which returns to the status it had before it was blocked. block and unblock apply to accepted work; unblock returns it to the status it had before. mark_live moves accepted work to live and opens its before/after measurement in Results from now, or from liveOn, the UTC day the fix went live (today or up to 14 days back); publishedUrl, when given, is added to the pages the action already carries, which all stay. mark_live with liveOn on live work moves its go-live date and rebuilds its measurement from that day; a measurement that has closed or is dated by a recorded page change keeps its date, refused with invalid_transition, and a page another action claimed from a detected change within 14 days of the date is refused with conflict. A liveOn outside the range is a 400. reopen moves live work back to accepted: its measurement, go-live date and published pages stay, and marking it live again continues the same measurement, except a lookalike action, whose measurement restarts from the new go-live time. accept, archive of a suggestion, restore of an archived suggestion and unaccept act on every suggestion merged into the named one. A high-stakes target page refuses accept with acknowledgment_required until acknowledgeHighStakes is true, which needs a human's confirmation; a locked page refuses it with page_locked. Repeating accept, unaccept, archive, block, unblock, reopen, or mark_live without a new page or go-live date returns changed false and writes nothing; restore of an action that is not archived is refused with invalid_transition, because the status it would return to is not known. Any other refusal is a 409 whose reason is invalid_transition, superseded (a newer action for the same Agent Picks loss is open) or conflict (the action, its merged suggestions or its target page's protection changed during the request, or the merged suggestions cannot all move in one change; read the action and retry); nothing is written. Some older archived suggestions can't be restored and are always refused with conflict; they stay archived. The key, its creator's membership and their permission are checked again where the change is written, so access revoked mid-request is refused with 403. A change queues action.updated webhooks naming the API as the actor. Nothing is published to the customer's site. Requires the `actions.write` scope.","operationId":"changeActionStatus","security":[{"bearerAuth":[]},{"oauthBearer":[]}],"x-devtune-required-scope":"actions.write","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}},{"name":"actionId","in":"path","required":true,"description":"The action","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["transition"],"properties":{"transition":{"type":"string","enum":["accept","unaccept","archive","restore","block","unblock","mark_live","reopen"]},"acknowledgeHighStakes":{"type":"boolean","description":"Only with accept. Confirms a human approved changing a high-stakes page."},"publishedUrl":{"type":"string","format":"uri","maxLength":2048,"description":"Only with mark_live. The http(s) page where the fix shipped."},"liveOn":{"type":"string","format":"date","description":"Only with mark_live. The UTC day the fix went live, today or up to 14 days back; defaults to today. On a live action it moves the go-live date."}}}}}},"responses":{"200":{"description":"The transition receipt","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ActionStatusReceipt"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"409":{"$ref":"#/components/responses/ConflictError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Actions"]}},"/projects/{projectId}/actions/{actionId}/events":{"get":{"summary":"List action edits","description":"Lists who changed an action's title, owner, channels, description or published pages, newest first, one event per changed field and one per published page added or removed. Owners are user ids, channels are arrays and pages are URLs. A member who made an edit is named while they are still on the account; edits made with an API key carry its key id, and DevTune's own edits are system. Lifecycle moves such as accepting, going live or archiving are not events. History starts when recording began, so older edits are not listed. Requires the `actions.read` scope.","operationId":"listActionEvents","x-devtune-required-scope":"actions.read","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}},{"name":"actionId","in":"path","required":true,"description":"The action","schema":{"type":"string","format":"uuid"}},{"name":"cursor","in":"query","schema":{"type":"string"},"description":"Opaque cursor from the previous response."},{"name":"pageSize","in":"query","schema":{"type":"integer","default":25,"minimum":1,"maximum":100},"description":"Results per page. Capped at 100."},{"name":"limit","in":"query","schema":{"type":"integer","default":25,"minimum":1,"maximum":100},"description":"The same setting as pageSize. Send either; both with different values returns 400."}],"responses":{"200":{"description":"Action edits","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ActionEventsData"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Actions"]}},"/projects/{projectId}/actions/{actionId}/draft":{"get":{"summary":"Get action content draft","description":"Returns an action's content draft for accounts with content drafts: the draft the action takes now, the models a run can use with their estimates, the run in flight, and the current draft with up to 10 earlier versions, each with its markdown, change list and fact check. Refused with 403 when content drafts are off, and 400 when actionId is not a UUID. Requires the `actions.read` scope.","operationId":"getActionContentDraft","security":[{"bearerAuth":[]},{"oauthBearer":[]}],"x-devtune-required-scope":"actions.read","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}},{"name":"actionId","in":"path","required":true,"description":"UUID of the action.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Action content draft","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ActionContentDraft"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Actions"]},"post":{"summary":"Start an action content draft","description":"Starts a content-draft run for the action with the same checks as Generate draft in the app: the Page Drafter for a create action, the Section Editor for a refresh action. A refusal carries its reason: 409 for not_content_action, read_only, brief_not_ready, brief_outdated (the brief predates the current brief format; regenerate it), no_target_page, page_locked, draft_kind_changed (the action now takes the other kind than the draftKind sent), or evidence_unavailable (the evidence of an Agent Picks Action cannot be read; run a new Agent Picks test to refresh it); 403 for forbidden or unavailable (content drafts are off). 409 without a reason means a draft is already being written for this action, or the requestIdempotencyKey was already used for a different request. The API key must have been created by a member of the project with project management permission; otherwise 403. A retry with the same requestIdempotencyKey returns the run that key started as it stands, without rechecking the action, which may have changed since. A retry for another action, draftKind or modelId (an omitted modelId means the account default) is a 409. A dispatch that fails during the request returns 500 and starts no run. The run uses agent credits; a draft is never published by DevTune. This endpoint has a stricter rate limit than read endpoints. Requires the `agents.run` scope.","operationId":"startActionContentDraft","x-devtune-required-scope":"agents.run","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}},{"name":"actionId","in":"path","required":true,"description":"UUID of the action.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"draftKind":{"type":"string","enum":["page","section_edits"],"description":"The draft the caller expects, from the draft read. When sent, the start refuses with draft_kind_changed if the action now takes the other kind."},"modelId":{"type":"string","minLength":1,"maxLength":120,"description":"A model id from the draft read; the default model when absent."},"requestIdempotencyKey":{"type":"string","format":"uuid"}},"required":["requestIdempotencyKey"]}}}},"responses":{"200":{"description":"Started draft run","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ContentDraftStartResult"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"402":{"description":"Insufficient included or purchased credits","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"409":{"$ref":"#/components/responses/ConflictError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"},"500":{"$ref":"#/components/responses/ServerError"}},"tags":["Actions"]}},"/projects/{projectId}/actions/{actionId}/draft/pull-request":{"post":{"summary":"Create action draft pull request","description":"Opens a pull request on the customer's content repo through the DevTune GitHub App, or a change request in their GitBook space, from the action's current content draft, or writes a newer draft to the open one. The page belongs to the content source with the longest matching URL prefix, and the source's provider decides which. One per action; DevTune writes only to its own branch or change request and never merges. A refresh draft's changes are applied to the page, matched after collapsing whitespace; if any does not match, nothing is written. A create draft becomes a new page named from the brief's target. A GitBook change request's description names who approved the draft, and DevTune checks GitBook stored the page as written. The pull request is read from GET /projects/{projectId}/actions/{actionId}/draft. A refusal is a 409 with its reason: no_draft, draft_not_latest, action_closed (live or archived), page_locked, no_content_source (details.sources lists the sources; retry with sourceId), source_needs_reconnect, file_not_found (details.tried lists what was tried; retry with filePath for GitHub or pagePath for GitBook), file_exists, draft_does_not_match_source (details.changes names the changes), branch_diverged (someone else pushed to DevTune's branch), change_request_diverged (someone else edited the change request), change_request_conflicts (the change request has conflicts in GitBook), page_not_preserved (GitBook stored the page differently), pull_request_in_progress or pull_request_merged; 400 for invalid_file_path; 403 when content pull requests are off; 503 for github_error or gitbook_error (details.detail is rate_limited when GitBook's rate limit for the connection is used up, recovery_incomplete when DevTune could not finish looking for a change request an earlier attempt may have opened, or recovery_stale when another attempt for the action finished while this one started; retry later). The API key must have been created by a member of the project with project management permission; otherwise 403. Requires the `actions.write` scope.","operationId":"createActionPullRequest","x-devtune-required-scope":"actions.write","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}},{"name":"actionId","in":"path","required":true,"description":"UUID of the action.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"properties":{"draftId":{"type":"string","format":"uuid","description":"The current draft version id from the draft read; refused with draft_not_latest if a newer draft replaced it."},"sourceId":{"type":"string","format":"uuid","description":"A content source id from GET /projects/{projectId}/setup/content-sources, for a page no source covers."},"filePath":{"type":"string","minLength":1,"maxLength":1000,"description":"The repo file to edit or create, under the source's folder, when DevTune cannot find or name it. GitHub sources only."},"pagePath":{"type":"string","minLength":1,"maxLength":1000,"description":"The GitBook page path to edit or create, below the space root, such as guides/quickstart, when DevTune cannot find or name it. GitBook sources only."}}}}}},"responses":{"200":{"description":"Action draft pull request","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ActionPullRequestData"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"409":{"$ref":"#/components/responses/ConflictError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"},"500":{"$ref":"#/components/responses/ServerError"},"503":{"headers":{"Retry-After":{"description":"Seconds to wait before retrying a 429 or retryable 503 response.","schema":{"type":"integer","minimum":1}}},"description":"GitHub or GitBook refused the request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"tags":["Actions"]},"delete":{"summary":"Close action draft pull request","description":"Closes the action's open pull request in GitHub and deletes DevTune's branch, or archives its GitBook change request. Works even when content pull requests are no longer enabled. The draft stays. A refusal is a 409 with its reason: no_pull_request, pull_request_merged or source_needs_reconnect (reconnect GitHub or GitBook, or close it there); 503 for github_error or gitbook_error. The API key must have been created by a member of the project with project management permission; otherwise 403. Requires the `actions.write` scope.","operationId":"closeActionPullRequest","x-devtune-required-scope":"actions.write","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}},{"name":"actionId","in":"path","required":true,"description":"UUID of the action.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Closed pull request","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ActionPullRequestData"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"409":{"$ref":"#/components/responses/ConflictError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"},"500":{"$ref":"#/components/responses/ServerError"},"503":{"headers":{"Retry-After":{"description":"Seconds to wait before retrying a 429 or retryable 503 response.","schema":{"type":"integer","minimum":1}}},"description":"GitHub refused the request","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"tags":["Actions"]}},"/projects/{projectId}/actions/{actionId}/brief":{"get":{"summary":"Get action brief","description":"Returns the stored brief status for an action and, when a brief is ready, the brief. A brief written to the brief contract (briefFormat 2) is returned as Markdown, brief first and its Evidence section after, and as the structured brief and evidence objects. A briefFormat 1 brief returns Markdown only, with brief and evidence null. Requires the `actions.read` scope.","operationId":"getActionBrief","security":[{"bearerAuth":[]},{"oauthBearer":[]}],"x-devtune-required-scope":"actions.read","parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"actionId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Action brief payload","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"actionId":{"type":"string","format":"uuid"},"actionTitle":{"type":"string"},"hasBrief":{"type":"boolean","description":"True when a brief is ready."},"status":{"type":["string","null"],"description":"The status of this action’s brief, not of the action itself. Null when no brief has been requested."},"generatedAt":{"type":["string","null"],"format":"date-time","description":"When the brief was generated. Null when none has been."},"updatedAt":{"type":["string","null"],"format":"date-time","description":"When the brief record last changed. Null when there is none."},"goal":{"type":["string","null"],"description":"What the work should achieve. Null until the brief is ready."},"targetAsset":{"type":["string","null"],"description":"The page or asset to create or change. Null until the brief is ready, or when the brief names none."},"audienceContext":{"type":["string","null"],"description":"Who the page is for. Null until the brief is ready, or when the brief records none."},"recommendedOwner":{"type":["string","null"],"description":"Who should do the work. Null until the brief is ready, or when the brief names no owner."},"effortLabel":{"type":["string","null"],"description":"How much work it is. Null until the brief is ready."},"acceptanceCriteria":{"type":"array","description":"Checks the finished work should pass. Empty until the brief is ready.","items":{"type":"string"}},"briefMarkdown":{"type":["string","null"],"description":"The brief as Markdown. For briefFormat 2: Task, Outline, Facts to use, Leave out, Rules and Done when, then an Evidence section that is context for a reviewer, not part of the specification. Null until the brief is ready."},"briefFormat":{"type":["integer","null"],"enum":[1,2,null],"description":"The format the brief was written in. 2 is the brief contract, which has the structured brief and evidence objects and is the only format a draft can be generated from; 1 is every earlier brief. Null when no brief is ready."},"brief":{"type":["object","null"],"description":"The structured brief a drafter works from: what to do and everything needed to do it. Null unless briefFormat is 2.","required":["task","outline","facts","leaveOut","rules","doneWhen"],"properties":{"task":{"type":"object","description":"What to write, for whom, and why it is worth citing.","required":["motion","page","format","audience","buyerQuestion","angle"],"properties":{"motion":{"type":"string","enum":["create","refresh"],"description":"create for a new page, refresh for an existing one."},"page":{"type":"string","description":"The page URL for a refresh, or the proposed slug or title for a create."},"format":{"type":"string","description":"The page format, such as Technical guide."},"audience":{"type":"string","description":"Who the page is for."},"buyerQuestion":{"type":"string","description":"The buyer question the page must answer."},"angle":{"type":"string","description":"What makes the page worth citing, in one or two sentences."}}},"outline":{"type":"array","description":"The page sections, in order: 1 to 12.","items":{"type":"object","required":["heading","mustTell"],"properties":{"heading":{"type":"string"},"mustTell":{"type":"string","description":"What the section must tell the reader."}}}},"facts":{"type":"array","description":"The product facts the outline sections use, each with its source and the sections it serves. A draft states no other product fact.","items":{"type":"object","required":["text","sourceType","sourceUrl","attribution"],"properties":{"text":{"type":"string"},"sourceType":{"type":"string","enum":["owned_content","project_fact","external_source"],"description":"owned_content: a page on the customer's site states it. project_fact: the project profile supplies it. external_source: another site states it."},"sourceUrl":{"type":["string","null"],"description":"The owned page that states the fact, or an external source URL. Null when the fact has no page."},"attribution":{"type":["string","null"],"description":"The external source, or the supplied project data the fact came from. Never null for an external or project fact."},"sourceRef":{"type":["string","null"],"description":"The supplied item the fact restates: a project data ref such as brandContext.keyFeatures[0] for a project fact, or the source packet URL for an external fact. Null for an owned fact, whose sourceUrl is its source. Absent on briefs written before facts carried refs."},"sections":{"type":"array","items":{"type":"integer","minimum":0},"description":"Zero-based indexes of the outline sections that use the fact. Absent on briefs written before facts named their sections."}}}},"leaveOut":{"type":"array","items":{"type":"string"},"description":"Facts the topic would normally want that have no verified source, so the page must not state them."},"rules":{"type":"object","required":["tone","length","structure","limits","protection"],"description":"How the page should be written.","properties":{"tone":{"type":"string","description":"The voice to write in."},"length":{"type":"string","description":"How long the page should be."},"structure":{"type":"array","description":"Structure rules, including DevTune's citability fixes and, when a missing page should redirect, the redirect to set up.","items":{"type":"string"}},"limits":{"type":"array","items":{"type":"string"},"description":"Limits on the draft, ending with the standing rules every brief carries."},"protection":{"type":"array","items":{"type":"string"},"description":"Page-protection constraints for a locked or retiring page."}}},"doneWhen":{"type":"array","items":{"type":"string"},"description":"Checks a reviewer makes on the page."},"inboundLinkPlan":{"type":"object","description":"Optional suggestions for links from related pages to the target, grounded in supplied content and one fixed crawl. A single-page draft leaves these edits outstanding. Unavailable evidence yields no edits; partial coverage never establishes orphan status or link depth. Every edit needs customer approval and a fresh crawl to verify the named edge. The plan is omitted when current brief inputs are stale or cannot be verified; the stored target brief remains available.","required":["targetUrl","generationId","coverage","coverageNote","edits"],"properties":{"targetUrl":{"type":"string","format":"uri","description":"The page the links should point to."},"generationId":{"type":["string","null"],"description":"Identifies the crawl the suggestions come from. Null when none was supplied, which leaves edits empty."},"coverage":{"type":"string","enum":["partial","unavailable"],"description":"partial when the plan was built from the crawl, which checks only a few related pages, never the whole site; unavailable when the crawl or page-protection evidence could not be read, which leaves edits empty."},"coverageNote":{"type":"string","description":"What the plan checked and left unverified, or why it proposes no edits, for a person to read."},"sourceCandidateUrls":{"type":"array","maxItems":8,"items":{"type":"string","format":"uri"},"description":"The related pages considered when the plan was made. They do not say the pages are current or may be edited."},"skipReason":{"type":"string","enum":["target_excluded"],"description":"The target is outside scope, locked or retiring, so no related-page edits are proposed."},"targetDepth":{"type":["integer","null"],"minimum":0,"description":"The target’s link depth in that crawl, in clicks. Null when not measured, or when current page protections could not be read. Not a visibility metric."},"edits":{"type":"array","maxItems":3,"description":"Up to three link edits on other pages, each needing approval before anyone makes it.","items":{"type":"object","required":["sourceUrl","sourceSection","placement","anchorText","evidence","edgeStatus"],"properties":{"sourceUrl":{"type":"string","format":"uri","description":"The page to add the link to."},"sourceSection":{"type":["string","null"],"description":"The section of that page. Null when the edit names none."},"placement":{"type":"string","description":"Where on the page the link goes."},"anchorText":{"type":"string"},"edgeStatus":{"type":"string","enum":["not_observed_in_generation"]},"evidence":{"type":"object","description":"The crawled text the edit is based on.","required":["excerpt","contentHash","crawledAt"],"properties":{"excerpt":{"type":"string","description":"The passage where the link fits."},"contentHash":{"type":"string","description":"A hash of the crawled page content."},"crawledAt":{"type":"string"}}}}}}}}}},"evidence":{"type":["object","null"],"description":"Why the page is worth writing. Context for a reviewer, never an instruction to the drafter. Null unless briefFormat is 2.","required":["whyNow","sourcePatterns","reasoningChecks","citability","platformGuidance"],"properties":{"whyNow":{"type":"array","description":"Why the page is worth writing now, such as the citation gap, the competitors cited instead and the prompts behind it.","items":{"type":"string"}},"sourcePatterns":{"type":"array","description":"What the cited pages, or the tools agents chose instead, do that this page can learn from.","items":{"type":"string"}},"reasoningChecks":{"type":"array","description":"Checks of the brief's own reasoning, for a reviewer.","items":{"type":"string"}},"citability":{"type":["object","null"],"description":"The target page's Audit citability. For a new page, the checklist it should meet, with no score. Null when the page to refresh matches no crawled page, or when a brief for an existing page proposes a new one.","required":["pageScore","statusLabel","factors","gaps"],"properties":{"pageScore":{"type":["number","null"],"description":"The page citability score, from 0 to 100. Null when not scored."},"statusLabel":{"type":["string","null"],"description":"The score’s label key: strong, needs_work, incomplete or blocked. Null when not scored."},"factors":{"type":"array","description":"The citability factors the brief fixes: on an existing page, those scoring below 60; for a new page, the checklist items, each scored 0.","items":{"type":"object","required":["factor","score","weight","detail"],"properties":{"factor":{"type":"string"},"score":{"type":"number","description":"Its score, from 0 to 100."},"weight":{"type":"number","description":"Its weight in pageScore."},"detail":{"type":["string","null"],"description":"What the check found, or on a new-page checklist what to do. Null when none is given."}}}},"gaps":{"type":"array","description":"Notes on what the citability score could not measure for the page, such as a factor not yet scored, for a person to read.","items":{"type":"string"}}}},"platformGuidance":{"type":"array","description":"Per AI platform, whether it already cites the customer on the action's prompts, with what to keep or add for it. Empty unless at least one platform cites the customer and another does not.","items":{"type":"string"}}}},"creditsUsed":{"type":["number","null"],"description":"Exact credits consumed by the settled brief. Null until billing is finalized.","maximum":2,"minimum":1},"wasCutShort":{"type":"boolean","description":"True when the brief was delivered after research reached its hard budget and the agent wrapped up early."},"sourceSnapshot":{"type":["object","null"],"additionalProperties":true,"description":"The inputs the brief was written from. Null until the brief is ready."},"robotsTxtDraft":{"type":["object","null"],"description":"The robots.txt edit that unblocks the answer engines an Audit finding found blocked, drafted from the robots.txt the audit read at observedAt. A human applies it; DevTune never writes, hosts or serves robots.txt. It covers only the finding's URLs still actionable and never changes the crawler's access to a URL acknowledged as intentional, and training crawlers are never unblocked. Null when the action has no robots.txt draft.","required":["version","files"],"properties":{"version":{"type":"integer","enum":[1]},"files":{"type":"array","description":"One entry per robots.txt file the blocked pages fall under.","items":{"type":"object","required":["robotsUrl","observedAt","pageUrls","draft"],"properties":{"robotsUrl":{"type":"string","format":"uri"},"observedAt":{"type":"string","format":"date-time","description":"When the audit read the file."},"pageUrls":{"type":"array","description":"The finding's blocked page URLs under this file.","items":{"type":"string","format":"uri"}},"draft":{"type":"object","description":"drafted carries the edited file and a unified diff that applies byte for byte; no_change means nothing in this file needs an edit; unavailable means the audit did not keep the file (robots_unavailable), no edit could lift the block without changing another path the crawler's rules name or an acknowledged URL (unsafe_edit), the diff would exceed 16 KB, too large to review safely (too_large), the file uses bare carriage-return line endings that a unified diff cannot express (unsupported_line_endings), or a sampled page redirects to an origin outside the project's domains, whose robots.txt is someone else's to change (unowned_origin). ownership_unknown means the project's domains could not be read during the audit, and no_primary_domain that the project has none; in both cases nothing is drafted. out_of_date means the stored draft would change how a URL acknowledged since then reads for a crawler it unblocks; every read re-checks stored drafts against the current acknowledgements and withholds them this way, so create the action again from a new audit. redirected means every blocked page in the file redirects; a sampled page that redirects is never drafted, its first hop included, and each is listed in skipped with reason redirected and path set to that sampled page's URL. check_unavailable means the acknowledgements or primary domains a read checks the draft against could not be read; try again. path_scoped_domain means the project's primary domain covers only a path on that host, and editing robots.txt needs the whole host. robots_redirected means the robots.txt request redirected to another URL, or another origin's robots.txt redirects to this file; either way it is never drafted. acknowledged_scope means the project has at least one acknowledged restriction for this crawler scope (provider and capability), on any host and URL, so no edit is drafted or shown for that crawler; tokens in other scopes still draft. Ownership is exact-host only: an origin is owned only when its host equals a host-only primary domain, with no subdomain expansion, and unowned_origin otherwise. too_large_to_check means checking the draft would exceed the read-time work budget. Ownership is re-derived from the project's current primary domains on every read, so a removed domain's file comes back with its ownership reason.","required":["status","skipped"],"properties":{"status":{"type":"string","enum":["drafted","no_change","unavailable"]},"reason":{"type":"string","description":"Why no draft is available. Present only when status is unavailable.","enum":["robots_unavailable","unsafe_edit","too_large","unsupported_line_endings","unowned_origin","ownership_unknown","no_primary_domain","out_of_date","redirected","check_unavailable","too_large_to_check","path_scoped_domain","robots_redirected","acknowledged_scope"]},"text":{"type":"string","description":"The edited robots.txt, in full. Present only when status is drafted."},"diff":{"type":"string","description":"A unified diff from the file the audit read to text. Present only when status is drafted."},"allows":{"type":"array","description":"One entry per crawler and blocked page the edit opens, with the line that opens it; several entries can share a line. Present only when status is drafted.","items":{"type":"object","required":["token","path","blockedBy","addedRule","match","group"],"properties":{"token":{"type":"string","description":"The crawler user-agent token the rule unblocks."},"path":{"type":"string","description":"The blocked page path the rule opens."},"blockedBy":{"type":"object","description":"The rule in the robots.txt the audit read that blocks the page.","required":["agent","directive","value"],"properties":{"agent":{"type":"string","description":"The User-agent group the rule sits in."},"directive":{"type":"string","enum":["allow","disallow"]},"value":{"type":"string","description":"The path pattern the rule names."}}},"addedRule":{"type":"string","description":"The line the edit adds, such as Allow: /docs/."},"match":{"type":"string","enum":["blocking_rule","exact_page"],"description":"blocking_rule mirrors the blocking rule, so the allow opens what that rule blocked; exact_page allows only this sampled page, because opening the whole rule could change a path another kept rule or an acknowledged restriction covers."},"group":{"type":"string","enum":["own","wildcard_copy"],"description":"wildcard_copy means the token had no group of its own, so its new group repeats the User-agent: * rules it stops reading."}}}},"skipped":{"type":"array","description":"Crawler and page pairs the edit leaves as they are, with why.","items":{"type":"object","required":["token","path","reason"],"properties":{"token":{"type":"string","description":"The crawler user-agent token."},"path":{"type":"string","description":"The page path, or the full URL of a sampled page that redirects."},"reason":{"type":"string","description":"training_crawler: training crawlers are never unblocked. not_answer_engine: the token is not an answer engine. already_allowed: nothing blocks the page. no_safe_edit: no rule lifts the block without changing another path or an acknowledged URL. redirected: the page redirects; edit the robots.txt where it lands. acknowledged_scope: a restriction for this crawler is acknowledged.","enum":["training_crawler","not_answer_engine","already_allowed","no_safe_edit","redirected","acknowledged_scope"]}}}}}}}}}}},"claimCheck":{"type":["object","null"],"description":"How the brief's claims held up against the project's owned content. Owned-content and project-fact claims are searched; other claims are set aside unchecked. The claims are, for a briefFormat 2 brief, its facts list, in list order; for an earlier brief, its claim references. Present only for projects with claim checks enabled and only after a check has run for the current brief; absent, never null, otherwise. Annotation only: it does not change the brief.","required":["questionVersion","model","checkedAt","coverage","results"],"properties":{"questionVersion":{"type":"integer","description":"Version of the check’s questions."},"model":{"type":"string","description":"The model that judged the claims."},"checkedAt":{"type":"string","format":"date-time"},"coverage":{"type":"object","description":"Every claim reference is checked, not checked because its owned-content search failed, or set aside by source type, so total is checked plus retrievalFailed plus the setAside counts.","required":["total","checked","retrievalFailed","setAside"],"properties":{"total":{"type":"integer","minimum":0,"description":"Claims in the brief."},"checked":{"type":"integer","minimum":0,"description":"Claims judged against owned content."},"retrievalFailed":{"type":"integer","minimum":0,"description":"Claims listed as unclear with reason retrieval_failed."},"setAside":{"type":"object","description":"Claims set aside unchecked, by source type. Claims whose search failed are counted in retrievalFailed instead.","required":["devtune_data","external_source","global_prior"],"properties":{"devtune_data":{"type":"integer","minimum":0,"description":"Claims based on DevTune's own data."},"external_source":{"type":"integer","minimum":0,"description":"Claims sourced from another site."},"global_prior":{"type":"integer","minimum":0,"description":"Claims from general knowledge."}}}}},"results":{"type":"array","description":"One result per claim, in the brief’s order.","items":{"type":"object","required":["claim","sourceRef","source","sourceType","judgment","confidence"],"properties":{"claim":{"type":"string"},"sourceRef":{"type":"string","deprecated":true,"description":"Deprecated: use source. Holds source.url, or an empty string when the source names no page; DevTune's internal references are not returned."},"source":{"type":"object","description":"Where the claim came from: a page on the project's site, the project profile, DevTune's own analysis, an external source, or general knowledge. url is the page the claim names, when it names one.","required":["kind","url"],"properties":{"kind":{"type":"string","enum":["site_page","project_profile","devtune_analysis","external","general"]},"url":{"type":["string","null"],"description":"The page the claim names. Null when it names none."}}},"sourceType":{"type":"string","description":"The claim’s source type. Only owned_content and project_fact claims are checked.","enum":["owned_content","project_fact","devtune_data","external_source","global_prior"]},"judgment":{"type":"string","enum":["supported","contradicted","unsupported","stale","unclear","not_checked"],"description":"supported or contradicted by owned content, unsupported (no owned passage states it), stale (supported only by dated content), unclear, or not_checked (set aside)."},"reason":{"type":"string","description":"Why a claim is unclear; project_profile on an unsupported claim taken from the project profile; or why a claim was set aside: its source type, or devtune_analysis when it is about DevTune's own metrics, settings or reasoning."},"confidence":{"type":"number","minimum":0,"maximum":1,"description":"How sure the check was, from 0 to 1. 0 for a claim not checked."},"evidenceSource":{"type":"string","enum":["retrieved","snapshot","both"],"description":"Where the claim's evidence came from: owned passages retrieved for the claim, the brief's own excerpts of the page it cites, or both. Absent when no evidence was found or the claim was not checked."},"evidenceUrls":{"type":"array","items":{"type":"string"},"description":"The owned pages a claim that is not supported was judged against."},"quote":{"type":"object","description":"The owned passage that contradicts the claim, quoted word for word.","required":["text","url"],"properties":{"text":{"type":"string"},"url":{"type":["string","null"],"description":"The page it is on. Null when not recorded."}}}}}}}}}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Actions"]},"post":{"summary":"Generate action brief","description":"Generates an execution-ready brief for an action DevTune suggested, including one created from a prompt, or reuses the one it has. It spends credits under the actions.write scope: a new brief reserves 2 credits and settles at 1–2 credits, depending on how much research it needs; reusing a ready or running brief is free. With no credits left, the request returns 400 with error quota_exhausted and reason credits_exhausted: add credits, then retry. A plan without briefs returns 403. A page the customer locked, including a redirect destination, refuses generation with HTTP 409, error conflict and reason page_locked. High-stakes pages allow brief generation. An action made with Create Action has no brief and returns Action not found. Repeating the request never duplicates work: status is ready when a usable brief exists, generating when generation is already running, or queued with an eventId when generation was queued. After queued or generating, poll Get action brief for status ready and read briefMarkdown. briefStyle accepts best_fit or an allowed content format; when project content preferences are enabled, a blocked format is refused. Requires the `actions.write` scope.","operationId":"generateActionBrief","x-devtune-required-scope":"actions.write","parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"actionId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":false,"content":{"application/json":{"schema":{"type":"object","properties":{"briefStyle":{"type":"string","enum":["best_fit","technical_guide","implementation_tutorial","docs_page","api_reference","blog_post","comparison_page","alternative_page","migration_guide","integration_page","benchmark_report","changelog_update","use_case_page","landing_page","community_post"],"description":"Optional brief style override. Defaults to best_fit and must respect project content preferences."}}}}}},"responses":{"200":{"description":"Action brief generation status","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"actionId":{"type":"string","format":"uuid"},"eventId":{"type":["string","null"],"description":"Identifies the background request DevTune sent to write or continue the brief. Absent, never null, when the brief is already ready."},"message":{"type":"string","description":"What happened, for a person to read."},"status":{"type":"string","enum":["ready","queued","generating"],"description":"ready when a current brief already exists, generating when one is being written, queued when one is waiting to be written, whether this request or an earlier one queued it."},"success":{"type":"boolean","description":"Always true on a 200 response."}},"required":["actionId","message","status","success"]},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"409":{"$ref":"#/components/responses/ConflictError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"},"500":{"$ref":"#/components/responses/ServerError"}},"tags":["Actions"]}},"/projects/{projectId}/code-mode/executions/{executionId}":{"get":{"summary":"Get code-mode operation receipts","description":"Returns execution status and bounded operation receipts for this same API key and project. Receipts exclude code, arguments and result data. A running or uncertain operation may have committed; never automatically repeat it. Executions are retained with the project. No other credential, including another key in the same account, can read the receipt. Requires the `project.read` scope.","operationId":"getCodeModeExecution","x-devtune-required-scope":"project.read","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}},{"name":"executionId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Execution receipt","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"executionId":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["in_progress","finished"],"description":"in_progress while the execution runs, finished once it has ended."},"outcome":{"type":"string","enum":["pending","succeeded","failed"],"description":"pending while in progress; succeeded or failed once finished."},"error":{"type":"string","description":"Public error code when the finished execution failed."},"startedAt":{"type":"string","format":"date-time"},"finishedAt":{"type":["string","null"],"format":"date-time","description":"When the execution ended. Null while it is in progress."},"retryable":{"type":"boolean","const":false,"description":"Always false: never repeat an execution automatically."},"operations":{"type":"array","maxItems":13,"description":"Each DevTune call the code made, in the order it made them.","items":{"type":"object","properties":{"sequence":{"type":"integer","description":"The call’s position in the execution, from 0."},"name":{"type":"string","description":"The tool the call used."},"write":{"type":"boolean","description":"True when the call can change project data."},"argumentsHash":{"type":"string","description":"A SHA-256 hash of the call’s arguments, to tell calls apart; for a webhook creation, of the argument names only. The arguments are not kept."},"resourceId":{"type":"string","format":"uuid","description":"The record the call read, created or changed, when it names one."},"status":{"type":"string","enum":["running","succeeded","rejected","uncertain"],"description":"running: not finished. succeeded: done. rejected: refused. uncertain: it may have taken effect; check before repeating it."},"startedAt":{"type":"string","format":"date-time"},"finishedAt":{"type":"string","format":"date-time","description":"When the call ended. Absent while it is running."},"error":{"type":"string","description":"The public error code of a rejected or uncertain call."}},"required":["sequence","name","write","argumentsHash","status","startedAt"]}}},"required":["outcome","executionId","status","startedAt","finishedAt","retryable","operations"]},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"},"500":{"$ref":"#/components/responses/ServerError"}},"tags":["Connection and usage"]}},"/connection":{"get":{"summary":"Discover the authorized project","description":"Returns the same connection context as the project-bound route, using the project already selected during OAuth consent or bound to the API key. Never lists other projects. Never cached. Requires the `project.read` scope.","operationId":"getAuthorizedConnection","security":[{"bearerAuth":[]},{"oauthBearer":[]}],"x-devtune-required-scope":"project.read","responses":{"200":{"description":"Connection context","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ConnectionContextData"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"},"500":{"$ref":"#/components/responses/ServerError"}},"tags":["Connection and usage"]}},"/projects/{projectId}/connection":{"get":{"summary":"Get the connection context","description":"Returns the project this API key or OAuth grant is bound to, its own domains, the scopes the credential holds now and which account features are on. A paused project keeps only read scopes and setup.write, which can resume it. An operation needs its scope and, where it has one, its feature; neither bypasses human approval, credits or the operation's own checks. Never cached. Every API key holds project.read without selecting it. Requires the `project.read` scope.","operationId":"getConnectionContext","security":[{"bearerAuth":[]},{"oauthBearer":[]}],"x-devtune-required-scope":"project.read","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Connection context","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ConnectionContextData"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"},"500":{"$ref":"#/components/responses/ServerError"}},"tags":["Connection and usage"]}},"/projects/{projectId}/usage":{"get":{"summary":"Get project status and usage","description":"Returns whether the project is paused and its usage: active prompts, credits spent this credit period, AI Traffic events this UTC month and tracked pages. Each figure carries its own period. Also returns when the project and its team account are scheduled to be deleted for inactivity. Every API key holds project.read without selecting it. Requires the `project.read` scope.","operationId":"getProjectUsage","x-devtune-required-scope":"project.read","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Project status and usage","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ProjectUsageData"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Connection and usage"]}},"/projects/{projectId}/traffic/summary":{"get":{"summary":"Get traffic summary","description":"Daily counts are null on UTC days without machine-sensor coverage and 0 on covered days with no events. Window totals sum covered days only and are null when no day was covered, or when the window is only partly covered and the sum is 0. Each total has a <field>Status sibling: measured when its coverage.coveredDays equals coverage.windowDays, lower_bound when the window is partly covered and the total is at least that number, unmeasured when the total is null. coverage.coveredDays reports measured days per count, each over coverage.windowDays. Observed bot rows retain their counts even when sensor state is unknown. Coverage follows the selected domain and pathPrefix; sensor state is project-wide. botEvents includes crawls and answer fetches. Empty bot counts and combined event and session counts need server-side coverage from edge middleware, Cloudflare or a Vercel log drain. With no server-side coverage or observed bot rows, combined counts are null. Referrals retain snippet coverage. pathPrefix requires domain. Requires the `traffic.read` scope.","operationId":"getTrafficSummary","security":[{"bearerAuth":[]},{"oauthBearer":[]}],"x-devtune-required-scope":"traffic.read","parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"windowDays","in":"query","schema":{"type":"integer","default":30,"enum":[30,90]},"description":"Fixed rolling window in days. Supported values are 30 and 90. Defaults to 30."},{"name":"domain","description":"Only traffic on this host, such as docs.example.com.","in":"query","schema":{"type":"string"}},{"name":"pathPrefix","in":"query","description":"Filter totals to this path and its descendants. Requires domain.","schema":{"type":"string","pattern":"^/"}}],"responses":{"200":{"description":"Traffic summary","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"totalEvents":{"type":["integer","null"],"description":"All measured traffic events, including bot, organic and other traffic, plus the AI referrals counted in referralEvents. Days whose referrals are not measured are left out. The sum of every day in daily that has a value. Null when no day has one, or when the window is only partly covered and the sum is 0."},"totalEventsStatus":{"$ref":"#/components/schemas/MeasureStatus","description":"How complete totalEvents is: measured when totalEvents's coverage.coveredDays equals coverage.windowDays."},"botEvents":{"type":["integer","null"],"description":"Of totalEvents, AI bot requests: crawlers, index bots, answer fetchers and acting agents. The sum of every day in daily that has a value. Null when no day has one, or when the window is only partly covered and the sum is 0."},"botEventsStatus":{"$ref":"#/components/schemas/MeasureStatus","description":"How complete botEvents is: measured when botEvents's coverage.coveredDays equals coverage.windowDays."},"referralEvents":{"type":["integer","null"],"description":"Visits referred by AI answers, on days when a referral sensor reported on every AI referral host (owned domains and hosts a server-side sensor reports). Hosts excluded in Traffic settings and hosts only the tracking snippet reports are left out of that check, and their referrals still count. Referral counts can cover days excluded from totalEvents. The sum of every day in daily that has a value. Null when no day has one, or when the window is only partly covered and the sum is 0."},"referralEventsStatus":{"$ref":"#/components/schemas/MeasureStatus","description":"How complete referralEvents is: measured when referralEvents's coverage.coveredDays equals coverage.windowDays."},"uniqueSessions":{"type":["integer","null"],"description":"All measured traffic sessions, including organic and other traffic. Days whose referrals come wholly or partly from a server-side sensor, which records no session, are left out. Sessions are counted per hour and summed, so this is not a count of distinct sessions. The sum of every day in daily that has a value. Null when no day has one, or when the window is only partly covered and the sum is 0."},"uniqueSessionsStatus":{"$ref":"#/components/schemas/MeasureStatus","description":"How complete uniqueSessions is: measured when uniqueSessions's coverage.coveredDays equals coverage.windowDays."},"botSessions":{"type":["integer","null"],"description":"Of uniqueSessions, AI bot sessions: crawlers, index bots, answer fetchers and acting agents. Sessions are counted per hour and summed, so this is not a count of distinct sessions. The sum of every day in daily that has a value. Null when no day has one, or when the window is only partly covered and the sum is 0."},"botSessionsStatus":{"$ref":"#/components/schemas/MeasureStatus","description":"How complete botSessions is: measured when botSessions's coverage.coveredDays equals coverage.windowDays."},"referralSessions":{"type":["integer","null"],"description":"Sessions referred by AI answers, on days whose referrals all come from the tracking snippet; a server-side sensor records no session. Referral counts can cover days excluded from uniqueSessions. Sessions are counted per hour and summed, so this is not a count of distinct sessions. The sum of every day in daily that has a value. Null when no day has one, or when the window is only partly covered and the sum is 0."},"referralSessionsStatus":{"$ref":"#/components/schemas/MeasureStatus","description":"How complete referralSessions is: measured when referralSessions's coverage.coveredDays equals coverage.windowDays."},"referralEventsObserved":{"type":["integer","null"],"description":"Every AI referral counted on days whose referrals are not measured. A lower bound, never included in referralEvents. Null when there are none."},"referralSourceChanged":{"type":"boolean","description":"True when any host's referral source changed on a day in the window, so referral counts before and after that day come from different sensors."},"coverage":{"type":["object","null"],"description":"UTC-day coverage per count. totalEvents and uniqueSessions need server-side coverage or an observed bot row on the day. totalEvents also needs that day's referrals to be measured, so a crawler-only project reads coveredDays.totalEvents = 0. Other counts need coverage for that measure or recorded events. Partial-window totals exclude unmeasured days. With domain or pathPrefix, coverage is limited to that host and prefix, and project-wide sensors do not count.","properties":{"coveredDays":{"type":"object","description":"Covered days in the window for each count, out of windowDays.","properties":{"totalEvents":{"type":"integer","minimum":0},"botEvents":{"type":"integer","minimum":0},"referralEvents":{"type":"integer","minimum":0},"uniqueSessions":{"type":"integer","minimum":0},"botSessions":{"type":"integer","minimum":0},"referralSessions":{"type":"integer","minimum":0}},"required":["totalEvents","botEvents","referralEvents","uniqueSessions","botSessions","referralSessions"]},"windowDays":{"type":"integer","enum":[30,90]}},"required":["coveredDays","windowDays"]},"daily":{"type":"array","description":"UTC days in ascending order. Each count is null when unmeasured and 0 for a covered day with no matching traffic. The final row is the current UTC day to date. Empty only when the summary row is unavailable.","items":{"type":"object","properties":{"date":{"type":"string","format":"date","description":"The UTC day."},"totalEvents":{"type":["integer","null"],"description":"All measured traffic events, including the AI referrals counted in referralEvents. Null when the day's referrals are not measured. Null on a day with no sensor coverage and no recorded events of this kind; 0 on a covered day with none."},"botEvents":{"type":["integer","null"],"description":"AI bot requests: crawlers, index bots, answer fetchers and acting agents. Null on a day with no sensor coverage and no recorded events of this kind; 0 on a covered day with none."},"referralEvents":{"type":["integer","null"],"description":"Visits referred by AI answers. Null on a day with no sensor coverage and no recorded events of this kind; 0 on a covered day with none."},"uniqueSessions":{"type":["integer","null"],"description":"All measured sessions, counted per hour and summed. Null when the day's referrals come wholly or partly from a server-side sensor, which records no session. Null on a day with no sensor coverage and no recorded events of this kind; 0 on a covered day with none."},"botSessions":{"type":["integer","null"],"description":"AI bot sessions: crawlers, index bots, answer fetchers and acting agents, counted per hour and summed. Null on a day with no sensor coverage and no recorded events of this kind; 0 on a covered day with none."},"referralSessions":{"type":["integer","null"],"description":"Sessions referred by AI answers, counted per hour and summed. Null unless the day's referrals all come from the tracking snippet. Null on a day with no sensor coverage and no recorded events of this kind; 0 on a covered day with none."},"referralEventsObserved":{"type":["integer","null"],"description":"Every AI referral counted on this day when its referrals are not measured: a lower bound. Null on a measured day or with nothing recorded."},"referralSource":{"type":["string","null"],"enum":["edge_middleware","snippet","mixed","observed",null],"description":"Where the day's referrals come from: edge_middleware or snippet for every AI referral host not excluded, mixed when those hosts differ, observed when any of them has only a lower bound. Null when the project has no such host that day."},"referralSourceChanged":{"type":"boolean","description":"True when any host's referral source differs from its previous day's."}},"required":["date","totalEvents","botEvents","referralEvents","uniqueSessions","botSessions","referralSessions","referralEventsObserved","referralSource","referralSourceChanged"]}},"codingAgents":{"type":["object","null"],"description":"Answer fetches made by coding agents (Aider, Claude Code, Crush, Gemini CLI, OpenCode and Qwen Code) since countedFrom, to the same pages AI Traffic counts as answer fetches and under the same domain and pathPrefix filters. They are a subset of answerFetches; compare them with answerFetches, not botEvents, which covers the whole window. Only the edge middleware and the tracking snippet can tell coding agents apart, and Cloudflare data can't, so coverage is partial whenever the span holds answer fetches that could not be attributed, and totalEvents then leaves those out. Fetches before countedFrom are not counted. state is not_yet_observed, with a null totalEvents, until the project records a coding-agent fetch. The whole field is null when the subset could not be read.","properties":{"state":{"type":"string","enum":["observed","not_yet_observed"],"description":"not_yet_observed until the project records its first coding-agent fetch."},"coverage":{"type":"string","enum":["complete","partial"],"description":"complete when every answer fetch in the span was attributed or ruled out; partial when unattributedAnswerFetches is above zero."},"countedFrom":{"type":"string","format":"date-time","description":"Start of the counted span: the later of the window start and the first hour DevTune counted coding agents. Both totalEvents and answerFetches start here."},"answerFetches":{"type":"integer","description":"Every answer fetch to the same pages and filters since countedFrom, read with totalEvents, so totalEvents never exceeds it."},"unattributedAnswerFetches":{"type":"integer","description":"Answer fetches inside answerFetches that could not be attributed to a coding agent, for example Cloudflare pulls, so totalEvents cannot include them."},"totalEvents":{"type":["integer","null"],"description":"Coding-agent answer fetches attributed since countedFrom. Null while state is not_yet_observed."},"agents":{"type":"array","description":"Totals per coding agent, largest first.","items":{"type":"object","properties":{"label":{"type":"string","description":"The coding agent, such as OpenCode or Aider."},"llmPlatform":{"type":"string","description":"The AI Traffic provider key the agent belongs to, such as anthropic."},"events":{"type":"integer","description":"Its answer fetches since countedFrom."}},"required":["label","llmPlatform","events"]}},"platforms":{"type":"array","description":"Totals per AI platform, largest first.","items":{"type":"object","properties":{"llmPlatform":{"type":"string","description":"The AI Traffic provider key."},"events":{"type":"integer","description":"Coding-agent answer fetches for the provider since countedFrom."}},"required":["llmPlatform","events"]}}},"required":["state","coverage","countedFrom","answerFetches","unattributedAnswerFetches","totalEvents","agents","platforms"]},"sensors":{"type":"object","description":"Project-wide sensor activity derived from current configuration or connections, the latest accepted measurement, and each source freshness window.","properties":{"asOf":{"type":"string","format":"date-time","description":"When sensor state was read."},"sources":{"type":"array","description":"One entry per sensor type DevTune accepts, whether or not the project has set it up.","items":{"type":"object","properties":{"source":{"type":"string","enum":["cloudflare","edge_push","ga4","posthog","gsc","snippet"]},"state":{"type":"string","enum":["active","inactive","no_recent_traffic"],"description":"`active` when a measurement was accepted inside the freshness window. `no_recent_traffic` when the configuration that measured is still current and its releases are still green, but nothing has arrived inside the window — the sensor is working and the site was quiet. `inactive` for a failed or superseded configuration, or one that has never accepted a measurement. `active` and `no_recent_traffic` are both healthy: only `inactive` needs attention, so test for `state === \"inactive\"` rather than `state !== \"active\"`, which reports a quiet but working sensor as broken."},"lastAcceptedAt":{"type":["string","null"],"format":"date-time","description":"When the sensor’s current configuration last delivered a measurement DevTune accepted. Null when it has not yet, including after a reconnect and while the connection is not active."},"freshnessWindowHours":{"type":"integer","enum":[2,22,46],"description":"How recent a measurement must be for state to be active, in hours."},"observedHostnames":{"type":"array","maxItems":20,"description":"Up to 20 most recently observed hostnames with accepted snippet or edge middleware activity under the current configuration. This is a recent sample, not a complete hostname inventory. Each timestamp is hostname-specific; this does not prove coverage of every route. Omitted until hostname evidence is available.","items":{"type":"object","properties":{"hostname":{"type":"string"},"lastAcceptedAt":{"type":"string","format":"date-time","description":"When activity from this hostname was last accepted."}},"required":["hostname","lastAcceptedAt"]}},"configuredDomains":{"type":"array","description":"Domains in this sensor configuration. This is configuration scope only; state and lastAcceptedAt remain source-level evidence across the configured domains. Omitted when domain configuration is not recorded.","items":{"type":"string"}}},"required":["source","state","lastAcceptedAt","freshnessWindowHours"]}}},"required":["asOf","sources"]},"edgeSensorSdk":{"type":["object","null"],"description":"The @devtune/ai-traffic versions the edge middleware reported in the last 14 days. A null version means 0.8.0 or earlier, which send no version. reportedVersions is empty when no edge middleware sent traffic in that time. The whole field is null when the version reports could not be read.","properties":{"latestVersion":{"type":"string","description":"Newest published release."},"reportedVersions":{"type":"array","description":"Versions still reporting, oldest first. A version is left out once a newer one has reported after it and it has not reported for an hour.","items":{"type":"object","properties":{"version":{"type":["string","null"],"description":"The reported version. Null for 0.8.0 or earlier."},"lastReportedAt":{"type":"string","format":"date-time"}},"required":["version","lastReportedAt"]}},"outdatedVersion":{"type":["object","null"],"description":"The oldest reporting version, when it is older than latestVersion. Null when every reporting version is current.","properties":{"version":{"type":["string","null"],"description":"The outdated version. Null for 0.8.0 or earlier."},"lastReportedAt":{"type":"string","format":"date-time"}},"required":["version","lastReportedAt"]},"upgradeAvailable":{"type":"boolean","description":"True when outdatedVersion is set: upgrade the middleware to latestVersion."}},"required":["latestVersion","reportedVersions","outdatedVersion","upgradeAvailable"]},"edgeSensorVercelLinks":{"type":"array","description":"Vercel projects linked to send edge middleware traffic with Vercel OIDC instead of an ingest key. An empty array when nothing is linked yet. Absent when the link state could not be read.","items":{"type":"object","properties":{"vercelProjectId":{"type":"string","description":"Vercel project ID (`prj_…`)."},"vercelOwnerId":{"type":"string","description":"Vercel team ID (`team_…`)."},"vercelOwnerSlug":{"type":["string","null"],"description":"Vercel team slug for display. Null until traffic from this link has been accepted."},"vercelProjectName":{"type":["string","null"],"description":"Vercel project name for display. Null until traffic from this link has been accepted."},"environments":{"type":"array","description":"Deployment environments whose traffic is accepted. Always includes production; preview only when turned on for this link.","items":{"type":"string","enum":["production","preview"]}},"lastSeenAt":{"type":["string","null"],"format":"date-time","description":"When traffic from this link was last accepted, refreshed at most every 10 minutes. Null means not seen yet."}},"required":["vercelProjectId","vercelOwnerId","vercelOwnerSlug","vercelProjectName","environments","lastSeenAt"]}},"vercelDrain":{"type":"object","description":"The project's Vercel log drain: the connection DevTune created through its Vercel integration and the sensor that counts each host the drain delivered records for. Absent only when the drain state could not be read.","properties":{"connection":{"type":["object","null"],"description":"The live drain connection. Null when the project has none.","properties":{"id":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["pending","active","errored","needs_reconnect"],"description":"pending: installed, no drain yet. active: the drain is sending records. errored: Vercel rejected the drain test or create; lastError says why. needs_reconnect: Vercel reports the drain errored or disabled; reconnect to resume."},"vercelTeamId":{"type":["string","null"],"description":"Vercel team ID (`team_…`). Null for a personal install."},"vercelProjectIds":{"type":"array","description":"Vercel projects (`prj_…`) the drain covers.","items":{"type":"string"}},"lastReceivedAt":{"type":["string","null"],"format":"date-time","description":"When the drain last delivered a batch, refreshed at most every minute. Null means none yet."},"lastError":{"type":["string","null"],"description":"Why the drain last failed. Null when it has not failed since it was last set up or activated."},"health":{"type":"object","description":"The latest hourly read of the drain from Vercel. drainStatus is enabled from activation; the other fields are null until the first read.","properties":{"drainStatus":{"type":["string","null"],"description":"The drain’s status as Vercel reports it, set to enabled when the drain is activated and updated on each check. Null before activation, or when Vercel reports none.","enum":["enabled","disabled","errored",null]},"disabledReason":{"type":["string","null"],"description":"Vercel's reason a drain is disabled, such as `account-plan-downgrade` or `disabled-by-owner`. Null when Vercel reports none."},"deliveryState":{"type":["string","null"],"enum":["healthy","failing","unknown",null],"description":"Delivery health over the last 24 hours. Null until the first read, or when Vercel reports none."},"deliveryAttempts":{"type":["integer","null"],"description":"Deliveries Vercel attempted in the last 24 hours. Null until the first read, or when Vercel reports no count."},"deliveryFailures":{"type":["integer","null"],"description":"Of deliveryAttempts, those that failed. Null until the first read, or when Vercel reports no count."},"checkedAt":{"type":["string","null"],"format":"date-time","description":"When DevTune last read the drain from Vercel. Null until the first read."}},"required":["drainStatus","disabledReason","deliveryState","deliveryAttempts","deliveryFailures","checkedAt"]},"behindCdn":{"type":"boolean","description":"True when the drain delivered a request from a Cloudflare or Fastly address in the last 7 days: Vercel sits behind another CDN, so those requests are not address-verified. Connect Cloudflare to measure that CDN directly."},"addressesHidden":{"type":"boolean","description":"True when the drain is delivering AI bot requests without client IP addresses, because the Vercel team has IP Address Visibility › Show in Log Drains off, so those requests cannot be verified. Clears with the next request that carries an address."},"hosts":{"type":"array","description":"Hosts the drain delivered records for, newest first, at most 50, with the one sensor that counts each. Records for a host Cloudflare or edge middleware already reports are not counted twice.","items":{"type":"object","properties":{"hostname":{"type":"string"},"source":{"type":"string","description":"The sensor that counts the host.","enum":["vercel_drain","edge_middleware","cloudflare"]},"lastReceivedAt":{"type":"string","format":"date-time","description":"When the drain last delivered a record for the host."}},"required":["hostname","source","lastReceivedAt"]}},"activatedAt":{"type":["string","null"],"format":"date-time","description":"When the drain was last activated. Null until it has been."},"createdAt":{"type":"string","format":"date-time"}},"required":["id","status","vercelTeamId","vercelProjectIds","lastReceivedAt","lastError","health","behindCdn","addressesHidden","hosts","activatedAt","createdAt"]},"pendingRemovals":{"type":"integer","description":"Vercel drains DevTune created for this project and is still trying to delete, after a disconnect, a reconnect or a project deletion. DevTune retries each hourly until Vercel confirms the delete."},"abandonedRemovals":{"type":"array","description":"Vercel drains DevTune gave up deleting for this project, newest first, at most 50: Vercel rejected the token, or a week of attempts failed. Each drain may still exist in Vercel; delete it in the Vercel team's Drains settings. A manager dismisses each one in AI Traffic settings once it is handled.","items":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"drainId":{"type":"string","description":"The Vercel drain ID to delete."},"vercelTeamId":{"type":["string","null"],"description":"Vercel team ID (`team_…`) that owns the drain. Null for a personal install."},"abandonedAt":{"type":"string","format":"date-time","description":"When DevTune stopped trying to delete it."}},"required":["id","drainId","vercelTeamId","abandonedAt"]}}},"required":["connection","pendingRemovals","abandonedRemovals"]},"provenance":{"type":"object","description":"Present when DevTune could not check whether some counts come from a source that cannot verify requests: some counts may then be unverified. Absent when that was checked.","properties":{"state":{"type":"string","enum":["unavailable"]}},"required":["state"]},"gitbook":{"type":"object","description":"GitBook-hosted docs: the project's GitBook connection, and the crawls and answer fetches GitBook supplied in this window. Present while the account is in the GitBook beta, while it still has a connection, or when GitBook counts are in the window, so counts GitBook supplied stay labelled after a disconnect. state unavailable means GitBook is known to be in use (the beta, a connection or recorded counts) but DevTune could not read the rest of its state, so some counts in this summary may be unverified GitBook counts; the other fields are then absent.","properties":{"state":{"type":"string","enum":["available","unavailable"],"description":"unavailable when GitBook is in use but its state could not be read; the other fields are then absent."},"connection":{"type":["object","null"],"description":"The live GitBook connection. Null when the project has none.","properties":{"id":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["active","needs_reauth"],"description":"active: DevTune pulls the site hourly. needs_reauth: GitBook rejected the API token; reconnect with a new token to resume."},"organizationId":{"type":"string"},"siteId":{"type":"string"},"siteUrl":{"type":"string","description":"The docs site's published URL."},"lastSyncedAt":{"type":["string","null"],"format":"date-time","description":"The end of the last hour read. Null until the first sync."},"firstSyncCompletedAt":{"type":["string","null"],"format":"date-time","description":"When the first sync finished reading the last 30 days. Null until it has."},"lastSyncError":{"type":["string","null"],"description":"Why the last sync failed, or which hour GitBook’s row limit left uncounted. Null otherwise, including when a sync paused for its request budget or GitBook’s rate limit and continues on the next run."},"hosts":{"type":"array","description":"The docs host, with the one sensor that counts it now: gitbook, or cloudflare or edge_middleware when that sensor reports the host. GitBook never adds hours another sensor covered.","items":{"type":"object","properties":{"hostname":{"type":"string"},"source":{"type":"string","description":"The sensor that counts the host.","enum":["gitbook","cloudflare","edge_middleware"]}},"required":["hostname","source"]}},"createdAt":{"type":"string","format":"date-time"}},"required":["id","status","organizationId","siteId","siteUrl","lastSyncedAt","firstSyncCompletedAt","lastSyncError","hosts","createdAt"]},"verification":{"type":"string","enum":["unverified"],"description":"Always unverified: GitBook reports no visitor IP address, so every GitBook count is recorded as unknown with reason no_address. Bots are identified by DevTune's registry from the user agent alone."},"notFoundDemand":{"type":"string","enum":["page_views_only"],"description":"GitBook documents a 404 only for page views on the site itself, so its 404 demand counts those page views and never server-side or Markdown fetches."},"contributions":{"type":"array","description":"Requests GitBook added to AI Traffic in this window, per host, within the same domain and pathPrefix filters as the totals, read from what was recorded, whatever the connection or beta flag is now. Empty when GitBook added nothing there.","items":{"type":"object","properties":{"hostname":{"type":"string"},"requests":{"type":"integer","description":"AI bot requests GitBook supplied for the host."},"notFoundRequests":{"type":"integer","description":"Of requests, page views that returned 404."}},"required":["hostname","requests","notFoundRequests"]}}},"required":["state"]},"window":{"type":"object","description":"The fixed window the figures cover, in UTC days.","properties":{"days":{"type":"integer"},"dateFrom":{"type":"string","format":"date","description":"First day of the window."},"dateTo":{"type":"string","format":"date","description":"Last day of the window, included."}},"required":["days","dateFrom","dateTo"]}},"required":["totalEvents","totalEventsStatus","botEvents","botEventsStatus","referralEvents","referralEventsStatus","uniqueSessions","uniqueSessionsStatus","botSessions","botSessionsStatus","referralSessions","referralSessionsStatus","referralEventsObserved","referralSourceChanged","coverage","daily","codingAgents","sensors","edgeSensorSdk","window"]},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["AI Traffic"]}},"/projects/{projectId}/traffic/search-rollups":{"get":{"summary":"Get Search Console and analytics totals","description":"Returns fixed-window totals for Google Search Console (gsc), legacy GA4 (ga4) or the selected analytics provider (analytics). Read the returned source window dates instead of assuming the requested window: Search Console days are Pacific-time days ending three days before today, and coverage is limited to the days available at the last successful sync. Totals are current only when every contributing connection that is not paused or disconnected covers the whole window; coverage is reported per contributor. When current totals are unavailable, the most recent totals DevTune has are returned with their own dates, availability partial, isCurrent false, status unmeasured and asOfDate; they are not current evidence or a verified comparison. A project with no earlier totals returns unavailable coverage and null totals. measurement.availability and explanation distinguish unavailable verification, partial evidence, a verified zero and fully measured totals; a null total is unmeasured. The analytics source also returns paginated daily session rows: limit (default 25, maximum 100) and cursor apply only to those rows. Pass pagination.nextCursor as cursor while pagination.hasMore. An import, a provider change or UTC midnight ends a continuation; start again without cursor. storedData dates show when DevTune holds data, not that every day between them is covered. Requires the `traffic.read` scope.","operationId":"getSearchRollups","x-devtune-required-scope":"traffic.read","parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"source","description":"gsc for Search Console, analytics for the selected analytics provider, or ga4 for legacy GA4 totals.","in":"query","required":true,"schema":{"type":"string","enum":["gsc","ga4","analytics"]}},{"name":"windowDays","in":"query","schema":{"type":"integer","default":30,"enum":[30,90]},"description":"Fixed rolling window in days. Supported values are 30 and 90. Defaults to 30."},{"name":"cursor","in":"query","schema":{"type":"string","maxLength":1024},"description":"pagination.nextCursor from the previous page, unchanged. Keep the other parameters the same: a cursor sent with different filters, or beside a deprecated position parameter, returns 400."},{"name":"generation","in":"query","schema":{"type":"string","minLength":1,"maxLength":128},"description":"Deprecated: cursor carries the generation. Required with analytics offsets after the first page; use pagination.generation.","deprecated":true},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":25},"description":"Daily analytics rows per page; ignored for other sources."},{"name":"pageSize","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":25},"description":"The same setting as limit. Send either; both with different values returns 400."},{"name":"offset","in":"query","schema":{"type":"integer","minimum":0,"default":0},"description":"Deprecated: Pass cursor instead. Daily analytics row offset; ignored for other sources.","deprecated":true}],"responses":{"200":{"description":"Search rollup totals and measurement coverage","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"source":{"type":"string","enum":["gsc","ga4","analytics"],"description":"The source the totals come from, as requested."},"items":{"type":"array","description":"Daily aggregate rows, present for source analytics. Row presence does not establish complete window coverage.","items":{"type":"object","properties":{"source":{"type":"string","enum":["analytics"]},"id":{"type":"string"},"day_bucket":{"type":"string","format":"date","description":"The day the sessions fall on."},"domain_canonical":{"type":"string","description":"The tracked domain the sessions landed on."},"landing_page_path":{"type":"string","description":"The landing page path, starting with /."},"source_class":{"type":["string","null"]},"llm_platform":{"type":["string","null"],"description":"The AI platform that referred the sessions. Null when they were not AI referrals."},"sessions":{"type":"integer","description":"Sessions in this row: one day, domain, landing page, traffic source and AI platform. Several rows can share a day and landing page."},"engaged_sessions":{"type":["integer","null"]},"synced_at":{"type":"string","format":"date-time","description":"When DevTune last synced the row."}},"required":["source","id","day_bucket","domain_canonical","landing_page_path","source_class","llm_platform","sessions","engaged_sessions","synced_at"]}},"pagination":{"type":"object","description":"Present for source analytics.","properties":{"hasMore":{"type":"boolean","description":"More rows follow this page."},"nextCursor":{"type":["string","null"],"description":"Pass as cursor to read the next page. Null on the last page."},"limit":{"type":"integer","description":"Rows per page."},"offset":{"type":"integer","description":"Rows before this page."},"nextOffset":{"type":["integer","null"],"deprecated":true,"description":"Deprecated: pass nextCursor as cursor. Null on the last page."},"generation":{"type":"string","description":"Identifies the data this page was read from. When it changes, start again without cursor."}},"required":["hasMore","nextCursor","limit","offset","nextOffset","generation"]},"aggregates":{"description":"Window totals: clicks, impressions, ctr and avgPosition for gsc; sessions and engagedSessions otherwise. A null total is unmeasured.","oneOf":[{"type":"object","properties":{"avgPosition":{"type":["number","null"],"description":"Average ranking position, weighted by impressions; 1 is the top result, and 0 means there were no impressions. Null when the window is not verified; when measurement.status is unmeasured, a value is the last published one, not a current total."},"clicks":{"type":["integer","null"],"description":"Clicks from Google Search. Null when the window is not verified; when measurement.status is unmeasured, a value is the last published one, not a current total."},"ctr":{"type":["number","null"],"description":"Clicks over impressions, from 0 to 1; 0 when there were no impressions. Null when the window is not verified; when measurement.status is unmeasured, a value is the last published one, not a current total."},"impressions":{"type":["integer","null"],"description":"Impressions in Google Search. Null when the window is not verified; when measurement.status is unmeasured, a value is the last published one, not a current total."},"measurement":{"$ref":"#/components/schemas/GscMeasurement"}},"required":["avgPosition","clicks","ctr","impressions","measurement"]},{"type":"object","properties":{"engagedSessions":{"type":["integer","null"],"description":"Engaged sessions in the window. Null unless measurement.status is measured, and always null for analytics when a PostHog provider is selected."},"measurement":{"type":"object","description":"Whether the analytics totals are verified.","properties":{"availability":{"type":"string","enum":["unavailable","partial","verified_zero","measured"],"description":"Distinguishes missing verification, partial evidence, verified zero, and complete totals. Partial evidence never unlocks full-window totals."},"explanation":{"type":"string","description":"A sentence describing availability, for a person to read. Do not parse it."},"contributors":{"type":"array","description":"One entry for each analytics connection behind the totals, with the days it covers.","items":{"type":"object","properties":{"connectionId":{"type":"string","format":"uuid"},"generation":{"type":"integer","description":"Identifies the import the connection’s data comes from."},"status":{"type":"string"},"coverageFrom":{"type":["string","null"],"format":"date","description":"For analytics, the window’s first day when the connection covers the whole window; null otherwise. For ga4, the first day the connection has data for; null when it has none."},"coverageThrough":{"type":["string","null"],"format":"date","description":"For analytics, the window’s last day when the connection covers the whole window; null otherwise. For ga4, the last day the connection has data for; null when it has none."}},"required":["connectionId","generation","status","coverageFrom","coverageThrough"]}},"coverageFrom":{"type":["string","null"],"format":"date","description":"For analytics, the window’s first day when every contributing connection covers the whole window; null otherwise. For ga4, the latest first day among the contributing connections that have data; null when none has."},"coverageThrough":{"type":["string","null"],"format":"date","description":"For analytics, the window’s last day when every contributing connection covers the whole window; null otherwise. For ga4, the earliest last day among the contributing connections that have data; null when none has."},"status":{"type":"string","enum":["measured","unmeasured"],"description":"measured when the totals are verified and current. Use availability when reporting evidence."}},"required":["availability","explanation","contributors","coverageFrom","coverageThrough","status"]},"sessions":{"type":["integer","null"],"description":"Sessions in the window. For ga4, null unless measurement.status is measured. For analytics, an unmeasured window reports the sessions recorded so far as a partial count, and null when none were recorded."}},"required":["engagedSessions","measurement","sessions"]}]},"window":{"type":"object","description":"The window the totals cover, as the source reports it: Pacific-time days for gsc and ga4, UTC days ending yesterday for analytics. For gsc it matches aggregates.measurement.sourceDateFrom and sourceDateTo.","properties":{"days":{"type":"integer","enum":[30,90]},"dateFrom":{"type":"string","format":"date","description":"First day of the window."},"dateTo":{"type":"string","format":"date","description":"Last day of the window, included."}},"required":["days","dateFrom","dateTo"]}},"required":["aggregates","source","window"]},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"},"500":{"$ref":"#/components/responses/ServerError"}},"tags":["AI Traffic"]}},"/projects/{projectId}/traffic/page-search-queries":{"get":{"summary":"Get a page's Search Console queries","description":"Returns the Search Console queries one page ranks for in a fixed 30- or 90-day window, filtered to a position band and an impression floor, ordered by impressions. The defaults (positions 8 to 20, at least 20 impressions, 90 days) select queries where the page already ranks and an edit to the page can move it. Each query carries its current clicks, impressions and average position and the prior values when the prior window is measured and Search Console reported the query in it. status is not_connected when no active Search Console connection covers the host and path of the page, and unmeasured when the window is not fully verified at the current publication; queries are returned only when status is measured, so an empty list then means no query qualified. truncated is true when more queries qualified than limit. searchConsole reports the window dates and coverage the queries were read from. Requires the `traffic.read` scope.","operationId":"getPageSearchQueries","x-devtune-required-scope":"traffic.read","parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"pageUrl","in":"query","required":true,"schema":{"type":"string","format":"uri","maxLength":2048},"description":"Absolute URL of the page. The host ignores case and a leading www; the path must match the page Search Console reports."},{"name":"windowDays","in":"query","schema":{"type":"integer","default":90,"enum":[30,90]},"description":"Fixed rolling window in days. Supported values are 30 and 90. Defaults to 90."},{"name":"minPosition","in":"query","schema":{"type":"number","minimum":1,"maximum":100,"default":8},"description":"Lowest average position to include, inclusive."},{"name":"maxPosition","in":"query","schema":{"type":"number","minimum":1,"maximum":100,"default":20},"description":"Highest average position to include, inclusive."},{"name":"minImpressions","in":"query","schema":{"type":"integer","minimum":0,"default":20},"description":"Fewest current-period impressions to include."},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":50},"description":"Most queries to return."}],"responses":{"200":{"description":"The page's queries and the coverage they were read from","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"status":{"type":"string","enum":["measured","unmeasured","not_connected"],"description":"measured when the window is verified at the current publication; unmeasured when it is not; not_connected when no active Search Console connection covers the host and path of the page. Only measured returns queries."},"generation":{"type":"string","description":"Identifies the Search Console publication the queries were read from."},"page":{"type":"object","properties":{"domain":{"type":"string","description":"Host, lowercased, without www."},"pagePath":{"type":"string","description":"Page path, starting with /."}},"required":["domain","pagePath"]},"filters":{"type":"object","description":"The filters the read applied.","properties":{"windowDays":{"type":"integer","enum":[30,90]},"minPosition":{"type":"number"},"maxPosition":{"type":"number"},"minImpressions":{"type":"integer"},"limit":{"type":"integer"}},"required":["windowDays","minPosition","maxPosition","minImpressions","limit"]},"searchConsole":{"$ref":"#/components/schemas/GscWindowPeriods"},"pageQueryCount":{"type":["integer","null"],"description":"Queries the page had in the current period before filtering. Null unless status is measured."},"matchingCount":{"type":["integer","null"],"description":"Queries that passed the filters, before limit. Null unless status is measured."},"truncated":{"type":"boolean","description":"True when matchingCount is greater than limit."},"queries":{"type":"array","items":{"type":"object","properties":{"query":{"type":"string"},"clicks":{"type":"integer"},"impressions":{"type":"integer"},"position":{"type":"number","description":"Average position, weighted by impressions; 1 is the top result."},"prior":{"description":"The same query on this page in the prior period. Null when the prior window is not measured or Search Console reported none.","oneOf":[{"type":"null"},{"type":"object","properties":{"clicks":{"type":"integer"},"impressions":{"type":"integer"},"position":{"type":"number"}},"required":["clicks","impressions","position"]}]}},"required":["query","clicks","impressions","position","prior"]}}},"required":["status","generation","page","filters","searchConsole","pageQueryCount","matchingCount","truncated","queries"]}}}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["AI Traffic"]}},"/projects/{projectId}/traffic/platforms":{"get":{"summary":"Get traffic by platform","description":"Returns LLM traffic grouped by AI platform, traffic type and bot class, with event counts, bot-identity verdict and unknown-reason sums, and percentages. Requires the `traffic.read` scope.","operationId":"getTrafficPlatforms","x-devtune-required-scope":"traffic.read","parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"windowDays","in":"query","schema":{"type":"integer","default":30,"enum":[30,90]},"description":"Fixed rolling window in days. Supported values are 30 and 90. Defaults to 30."},{"name":"domain","description":"Only traffic on this host, such as docs.example.com.","in":"query","schema":{"type":"string"}}],"responses":{"200":{"description":"Platform breakdown","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"platforms":{"type":"array","description":"One row per AI Traffic provider, traffic type and bot class.","items":{"type":"object","properties":{"platform":{"type":"string","description":"The AI Traffic provider key, such as openai."},"trafficType":{"type":"string","description":"bot for AI bot requests (botClass gives the kind), referral for visits from AI answers, organic for other visits."},"botClass":{"type":"string","nullable":true,"enum":["training_crawler","index_bot","answer_fetcher","acting_agent",null],"description":"Machine traffic class for bot rows. A platform with both training crawls and answer fetches returns one row per class so the two are never combined. Null for referral rows."},"totalEvents":{"type":"integer","description":"Events in this row."},"verifiedEvents":{"type":"integer","minimum":0,"description":"Events in this row whose claimed operator was confirmed, by the peer address against the networks the operator publishes or by a Web Bot Auth signature. Always 0 for referral rows."},"verifiedBySignatureEvents":{"type":["integer","null"],"minimum":0,"description":"Of verifiedEvents, events verified by a Web Bot Auth signature; the rest were verified by their peer address. Null when the method was not recorded for some verified events in this window. Events stored before DevTune began recording the method have none, so windows reaching back before then read null."},"failedEvents":{"type":"integer","minimum":0,"description":"Events contradicted by the operator check and best treated as spoofed."},"unknownNoAddressEvents":{"type":"integer","minimum":0,"description":"Unknown events for which the sensor supplied no peer address."},"unknownNoContractEvents":{"type":"integer","minimum":0,"description":"Unknown events whose claimed operator publishes no supported identity-checking contract."},"unknownCheckUnavailableEvents":{"type":"integer","minimum":0,"description":"Unknown events for which an available operator check could not complete. The before-verification remainder is totalEvents minus verifiedEvents, failedEvents, and all three unknown-reason counts."},"uniqueSessions":{"type":["integer","null"],"description":"Events in this row that carried a session ID. Not a count of distinct sessions: a session with several events counts once per event. Null for a referral row that includes referrals from a server-side sensor, which records no session."},"percentage":{"type":"number","minimum":0,"maximum":100,"description":"Percent share of total platform traffic events, expressed on a 0-100 scale."},"measurement":{"type":["string","null"],"enum":["measured","partial","observed",null],"description":"For referral rows: measured when referrals were measured on every day of the window, partial when on some, observed when on none. A partial or observed row includes lower-bound counts from days that were not measured. Null for bot and organic rows."}}}},"actingAgents":{"type":"object","description":"Omitted when it could not be read. Requests for pages, not assets, from third-party agents acting on the site for a person, such as booking or filling in a form, as shown in AI Traffic. They are never counted as training crawls or answer fetches. Only agents that identify themselves are counted; browser-based agents without an identifying user agent are not, so every count is a minimum. Covers the response window and the same number of days before it, filtered to the exact host when domain is given. A null count is unmeasured: no acting-agent requests, and no server-side sensor (edge middleware or Cloudflare) covering the whole site on every day of that period. The browser snippet never makes a zero, because acting agents fetch pages server-side. With a domain filter, a period with no acting-agent requests is null, because coverage is recorded per site, not per host.","required":["currentRequests","priorRequests","currentVerifiedRequests","currentFailedRequests","platforms"],"properties":{"currentRequests":{"type":["integer","null"],"minimum":0,"description":"Acting-agent requests in the response window. Null when unmeasured."},"priorRequests":{"type":["integer","null"],"minimum":0,"description":"Acting-agent requests in the same number of days before the window. Null when unmeasured."},"currentVerifiedRequests":{"type":["integer","null"],"minimum":0,"description":"Of currentRequests, those whose claimed operator was confirmed. Null when currentRequests is null."},"currentFailedRequests":{"type":["integer","null"],"minimum":0,"description":"Of currentRequests, those the operator check contradicted. Null when currentRequests is null."},"platforms":{"type":"array","description":"One entry per platform with acting-agent requests in either period, most current requests first.","items":{"type":"object","required":["platform","currentRequests","priorRequests"],"properties":{"platform":{"type":"string","description":"The AI Traffic provider key."},"currentRequests":{"type":["integer","null"],"minimum":0,"description":"Its requests in the response window. Null when unmeasured."},"priorRequests":{"type":["integer","null"],"minimum":0,"description":"Its requests in the period before. Null when unmeasured."}}}}}}}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["AI Traffic"]}},"/projects/{projectId}/intelligence/forecast":{"get":{"summary":"Get the intelligence forecast","description":"Projects primary citations and presence rate forward from a straight-line fit over the daily figures in the window. It is a trend extrapolation, not a measured result. Presence-rate values are ratios from 0 to 1 and are null when no response in the window has usable citation data. With no data in the window, forecast is empty, dataPoints is 0 and trend and the confidence values are null. Requires the `intelligence.read` scope.","operationId":"getIntelligenceForecast","x-devtune-required-scope":"intelligence.read","parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"windowDays","in":"query","schema":{"type":"integer","default":30,"enum":[30,90]},"description":"Fixed rolling window of history the forecast fits, in days. Supported values are 30 and 90. Defaults to 30."},{"name":"horizonDays","in":"query","schema":{"type":"integer","default":30,"enum":[7,14,30,60,90]},"description":"How many days ahead to project. Defaults to 30."}],"responses":{"200":{"description":"Intelligence forecast","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","required":["forecast","summary","window"],"properties":{"forecast":{"type":"array","description":"One projected day per horizon day, starting the day after the last day with data. Empty when the window has no data.","items":{"type":"object","required":["date","dayOffset","presenceRate","primaryCitations"],"properties":{"date":{"type":"string","format":"date"},"dayOffset":{"type":"integer","description":"Days after the last day with data."},"presenceRate":{"type":["number","null"],"description":"Projected share of responses citing the project, 0 to 1. Null when the window has no usable citation data."},"primaryCitations":{"type":"number","description":"Projected primary citations that day, never below 0."}}}},"summary":{"type":"object","properties":{"confidence":{"type":"object","description":"How closely the fit follows the history, 0 to 1. Null with no data.","properties":{"presenceRate":{"type":["number","null"]},"primaryCitations":{"type":["number","null"]}}},"dataPoints":{"type":"integer","description":"Days of history the fit used."},"forecastAverage":{"type":"object","description":"Average of the projected days.","properties":{"presenceRate":{"type":["number","null"]},"primaryCitations":{"type":"number"}}},"horizonDays":{"type":"integer"},"projectedDeltaPercent":{"type":"object","description":"Change from recentAverage to forecastAverage, in percent. When a recentAverage value is 0, its delta is 100 if the forecastAverage value is above 0 and 0 otherwise, whatever the history before the last 7 days did.","properties":{"presenceRate":{"type":["number","null"]},"primaryCitations":{"type":"number"}}},"recentAverage":{"type":"object","description":"Average of the last 7 days with data.","properties":{"presenceRate":{"type":["number","null"]},"primaryCitations":{"type":"number"}}},"trend":{"type":["string","null"],"enum":["increase","decrease","stable",null],"description":"From the unrounded change in projected primary citations, which projectedDeltaPercent.primaryCitations reports rounded: increase at 5 or more, decrease at -5 or less, otherwise stable. A recent average of 0 reads as increase whenever the forecast average is above 0, including after citations fell to 0. Null with no data."},"window":{"type":"object","description":"The fixed window the figures cover, in UTC days.","properties":{"days":{"type":"integer"},"dateFrom":{"type":"string","format":"date","description":"First day of the window."},"dateTo":{"type":"string","format":"date","description":"Last day of the window, included."}},"required":["days","dateFrom","dateTo"]}}},"window":{"type":"object","description":"The fixed window the figures cover, in UTC days.","properties":{"days":{"type":"integer"},"dateFrom":{"type":"string","format":"date","description":"First day of the window."},"dateTo":{"type":"string","format":"date","description":"Last day of the window, included."}},"required":["days","dateFrom","dateTo"]}}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Results"]}},"/projects/{projectId}/intelligence/what-works":{"get":{"summary":"Get what-works recommendations","description":"Returns which channels have shipped improvements, from Results whose 14-day before/after comparison has closed as improved on citations or answer fetches. The window selects when those results became available. Open, mixed, no_clear_change, declined and unmeasured results are excluded. sampleSize counts the improved actions per channel, not every attempt. hasCorrelatedData is true when any eligible improvement exists; lastCorrelatedAt is when the latest one became available. A before/after improvement does not show that the action caused it. With no eligible improvements, recommendations is empty and emptyState explains why. Its counts cover actions completed in the window: markedInterventions counts those actions and correlatedInterventions counts the ones with a conclusive result, including declines. recentEligibleCorrelations is zero when no eligible improvement falls in the window. Requires the `intelligence.read` scope.","operationId":"getWhatWorksRecommendations","x-devtune-required-scope":"intelligence.read","parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"windowDays","in":"query","schema":{"type":"integer","default":30,"enum":[30,90]},"description":"Fixed rolling window in days. Supported values are 30 and 90. Defaults to 30."},{"name":"limit","in":"query","schema":{"type":"integer","default":5,"minimum":1,"maximum":20}}],"responses":{"200":{"description":"What-works recommendations","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"recommendations":{"type":"array","description":"One per channel with improvements in the window, highest avgImpactScore first, up to limit. Empty with emptyState set when there are none.","items":{"type":"object","properties":{"recommendation":{"type":"string","description":"The channel hint, for a person to read."},"channel":{"type":"string","description":"The channel that shipped the improvements."},"sampleSize":{"type":"integer","description":"Improved actions in the channel."},"avgImpactScore":{"type":"number","description":"An average discounted by directional confidence. Prefer sampleSize and Results."},"avgMeasuredEffect":{"type":"number","description":"An average that mixes citation and answer-fetch changes, so it has no single unit and is not citation lift. Prefer Results."},"lastCorrelatedAt":{"type":["string","null"],"format":"date-time","description":"When the latest improvement became available."}}}},"hasCorrelatedData":{"type":"boolean","description":"True when recommendations is not empty: at least one eligible improvement became available in the window."},"emptyState":{"type":["object","null"],"description":"Why recommendations is empty. Null when it is not.","properties":{"reason":{"type":"string","description":"no_marked_interventions: no action was completed for measurement. no_correlated_interventions: none has a conclusive result. no_recent_correlations: none improved in the window.","enum":["no_marked_interventions","no_correlated_interventions","no_recent_correlations","summary_not_refreshed"]},"message":{"type":"string","description":"reason, for a person to read."},"requirements":{"type":"array","description":"What it takes to get recommendations.","items":{"type":"string"}},"counts":{"type":"object","description":"Counts over actions completed in the window.","properties":{"markedInterventions":{"type":"integer","description":"Actions completed for measurement."},"correlatedInterventions":{"type":"integer","description":"Of those, actions with a conclusive result, declines included."},"recentEligibleCorrelations":{"type":"integer","description":"Eligible improvements in the window: 0 here."}}}}},"window":{"type":"object","description":"The fixed window the figures cover, in UTC days.","properties":{"days":{"type":"integer"},"dateFrom":{"type":"string","format":"date","description":"First day of the window."},"dateTo":{"type":"string","format":"date","description":"Last day of the window, included."}},"required":["days","dateFrom","dateTo"]}}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Results"]}},"/projects/{projectId}/library":{"get":{"summary":"List Library items","description":"Lists the project's Library items: Current versions only, unless includeSuperseded is true. Content drafts are listed only when the account has content drafts and the credential also holds actions.read. Pass pagination.nextCursor back as cursor, with includeSuperseded unchanged, for the next page; a page can hold fewer items than limit while more remain. Reading spends nothing. Requires the `library.read` scope.","operationId":"listLibraryItems","x-devtune-required-scope":"library.read","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}},{"name":"cursor","in":"query","schema":{"type":"string"},"description":"Opaque cursor from the previous response."},{"name":"includeSuperseded","in":"query","schema":{"type":"boolean","default":false},"description":"Also return Superseded versions. By default only Current versions are returned."},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":25}},{"name":"pageSize","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":25},"description":"The same setting as limit. Send either; both with different values returns 400."}],"responses":{"200":{"description":"Library items","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/LibraryItemsData"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Library and Knowledge"]},"post":{"summary":"Upload a text document to the Library","description":"Uploads text to the Library as a new item. With replaceItemId it becomes the new Current version of that upload, and the old version stays in its version chain as Superseded. Library search, and agents when Available to agents is on, use the text once indexing finishes. The write acts as the member who created the API key. That member needs the account's member-management permission, which only the Owner role has by default, and the plan must include the Library; otherwise 403. A project holds at most 1,000 files or 2 GB; a full Library returns 403. An unknown replaceItemId returns 404, and a new version of the same upload already in progress returns 409. Each call creates a new item, so retrying after a lost response uploads a second copy: list the Library first to check whether the upload arrived. Requires the `library.write` scope.","operationId":"uploadLibraryItem","x-devtune-required-scope":"library.write","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["content","title"],"properties":{"content":{"type":"string","maxLength":500000,"description":"The document text, up to 500,000 characters."},"replaceItemId":{"type":"string","format":"uuid","description":"An upload in this project to replace. The new text becomes its Current version."},"title":{"type":"string","maxLength":500,"description":"The item title, up to 500 characters."}}}}}},"responses":{"200":{"description":"Uploaded Library item","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/LibraryItemData"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Library and Knowledge"]}},"/projects/{projectId}/library/search":{"get":{"summary":"Search Library items","description":"Searches the project's Library items by title and text, ranking keyword and meaning matches together; without query it lists items as List Library items does. Returns Current versions only, unless includeSuperseded is true. Content drafts are included only when the account has content drafts and the credential also holds actions.read. Pass pagination.nextCursor back as cursor with query and includeSuperseded unchanged; a page can hold fewer items than limit while more remain. Reading spends nothing. Requires the `library.read` scope.","operationId":"searchLibraryItems","x-devtune-required-scope":"library.read","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}},{"name":"cursor","in":"query","schema":{"type":"string"},"description":"Opaque cursor from the previous response."},{"name":"query","in":"query","schema":{"type":"string","maxLength":500},"description":"Words to search for in item titles and text. Omit it to list items."},{"name":"includeSuperseded","in":"query","schema":{"type":"boolean","default":false},"description":"Also return Superseded versions. By default only Current versions are returned."},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":25}},{"name":"pageSize","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":25},"description":"The same setting as limit. Send either; both with different values returns 400."}],"responses":{"200":{"description":"Library search results","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/LibraryItemsData"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Library and Knowledge"]}},"/projects/{projectId}/library/{itemId}":{"get":{"summary":"Get a Library item","description":"Returns one Library item with its full text as contentText and contentMarkdown, its provenance and its version history. A content draft is returned only when the account has content drafts and the credential also holds actions.read; otherwise it returns 404, as does an item outside this project. Requires the `library.read` scope.","operationId":"readLibraryItem","x-devtune-required-scope":"library.read","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}},{"name":"itemId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Library item","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/LibraryItemData"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Library and Knowledge"]},"patch":{"summary":"Update Library grounding availability or version link","description":"Turns Available to agents on or off for an item (groundingEnabled), or links a Superseded item to the later version that replaced it in the same version chain (successorId; null removes the link). Send at least one of them; sending the values the item already has changes nothing. The write acts as the member who created the API key. That member needs the account's member-management permission, which only the Owner role has by default; otherwise 403. Returns 400 when the successor is the item itself, is in another version chain or would make a loop, and when turning Available to agents on for a Superseded brief. successorId on an item that is not Superseded returns 404, as do content drafts, which can't be changed here. Requires the `library.write` scope.","operationId":"updateLibraryItem","x-devtune-required-scope":"library.write","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}},{"name":"itemId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"groundingEnabled":{"type":"boolean","description":"Available to agents: whether agents may use this item as grounding."},"successorId":{"type":["string","null"],"format":"uuid","description":"For a Superseded item, the later version in the same chain that replaced it. null removes the link."}}}}}},"responses":{"200":{"description":"Updated Library item","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/LibraryItemData"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Library and Knowledge"]}},"/projects/{projectId}/knowledge":{"get":{"summary":"Get the Knowledge Profile and proposals","description":"Returns the project's Company context (companyContext), its agent preferences in profile (Tone and Instructions for agents), and its proposed changes, newest first: Pending, Approved and Dismissed. profile is null until a preference is first saved. Pass pagination.nextCursor back as cursor for more proposals. Reading spends nothing. Requires the `library.read` scope.","operationId":"readKnowledgeProfile","x-devtune-required-scope":"library.read","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}},{"name":"cursor","in":"query","schema":{"type":"string"},"description":"Opaque cursor from the previous response."},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":25}},{"name":"pageSize","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":25},"description":"The same setting as limit. Send either; both with different values returns 400."}],"responses":{"200":{"description":"Knowledge Profile and proposals","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/KnowledgeData"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Library and Knowledge"]},"patch":{"summary":"Update one Knowledge Profile field","description":"Saves a direct edit to one field: company_context (Company context), tone (Tone) or saved_instructions (Instructions for agents). The value replaces the field at once, without going through proposal review, and applies to future agent tasks; it does not rewrite earlier outputs. Sending the same value again leaves the same result, so a retry is safe. An empty value or one over 12,000 characters returns 400. Requires the `library.write` scope.","operationId":"updateKnowledgeProfile","x-devtune-required-scope":"library.write","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["targetField","value"],"properties":{"targetField":{"type":"string","enum":["company_context","tone","saved_instructions"],"description":"The field to replace: Company context, Tone or Instructions for agents."},"value":{"type":"string","minLength":1,"maxLength":12000,"description":"The new text for the field. It replaces the current text."}}}}}},"responses":{"200":{"description":"Updated Knowledge Profile","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/KnowledgeProfile"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Library and Knowledge"]}},"/projects/{projectId}/knowledge/proposals/{proposalId}":{"post":{"summary":"Review a Knowledge Profile proposal","description":"Decides a proposed Knowledge Profile change. approve writes the proposed value into its field, replacing the current value; dismiss leaves the profile unchanged; restore returns a Dismissed proposal to Pending. Repeating an approve or dismiss that already happened returns the proposal unchanged, so a retry is safe. Returns 409 when the proposal can't make that change: approving or dismissing one that is not Pending, restoring one that is not Dismissed, restoring while the same proposal is already Pending, or restoring when its source already has 20 Pending proposals. Requires the `library.write` scope.","operationId":"reviewKnowledgeProfileProposal","x-devtune-required-scope":"library.write","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}},{"name":"proposalId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["decision"],"properties":{"decision":{"type":"string","enum":["approve","dismiss","restore"],"description":"approve applies a Pending proposal, dismiss rejects it, and restore returns a Dismissed proposal to Pending."}}}}}},"responses":{"200":{"description":"Reviewed proposal","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/KnowledgeProposalData"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"409":{"$ref":"#/components/responses/ConflictError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Library and Knowledge"]}},"/projects/{projectId}/setup/brands":{"get":{"summary":"List brands","description":"Pages the project's brands: the primary brand and the competitor brands AI Search measures against. Only tracked brands are listed unless includeArchived is true; an archived brand has archivedAt set, is no longer tracked and keeps its recorded history. Pass a tracked brand's id as brandId to createDomain, updateDomain, createBrandTerm or updateBrandTerm, and an archived one to restoreBrand. Ordered by id. Pass nextCursor back as cursor with includeArchived unchanged. This is a live listing, not a snapshot. Requires the `setup.read` scope.","operationId":"listBrands","x-devtune-required-scope":"setup.read","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}},{"name":"includeArchived","in":"query","schema":{"type":"boolean","default":false}},{"name":"pageSize","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":50}},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":50},"description":"The same setting as pageSize. Send either; both with different values returns 400."},{"name":"cursor","in":"query","schema":{"type":"string","minLength":1,"maxLength":512}}],"responses":{"200":{"description":"One page","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/SetupBrandsPage"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Setup"]},"post":{"summary":"Create a brand","description":"Adds a primary or competitor brand to this project. A brand with the same name in any case returns 409. When that brand is archived, the 409 has reason brand_archived and details.brandId; restore it with restoreBrand instead. Concurrent brand changes return 409 with reason concurrent_brand_update and retryable true; retry the request. Requires the `setup.write` scope.","operationId":"createBrand","x-devtune-required-scope":"setup.write","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["name","brandType"],"properties":{"name":{"type":"string","minLength":1,"maxLength":100},"brandType":{"type":"string","enum":["primary","competitor"]}}}}}},"responses":{"201":{"description":"The brand","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/SetupBrandWrite"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"409":{"$ref":"#/components/responses/ConflictError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Setup"]}},"/projects/{projectId}/setup/brands/{brandId}":{"patch":{"summary":"Update a brand","description":"Renames a brand or changes its brandType. A type change relabels its domains and brand terms in one transaction. Becoming primary is refused with 400 if a home URL is not canonical for a primary brand. Recorded citations keep their measured labels; mentions in earlier answers show the new type. Provide name, brandType or both. A name another brand holds returns 409. An archived brand returns 409 with reason brand_archived; restore it first. A brand outside this project returns 404. Concurrent brand changes return 409 with reason concurrent_brand_update and retryable true; retry the request. Requires the `setup.write` scope.","operationId":"updateBrand","x-devtune-required-scope":"setup.write","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}},{"name":"brandId","in":"path","required":true,"description":"A brand id from listBrands.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"minProperties":1,"properties":{"name":{"type":"string","minLength":1,"maxLength":100},"brandType":{"type":"string","enum":["primary","competitor"]}}}}}},"responses":{"200":{"description":"The updated brand","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/SetupBrandWrite"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"409":{"$ref":"#/components/responses/ConflictError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Setup"]},"delete":{"summary":"Archive a brand","description":"Archives a competitor brand: DevTune stops tracking it from now and leaves it out of positioning comparisons. Its recorded citations, mentions, domains and brand terms stay as history. Restore it with restoreBrand. The primary brand cannot be archived and returns 409 with reason primary_brand_not_archivable. Archiving an archived brand succeeds again. A brand outside this project returns 404. On plans with Positioning, a current positioning competitor returns 409 with error conflict and reason brand_in_competitor_set unless confirm=true acknowledges the change to future comparisons and the end of any comparable history for the current set. Brands outside the set, and plans without Positioning, need no confirmation; recorded positioning sets are kept. Concurrent brand changes return 409 with reason concurrent_brand_update and retryable true; retry the request. Requires the `setup.write` scope.","operationId":"deleteBrand","x-devtune-required-scope":"setup.write","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}},{"name":"brandId","in":"path","required":true,"description":"A brand id from listBrands.","schema":{"type":"string","format":"uuid"}},{"name":"confirm","in":"query","description":"Set true to confirm archiving a current positioning competitor on a plan with Positioning.","schema":{"type":"boolean","default":false}}],"responses":{"200":{"description":"The archived brand","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/SetupBrandDeletion"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"409":{"$ref":"#/components/responses/ConflictError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Setup"]}},"/projects/{projectId}/setup/brands/{brandId}/restore":{"post":{"summary":"Restore a brand","description":"Restores an archived brand: DevTune tracks it again from now, and the days it was archived stay untracked. Restoring a tracked brand succeeds again. A brand outside this project returns 404. Concurrent brand changes return 409 with reason concurrent_brand_update and retryable true; retry the request. Requires the `setup.write` scope.","operationId":"restoreBrand","x-devtune-required-scope":"setup.write","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}},{"name":"brandId","in":"path","required":true,"description":"A brand id from listBrands.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The restored brand","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/SetupBrandWrite"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"409":{"$ref":"#/components/responses/ConflictError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Setup"]}},"/projects/{projectId}/setup/domains":{"get":{"summary":"List tracked domains","description":"Pages the domains and URLs added to the project's brands, which decide whose a citation is. Domains recorded only because AI answers cited them are not listed. Ordered by id. Pass nextCursor back as cursor. This is a live listing, not a snapshot. Requires the `setup.read` scope.","operationId":"listDomains","x-devtune-required-scope":"setup.read","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}},{"name":"pageSize","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":50}},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":50},"description":"The same setting as pageSize. Send either; both with different values returns 400."},{"name":"cursor","in":"query","schema":{"type":"string","minLength":1,"maxLength":512}}],"responses":{"200":{"description":"One page","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/SetupDomainsPage"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Setup"]},"post":{"summary":"Add a domain","description":"Adds a domain or URL to a brand of this project. It counts as the brand's from today; citations already recorded keep their labels. A domain the project already holds, including one recorded from citations, moves to this brand, and previousBrandId names the brand it left. A host claimed by another brand through another tracked domain or alias returns 409 with reason host_owned_by_another_brand. A spelling that cannot be tracked returns 400 with the reason. An archived brand returns 409 with reason brand_archived. A brand outside this project returns 404. Concurrent domain ownership changes return 409 with reason concurrent_domain_update and retryable true; retry the request. Requires the `setup.write` scope.","operationId":"createDomain","x-devtune-required-scope":"setup.write","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["domain","brandId"],"properties":{"domain":{"type":"string","minLength":1,"maxLength":2048},"brandId":{"type":"string","format":"uuid"}}}}}},"responses":{"201":{"description":"The domain","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/SetupDomainCreate"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"409":{"$ref":"#/components/responses/ConflictError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Setup"]}},"/projects/{projectId}/setup/domains/{domainId}":{"patch":{"summary":"Update a domain","description":"Changes one thing about a tracked domain per request: domain renames it using the same validation as the app, brandId moves it to another brand of this project (null unassigns it), domainType tags it, or siteType sets how Audit scores one of the primary brand's domains. Only domains added to a brand can be tagged or given a site type; others return 409. A rename with dependent measurement data returns 409. A brand move conflicting with another domain or alias returns 409 with reason host_owned_by_another_brand, and a move to an archived brand returns 409 with reason brand_archived. A domain outside this project returns 404. Concurrent domain ownership changes return 409 with reason concurrent_domain_update and retryable true; retry the request. Requires the `setup.write` scope.","operationId":"updateDomain","x-devtune-required-scope":"setup.write","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}},{"name":"domainId","in":"path","required":true,"description":"A domain id from listDomains.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"description":"Exactly one of domain, brandId, domainType or siteType, beside the path domainId.","oneOf":[{"type":"object","additionalProperties":false,"required":["domain"],"properties":{"domain":{"type":"string","minLength":1,"maxLength":2048}}},{"type":"object","additionalProperties":false,"required":["brandId"],"properties":{"brandId":{"type":["string","null"],"format":"uuid"}}},{"type":"object","additionalProperties":false,"required":["domainType"],"properties":{"domainType":{"type":"string","enum":["docs","github","blog","package","home","other"]}}},{"type":"object","additionalProperties":false,"required":["siteType"],"properties":{"siteType":{"type":"string","enum":["content","api_application"]}}}]}}}},"responses":{"200":{"description":"The updated domain","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/SetupDomainWrite"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"409":{"$ref":"#/components/responses/ConflictError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Setup"]},"delete":{"summary":"Delete a domain","description":"Stops tracking a domain. Citations already recorded keep their labels. This cannot be undone; add it again with createDomain. A domain already deleted, or outside this project, returns 404. Requires the `setup.write` scope.","operationId":"deleteDomain","x-devtune-required-scope":"setup.write","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}},{"name":"domainId","in":"path","required":true,"description":"A domain id from listDomains.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The deleted domain","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/SetupDomainDeletion"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Setup"]}},"/projects/{projectId}/setup/keywords":{"get":{"summary":"List brand terms","description":"Pages the project's brand terms: the brand and product names DevTune watches for in AI answers to count mentions. Only active terms are listed unless includeInactive is true; a retired term is one deleted while assigned to a brand, kept so earlier mentions stay attributed. Ordered by id. Pass nextCursor back as cursor with includeInactive unchanged. This is a live listing, not a snapshot. Requires the `setup.read` scope.","operationId":"listBrandTerms","x-devtune-required-scope":"setup.read","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}},{"name":"includeInactive","in":"query","schema":{"type":"boolean","default":false}},{"name":"pageSize","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":50}},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":50},"description":"The same setting as pageSize. Send either; both with different values returns 400."},{"name":"cursor","in":"query","schema":{"type":"string","minLength":1,"maxLength":512}}],"responses":{"200":{"description":"One page","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/SetupBrandTermsPage"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Setup"]},"post":{"summary":"Add a brand term","description":"Adds a brand term, optionally assigned to a brand of this project. Terms are matched in any case and do not run as prompts or spend credits. Adding a retired term again under the same brand restores it and returns restored true. A term already active, or retired under another brand, returns 409. An archived brand returns 409 with reason brand_archived. A brand outside this project returns 404. Requires the `setup.write` scope.","operationId":"createBrandTerm","x-devtune-required-scope":"setup.write","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["term"],"properties":{"term":{"type":"string","minLength":1,"maxLength":200},"brandId":{"type":["string","null"],"format":"uuid"}}}}}},"responses":{"201":{"description":"The brand term","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/SetupBrandTermCreate"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"409":{"$ref":"#/components/responses/ConflictError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Setup"]}},"/projects/{projectId}/setup/keywords/{keywordId}":{"patch":{"summary":"Update a brand term","description":"Assigns a brand term to another brand of this project, or unassigns it with null. An archived brand returns 409 with reason brand_archived. A term or brand outside this project returns 404. Requires the `setup.write` scope.","operationId":"updateBrandTerm","x-devtune-required-scope":"setup.write","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}},{"name":"keywordId","in":"path","required":true,"description":"A brand term id from listBrandTerms.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["brandId"],"properties":{"brandId":{"type":["string","null"],"format":"uuid"}}}}}},"responses":{"200":{"description":"The updated brand term","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/SetupBrandTermWrite"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"409":{"$ref":"#/components/responses/ConflictError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Setup"]},"delete":{"summary":"Delete a brand term","description":"Deletes a brand term. A term assigned to a brand is retired rather than removed, so earlier mentions stay attributed, and retired is true; adding it again restores it. An unassigned term is removed. A term already removed, or outside this project, returns 404. Requires the `setup.write` scope.","operationId":"deleteBrandTerm","x-devtune-required-scope":"setup.write","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}},{"name":"keywordId","in":"path","required":true,"description":"A brand term id from listBrandTerms.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The deleted brand term","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/SetupBrandTermDeletion"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Setup"]}},"/projects/{projectId}/setup/platforms":{"get":{"summary":"List probed platforms","description":"Lists the AI platforms the project can be probed on. enabled is true when scheduled probes run on the platform now: the project has it switched on, the plan includes it, and the project is not paused; Grok also needs DevTune to be collecting it. available is true when the account's plan includes the platform, whatever the project's setting. If the plan can't be checked, returns 500 rather than a partial list. Requires the `setup.read` scope.","operationId":"getSearchPlatforms","x-devtune-required-scope":"setup.read","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The platforms","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/SetupSearchPlatforms"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Setup"]}},"/projects/{projectId}/setup/content-sources":{"get":{"summary":"List content sources","description":"Lists the project's content sources, for accounts with content pull requests: each maps a site URL prefix onto a GitHub repository, branch and folder that DevTune can open pull requests on (provider github), or a GitBook space it can open change requests in (provider gitbook). Sources are added and removed in the app. Refused with 403 when content pull requests are off. Requires the `setup.read` scope.","operationId":"getContentSources","x-devtune-required-scope":"setup.read","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The content sources","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/SetupContentSources"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Setup"]}},"/projects/{projectId}/setup/project":{"patch":{"summary":"Pause or resume a project","description":"Pauses or resumes the project. A paused project keeps its history readable but runs nothing new, frees its paid project slot, and refuses every other change until it is resumed. Pausing needs the Agency plan (403 with reason agency_only). Resuming needs a free project slot (409 with reason no_free_slot) and room in the prompt allowance (409 with reason prompt_allowance). Requires the `setup.write` scope.","operationId":"setProjectPaused","x-devtune-required-scope":"setup.write","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","additionalProperties":false,"required":["paused"],"properties":{"paused":{"type":"boolean","description":"true to pause the project, false to resume it."}}}}}},"responses":{"200":{"description":"Whether the project is paused","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/ProjectPausedState"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"409":{"$ref":"#/components/responses/ConflictError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Setup"]}},"/projects/{projectId}/setup/catalogue/agent-picks":{"get":{"summary":"Get the Agent Picks catalogue","description":"Lists what the setup wizard in the app offers for saveAgentPicksSetup and saveAgentPicksJob: published categories with their active tools, the test apps agents start in (one with category_ids only for those categories), and the agents with the models each accepts and whether the account’s Agent Picks plan includes it. An unpublished category or retired tool is refused with 400 invalid_setup or invalid_job. An account without Agent Picks gets 403. Takes no query parameters. Requires the `setup.read` scope.","operationId":"getAgentPicksCatalogue","x-devtune-required-scope":"setup.read","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The catalogue","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/SetupAgentPicksCatalogue"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Setup"]}},"/projects/{projectId}/setup/catalogue/platforms":{"get":{"summary":"Get the AI platform catalogue","description":"Lists the AI platforms DevTune probes, in display order, with whether the account’s plan includes each. Takes no query parameters. Requires the `setup.read` scope.","operationId":"getSearchPlatformCatalogue","x-devtune-required-scope":"setup.read","parameters":[{"name":"projectId","in":"path","required":true,"description":"The project the credential is bound to. Read it from GET /connection.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"The platforms","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/SetupSearchPlatformCatalogue"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Setup"]}},"/agent/setup-draft":{"get":{"summary":"Get the agent setup draft","description":"Reads the setup draft of the unclaimed agent registration the token belongs to. A claimed or expired registration has none (404 registration_unavailable). Takes an agent access token, not an API key. Takes no query parameters. Requires the `setup.draft` scope.","operationId":"getAgentSetupDraft","x-devtune-required-scope":"setup.draft","security":[{"agentBearer":[]}],"responses":{"200":{"description":"The draft","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AgentSetupDraftState"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Agent registration"]},"put":{"summary":"Replace the agent setup draft","description":"Replaces the draft whole. It validates as the post-claim API would: the Agent Picks setup and job as saveAgentPicksSetup and saveAgentPicksJob accept them, with ids in the published catalogue. A refused draft is 400 invalid_draft naming the first problem. Takes an agent access token, not an API key. Requires the `setup.draft` scope.","operationId":"saveAgentSetupDraft","x-devtune-required-scope":"setup.draft","security":[{"agentBearer":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AgentSetupDraft"}}}},"responses":{"200":{"description":"The saved draft","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AgentSetupDraftState"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Agent registration"]}},"/agent/catalogue/agent-picks":{"get":{"summary":"Get the Agent Picks catalogue for a draft","description":"Lists the published categories with their active tools, the test apps agents start in and the agents with their models that a draft’s Agent Picks section may name. Which agents a plan includes is known only after the claim. Takes an agent access token, not an API key. Takes no query parameters. Requires the `setup.draft` scope.","operationId":"getPublicAgentPicksCatalogue","x-devtune-required-scope":"setup.draft","security":[{"agentBearer":[]}],"responses":{"200":{"description":"The catalogue","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/AgentPicksPublicCatalogue"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Agent registration"]}},"/agent/catalogue/platforms":{"get":{"summary":"Get the AI platform catalogue for a draft","description":"Lists the AI platforms DevTune probes, in display order. Which ones a plan includes is known only after the claim. Takes an agent access token, not an API key. Takes no query parameters. Requires the `setup.draft` scope.","operationId":"getPublicSearchPlatformCatalogue","x-devtune-required-scope":"setup.draft","security":[{"agentBearer":[]}],"responses":{"200":{"description":"The platforms","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"$ref":"#/components/schemas/SearchPlatformPublicCatalogue"},"meta":{"$ref":"#/components/schemas/Meta"}},"required":["data","meta"]}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Agent registration"]}},"/projects/{projectId}/webhooks":{"get":{"summary":"List webhook subscriptions","description":"Returns active webhook subscriptions for the project. Requires the `webhooks.read` scope.","operationId":"listWebhooks","x-devtune-required-scope":"webhooks.read","parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Webhook subscriptions list","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"webhooks":{"type":"array","description":"The project’s active webhook subscriptions. Signing secrets are never listed.","items":{"$ref":"#/components/schemas/Webhook"}}}},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Webhooks"]},"post":{"summary":"Create a webhook subscription","description":"Registers a new webhook subscription. Returns the subscription including the signing secret (only shown once). Each call creates a subscription, so a retry after a lost response creates a second one: list the subscriptions and delete the extra. Requires the `webhooks.write` scope.","operationId":"createWebhook","security":[{"bearerAuth":[]},{"oauthBearer":[]}],"x-devtune-required-scope":"webhooks.write","parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["url","events"],"properties":{"url":{"type":"string","format":"uri","maxLength":2048,"description":"Public HTTPS URL to POST webhook payloads to"},"events":{"$ref":"#/components/schemas/WebhookEvents","description":"Event types to subscribe to"}}}}}},"responses":{"201":{"description":"Webhook created","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"allOf":[{"$ref":"#/components/schemas/Webhook"},{"type":"object","properties":{"secret":{"type":"string","description":"HMAC-SHA256 signing secret. Only returned on creation."}},"required":["secret"]}]},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"400":{"$ref":"#/components/responses/BadRequestError"},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Webhooks"]}},"/projects/{projectId}/webhooks/{webhookId}":{"delete":{"summary":"Deactivate a webhook subscription","description":"Deactivates a webhook subscription: it stops receiving events and leaves the list. Deactivating it again returns the same result. Requires the `webhooks.write` scope.","operationId":"deleteWebhook","security":[{"bearerAuth":[]},{"oauthBearer":[]}],"x-devtune-required-scope":"webhooks.write","parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"webhookId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Webhook deleted","content":{"application/json":{"schema":{"type":"object","properties":{"deleted":{"type":"boolean","description":"Always true: the subscription is off and receives no more events."}}}}}},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Webhooks"]}},"/projects/{projectId}/webhooks/{webhookId}/rotate-secret":{"post":{"summary":"Rotate a webhook signing secret","description":"Generates a new HMAC-SHA256 signing secret for the webhook subscription. The old secret is invalidated immediately. The new secret is only returned once in this response; if you lose the response, rotate again. Requires the `webhooks.write` scope.","operationId":"rotateWebhookSecret","x-devtune-required-scope":"webhooks.write","parameters":[{"name":"projectId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"webhookId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Secret rotated","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"object","properties":{"secret":{"type":"string","description":"New HMAC-SHA256 signing secret. Only returned once."}},"required":["secret"]},"meta":{"$ref":"#/components/schemas/Meta"}}}}}},"401":{"$ref":"#/components/responses/UnauthorizedError"},"403":{"$ref":"#/components/responses/ForbiddenError"},"404":{"$ref":"#/components/responses/NotFoundError"},"429":{"$ref":"#/components/responses/RateLimitExceededError"}},"tags":["Webhooks"]}}}}