{"openapi":"3.1.0","info":{"title":"HitSonar API","version":"3.2.0","summary":"YouTube keyword research: search demand, trends, and who ranks, as one score.","description":"HitSonar turns YouTube's public signals into a single opportunity score for a phrase. `score.overall` is the only score to show; everything else in a response is evidence for it. Request and response body schemas here are generated from the same Zod contract the server validates against (`src/lib/contract.ts`), so a body cannot drift from what the API sends; endpoint metadata and documented response headers are written beside it and checked against the runtime by tests instead. `info.version` is the API contract version; it moves independently of `score.version`, the scoring model, which is reported per-response and never in this document. Full reference and a live try-it panel: https://hitsonar.com/developers","contact":{"url":"https://hitsonar.com/developers"},"license":{"name":"Proprietary","url":"https://hitsonar.com/developers"}},"servers":[{"url":"https://app.hitsonar.com","description":"Production"}],"security":[{"bearerAuth":[]}],"tags":[{"name":"Research","description":"The two research endpoints."},{"name":"Titles","description":"Research, search-intent checking, first-page style and title writing, from a title alone."},{"name":"Status","description":"No key required."}],"paths":{"/v2/analyze":{"post":{"tags":["Research"],"summary":"Score a phrase","description":"The full analysis: autocomplete, Trends, the first page of YouTube results, and the HitSonar Score built from them. A cold run takes about 20 seconds; a repeat of the same phrase within a day is served from cache in milliseconds and costs nothing against the monthly allowance.","operationId":"analyze","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"phrase":{"description":"2 to 80 characters. Lower-cased and single-spaced before use; the response echoes what was actually scored.","type":"string"},"geo":{"default":"US","type":"string","enum":["US"],"description":"The only geography supported today."},"fresh":{"default":false,"description":"Run it again instead of serving a cached result. Costs one analysis.","type":"boolean"}},"required":["phrase"]},"example":{"phrase":"how to start a podcast","geo":"US"}}}},"responses":{"200":{"description":"The analysis.","headers":{"X-Request-Id":{"description":"Every /v2 response carries one. Also present in the JSON body on an error.","schema":{"type":"string","example":"req_9Kx3mZq2pR8vNc1w"}},"X-Usage-Limit":{"description":"Sent on organization-metered analyses only; absent for operator-configured keys, which have no organization allowance. Full analyses this organization may run this calendar month.","schema":{"type":"integer"}},"X-Usage-Remaining":{"description":"Sent on organization-metered analyses only; absent for operator-configured keys, which have no organization allowance. Analyses left this month.","schema":{"type":"integer"}},"X-Usage-Reset":{"description":"Sent on organization-metered analyses only; absent for operator-configured keys, which have no organization allowance. Seconds until the monthly allowance resets, UTC.","schema":{"type":"integer"}},"X-RateLimit-Limit":{"description":"Sent on organization API-key requests where a rate-limit state exists; absent for operator-configured keys and for requests with no identified key. Requests this key may send per minute.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Sent on organization API-key requests where a rate-limit state exists; absent for operator-configured keys and for requests with no identified key. Requests left in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Sent on organization API-key requests where a rate-limit state exists; absent for operator-configured keys and for requests with no identified key. Seconds until the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"type":"object","properties":{"phrase":{"type":"string","minLength":2,"maxLength":80,"description":"As scored: lower-cased, single-spaced, cut at 80 characters."},"cached":{"type":"boolean"},"geo":{"type":"string","enum":["US"],"description":"The only geography supported today."},"analysisStatus":{"type":"string","enum":["complete","degraded","provisional"],"description":"\"complete\": every signal answered. \"degraded\": there is a first page, but a supporting signal failed. \"provisional\": no first page; mark it provisional on screen."},"volumeStatus":{"type":"string","enum":["modeled","provider","provider_no_data_fallback_modeled","provider_failed_fallback_modeled"]},"signals":{"type":"object","properties":{"autocomplete":{"type":"string","enum":["ok","failed"]},"trends":{"type":"string","enum":["ok","no_data","failed"],"description":"\"ok\": Trends returned a usable series and a direction was determined. \"no_data\": Trends answered, but there is no usable direction -- too few points to judge, or a series of zeroes for a phrase too small to chart, where no meaningful recent/prior ratio exists. \"failed\": the Trends request itself did not answer. Both \"no_data\" and \"failed\" leave `trend` null."},"ranking":{"type":"string","enum":["ok","no_results","failed","off"]},"volume":{"type":"string","enum":["modeled","provider","provider_no_data_fallback_modeled","provider_failed_fallback_modeled"]}},"required":["autocomplete","trends","ranking","volume"],"additionalProperties":false},"geoCoverage":{"type":"object","properties":{"autocomplete":{"type":"string"},"trends":{"type":"string"},"ranking":{"type":"string","description":"Notes that view and subscriber counts on the page are worldwide, even though the search itself targets `geo`."},"searches":{"type":"string","description":"Notes that the modeled estimate was fitted to worldwide reference volumes."}},"required":["autocomplete","trends","ranking","searches"],"additionalProperties":false},"suggestions":{"type":"array","items":{"type":"object","properties":{"phrase":{"type":"string","description":"A phrase YouTube's autocomplete returned in response to one of HitSonar's direct or expanded queries. That it was returned does not by itself establish search volume, a number of users, or that the phrase is relevant to the seed."},"hits":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991,"description":"How many of HitSonar's expansion queries surfaced this phrase -- recurrence across the fan-out, not search volume, demand, relevance or validity. Unrelated expansions can independently surface the same irrelevant completion, so a high count is not on its own a reason to trust a phrase."},"sources":{"type":"array","items":{"type":"string"},"description":"Which expansions surfaced it, e.g. \"for\", \"tutorial\", a letter of the alphabet. A list containing \"seed\" means YouTube returned this phrase for the bare seed itself, which is direct evidence; anything else means it was found only by asking about a query HitSonar composed. Filter on that to separate the two: `sources.includes(\"seed\")`."}},"required":["phrase","hits","sources"],"additionalProperties":false}},"directSuggestions":{"type":"array","items":{"type":"object","properties":{"phrase":{"type":"string","description":"A phrase YouTube's autocomplete returned in response to one of HitSonar's direct or expanded queries. That it was returned does not by itself establish search volume, a number of users, or that the phrase is relevant to the seed."},"hits":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991,"description":"How many of HitSonar's expansion queries surfaced this phrase -- recurrence across the fan-out, not search volume, demand, relevance or validity. Unrelated expansions can independently surface the same irrelevant completion, so a high count is not on its own a reason to trust a phrase."},"sources":{"type":"array","items":{"type":"string"},"description":"Which expansions surfaced it, e.g. \"for\", \"tutorial\", a letter of the alphabet. A list containing \"seed\" means YouTube returned this phrase for the bare seed itself, which is direct evidence; anything else means it was found only by asking about a query HitSonar composed. Filter on that to separate the two: `sources.includes(\"seed\")`."}},"required":["phrase","hits","sources"],"additionalProperties":false},"description":"The subset of `suggestions` YouTube returned for the BARE phrase -- those whose `sources` contain \"seed\". Direct evidence, as opposed to a phrase found only by asking about a query HitSonar composed. Derivable from `suggestions` yourself with `sources.includes(\"seed\")`; provided because the length of `suggestions` is discovery breadth and reads far too easily as demand. An empty array beside a long `suggestions` is what a phrase nobody searches looks like."},"seedSuggestions":{"anyOf":[{"type":"integer","minimum":0,"maximum":14},{"type":"null"}],"description":"Completions YouTube offered for the BARE phrase, 0 to 14. Null when that lookup failed. This, not the length of `suggestions`, is the direct evidence that people type the phrase: 10 or more is strong, 5 to 9 is thin, 4 or fewer is evidence almost nobody types it (see score.lowSuggestionWarning). The expansion fans the seed through roughly forty query shapes, so `suggestions` will be long for almost any sufficiently long string and must never be read as demand."},"seedEvidence":{"type":"object","properties":{"completions":{"anyOf":[{"type":"integer","minimum":0,"maximum":14},{"type":"null"}],"description":"The same value as the response's `seedSuggestions`, repeated here so this object stands alone."},"status":{"type":"string","enum":["strong","thin","very_thin","unknown"],"description":"How to read that count, using the same bands the scoring model uses so there are not two definitions of \"thin\": \"strong\" is 10 or more, \"thin\" is 5 to 9, \"very_thin\" is 4 or fewer, \"unknown\" means the lookup failed. Not a score, and never a substitute for `score.overall`."}},"required":["completions","status"],"additionalProperties":false,"description":"Direct autocomplete evidence for the phrase itself, separated from the expansion. Derived from `seedSuggestions`, which is already in this response; it exists so the distinction is harder to miss than a sentence in a description."},"trend":{"anyOf":[{"type":"object","properties":{"points":{"type":"array","items":{"type":"object","properties":{"week":{"type":"string","description":"e.g. \"Sep 21, 2025\"."},"value":{"type":"number","minimum":0,"maximum":100}},"required":["week","value"],"additionalProperties":false},"description":"One point roughly every week for the last year, on Google's 0 to 100 Trends index."},"recent":{"type":"number","minimum":0,"maximum":100,"description":"Average of the last 8 points."},"prior":{"type":"number","minimum":0,"maximum":100,"description":"Average of the 12 points before those."},"direction":{"type":"string","enum":["rising","steady","falling"],"description":"Direction of the last 8 weeks against the 12 before that, on Google's YouTube-search Trends index. Present only when a usable ratio could be taken; see `trend` and `signals.trends`."},"peak":{"description":"The week label of the highest point, or null if there were no points.","type":["string","null"]},"rising":{"type":"array","items":{"type":"string"},"description":"Related queries Trends reports as gaining interest. Can be empty."}},"required":["points","recent","prior","direction","peak","rising"],"additionalProperties":false},{"type":"null"}],"description":"Null when no usable trend reading is available. That is not the same as Google failing: read `signals.trends` to tell `no_data` (Trends answered, but there is no usable direction) from `failed` (the request itself did not answer)."},"ranking":{"anyOf":[{"type":"object","properties":{"totalResults":{"type":"integer","minimum":0,"maximum":9007199254740991,"description":"YouTube's reported count for the whole search, not just the page read."},"medianViews":{"type":"integer","minimum":0,"maximum":9007199254740991},"medianViewsPerMonth":{"type":"integer","minimum":0,"maximum":9007199254740991},"medianSubscribers":{"anyOf":[{"type":"integer","minimum":0,"maximum":9007199254740991},{"type":"null"}],"description":"Median among channels whose count is known. Null when none publish one."},"subscriberDataCoverage":{"type":"number","minimum":0,"maximum":1,"description":"Share of the 20 first-page videos whose channel size is known."},"smallChannelShare":{"type":"number","minimum":0,"maximum":1,"description":"Among channels whose size is known: share under 10,000 subscribers."},"bigChannelShare":{"type":"number","minimum":0,"maximum":1,"description":"Among channels whose size is known: share at or over 100,000 subscribers."},"recentShare":{"type":"number","minimum":0,"maximum":1,"description":"Share of the page uploaded within the last 12 months."},"phraseInTitleShare":{"type":"number","minimum":0,"maximum":1,"description":"Share of first-page titles that go after the phrase (src/lib/title-match.ts): token-matched, plural-folded, punctuation-normalized."},"serpOpening":{"type":"integer","minimum":0,"maximum":100,"description":"Describes this one results page: small channels and fresh uploads raise it, large channels lower it. Not an opportunity score, and not an input to one; \"score.overall\" is the only opportunity score."},"verdict":{"type":"string","description":"A one-sentence read of the page, in plain words."},"videos":{"type":"array","items":{"type":"object","properties":{"title":{"type":"string"},"channel":{"type":"string"},"subscribers":{"anyOf":[{"type":"integer","minimum":0,"maximum":9007199254740991},{"type":"null"}],"description":"Null when the channel hides its subscriber count. Never 0 for unknown."},"views":{"type":"integer","minimum":0,"maximum":9007199254740991},"monthsOld":{"type":"number","minimum":0,"description":"Age of the video, floored at 0.5."},"url":{"type":"string","format":"uri"}},"required":["title","channel","subscribers","views","monthsOld","url"],"additionalProperties":false},"description":"Up to 20, the first page, sorted by views descending."},"titleShapes":{"type":"array","items":{"type":"string"},"description":"Need-phrases that recur in first-page titles, e.g. \"for beginners\". Up to 8."}},"required":["totalResults","medianViews","medianViewsPerMonth","medianSubscribers","subscriberDataCoverage","smallChannelShare","bigChannelShare","recentShare","phraseInTitleShare","serpOpening","verdict","videos","titleShapes"],"additionalProperties":false},{"type":"null"}],"description":"Null when ranking was not switched on, found no results, or the YouTube lookup failed (see rankingError)."},"score":{"type":"object","properties":{"version":{"type":"string","description":"The scoring model that produced this score (SCORING_VERSION). Store it beside any score you keep: scores from different versions are not comparable."},"demand":{"type":"integer","minimum":0,"maximum":100,"description":"How much a phrase is searched, on a log scale."},"competition":{"type":"integer","minimum":0,"maximum":100,"description":"How hard the first page is to break into. Lower is easier."},"overall":{"type":"integer","minimum":0,"maximum":100,"description":"The HitSonar Score. The only opportunity number in the response."},"grade":{"type":"string","enum":["A","B","C","D","F"]},"searchesLow":{"anyOf":[{"type":"integer","minimum":0,"maximum":9007199254740991},{"type":"null"}],"description":"Rough low end of a monthly-search range. Null until there is something to estimate from."},"searchesHigh":{"anyOf":[{"type":"integer","minimum":0,"maximum":9007199254740991},{"type":"null"}]},"searchBand":{"anyOf":[{"type":"object","properties":{"label":{"type":"string","description":"A tenfold range, e.g. \"10K to 100K\"."},"low":{"type":"integer","minimum":0,"maximum":9007199254740991},"high":{"anyOf":[{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991},{"type":"null"}],"description":"Null for the open-ended top band, \"over 1M\"."}},"required":["label","low","high"],"additionalProperties":false,"description":"The honest resolution of a modeled search figure. Lead a UI with this, not the raw range."},{"type":"null"}]},"searchSource":{"description":"\"modeled\", or the name of a paid volume provider.","type":["string","null"]},"confidence":{"type":"string","enum":["low","medium","high"],"description":"How much of the evidence a score rests on answered."},"competitionConfidence":{"type":"string","enum":["low","medium","high"],"description":"How much of the evidence a score rests on answered."},"lowSuggestionWarning":{"type":"string","enum":["none","thin","very_thin","unknown"],"description":"Evidence that few people type the phrase at all, reported beside the score."},"notes":{"type":"array","items":{"type":"string"}},"explanation":{"type":"object","properties":{"inputs":{"type":"object","properties":{"medianViewsPerMonth":{"anyOf":[{"type":"integer","minimum":0,"maximum":9007199254740991},{"type":"null"}],"description":"Null without a first page."},"phraseInTitleShare":{"anyOf":[{"type":"number","minimum":0,"maximum":1},{"type":"null"}]},"medianSubscribers":{"anyOf":[{"type":"integer","minimum":0,"maximum":9007199254740991},{"type":"null"}]},"subscriberDataCoverage":{"anyOf":[{"type":"number","minimum":0,"maximum":1},{"type":"null"}]},"seedSuggestions":{"anyOf":[{"type":"integer","minimum":0,"maximum":14},{"type":"null"}],"description":"Completions YouTube offered for the BARE phrase, 0 to 14. Null when that lookup failed. This, not the length of `suggestions`, is the direct evidence that people type the phrase: 10 or more is strong, 5 to 9 is thin, 4 or fewer is evidence almost nobody types it (see score.lowSuggestionWarning). The expansion fans the seed through roughly forty query shapes, so `suggestions` will be long for almost any sufficiently long string and must never be read as demand."},"trendDirection":{"anyOf":[{"type":"string","enum":["rising","steady","falling"],"description":"Direction of the last 8 weeks against the 12 before that, on Google's YouTube-search Trends index. Present only when a usable ratio could be taken; see `trend` and `signals.trends`."},{"type":"null"}]},"completions":{"type":"integer","minimum":0,"maximum":9007199254740991,"description":"Suggestions that are true completions of the phrase. Always present; only used to score when there is no first page."},"measuredSearches":{"anyOf":[{"type":"number","minimum":0},{"type":"null"}],"description":"The paid provider's figure, when one answered."}},"required":["medianViewsPerMonth","phraseInTitleShare","medianSubscribers","subscriberDataCoverage","seedSuggestions","trendDirection","completions","measuredSearches"],"additionalProperties":false},"searches":{"type":"object","properties":{"fitted":{"anyOf":[{"type":"integer","minimum":0,"maximum":9007199254740991},{"type":"null"}],"description":"Monthly searches from the first page alone, before the suggestion-box scaling. Null without a first page."},"suggestionFactor":{"type":"number","minimum":0,"maximum":1,"description":"0 to 1. Scales `fitted` down for a thin suggestion box; 1 when the box is full or the count is unknown."},"modeled":{"anyOf":[{"type":"integer","minimum":0,"maximum":9007199254740991},{"type":"null"}],"description":"fitted * suggestionFactor. Null without a first page."},"used":{"anyOf":[{"type":"integer","minimum":0,"maximum":9007199254740991},{"type":"null"}],"description":"The figure demand was actually computed from: a provider's figure if one answered, `modeled` otherwise."},"source":{"description":"\"modeled\", a provider name, or null when there is no figure at all.","type":["string","null"]},"formula":{"type":"string","description":"The formula with its constants filled in, for this run."}},"required":["fitted","suggestionFactor","modeled","used","source","formula"],"additionalProperties":false},"demand":{"type":"object","properties":{"base":{"type":"number","description":"Before the trend nudge."},"trendNudge":{"type":"number","description":"+5, -5, or 0, from Trends direction."},"value":{"type":"integer","minimum":0,"maximum":100,"description":"Equal to score.demand."},"formula":{"type":"string"}},"required":["base","trendNudge","value","formula"],"additionalProperties":false},"competition":{"type":"object","properties":{"value":{"type":"integer","minimum":0,"maximum":100,"description":"Equal to score.competition."},"basis":{"type":"string","enum":["fitted","default"],"description":"\"fitted\" when a first page with known channel sizes was read; \"default\" (the middle, 50) otherwise."},"formula":{"type":"string"}},"required":["value","basis","formula"],"additionalProperties":false},"overall":{"type":"object","properties":{"value":{"type":"integer","minimum":0,"maximum":100,"description":"Equal to score.overall."},"formula":{"type":"string"},"grades":{"type":"string","description":"The letter-grade cutoffs used, e.g. \"A >= 70, B >= 55, ...\"."}},"required":["value","formula","grades"],"additionalProperties":false}},"required":["inputs","searches","demand","competition","overall"],"additionalProperties":false,"description":"Every input, intermediate value and formula behind the score, so it can be reproduced without reading server logs."}},"required":["version","demand","competition","overall","grade","searchesLow","searchesHigh","searchBand","searchSource","confidence","competitionConfidence","lowSuggestionWarning","notes","explanation"],"additionalProperties":false,"description":"The one opportunity score for a phrase. `overall` and `grade` are what a caller shows; everything else here is evidence for it."},"rankingError":{"description":"Present only when the YouTube lookup threw.","type":"string"},"ranAt":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"}},"required":["phrase","cached","geo","analysisStatus","volumeStatus","signals","geoCoverage","suggestions","directSuggestions","seedSuggestions","seedEvidence","trend","ranking","score","ranAt"],"additionalProperties":false}}}},"400":{"description":"Phrase shorter than two characters, or an unsupported `geo`. `error.param` names the field.","headers":{"X-Request-Id":{"description":"Every /v2 response carries one. Also present in the JSON body on an error.","schema":{"type":"string","example":"req_9Kx3mZq2pR8vNc1w"}}},"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["unauthorized","invalid_request","insufficient_context","rate_limited","organization_limit","global_limit","service_unavailable"]},"message":{"type":"string"},"type":{"type":"string","enum":["authentication_error","invalid_request_error","rate_limit_error","usage_limit_error","api_error"]},"param":{"description":"The request field the error is about, when there is one.","type":["string","null"]},"requestId":{"type":"string","description":"Correlation id for support, e.g. \"req_...\"."}},"required":["code","message","type","param","requestId"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"Missing or unknown key. No rate-limit state is reported: there is no identified key to report one for.","headers":{"X-Request-Id":{"description":"Every /v2 response carries one. Also present in the JSON body on an error.","schema":{"type":"string","example":"req_9Kx3mZq2pR8vNc1w"}}},"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["unauthorized","invalid_request","insufficient_context","rate_limited","organization_limit","global_limit","service_unavailable"]},"message":{"type":"string"},"type":{"type":"string","enum":["authentication_error","invalid_request_error","rate_limit_error","usage_limit_error","api_error"]},"param":{"description":"The request field the error is about, when there is one.","type":["string","null"]},"requestId":{"type":"string","description":"Correlation id for support, e.g. \"req_...\"."}},"required":["code","message","type","param","requestId"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}},"429":{"description":"Either this key is sending requests too fast (`error.code` is `rate_limited`; `Retry-After` is the authoritative header for how long to wait), or this organization's or the service's monthly analyses are used (`error.code` is `organization_limit` or `global_limit`; see the `usage` object).","headers":{"X-Request-Id":{"description":"Every /v2 response carries one. Also present in the JSON body on an error.","schema":{"type":"string","example":"req_9Kx3mZq2pR8vNc1w"}},"Retry-After":{"description":"Seconds to wait. Authoritative on a throttle rejection, and present only for the per-key rate limit -- an exhausted monthly allowance does not set it.","schema":{"type":"integer"}},"X-RateLimit-Limit":{"description":"Sent on organization API-key requests where a rate-limit state exists; absent for operator-configured keys and for requests with no identified key. Requests this key may send per minute.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Sent on organization API-key requests where a rate-limit state exists; absent for operator-configured keys and for requests with no identified key. Requests left in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Sent on organization API-key requests where a rate-limit state exists; absent for operator-configured keys and for requests with no identified key. Seconds until the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"oneOf":[{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["unauthorized","invalid_request","insufficient_context","rate_limited","organization_limit","global_limit","service_unavailable"]},"message":{"type":"string"},"type":{"type":"string","enum":["authentication_error","invalid_request_error","rate_limit_error","usage_limit_error","api_error"]},"param":{"description":"The request field the error is about, when there is one.","type":["string","null"]},"requestId":{"type":"string","description":"Correlation id for support, e.g. \"req_...\"."}},"required":["code","message","type","param","requestId"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["unauthorized","invalid_request","insufficient_context","rate_limited","organization_limit","global_limit","service_unavailable"]},"message":{"type":"string"},"type":{"type":"string","enum":["authentication_error","invalid_request_error","rate_limit_error","usage_limit_error","api_error"]},"param":{"description":"The request field the error is about, when there is one.","type":["string","null"]},"requestId":{"type":"string","description":"Correlation id for support, e.g. \"req_...\"."}},"required":["code","message","type","param","requestId"],"additionalProperties":false},"usage":{"type":"object","properties":{"used":{"type":"integer","minimum":0,"maximum":9007199254740991},"limit":{"type":"integer","minimum":0,"maximum":9007199254740991},"resetsAt":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"}},"required":["used","limit","resetsAt"],"additionalProperties":false,"description":"Present only when error.code is organization_limit or global_limit."}},"required":["error","usage"],"additionalProperties":false}]}}}},"503":{"description":"YouTube or Trends did not answer, or the usage check itself failed. Not charged against any allowance.","headers":{"X-Request-Id":{"description":"Every /v2 response carries one. Also present in the JSON body on an error.","schema":{"type":"string","example":"req_9Kx3mZq2pR8vNc1w"}}},"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["unauthorized","invalid_request","insufficient_context","rate_limited","organization_limit","global_limit","service_unavailable"]},"message":{"type":"string"},"type":{"type":"string","enum":["authentication_error","invalid_request_error","rate_limit_error","usage_limit_error","api_error"]},"param":{"description":"The request field the error is about, when there is one.","type":["string","null"]},"requestId":{"type":"string","description":"Correlation id for support, e.g. \"req_...\"."}},"required":["code","message","type","param","requestId"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}}},"/v2/suggest":{"get":{"tags":["Research"],"summary":"Autocomplete only","description":"Raw YouTube autocomplete discovery around a seed. Costs no YouTube quota and no monthly allowance. The endpoint fans the seed through roughly forty query shapes -- the bare phrase, question and intent prefixes, narrowing suffixes and the alphabet -- so THE NUMBER OF SUGGESTIONS RETURNED IS NOT A DEMAND ESTIMATE: a meaningless string will still come back with plenty. Read `seedSuggestions` for direct bare-phrase evidence, `hits` for recurrence across the expansion queries, and `sources` to tell a direct completion (contains \"seed\") from one found only via a composed query. Use it to inspect how autocomplete responds around a phrase before deciding what to analyze; opportunity evaluation is POST /v2/analyze, which is the only thing that produces `score.overall`.","operationId":"suggest","parameters":[{"name":"q","in":"query","required":true,"schema":{"type":"string"},"example":"how to start a podcast"},{"name":"geo","in":"query","required":false,"schema":{"default":"US","type":"string","enum":["US"],"description":"The only geography supported today."}}],"responses":{"200":{"description":"The expansion.","headers":{"X-Request-Id":{"description":"Every /v2 response carries one. Also present in the JSON body on an error.","schema":{"type":"string","example":"req_9Kx3mZq2pR8vNc1w"}},"X-RateLimit-Limit":{"description":"Sent on organization API-key requests where a rate-limit state exists; absent for operator-configured keys and for requests with no identified key. Requests this key may send per minute.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Sent on organization API-key requests where a rate-limit state exists; absent for operator-configured keys and for requests with no identified key. Requests left in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Sent on organization API-key requests where a rate-limit state exists; absent for operator-configured keys and for requests with no identified key. Seconds until the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"type":"object","properties":{"phrase":{"type":"string"},"geo":{"type":"string","enum":["US"],"description":"The only geography supported today."},"suggestions":{"type":"array","items":{"type":"object","properties":{"phrase":{"type":"string","description":"A phrase YouTube's autocomplete returned in response to one of HitSonar's direct or expanded queries. That it was returned does not by itself establish search volume, a number of users, or that the phrase is relevant to the seed."},"hits":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991,"description":"How many of HitSonar's expansion queries surfaced this phrase -- recurrence across the fan-out, not search volume, demand, relevance or validity. Unrelated expansions can independently surface the same irrelevant completion, so a high count is not on its own a reason to trust a phrase."},"sources":{"type":"array","items":{"type":"string"},"description":"Which expansions surfaced it, e.g. \"for\", \"tutorial\", a letter of the alphabet. A list containing \"seed\" means YouTube returned this phrase for the bare seed itself, which is direct evidence; anything else means it was found only by asking about a query HitSonar composed. Filter on that to separate the two: `sources.includes(\"seed\")`."}},"required":["phrase","hits","sources"],"additionalProperties":false}},"directSuggestions":{"type":"array","items":{"type":"object","properties":{"phrase":{"type":"string","description":"A phrase YouTube's autocomplete returned in response to one of HitSonar's direct or expanded queries. That it was returned does not by itself establish search volume, a number of users, or that the phrase is relevant to the seed."},"hits":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991,"description":"How many of HitSonar's expansion queries surfaced this phrase -- recurrence across the fan-out, not search volume, demand, relevance or validity. Unrelated expansions can independently surface the same irrelevant completion, so a high count is not on its own a reason to trust a phrase."},"sources":{"type":"array","items":{"type":"string"},"description":"Which expansions surfaced it, e.g. \"for\", \"tutorial\", a letter of the alphabet. A list containing \"seed\" means YouTube returned this phrase for the bare seed itself, which is direct evidence; anything else means it was found only by asking about a query HitSonar composed. Filter on that to separate the two: `sources.includes(\"seed\")`."}},"required":["phrase","hits","sources"],"additionalProperties":false},"description":"The subset of `suggestions` YouTube returned for the BARE phrase -- those whose `sources` contain \"seed\". Direct evidence, as opposed to a phrase found only by asking about a query HitSonar composed. Derivable from `suggestions` yourself with `sources.includes(\"seed\")`; provided because the length of `suggestions` is discovery breadth and reads far too easily as demand. An empty array beside a long `suggestions` is what a phrase nobody searches looks like."},"seedSuggestions":{"anyOf":[{"type":"integer","minimum":0,"maximum":14},{"type":"null"}],"description":"Completions YouTube offered for the BARE phrase, 0 to 14. Null when that lookup failed. This, not the length of `suggestions`, is the direct evidence that people type the phrase: 10 or more is strong, 5 to 9 is thin, 4 or fewer is evidence almost nobody types it (see score.lowSuggestionWarning). The expansion fans the seed through roughly forty query shapes, so `suggestions` will be long for almost any sufficiently long string and must never be read as demand."},"seedEvidence":{"type":"object","properties":{"completions":{"anyOf":[{"type":"integer","minimum":0,"maximum":14},{"type":"null"}],"description":"The same value as the response's `seedSuggestions`, repeated here so this object stands alone."},"status":{"type":"string","enum":["strong","thin","very_thin","unknown"],"description":"How to read that count, using the same bands the scoring model uses so there are not two definitions of \"thin\": \"strong\" is 10 or more, \"thin\" is 5 to 9, \"very_thin\" is 4 or fewer, \"unknown\" means the lookup failed. Not a score, and never a substitute for `score.overall`."}},"required":["completions","status"],"additionalProperties":false,"description":"Direct autocomplete evidence for the phrase itself, separated from the expansion. Derived from `seedSuggestions`, which is already in this response; it exists so the distinction is harder to miss than a sentence in a description."}},"required":["phrase","geo","suggestions","directSuggestions","seedSuggestions","seedEvidence"],"additionalProperties":false}}}},"400":{"description":"`q` shorter than two characters, or an unsupported `geo`.","headers":{"X-Request-Id":{"description":"Every /v2 response carries one. Also present in the JSON body on an error.","schema":{"type":"string","example":"req_9Kx3mZq2pR8vNc1w"}}},"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["unauthorized","invalid_request","insufficient_context","rate_limited","organization_limit","global_limit","service_unavailable"]},"message":{"type":"string"},"type":{"type":"string","enum":["authentication_error","invalid_request_error","rate_limit_error","usage_limit_error","api_error"]},"param":{"description":"The request field the error is about, when there is one.","type":["string","null"]},"requestId":{"type":"string","description":"Correlation id for support, e.g. \"req_...\"."}},"required":["code","message","type","param","requestId"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"Missing or unknown key. No rate-limit state is reported: there is no identified key to report one for.","headers":{"X-Request-Id":{"description":"Every /v2 response carries one. Also present in the JSON body on an error.","schema":{"type":"string","example":"req_9Kx3mZq2pR8vNc1w"}}},"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["unauthorized","invalid_request","insufficient_context","rate_limited","organization_limit","global_limit","service_unavailable"]},"message":{"type":"string"},"type":{"type":"string","enum":["authentication_error","invalid_request_error","rate_limit_error","usage_limit_error","api_error"]},"param":{"description":"The request field the error is about, when there is one.","type":["string","null"]},"requestId":{"type":"string","description":"Correlation id for support, e.g. \"req_...\"."}},"required":["code","message","type","param","requestId"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}},"429":{"description":"This key is sending requests too fast. `Retry-After` is the authoritative header for how long to wait.","headers":{"X-Request-Id":{"description":"Every /v2 response carries one. Also present in the JSON body on an error.","schema":{"type":"string","example":"req_9Kx3mZq2pR8vNc1w"}},"Retry-After":{"description":"Seconds to wait.","schema":{"type":"integer"}},"X-RateLimit-Limit":{"description":"Sent on organization API-key requests where a rate-limit state exists; absent for operator-configured keys and for requests with no identified key. Requests this key may send per minute.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Sent on organization API-key requests where a rate-limit state exists; absent for operator-configured keys and for requests with no identified key. Requests left in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Sent on organization API-key requests where a rate-limit state exists; absent for operator-configured keys and for requests with no identified key. Seconds until the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["unauthorized","invalid_request","insufficient_context","rate_limited","organization_limit","global_limit","service_unavailable"]},"message":{"type":"string"},"type":{"type":"string","enum":["authentication_error","invalid_request_error","rate_limit_error","usage_limit_error","api_error"]},"param":{"description":"The request field the error is about, when there is one.","type":["string","null"]},"requestId":{"type":"string","description":"Correlation id for support, e.g. \"req_...\"."}},"required":["code","message","type","param","requestId"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}}},"/v2/titles":{"post":{"tags":["Titles"],"summary":"Recommend titles from a title","description":"The whole title workflow from a working title and nothing else. HitSonar reads what the video appears to be about, derives research topics from it, measures them with the same engine POST /v2/analyze uses, REJECTS the ones whose first page belongs to a different kind of video, reads how that page's own titles are built, scores the title you sent, and writes and scores alternatives. You do not supply a keyword, a search volume, a competition figure or a brief.\n\nTwo numbers come back and they are different things. `research.selected.opportunityScore` is the HitSonar Score for the TOPIC -- is this worth pursuing -- and is the same `score.overall` POST /v2/analyze returns. `titleScore.overall` is the Title Opportunity Score for a TITLE -- how well it is built against that evidence. Neither is a prediction, and neither is produced by a model: both are computed. The model proposes candidate topics and writes the titles; the deterministic title score decides which one is recommended.\n\nCosts one title generation from the organization's monthly generation allowance, which is separate from analyses. It can also spend up to three NEW analyses measuring candidate topics; `research.analysesRun` reports how many it actually spent, and anything this organization already analyzed recently is reused for free. Allow 120 seconds: a request that has to measure three cold topics does real work.","operationId":"generateTitles","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"title":{"description":"The video's title, existing or working. The only required field: HitSonar derives the research topics, measures them, checks search intent and reads the first page's title style from this alone. Kept exactly as sent apart from trimming and collapsing runs of whitespace, because it is scored and echoed back.","type":"string"},"geo":{"default":"US","type":"string","enum":["US"],"description":"The only geography supported today."},"description":{"description":"What actually happens in the video. Not required -- but with it, nothing about the video's contents has to be inferred.","type":"string","maxLength":800},"targetViewer":{"type":"string","maxLength":120},"format":{"description":"Free text, e.g. \"Tutorial\". Steers the writing; not the same thing as the `contentFormat` HitSonar reads from the title.","type":"string","maxLength":60},"angle":{"type":"string","maxLength":60},"tone":{"type":"string","maxLength":60},"direction":{"type":"string","enum":["emotional","curiosity","search","shorter","direct"],"description":"Steer the whole set. Omit it for a mixed set."},"count":{"description":"How many titles to return, 1 to 10. Default 10. This TRIMS the set rather than shrinking the generation: the model is asked for the same 5 to 10 either way, and the deterministic title score still picks `recommended` from all of them, so asking for fewer costs no less and gives no worse a top pick. `recommended` is one of the count, so `count: 3` returns one recommendation and two alternatives.","type":"integer","minimum":1,"maximum":10},"researchId":{"description":"Start from an analysis this organization already has, instead of deriving topics. Must belong to the API key's organization: an id from another organization answers exactly as a non-existent one does, so existence cannot be probed.","type":"string","maxLength":64},"challengeTopic":{"default":false,"description":"Only meaningful with `researchId`. Derives and measures other possible topics too and lets them compete with it, which can cost analyses.","type":"boolean"}},"required":["title"]},"examples":{"titleOnly":{"summary":"A title and nothing else","value":{"title":"No Condemnation — Putting Down the Shame You Were Never Meant to Carry"}},"withContext":{"summary":"With optional context, so nothing about the video's contents has to be inferred","value":{"title":"No Condemnation — Putting Down the Shame You Were Never Meant to Carry","description":"A 12-minute teaching on why guilt outlives forgiveness, and one practice for noticing it.","targetViewer":"Someone who believes they are forgiven and does not feel it","format":"Teaching","tone":"Direct","direction":"search"}},"existingResearch":{"summary":"Start from an analysis this organization already has","value":{"title":"No Condemnation — Putting Down the Shame You Were Never Meant to Carry","researchId":"res_8Kd2mQ1pR9vNc4w"}}}}}},"responses":{"200":{"description":"The recommendation. `research.status` is `no_suitable_topic` when every candidate was rejected on search intent or had no analysis; titles are still returned in that case, from the brief alone, and must not be presented as search-backed. Read `research.quality` before showing any of this as evidence.","headers":{"X-Request-Id":{"description":"Every /v2 response carries one. Also present in the JSON body on an error.","schema":{"type":"string","example":"req_9Kx3mZq2pR8vNc1w"}},"X-RateLimit-Limit":{"description":"Sent on organization API-key requests where a rate-limit state exists; absent for operator-configured keys and for requests with no identified key. Requests this key may send per minute.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Sent on organization API-key requests where a rate-limit state exists; absent for operator-configured keys and for requests with no identified key. Requests left in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Sent on organization API-key requests where a rate-limit state exists; absent for operator-configured keys and for requests with no identified key. Seconds until the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"type":"object","properties":{"input":{"type":"object","properties":{"title":{"type":"string","description":"As scored: trimmed, with runs of whitespace collapsed. Otherwise exactly what was sent."},"direction":{"anyOf":[{"type":"string","enum":["emotional","curiosity","search","shorter","direct"],"description":"Steer the whole set. Omit it for a mixed set."},{"type":"null"}]}},"required":["title","direction"],"additionalProperties":false},"context":{"anyOf":[{"type":"object","properties":{"subject":{"type":"string"},"viewerProblem":{"type":"string"},"contentIntent":{"type":"string"},"contentFormat":{"type":"string","enum":["teaching","tutorial","music","story","review","news","entertainment","other"],"description":"What kind of video this appears to be, from a closed list. A reading of the title, not a measurement."},"candidateTopics":{"type":"array","items":{"type":"string"},"description":"The phrases HitSonar decided were worth measuring, before any of them were measured."}},"required":["subject","viewerProblem","contentIntent","contentFormat","candidateTopics"],"additionalProperties":false},{"type":"null"}],"description":"What the title was read as. HYPOTHESES, not evidence: a reading of the title, used to decide what to go and measure. No number here, and nothing about what the video contains. Null when a `researchId` supplied the subject instead."},"research":{"type":"object","properties":{"status":{"type":"string","enum":["selected","no_suitable_topic"],"description":"\"no_suitable_topic\" means every candidate was rejected on search intent or had no analysis. Recommendations are still returned, from the brief alone -- do not present them as search-backed."},"selected":{"anyOf":[{"type":"object","properties":{"phrase":{"type":"string"},"researchId":{"description":"Null when the analysis could not be filed. The analysis itself still happened.","type":["string","null"]},"opportunityScore":{"type":"integer","minimum":0,"maximum":100,"description":"`score.overall`, from the same engine POST /v2/analyze uses. The only opportunity score in HitSonar."},"analysisStatus":{"type":"string","enum":["complete","degraded","provisional"]},"demand":{"type":"integer","minimum":0,"maximum":100},"competition":{"type":"integer","minimum":0,"maximum":100,"description":"Lower is easier."},"searchBandLabel":{"type":["string","null"]},"trendDirection":{"anyOf":[{"type":"string","enum":["rising","steady","falling"],"description":"Direction of the last 8 weeks against the 12 before that, on Google's YouTube-search Trends index. Present only when a usable ratio could be taken; see `trend` and `signals.trends`."},{"type":"null"}]},"intentFit":{"type":"string","enum":["strong","acceptable","mismatch"],"description":"Whether the first page belongs to the same kind of video. Three words, not a fourth score. \"mismatch\" is declared only when the page positively reads as a different, incompatible kind of video -- a devotional aimed at a page of worship songs; \"acceptable\" covers both a compatible kind and a page nothing identifies, because absence of evidence is not evidence."},"intentReason":{"type":"string"},"reused":{"type":"boolean","description":"True when this analysis was reused rather than paid for, so it cost no allowance."}},"required":["phrase","researchId","opportunityScore","analysisStatus","demand","competition","searchBandLabel","trendDirection","intentFit","intentReason","reused"],"additionalProperties":false},{"type":"null"}]},"considered":{"type":"array","items":{"type":"object","properties":{"phrase":{"type":"string"},"researchId":{"type":["string","null"]},"opportunityScore":{"anyOf":[{"type":"integer","minimum":0,"maximum":100},{"type":"null"}],"description":"`score.overall` from a real analysis. Null when none ran -- never 0, which would read as a judgement nobody made."},"analysisStatus":{"anyOf":[{"type":"string","enum":["complete","degraded","provisional"]},{"type":"null"}]},"hasRanking":{"type":"boolean","description":"Whether a first page was read, which is what makes a style profile possible."},"intentFit":{"anyOf":[{"type":"string","enum":["strong","acceptable","mismatch"],"description":"Whether the first page belongs to the same kind of video. Three words, not a fourth score. \"mismatch\" is declared only when the page positively reads as a different, incompatible kind of video -- a devotional aimed at a page of worship songs; \"acceptable\" covers both a compatible kind and a page nothing identifies, because absence of evidence is not evidence."},{"type":"null"}]},"decision":{"type":"string","enum":["selected","considered","rejected"]},"reason":{"type":"string","description":"Why it was selected, kept or rejected, in plain words. A rejection always quotes the count behind it."}},"required":["phrase","researchId","opportunityScore","analysisStatus","hasRanking","intentFit","decision","reason"],"additionalProperties":false},"description":"Every finalist and what happened to it. The rejections are the useful part: they say which search angles HitSonar looked at and would not recommend."},"quality":{"type":"string","enum":["complete","provisional","degraded","none"],"description":"How trustworthy the evidence is. \"none\" means no analysis stands behind these recommendations."},"skipped":{"type":"array","items":{"type":"object","properties":{"phrase":{"type":"string"},"reason":{"type":"string"}},"required":["phrase","reason"],"additionalProperties":false},"description":"Shortlisted but never analyzed -- the limit was reached, or the allowance was."},"analysesRun":{"type":"integer","minimum":0,"maximum":9007199254740991,"description":"New uncached analyses this request spent from the monthly allowance. Reused and cached ones are not counted."}},"required":["status","selected","considered","quality","skipped","analysesRun"],"additionalProperties":false},"titleStyle":{"anyOf":[{"type":"object","properties":{"sampleSize":{"type":"integer","minimum":0,"maximum":9007199254740991,"description":"Distinct first-page titles every count below is out of."},"duplicatesDropped":{"type":"integer","minimum":0,"maximum":9007199254740991,"description":"Identical titles that collapsed into one, so the counts can be reconciled with the page."},"averageCharacters":{"type":"integer","minimum":0,"maximum":9007199254740991},"medianCharacters":{"type":"integer","minimum":0,"maximum":9007199254740991},"shortestCharacters":{"type":"integer","minimum":0,"maximum":9007199254740991},"longestCharacters":{"type":"integer","minimum":0,"maximum":9007199254740991},"typicalRange":{"anyOf":[{"type":"object","properties":{"low":{"type":"integer","minimum":0,"maximum":9007199254740991},"high":{"type":"integer","minimum":0,"maximum":9007199254740991}},"required":["low","high"],"additionalProperties":false},{"type":"null"}],"description":"The middle half of the lengths, 25th to 75th percentile."},"separators":{"type":"object","properties":{"colon":{"type":"object","properties":{"count":{"type":"integer","minimum":0,"maximum":9007199254740991},"share":{"type":"number","minimum":0,"maximum":1,"description":"count / sampleSize, rounded to two places."}},"required":["count","share"],"additionalProperties":false},"pipe":{"type":"object","properties":{"count":{"type":"integer","minimum":0,"maximum":9007199254740991},"share":{"type":"number","minimum":0,"maximum":1,"description":"count / sampleSize, rounded to two places."}},"required":["count","share"],"additionalProperties":false},"hyphen":{"type":"object","properties":{"count":{"type":"integer","minimum":0,"maximum":9007199254740991},"share":{"type":"number","minimum":0,"maximum":1,"description":"count / sampleSize, rounded to two places."}},"required":["count","share"],"additionalProperties":false},"enDash":{"type":"object","properties":{"count":{"type":"integer","minimum":0,"maximum":9007199254740991},"share":{"type":"number","minimum":0,"maximum":1,"description":"count / sampleSize, rounded to two places."}},"required":["count","share"],"additionalProperties":false},"emDash":{"type":"object","properties":{"count":{"type":"integer","minimum":0,"maximum":9007199254740991},"share":{"type":"number","minimum":0,"maximum":1,"description":"count / sampleSize, rounded to two places."}},"required":["count","share"],"additionalProperties":false},"slash":{"type":"object","properties":{"count":{"type":"integer","minimum":0,"maximum":9007199254740991},"share":{"type":"number","minimum":0,"maximum":1,"description":"count / sampleSize, rounded to two places."}},"required":["count","share"],"additionalProperties":false}},"required":["colon","pipe","hyphen","enDash","emDash","slash"],"additionalProperties":false,"description":"All six, always, including the ones nobody used -- an absent convention is a finding too. A hyphen inside a word is spelling, not a separator, and is not counted."},"quotes":{"type":"object","properties":{"count":{"type":"integer","minimum":0,"maximum":9007199254740991},"share":{"type":"number","minimum":0,"maximum":1,"description":"count / sampleSize, rounded to two places."},"straight":{"type":"integer","minimum":0,"maximum":9007199254740991},"curly":{"type":"integer","minimum":0,"maximum":9007199254740991}},"required":["count","share","straight","curly"],"additionalProperties":false},"parentheses":{"type":"object","properties":{"count":{"type":"integer","minimum":0,"maximum":9007199254740991},"share":{"type":"number","minimum":0,"maximum":1,"description":"count / sampleSize, rounded to two places."}},"required":["count","share"],"additionalProperties":false},"brackets":{"type":"object","properties":{"count":{"type":"integer","minimum":0,"maximum":9007199254740991},"share":{"type":"number","minimum":0,"maximum":1,"description":"count / sampleSize, rounded to two places."}},"required":["count","share"],"additionalProperties":false},"questions":{"type":"object","properties":{"count":{"type":"integer","minimum":0,"maximum":9007199254740991},"share":{"type":"number","minimum":0,"maximum":1,"description":"count / sampleSize, rounded to two places."}},"required":["count","share"],"additionalProperties":false},"numberLed":{"type":"object","properties":{"count":{"type":"integer","minimum":0,"maximum":9007199254740991},"share":{"type":"number","minimum":0,"maximum":1,"description":"count / sampleSize, rounded to two places."}},"required":["count","share"],"additionalProperties":false,"description":"Titles that OPEN with a number. `numbers` is titles containing one anywhere."},"numbers":{"type":"object","properties":{"count":{"type":"integer","minimum":0,"maximum":9007199254740991},"share":{"type":"number","minimum":0,"maximum":1,"description":"count / sampleSize, rounded to two places."}},"required":["count","share"],"additionalProperties":false},"topicPosition":{"type":"object","properties":{"beginning":{"type":"integer","minimum":0,"maximum":9007199254740991},"firstHalf":{"type":"integer","minimum":0,"maximum":9007199254740991},"secondHalf":{"type":"integer","minimum":0,"maximum":9007199254740991},"absent":{"type":"integer","minimum":0,"maximum":9007199254740991}},"required":["beginning","firstHalf","secondHalf","absent"],"additionalProperties":false,"description":"Where the selected phrase sits in each title. `beginning` is a subset of `firstHalf`; firstHalf + secondHalf + absent is the sample."},"shapes":{"type":"array","items":{"type":"object","properties":{"shape":{"type":"string"},"count":{"type":"integer","minimum":0,"maximum":9007199254740991},"share":{"type":"number","minimum":0,"maximum":1}},"required":["shape","count","share"],"additionalProperties":false},"description":"One shape per title, most common first, so the counts add up to the sample."},"commonShapes":{"type":"array","items":{"type":"string"},"description":"The structures worth telling a writer about: at least two titles and at least a fifth of the page."},"observations":{"type":"array","items":{"type":"string"},"description":"The same findings as sentences, e.g. \"6 of 10 titles use a colon.\""}},"required":["sampleSize","duplicatesDropped","averageCharacters","medianCharacters","shortestCharacters","longestCharacters","typicalRange","separators","quotes","parentheses","brackets","questions","numberLed","numbers","topicPosition","shapes","commonShapes","observations"],"additionalProperties":false},{"type":"null"}],"description":"How the titles on the SELECTED phrase's first page are actually built, counted from those titles. Deterministic and never produced by a model. Read it as prevalence and nothing more: punctuation does not make a video rank, and there is no universal formula here -- the profile is read from that page every time, because a sermon page and a gaming page do not look alike. Null when no first page was read."},"currentTitle":{"type":"object","properties":{"title":{"type":"string"},"titleScore":{"anyOf":[{"type":"object","properties":{"overall":{"type":"integer","minimum":0,"maximum":100,"description":"The Title Opportunity Score: how well this TITLE is built against the selected research. Never the same number as `score.overall`, which describes a topic. Usually the sum of `dimensions`, but LOWER than that sum when `appealFloor` is present -- so display this number rather than re-adding the breakdown."},"characters":{"type":"integer","minimum":0,"maximum":9007199254740991},"dimensions":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string","enum":["coverage","alignment","differentiation","clickAppeal","clarity","length"],"description":"Weights, as of titleScoreVersion 4.1.0: coverage 20, alignment 15, differentiation 15, clickAppeal 30, clarity 10, length 10. Read as two halves -- coverage and alignment are whether the video can be FOUND (35 points); clickAppeal, differentiation and clarity are whether it is worth CHOOSING (55). Appeal outweighs findability on purpose: inserting the researched phrase is the half a rewrite can always improve mechanically, and the reason someone clicks is the half it can delete by accident. A rewrite that raises one and lowers the other is making a trade, and the breakdown is where a caller can see which. Do not assume these numbers: read `weight`, and read `titleScoreVersion` to know whether two stored scores are comparable."},"label":{"type":"string"},"weight":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991,"description":"The most this dimension can contribute."},"earned":{"type":"integer","minimum":0,"maximum":9007199254740991,"description":"0 to `weight`."},"note":{"type":"string","description":"What this dimension found, in plain words."}},"required":["key","label","weight","earned","note"],"additionalProperties":false}},"strongest":{"type":"object","properties":{"key":{"type":"string","enum":["coverage","alignment","differentiation","clickAppeal","clarity","length"],"description":"Weights, as of titleScoreVersion 4.1.0: coverage 20, alignment 15, differentiation 15, clickAppeal 30, clarity 10, length 10. Read as two halves -- coverage and alignment are whether the video can be FOUND (35 points); clickAppeal, differentiation and clarity are whether it is worth CHOOSING (55). Appeal outweighs findability on purpose: inserting the researched phrase is the half a rewrite can always improve mechanically, and the reason someone clicks is the half it can delete by accident. A rewrite that raises one and lowers the other is making a trade, and the breakdown is where a caller can see which. Do not assume these numbers: read `weight`, and read `titleScoreVersion` to know whether two stored scores are comparable."},"label":{"type":"string"},"weight":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991,"description":"The most this dimension can contribute."},"earned":{"type":"integer","minimum":0,"maximum":9007199254740991,"description":"0 to `weight`."},"note":{"type":"string","description":"What this dimension found, in plain words."}},"required":["key","label","weight","earned","note"],"additionalProperties":false},"weakest":{"type":"object","properties":{"key":{"type":"string","enum":["coverage","alignment","differentiation","clickAppeal","clarity","length"],"description":"Weights, as of titleScoreVersion 4.1.0: coverage 20, alignment 15, differentiation 15, clickAppeal 30, clarity 10, length 10. Read as two halves -- coverage and alignment are whether the video can be FOUND (35 points); clickAppeal, differentiation and clarity are whether it is worth CHOOSING (55). Appeal outweighs findability on purpose: inserting the researched phrase is the half a rewrite can always improve mechanically, and the reason someone clicks is the half it can delete by accident. A rewrite that raises one and lowers the other is making a trade, and the breakdown is where a caller can see which. Do not assume these numbers: read `weight`, and read `titleScoreVersion` to know whether two stored scores are comparable."},"label":{"type":"string"},"weight":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991,"description":"The most this dimension can contribute."},"earned":{"type":"integer","minimum":0,"maximum":9007199254740991,"description":"0 to `weight`."},"note":{"type":"string","description":"What this dimension found, in plain words."}},"required":["key","label","weight","earned","note"],"additionalProperties":false,"description":"The dimension that earned least of its own weight -- the thing worth fixing."},"appealFloor":{"anyOf":[{"type":"object","properties":{"rawOverall":{"type":"integer","minimum":0,"maximum":100,"description":"What the six dimensions summed to before the cap."},"cap":{"type":"integer","minimum":0,"maximum":100,"description":"The most an overall score may be at this `clickAppeal`: 69 below 10 of 30, 79 below 15."},"note":{"type":"string","description":"Why the score was held, in plain words, for showing a creator."}},"required":["rawOverall","cap","note"],"additionalProperties":false},{"type":"null"}],"description":"Null on nearly every title. Present only when a weak `clickAppeal` held `overall` below what the dimensions add up to -- the one rule in this score that is not a weighted sum. It exists because 30 of 100 points still leaves 70, enough for a title that is easy to find and gives a viewer no reason to choose it to read as excellent. When it is present, `overall` and `rawOverall` differ on purpose and both are given so a caller can explain the gap instead of appearing to add up wrongly."}},"required":["overall","characters","dimensions","strongest","weakest","appealFloor"],"additionalProperties":false,"description":"Deterministic, computed from the title and the evidence, never asked of a model: the same title in the same context always scores the same. It is not a prediction -- it does not say a video will rank, or how many views or clicks it will get. In particular: `clickAppeal` scores textual signals associated with viewer pull, including relevance, information gaps, stakes, specificity, contrast, and comprehension. It is a deterministic title-text heuristic, not a measurement of viewer psychology and not a CTR prediction. A token alone never scores it -- a word nominates a candidate structure, which earns points only once it composes with a recognisable subject and the researched context -- and a title about a different subject is held down however well it reads. Do not convert it into a percentage or a rate. Store `titleScoreVersion` beside any of these you keep."},{"type":"null"}],"description":"Null only for an empty title."}},"required":["title","titleScore"],"additionalProperties":false},"recommended":{"anyOf":[{"type":"object","properties":{"title":{"type":"string"},"rationale":{"type":"string","description":"What the title does -- the angle it takes. Never a claim about performance."},"titleScore":{"type":"object","properties":{"overall":{"type":"integer","minimum":0,"maximum":100,"description":"The Title Opportunity Score: how well this TITLE is built against the selected research. Never the same number as `score.overall`, which describes a topic. Usually the sum of `dimensions`, but LOWER than that sum when `appealFloor` is present -- so display this number rather than re-adding the breakdown."},"characters":{"type":"integer","minimum":0,"maximum":9007199254740991},"dimensions":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string","enum":["coverage","alignment","differentiation","clickAppeal","clarity","length"],"description":"Weights, as of titleScoreVersion 4.1.0: coverage 20, alignment 15, differentiation 15, clickAppeal 30, clarity 10, length 10. Read as two halves -- coverage and alignment are whether the video can be FOUND (35 points); clickAppeal, differentiation and clarity are whether it is worth CHOOSING (55). Appeal outweighs findability on purpose: inserting the researched phrase is the half a rewrite can always improve mechanically, and the reason someone clicks is the half it can delete by accident. A rewrite that raises one and lowers the other is making a trade, and the breakdown is where a caller can see which. Do not assume these numbers: read `weight`, and read `titleScoreVersion` to know whether two stored scores are comparable."},"label":{"type":"string"},"weight":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991,"description":"The most this dimension can contribute."},"earned":{"type":"integer","minimum":0,"maximum":9007199254740991,"description":"0 to `weight`."},"note":{"type":"string","description":"What this dimension found, in plain words."}},"required":["key","label","weight","earned","note"],"additionalProperties":false}},"strongest":{"type":"object","properties":{"key":{"type":"string","enum":["coverage","alignment","differentiation","clickAppeal","clarity","length"],"description":"Weights, as of titleScoreVersion 4.1.0: coverage 20, alignment 15, differentiation 15, clickAppeal 30, clarity 10, length 10. Read as two halves -- coverage and alignment are whether the video can be FOUND (35 points); clickAppeal, differentiation and clarity are whether it is worth CHOOSING (55). Appeal outweighs findability on purpose: inserting the researched phrase is the half a rewrite can always improve mechanically, and the reason someone clicks is the half it can delete by accident. A rewrite that raises one and lowers the other is making a trade, and the breakdown is where a caller can see which. Do not assume these numbers: read `weight`, and read `titleScoreVersion` to know whether two stored scores are comparable."},"label":{"type":"string"},"weight":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991,"description":"The most this dimension can contribute."},"earned":{"type":"integer","minimum":0,"maximum":9007199254740991,"description":"0 to `weight`."},"note":{"type":"string","description":"What this dimension found, in plain words."}},"required":["key","label","weight","earned","note"],"additionalProperties":false},"weakest":{"type":"object","properties":{"key":{"type":"string","enum":["coverage","alignment","differentiation","clickAppeal","clarity","length"],"description":"Weights, as of titleScoreVersion 4.1.0: coverage 20, alignment 15, differentiation 15, clickAppeal 30, clarity 10, length 10. Read as two halves -- coverage and alignment are whether the video can be FOUND (35 points); clickAppeal, differentiation and clarity are whether it is worth CHOOSING (55). Appeal outweighs findability on purpose: inserting the researched phrase is the half a rewrite can always improve mechanically, and the reason someone clicks is the half it can delete by accident. A rewrite that raises one and lowers the other is making a trade, and the breakdown is where a caller can see which. Do not assume these numbers: read `weight`, and read `titleScoreVersion` to know whether two stored scores are comparable."},"label":{"type":"string"},"weight":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991,"description":"The most this dimension can contribute."},"earned":{"type":"integer","minimum":0,"maximum":9007199254740991,"description":"0 to `weight`."},"note":{"type":"string","description":"What this dimension found, in plain words."}},"required":["key","label","weight","earned","note"],"additionalProperties":false,"description":"The dimension that earned least of its own weight -- the thing worth fixing."},"appealFloor":{"anyOf":[{"type":"object","properties":{"rawOverall":{"type":"integer","minimum":0,"maximum":100,"description":"What the six dimensions summed to before the cap."},"cap":{"type":"integer","minimum":0,"maximum":100,"description":"The most an overall score may be at this `clickAppeal`: 69 below 10 of 30, 79 below 15."},"note":{"type":"string","description":"Why the score was held, in plain words, for showing a creator."}},"required":["rawOverall","cap","note"],"additionalProperties":false},{"type":"null"}],"description":"Null on nearly every title. Present only when a weak `clickAppeal` held `overall` below what the dimensions add up to -- the one rule in this score that is not a weighted sum. It exists because 30 of 100 points still leaves 70, enough for a title that is easy to find and gives a viewer no reason to choose it to read as excellent. When it is present, `overall` and `rawOverall` differ on purpose and both are given so a caller can explain the gap instead of appearing to add up wrongly."}},"required":["overall","characters","dimensions","strongest","weakest","appealFloor"],"additionalProperties":false,"description":"Deterministic, computed from the title and the evidence, never asked of a model: the same title in the same context always scores the same. It is not a prediction -- it does not say a video will rank, or how many views or clicks it will get. In particular: `clickAppeal` scores textual signals associated with viewer pull, including relevance, information gaps, stakes, specificity, contrast, and comprehension. It is a deterministic title-text heuristic, not a measurement of viewer psychology and not a CTR prediction. A token alone never scores it -- a word nominates a candidate structure, which earns points only once it composes with a recognisable subject and the researched context -- and a title about a different subject is held down however well it reads. Do not convert it into a percentage or a rate. Store `titleScoreVersion` beside any of these you keep."}},"required":["title","rationale","titleScore"],"additionalProperties":false},{"type":"null"}],"description":"The highest `titleScore.overall`. Chosen by that deterministic score, not by the model."},"alternatives":{"type":"array","items":{"type":"object","properties":{"title":{"type":"string"},"rationale":{"type":"string","description":"What the title does -- the angle it takes. Never a claim about performance."},"titleScore":{"type":"object","properties":{"overall":{"type":"integer","minimum":0,"maximum":100,"description":"The Title Opportunity Score: how well this TITLE is built against the selected research. Never the same number as `score.overall`, which describes a topic. Usually the sum of `dimensions`, but LOWER than that sum when `appealFloor` is present -- so display this number rather than re-adding the breakdown."},"characters":{"type":"integer","minimum":0,"maximum":9007199254740991},"dimensions":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string","enum":["coverage","alignment","differentiation","clickAppeal","clarity","length"],"description":"Weights, as of titleScoreVersion 4.1.0: coverage 20, alignment 15, differentiation 15, clickAppeal 30, clarity 10, length 10. Read as two halves -- coverage and alignment are whether the video can be FOUND (35 points); clickAppeal, differentiation and clarity are whether it is worth CHOOSING (55). Appeal outweighs findability on purpose: inserting the researched phrase is the half a rewrite can always improve mechanically, and the reason someone clicks is the half it can delete by accident. A rewrite that raises one and lowers the other is making a trade, and the breakdown is where a caller can see which. Do not assume these numbers: read `weight`, and read `titleScoreVersion` to know whether two stored scores are comparable."},"label":{"type":"string"},"weight":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991,"description":"The most this dimension can contribute."},"earned":{"type":"integer","minimum":0,"maximum":9007199254740991,"description":"0 to `weight`."},"note":{"type":"string","description":"What this dimension found, in plain words."}},"required":["key","label","weight","earned","note"],"additionalProperties":false}},"strongest":{"type":"object","properties":{"key":{"type":"string","enum":["coverage","alignment","differentiation","clickAppeal","clarity","length"],"description":"Weights, as of titleScoreVersion 4.1.0: coverage 20, alignment 15, differentiation 15, clickAppeal 30, clarity 10, length 10. Read as two halves -- coverage and alignment are whether the video can be FOUND (35 points); clickAppeal, differentiation and clarity are whether it is worth CHOOSING (55). Appeal outweighs findability on purpose: inserting the researched phrase is the half a rewrite can always improve mechanically, and the reason someone clicks is the half it can delete by accident. A rewrite that raises one and lowers the other is making a trade, and the breakdown is where a caller can see which. Do not assume these numbers: read `weight`, and read `titleScoreVersion` to know whether two stored scores are comparable."},"label":{"type":"string"},"weight":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991,"description":"The most this dimension can contribute."},"earned":{"type":"integer","minimum":0,"maximum":9007199254740991,"description":"0 to `weight`."},"note":{"type":"string","description":"What this dimension found, in plain words."}},"required":["key","label","weight","earned","note"],"additionalProperties":false},"weakest":{"type":"object","properties":{"key":{"type":"string","enum":["coverage","alignment","differentiation","clickAppeal","clarity","length"],"description":"Weights, as of titleScoreVersion 4.1.0: coverage 20, alignment 15, differentiation 15, clickAppeal 30, clarity 10, length 10. Read as two halves -- coverage and alignment are whether the video can be FOUND (35 points); clickAppeal, differentiation and clarity are whether it is worth CHOOSING (55). Appeal outweighs findability on purpose: inserting the researched phrase is the half a rewrite can always improve mechanically, and the reason someone clicks is the half it can delete by accident. A rewrite that raises one and lowers the other is making a trade, and the breakdown is where a caller can see which. Do not assume these numbers: read `weight`, and read `titleScoreVersion` to know whether two stored scores are comparable."},"label":{"type":"string"},"weight":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991,"description":"The most this dimension can contribute."},"earned":{"type":"integer","minimum":0,"maximum":9007199254740991,"description":"0 to `weight`."},"note":{"type":"string","description":"What this dimension found, in plain words."}},"required":["key","label","weight","earned","note"],"additionalProperties":false,"description":"The dimension that earned least of its own weight -- the thing worth fixing."},"appealFloor":{"anyOf":[{"type":"object","properties":{"rawOverall":{"type":"integer","minimum":0,"maximum":100,"description":"What the six dimensions summed to before the cap."},"cap":{"type":"integer","minimum":0,"maximum":100,"description":"The most an overall score may be at this `clickAppeal`: 69 below 10 of 30, 79 below 15."},"note":{"type":"string","description":"Why the score was held, in plain words, for showing a creator."}},"required":["rawOverall","cap","note"],"additionalProperties":false},{"type":"null"}],"description":"Null on nearly every title. Present only when a weak `clickAppeal` held `overall` below what the dimensions add up to -- the one rule in this score that is not a weighted sum. It exists because 30 of 100 points still leaves 70, enough for a title that is easy to find and gives a viewer no reason to choose it to read as excellent. When it is present, `overall` and `rawOverall` differ on purpose and both are given so a caller can explain the gap instead of appearing to add up wrongly."}},"required":["overall","characters","dimensions","strongest","weakest","appealFloor"],"additionalProperties":false,"description":"Deterministic, computed from the title and the evidence, never asked of a model: the same title in the same context always scores the same. It is not a prediction -- it does not say a video will rank, or how many views or clicks it will get. In particular: `clickAppeal` scores textual signals associated with viewer pull, including relevance, information gaps, stakes, specificity, contrast, and comprehension. It is a deterministic title-text heuristic, not a measurement of viewer psychology and not a CTR prediction. A token alone never scores it -- a word nominates a candidate structure, which earns points only once it composes with a recognisable subject and the researched context -- and a title about a different subject is held down however well it reads. Do not convert it into a percentage or a rate. Store `titleScoreVersion` beside any of these you keep."}},"required":["title","rationale","titleScore"],"additionalProperties":false},"description":"The rest, `titleScore.overall` descending."},"brief":{"type":"object","properties":{"contentSupplied":{"type":"boolean","description":"False when no `description` was sent and the brief was inferred from the title. Nothing about the video's contents was invented either way, but a caller should know which it is looking at."}},"required":["contentSupplied"],"additionalProperties":false},"titleScoreVersion":{"type":"string","description":"The Title Opportunity Score's own version. Independent of both `score.version` and `apiVersion`; store it beside any title score you keep."},"usage":{"type":"object","properties":{"credits":{"anyOf":[{"type":"object","properties":{"used":{"type":"integer","minimum":0,"maximum":9007199254740991},"limit":{"type":"integer","minimum":0,"maximum":9007199254740991},"remaining":{"type":"integer","minimum":0,"maximum":9007199254740991},"resetsAt":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"},"perCall":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991,"description":"What one metered call costs. Divide `remaining` by it for calls left."}},"required":["used","limit","remaining","resetsAt","perCall"],"additionalProperties":false},{"type":"null"}]}},"required":["credits"],"additionalProperties":false,"description":"This organization's credits for the calendar month, in CREDITS rather than calls. One pool: a keyword analysis, a set of titles and a search for related topics all cost the same and all draw on it. This whole request cost one call's worth however many topics it measured internally -- `research.analysesRun` reports that for transparency and is NOT billed separately. Null when metering is not configured on the deployment."}},"required":["input","context","research","titleStyle","currentTitle","recommended","alternatives","brief","titleScoreVersion","usage"],"additionalProperties":false}}}},"400":{"description":"`title` missing, shorter than two characters or longer than 120; an unsupported `geo`; or a `researchId` that is not available to this key. `error.param` names the field.","headers":{"X-Request-Id":{"description":"Every /v2 response carries one. Also present in the JSON body on an error.","schema":{"type":"string","example":"req_9Kx3mZq2pR8vNc1w"}}},"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["unauthorized","invalid_request","insufficient_context","rate_limited","organization_limit","global_limit","service_unavailable"]},"message":{"type":"string"},"type":{"type":"string","enum":["authentication_error","invalid_request_error","rate_limit_error","usage_limit_error","api_error"]},"param":{"description":"The request field the error is about, when there is one.","type":["string","null"]},"requestId":{"type":"string","description":"Correlation id for support, e.g. \"req_...\"."}},"required":["code","message","type","param","requestId"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"Missing or unknown key, or an operator-configured key -- this endpoint needs an organization key, because a generation is counted against an organization's allowance. No rate-limit state is reported.","headers":{"X-Request-Id":{"description":"Every /v2 response carries one. Also present in the JSON body on an error.","schema":{"type":"string","example":"req_9Kx3mZq2pR8vNc1w"}}},"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["unauthorized","invalid_request","insufficient_context","rate_limited","organization_limit","global_limit","service_unavailable"]},"message":{"type":"string"},"type":{"type":"string","enum":["authentication_error","invalid_request_error","rate_limit_error","usage_limit_error","api_error"]},"param":{"description":"The request field the error is about, when there is one.","type":["string","null"]},"requestId":{"type":"string","description":"Correlation id for support, e.g. \"req_...\"."}},"required":["code","message","type","param","requestId"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}},"422":{"description":"`error.code` is `insufficient_context`: the request was well formed, and the title does not say enough about the video to research it. \"Episode 11\", \"Sunday Message\" and \"My New Video\" name an occasion or the fact that a video exists rather than a subject, and HitSonar will not invent one. Send a `description`, or a more descriptive working title.","headers":{"X-Request-Id":{"description":"Every /v2 response carries one. Also present in the JSON body on an error.","schema":{"type":"string","example":"req_9Kx3mZq2pR8vNc1w"}}},"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["unauthorized","invalid_request","insufficient_context","rate_limited","organization_limit","global_limit","service_unavailable"]},"message":{"type":"string"},"type":{"type":"string","enum":["authentication_error","invalid_request_error","rate_limit_error","usage_limit_error","api_error"]},"param":{"description":"The request field the error is about, when there is one.","type":["string","null"]},"requestId":{"type":"string","description":"Correlation id for support, e.g. \"req_...\"."}},"required":["code","message","type","param","requestId"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}},"429":{"description":"Either this key is sending requests too fast (`error.code` is `rate_limited`; `Retry-After` is authoritative), or this organization's or the service's monthly TITLE GENERATIONS are used (`organization_limit` or `global_limit`). An exhausted ANALYSIS allowance does not fail this endpoint: candidate topics simply go unmeasured and appear in `research.skipped`.","headers":{"X-Request-Id":{"description":"Every /v2 response carries one. Also present in the JSON body on an error.","schema":{"type":"string","example":"req_9Kx3mZq2pR8vNc1w"}},"Retry-After":{"description":"Seconds to wait. Authoritative on a throttle rejection, and present only for the per-key rate limit -- an exhausted monthly allowance does not set it.","schema":{"type":"integer"}},"X-RateLimit-Limit":{"description":"Sent on organization API-key requests where a rate-limit state exists; absent for operator-configured keys and for requests with no identified key. Requests this key may send per minute.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Sent on organization API-key requests where a rate-limit state exists; absent for operator-configured keys and for requests with no identified key. Requests left in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Sent on organization API-key requests where a rate-limit state exists; absent for operator-configured keys and for requests with no identified key. Seconds until the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"oneOf":[{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["unauthorized","invalid_request","insufficient_context","rate_limited","organization_limit","global_limit","service_unavailable"]},"message":{"type":"string"},"type":{"type":"string","enum":["authentication_error","invalid_request_error","rate_limit_error","usage_limit_error","api_error"]},"param":{"description":"The request field the error is about, when there is one.","type":["string","null"]},"requestId":{"type":"string","description":"Correlation id for support, e.g. \"req_...\"."}},"required":["code","message","type","param","requestId"],"additionalProperties":false}},"required":["error"],"additionalProperties":false},{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["unauthorized","invalid_request","insufficient_context","rate_limited","organization_limit","global_limit","service_unavailable"]},"message":{"type":"string"},"type":{"type":"string","enum":["authentication_error","invalid_request_error","rate_limit_error","usage_limit_error","api_error"]},"param":{"description":"The request field the error is about, when there is one.","type":["string","null"]},"requestId":{"type":"string","description":"Correlation id for support, e.g. \"req_...\"."}},"required":["code","message","type","param","requestId"],"additionalProperties":false},"usage":{"type":"object","properties":{"used":{"type":"integer","minimum":0,"maximum":9007199254740991},"limit":{"type":"integer","minimum":0,"maximum":9007199254740991},"resetsAt":{"type":"string","format":"date-time","pattern":"^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d:[0-5]\\d(?:\\.\\d+)?(?:Z))$"}},"required":["used","limit","resetsAt"],"additionalProperties":false,"description":"Present only when error.code is organization_limit or global_limit."}},"required":["error","usage"],"additionalProperties":false}]}}}},"503":{"description":"No model provider is configured on this deployment (`titles` is false in GET /v2/health), or the model did not answer. Nothing is charged. HitSonar never falls back to composing a title from a template, so there is no degraded success here.","headers":{"X-Request-Id":{"description":"Every /v2 response carries one. Also present in the JSON body on an error.","schema":{"type":"string","example":"req_9Kx3mZq2pR8vNc1w"}}},"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["unauthorized","invalid_request","insufficient_context","rate_limited","organization_limit","global_limit","service_unavailable"]},"message":{"type":"string"},"type":{"type":"string","enum":["authentication_error","invalid_request_error","rate_limit_error","usage_limit_error","api_error"]},"param":{"description":"The request field the error is about, when there is one.","type":["string","null"]},"requestId":{"type":"string","description":"Correlation id for support, e.g. \"req_...\"."}},"required":["code","message","type","param","requestId"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}}},"/v2/titles/score":{"post":{"tags":["Titles"],"summary":"Score candidate titles against research you already have","description":"Scores titles you supply against an analysis this organization already paid for. Everything a title score needs was bought by that analysis, so scoring another candidate against it is arithmetic.\n\nUse this instead of calling POST /v2/titles once per title. Six candidates are one request here, against one shared piece of evidence; six requests there would be thirty credits and six DIFFERENT pieces of research, which is not a comparison.\n\nCosts nothing. No credits, no analyses, no title generations: no YouTube call, no autocomplete, no Trends request and no model request happens. The per-key rate limit still applies.\n\nThe titles may come from anywhere -- HitSonar, another tool, a spreadsheet, a person. Results are returned in the order sent and are deliberately NOT ranked and NOT scored against each other: the same title with the same `researchId` always returns the same number, whatever else was in the request. Sort by `titleScore.overall` if you want a ranking.","operationId":"scoreTitles","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"researchId":{"type":"string","minLength":1,"maxLength":64,"description":"An analysis this organization already has, from `research.selected.researchId` on POST /v2/titles or from POST /v2/analyze. Must belong to the API key's organization: an id from another organization answers exactly as a non-existent one does, so existence cannot be probed."},"titles":{"minItems":1,"maxItems":50,"type":"array","items":{"type":"string"},"description":"The candidate titles to score, 1 to 50. They may come from anywhere -- HitSonar, a model, a spreadsheet, a person."},"subject":{"description":"What the video is actually about, if that is narrower or different from the analysed phrase. Used only by `clickAppeal`, which is relevance-gated: a title about a different subject scores near zero there however well it reads. Defaults to the phrase.","type":"string","maxLength":200}},"required":["researchId","titles"]},"examples":{"candidates":{"summary":"Score six candidates against one analysis","value":{"researchId":"res_8Kd2mQ1pR9vNc4w","titles":["Overcoming Shame: How to Stop Carrying What You Were Forgiven For","No Condemnation — Putting Down the Shame You Were Never Meant to Carry","Why I Stopped Carrying Shame I Was Never Meant to Carry"]}},"withSubject":{"summary":"When the video is narrower than the phrase that was analysed","value":{"researchId":"res_8Kd2mQ1pR9vNc4w","titles":["Why I Stopped Carrying Shame I Was Never Meant to Carry"],"subject":"shame after forgiveness"}}}}}},"responses":{"200":{"description":"Every title scored against the same evidence, in the order sent.","headers":{"X-Request-Id":{"description":"Every /v2 response carries one. Also present in the JSON body on an error.","schema":{"type":"string","example":"req_9Kx3mZq2pR8vNc1w"}},"X-RateLimit-Limit":{"description":"Sent on organization API-key requests where a rate-limit state exists; absent for operator-configured keys and for requests with no identified key. Requests this key may send per minute.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Sent on organization API-key requests where a rate-limit state exists; absent for operator-configured keys and for requests with no identified key. Requests left in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Sent on organization API-key requests where a rate-limit state exists; absent for operator-configured keys and for requests with no identified key. Seconds until the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"type":"object","properties":{"researchId":{"type":"string","description":"Echoed back, so a stored score can be traced to the evidence it was computed against."},"phrase":{"type":"string","description":"The analysed phrase these titles were scored against."},"geo":{"type":"string","enum":["US"],"description":"The analysis's own geography. Not a parameter here -- the research carries it."},"opportunityScore":{"type":"integer","minimum":0,"maximum":100,"description":"`score.overall` from that analysis: the TOPIC's number, unchanged by anything here."},"titleStyle":{"anyOf":[{"anyOf":[{"type":"object","properties":{"sampleSize":{"type":"integer","minimum":0,"maximum":9007199254740991,"description":"Distinct first-page titles every count below is out of."},"duplicatesDropped":{"type":"integer","minimum":0,"maximum":9007199254740991,"description":"Identical titles that collapsed into one, so the counts can be reconciled with the page."},"averageCharacters":{"type":"integer","minimum":0,"maximum":9007199254740991},"medianCharacters":{"type":"integer","minimum":0,"maximum":9007199254740991},"shortestCharacters":{"type":"integer","minimum":0,"maximum":9007199254740991},"longestCharacters":{"type":"integer","minimum":0,"maximum":9007199254740991},"typicalRange":{"anyOf":[{"type":"object","properties":{"low":{"type":"integer","minimum":0,"maximum":9007199254740991},"high":{"type":"integer","minimum":0,"maximum":9007199254740991}},"required":["low","high"],"additionalProperties":false},{"type":"null"}],"description":"The middle half of the lengths, 25th to 75th percentile."},"separators":{"type":"object","properties":{"colon":{"type":"object","properties":{"count":{"type":"integer","minimum":0,"maximum":9007199254740991},"share":{"type":"number","minimum":0,"maximum":1,"description":"count / sampleSize, rounded to two places."}},"required":["count","share"],"additionalProperties":false},"pipe":{"type":"object","properties":{"count":{"type":"integer","minimum":0,"maximum":9007199254740991},"share":{"type":"number","minimum":0,"maximum":1,"description":"count / sampleSize, rounded to two places."}},"required":["count","share"],"additionalProperties":false},"hyphen":{"type":"object","properties":{"count":{"type":"integer","minimum":0,"maximum":9007199254740991},"share":{"type":"number","minimum":0,"maximum":1,"description":"count / sampleSize, rounded to two places."}},"required":["count","share"],"additionalProperties":false},"enDash":{"type":"object","properties":{"count":{"type":"integer","minimum":0,"maximum":9007199254740991},"share":{"type":"number","minimum":0,"maximum":1,"description":"count / sampleSize, rounded to two places."}},"required":["count","share"],"additionalProperties":false},"emDash":{"type":"object","properties":{"count":{"type":"integer","minimum":0,"maximum":9007199254740991},"share":{"type":"number","minimum":0,"maximum":1,"description":"count / sampleSize, rounded to two places."}},"required":["count","share"],"additionalProperties":false},"slash":{"type":"object","properties":{"count":{"type":"integer","minimum":0,"maximum":9007199254740991},"share":{"type":"number","minimum":0,"maximum":1,"description":"count / sampleSize, rounded to two places."}},"required":["count","share"],"additionalProperties":false}},"required":["colon","pipe","hyphen","enDash","emDash","slash"],"additionalProperties":false,"description":"All six, always, including the ones nobody used -- an absent convention is a finding too. A hyphen inside a word is spelling, not a separator, and is not counted."},"quotes":{"type":"object","properties":{"count":{"type":"integer","minimum":0,"maximum":9007199254740991},"share":{"type":"number","minimum":0,"maximum":1,"description":"count / sampleSize, rounded to two places."},"straight":{"type":"integer","minimum":0,"maximum":9007199254740991},"curly":{"type":"integer","minimum":0,"maximum":9007199254740991}},"required":["count","share","straight","curly"],"additionalProperties":false},"parentheses":{"type":"object","properties":{"count":{"type":"integer","minimum":0,"maximum":9007199254740991},"share":{"type":"number","minimum":0,"maximum":1,"description":"count / sampleSize, rounded to two places."}},"required":["count","share"],"additionalProperties":false},"brackets":{"type":"object","properties":{"count":{"type":"integer","minimum":0,"maximum":9007199254740991},"share":{"type":"number","minimum":0,"maximum":1,"description":"count / sampleSize, rounded to two places."}},"required":["count","share"],"additionalProperties":false},"questions":{"type":"object","properties":{"count":{"type":"integer","minimum":0,"maximum":9007199254740991},"share":{"type":"number","minimum":0,"maximum":1,"description":"count / sampleSize, rounded to two places."}},"required":["count","share"],"additionalProperties":false},"numberLed":{"type":"object","properties":{"count":{"type":"integer","minimum":0,"maximum":9007199254740991},"share":{"type":"number","minimum":0,"maximum":1,"description":"count / sampleSize, rounded to two places."}},"required":["count","share"],"additionalProperties":false,"description":"Titles that OPEN with a number. `numbers` is titles containing one anywhere."},"numbers":{"type":"object","properties":{"count":{"type":"integer","minimum":0,"maximum":9007199254740991},"share":{"type":"number","minimum":0,"maximum":1,"description":"count / sampleSize, rounded to two places."}},"required":["count","share"],"additionalProperties":false},"topicPosition":{"type":"object","properties":{"beginning":{"type":"integer","minimum":0,"maximum":9007199254740991},"firstHalf":{"type":"integer","minimum":0,"maximum":9007199254740991},"secondHalf":{"type":"integer","minimum":0,"maximum":9007199254740991},"absent":{"type":"integer","minimum":0,"maximum":9007199254740991}},"required":["beginning","firstHalf","secondHalf","absent"],"additionalProperties":false,"description":"Where the selected phrase sits in each title. `beginning` is a subset of `firstHalf`; firstHalf + secondHalf + absent is the sample."},"shapes":{"type":"array","items":{"type":"object","properties":{"shape":{"type":"string"},"count":{"type":"integer","minimum":0,"maximum":9007199254740991},"share":{"type":"number","minimum":0,"maximum":1}},"required":["shape","count","share"],"additionalProperties":false},"description":"One shape per title, most common first, so the counts add up to the sample."},"commonShapes":{"type":"array","items":{"type":"string"},"description":"The structures worth telling a writer about: at least two titles and at least a fifth of the page."},"observations":{"type":"array","items":{"type":"string"},"description":"The same findings as sentences, e.g. \"6 of 10 titles use a colon.\""}},"required":["sampleSize","duplicatesDropped","averageCharacters","medianCharacters","shortestCharacters","longestCharacters","typicalRange","separators","quotes","parentheses","brackets","questions","numberLed","numbers","topicPosition","shapes","commonShapes","observations"],"additionalProperties":false},{"type":"null"}],"description":"How the titles on the SELECTED phrase's first page are actually built, counted from those titles. Deterministic and never produced by a model. Read it as prevalence and nothing more: punctuation does not make a video rank, and there is no universal formula here -- the profile is read from that page every time, because a sermon page and a gaming page do not look alike. Null when no first page was read."},{"type":"null"}],"description":"How the first page's own titles are built, when one was read. Null when the analysis has no ranking."},"scored":{"type":"array","items":{"type":"object","properties":{"title":{"type":"string","description":"As scored: trimmed, with runs of whitespace collapsed."},"titleScore":{"anyOf":[{"type":"object","properties":{"overall":{"type":"integer","minimum":0,"maximum":100,"description":"The Title Opportunity Score: how well this TITLE is built against the selected research. Never the same number as `score.overall`, which describes a topic. Usually the sum of `dimensions`, but LOWER than that sum when `appealFloor` is present -- so display this number rather than re-adding the breakdown."},"characters":{"type":"integer","minimum":0,"maximum":9007199254740991},"dimensions":{"type":"array","items":{"type":"object","properties":{"key":{"type":"string","enum":["coverage","alignment","differentiation","clickAppeal","clarity","length"],"description":"Weights, as of titleScoreVersion 4.1.0: coverage 20, alignment 15, differentiation 15, clickAppeal 30, clarity 10, length 10. Read as two halves -- coverage and alignment are whether the video can be FOUND (35 points); clickAppeal, differentiation and clarity are whether it is worth CHOOSING (55). Appeal outweighs findability on purpose: inserting the researched phrase is the half a rewrite can always improve mechanically, and the reason someone clicks is the half it can delete by accident. A rewrite that raises one and lowers the other is making a trade, and the breakdown is where a caller can see which. Do not assume these numbers: read `weight`, and read `titleScoreVersion` to know whether two stored scores are comparable."},"label":{"type":"string"},"weight":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991,"description":"The most this dimension can contribute."},"earned":{"type":"integer","minimum":0,"maximum":9007199254740991,"description":"0 to `weight`."},"note":{"type":"string","description":"What this dimension found, in plain words."}},"required":["key","label","weight","earned","note"],"additionalProperties":false}},"strongest":{"type":"object","properties":{"key":{"type":"string","enum":["coverage","alignment","differentiation","clickAppeal","clarity","length"],"description":"Weights, as of titleScoreVersion 4.1.0: coverage 20, alignment 15, differentiation 15, clickAppeal 30, clarity 10, length 10. Read as two halves -- coverage and alignment are whether the video can be FOUND (35 points); clickAppeal, differentiation and clarity are whether it is worth CHOOSING (55). Appeal outweighs findability on purpose: inserting the researched phrase is the half a rewrite can always improve mechanically, and the reason someone clicks is the half it can delete by accident. A rewrite that raises one and lowers the other is making a trade, and the breakdown is where a caller can see which. Do not assume these numbers: read `weight`, and read `titleScoreVersion` to know whether two stored scores are comparable."},"label":{"type":"string"},"weight":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991,"description":"The most this dimension can contribute."},"earned":{"type":"integer","minimum":0,"maximum":9007199254740991,"description":"0 to `weight`."},"note":{"type":"string","description":"What this dimension found, in plain words."}},"required":["key","label","weight","earned","note"],"additionalProperties":false},"weakest":{"type":"object","properties":{"key":{"type":"string","enum":["coverage","alignment","differentiation","clickAppeal","clarity","length"],"description":"Weights, as of titleScoreVersion 4.1.0: coverage 20, alignment 15, differentiation 15, clickAppeal 30, clarity 10, length 10. Read as two halves -- coverage and alignment are whether the video can be FOUND (35 points); clickAppeal, differentiation and clarity are whether it is worth CHOOSING (55). Appeal outweighs findability on purpose: inserting the researched phrase is the half a rewrite can always improve mechanically, and the reason someone clicks is the half it can delete by accident. A rewrite that raises one and lowers the other is making a trade, and the breakdown is where a caller can see which. Do not assume these numbers: read `weight`, and read `titleScoreVersion` to know whether two stored scores are comparable."},"label":{"type":"string"},"weight":{"type":"integer","exclusiveMinimum":0,"maximum":9007199254740991,"description":"The most this dimension can contribute."},"earned":{"type":"integer","minimum":0,"maximum":9007199254740991,"description":"0 to `weight`."},"note":{"type":"string","description":"What this dimension found, in plain words."}},"required":["key","label","weight","earned","note"],"additionalProperties":false,"description":"The dimension that earned least of its own weight -- the thing worth fixing."},"appealFloor":{"anyOf":[{"type":"object","properties":{"rawOverall":{"type":"integer","minimum":0,"maximum":100,"description":"What the six dimensions summed to before the cap."},"cap":{"type":"integer","minimum":0,"maximum":100,"description":"The most an overall score may be at this `clickAppeal`: 69 below 10 of 30, 79 below 15."},"note":{"type":"string","description":"Why the score was held, in plain words, for showing a creator."}},"required":["rawOverall","cap","note"],"additionalProperties":false},{"type":"null"}],"description":"Null on nearly every title. Present only when a weak `clickAppeal` held `overall` below what the dimensions add up to -- the one rule in this score that is not a weighted sum. It exists because 30 of 100 points still leaves 70, enough for a title that is easy to find and gives a viewer no reason to choose it to read as excellent. When it is present, `overall` and `rawOverall` differ on purpose and both are given so a caller can explain the gap instead of appearing to add up wrongly."}},"required":["overall","characters","dimensions","strongest","weakest","appealFloor"],"additionalProperties":false,"description":"Deterministic, computed from the title and the evidence, never asked of a model: the same title in the same context always scores the same. It is not a prediction -- it does not say a video will rank, or how many views or clicks it will get. In particular: `clickAppeal` scores textual signals associated with viewer pull, including relevance, information gaps, stakes, specificity, contrast, and comprehension. It is a deterministic title-text heuristic, not a measurement of viewer psychology and not a CTR prediction. A token alone never scores it -- a word nominates a candidate structure, which earns points only once it composes with a recognisable subject and the researched context -- and a title about a different subject is held down however well it reads. Do not convert it into a percentage or a rate. Store `titleScoreVersion` beside any of these you keep."},{"type":"null"}],"description":"Null only for a title that is empty once trimmed."}},"required":["title","titleScore"],"additionalProperties":false},"description":"One entry per title sent, in the order sent. Deliberately NOT ranked and NOT sibling-aware: a title's score here does not depend on what was posted beside it, so the same title with the same researchId always returns the same number. Sort by `titleScore.overall` if you want a ranking."},"titleScoreVersion":{"type":"string","description":"The Title Opportunity Score's own version. Store it beside any score you keep."}},"required":["researchId","phrase","geo","opportunityScore","titleStyle","scored","titleScoreVersion"],"additionalProperties":false}}}},"400":{"description":"`researchId` missing or not available to this key, `titles` empty or longer than 50, or a title shorter than two characters or longer than 120. `error.param` names the field.","headers":{"X-Request-Id":{"description":"Every /v2 response carries one. Also present in the JSON body on an error.","schema":{"type":"string","example":"req_9Kx3mZq2pR8vNc1w"}}},"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["unauthorized","invalid_request","insufficient_context","rate_limited","organization_limit","global_limit","service_unavailable"]},"message":{"type":"string"},"type":{"type":"string","enum":["authentication_error","invalid_request_error","rate_limit_error","usage_limit_error","api_error"]},"param":{"description":"The request field the error is about, when there is one.","type":["string","null"]},"requestId":{"type":"string","description":"Correlation id for support, e.g. \"req_...\"."}},"required":["code","message","type","param","requestId"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}},"401":{"description":"Missing or unknown key, or an operator-configured key -- this endpoint needs an organization key, because research belongs to an organization. No rate-limit state is reported.","headers":{"X-Request-Id":{"description":"Every /v2 response carries one. Also present in the JSON body on an error.","schema":{"type":"string","example":"req_9Kx3mZq2pR8vNc1w"}}},"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["unauthorized","invalid_request","insufficient_context","rate_limited","organization_limit","global_limit","service_unavailable"]},"message":{"type":"string"},"type":{"type":"string","enum":["authentication_error","invalid_request_error","rate_limit_error","usage_limit_error","api_error"]},"param":{"description":"The request field the error is about, when there is one.","type":["string","null"]},"requestId":{"type":"string","description":"Correlation id for support, e.g. \"req_...\"."}},"required":["code","message","type","param","requestId"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}},"429":{"description":"This key is sending requests too fast. `Retry-After` is authoritative. There is no monthly allowance to exhaust here, because this endpoint spends nothing.","headers":{"X-Request-Id":{"description":"Every /v2 response carries one. Also present in the JSON body on an error.","schema":{"type":"string","example":"req_9Kx3mZq2pR8vNc1w"}},"Retry-After":{"description":"Seconds to wait.","schema":{"type":"integer"}},"X-RateLimit-Limit":{"description":"Sent on organization API-key requests where a rate-limit state exists; absent for operator-configured keys and for requests with no identified key. Requests this key may send per minute.","schema":{"type":"integer"}},"X-RateLimit-Remaining":{"description":"Sent on organization API-key requests where a rate-limit state exists; absent for operator-configured keys and for requests with no identified key. Requests left in the current window.","schema":{"type":"integer"}},"X-RateLimit-Reset":{"description":"Sent on organization API-key requests where a rate-limit state exists; absent for operator-configured keys and for requests with no identified key. Seconds until the window resets.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["unauthorized","invalid_request","insufficient_context","rate_limited","organization_limit","global_limit","service_unavailable"]},"message":{"type":"string"},"type":{"type":"string","enum":["authentication_error","invalid_request_error","rate_limit_error","usage_limit_error","api_error"]},"param":{"description":"The request field the error is about, when there is one.","type":["string","null"]},"requestId":{"type":"string","description":"Correlation id for support, e.g. \"req_...\"."}},"required":["code","message","type","param","requestId"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}},"503":{"description":"Accounts are not configured on this deployment, or the research store could not be read. Nothing is charged, because nothing is charged here at all.","headers":{"X-Request-Id":{"description":"Every /v2 response carries one. Also present in the JSON body on an error.","schema":{"type":"string","example":"req_9Kx3mZq2pR8vNc1w"}}},"content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"object","properties":{"code":{"type":"string","enum":["unauthorized","invalid_request","insufficient_context","rate_limited","organization_limit","global_limit","service_unavailable"]},"message":{"type":"string"},"type":{"type":"string","enum":["authentication_error","invalid_request_error","rate_limit_error","usage_limit_error","api_error"]},"param":{"description":"The request field the error is about, when there is one.","type":["string","null"]},"requestId":{"type":"string","description":"Correlation id for support, e.g. \"req_...\"."}},"required":["code","message","type","param","requestId"],"additionalProperties":false}},"required":["error"],"additionalProperties":false}}}}}}},"/v2/health":{"get":{"tags":["Status"],"summary":"What is switched on","description":"Open, no key. Says nothing a stranger could use. Takes no key, so it reports no rate-limit or usage state.","operationId":"health","security":[],"responses":{"200":{"description":"Status.","headers":{"X-Request-Id":{"description":"Every /v2 response carries one. Also present in the JSON body on an error.","schema":{"type":"string","example":"req_9Kx3mZq2pR8vNc1w"}}},"content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"ranking":{"type":"boolean","description":"Whether a YouTube Data API key is configured."},"clients":{"type":"boolean","description":"Whether at least one operator-configured key exists."},"volume":{"type":"string","description":"The volume source in force: \"modeled\" or a provider name."},"scoringVersion":{"type":"string"},"apiVersion":{"type":"string"},"geos":{"type":"array","items":{"type":"string"}},"accounts":{"type":"boolean"},"youtubeCache":{"type":"boolean"},"titles":{"type":"boolean","description":"Whether a model provider is configured for AI title generation. When false, POST /v2/titles answers 503 on this deployment and the signed-in Title Studio says so, because there is no honest fallback for a title -- gluing a keyword into a template is what that endpoint exists to replace. Reported here so an operator can see what a deployment has switched on, the same as `accounts` and `youtubeCache`."}},"required":["ok","ranking","clients","volume","scoringVersion","apiVersion","geos","accounts","youtubeCache","titles"],"additionalProperties":false}}}}}}}},"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","bearerFormat":"hs_...","description":"An API key from https://app.hitsonar.com/console, sent as `Authorization: Bearer hs_...`."}}}}