← Developers

Changelog

Every change to the API contract, the scoring model, the SDKs, or this documentation that a caller could notice. Dated, newest first. A change tagged Breaking Change is also covered by the versioning and deprecation policy.

2026-09-27
API

API contract 3.1.0: POST /v2/titles takes a count

Additive. `count` (1 to 10, default 10) says how many titles to return. It TRIMS the set rather than shrinking the generation: the model is asked for the same 5 to 10 titles whatever you send, and the deterministic title score still picks `recommended` from all of them, so `count: 3` gives exactly the same top pick as `count: 10` with fewer alternatives beside it. Asking for fewer therefore costs no less -- it is one model request either way, and one call's worth of credits -- and it gives no worse a recommendation, which asking a model for three titles instead of nine would. `recommended` counts toward the total, so `count: 3` returns one recommendation and two alternatives. The parameter existed in the service's own types from the day POST /v2/titles shipped and was wired to nothing: it was accepted, ignored, and looked like a control. API_VERSION moves its minor number so a caller can tell whether the build it is talking to honours it. Both SDKs updated.

2026-09-27
Breaking Change

API contract 3.0.0: credits replace the two monthly allowances

Corrected the same day. This shipped first as 2.2.0, a minor bump, while being tagged a breaking change in this very entry -- the two cannot both be right, and the policy is the one that holds: renaming a response field takes API_VERSION's MAJOR number and advance notice. It is 3.0.0, and the tempting argument was declined, namely that POST /v2/titles had shipped only hours earlier so no caller could have integrated against the old field name. That reasoning would excuse every rename made quickly enough, which is what the policy exists to prevent. An organization now spends from ONE monthly pool of credits instead of two separate allowances. A call costs 5 credits whatever it is, so plans read as calls: Free grants 50 credits (10 calls), Pro grants 2,000 (400 calls). GET /v2/suggest is free and spends nothing at all, as are a cached repeat, a provisional result, and any run that fails. Why: the old design gave 50 analyses and 200 title generations that could not be spent on each other, so a customer could be unable to run research while two hundred unusable generations sat beside it. One pool means "credits left" is a number a customer can act on. Breaking for anyone reading the numbers. X-Usage-Limit, -Remaining and -Reset on POST /v2/analyze now report CREDITS rather than analyses -- the header names and their meaning as a limit are unchanged, but the unit is not, so divide by 5 for calls. POST /v2/titles replaces `usage.generations` with `usage.credits`, which carries used, limit, remaining, resetsAt and perCall. A 429 with organization_limit now means credits are gone rather than one allowance of two. One consequence worth noting: a POST /v2/titles request is ONE call however many keyword topics it measures internally. `research.analysesRun` still reports that number for transparency and is no longer billed separately. Credits price the call, not what the call costs to serve. Nothing about YouTube quota reaches a customer.

2026-09-28
API

API contract 3.2.0: POST /v2/titles/score, for titles you already have

A new endpoint, which is additive: nothing changed for an existing caller, and API_VERSION moves to 3.2.0 so you can tell which build you are talking to. POST /v2/titles/score takes a `researchId` this organization already has and up to 50 candidate titles, and returns a Title Opportunity Score for each against that one piece of research. The titles may come from anywhere -- HitSonar, another tool, a spreadsheet, a person. It costs NOTHING. No credits, no analyses, no title generations, and no YouTube, autocomplete, Trends or model request: it reads one saved analysis and runs a pure function. The per-key rate limit is what bounds it. Why it exists: scoring a title you wrote yourself used to mean POST /v2/titles, once per title. Six candidates were six requests and thirty credits -- and, worse than the cost, six DIFFERENT pieces of research, so the six scores were not comparable with each other. One request here scores them all against one piece of evidence. Results come back in the order sent, 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, which makes a score cacheable and explainable. Sort by `titleScore.overall` for a ranking. POST /v2/titles does the opposite on purpose, because there the titles compete to be recommended and near-duplicates should score each other down. Needs an organization key (hs_...), like POST /v2/titles, because research belongs to an organization. A researchId from another organization answers exactly as a non-existent one does. Both SDKs are updated.

2026-09-28
Scoring

Title Opportunity Score 4.1.0: a stated task and a category label stop scoring the same

`titleScoreVersion` moves to 4.1.0. A 4.0.0 score is not comparable with a 4.1.0 one. API_VERSION and SCORING_VERSION do not move, the response shape is unchanged, and no weight moved -- Click Appeal is still 30 of 100 and its six components are unchanged. The defect: Click Appeal's direct-utility credit, which a title earns for satisfying a need rather than opening a question, did not distinguish what was being offered. "How to Install Tailwind CSS in Next.js" and "Tailwind CSS Guide" both earned 4 of 7, so a task aimed squarely at what the viewer searched for was worth exactly what a genre label was. A search-intent viewer came for the answer, and one of those titles says what they will be able to do while the other names a shape of content. Now: a task aimed at the researched subject earns 6, a task aimed at part of it 4, a bare form noun 2. It moves scores in BOTH directions, which is the point -- the tutorial rises from 13 to 15 of 30 and the bare label falls from 11 to 9. Only titles that open no question at all are affected; anything with a real information gap is untouched. One consequence worth knowing: 15 is the threshold at which the Click Appeal floor stops capping an overall score, so a plain well-formed tutorial title on its researched subject is no longer held at 79. That threshold itself did not move.

2026-09-28
Scoring

Title Opportunity Score 4.0.0: Click Appeal measured by composition, not by vocabulary

`titleScoreVersion` moves to 4.0.0. Scores from 3.0.0 are not comparable with it. API_VERSION does not move and SCORING_VERSION does not move: the response shape is unchanged and the topic model is untouched. Every weight is unchanged too -- Click Appeal is still 30 points -- so this release moves numbers without moving the breakdown. What it is: Click Appeal 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. It must never be rendered as a percentage or a rate. What changed: 3.0.0 measured three things and much of its viewer-pull half amounted to whether a word from a list appeared. 4.0.0 measures six, and a token no longer scores on its own. A word NOMINATES a candidate structure, which earns nothing until it composes -- does the structure have a subject and an object, does it relate to the researched context, does it form a proposition. So "Why I Stopped Putting My Emergency Fund in Index Funds" scores a strong information gap (a named subject, an identifiable prior behaviour, a reversal, a cause left unresolved) while "Why Morning Routine" -- same word -- scores zero for curiosity, because nothing in it could have a cause. "Without" earns its points from a constraint applied to a recognisable task, not from appearing; "stopped" earns contrast from the relationship between expected and stated behaviour, not from appearing. The six internal components are relevance 0-7, information gap 0-7, stakes 0-5, specificity 0-5, contrast 0-4, comprehension 0-2. They remain internal evidence: the public response still carries ONE `clickAppeal` dimension and its `note`. Direct utility is a complete route. A plain "How to install X in Y" opens no loop, reverses nothing and risks nothing, and still scores well on relevance -- HitSonar is not a clickbait detector and a search-intent title wins by satisfying a need. New behaviour worth knowing: Click Appeal is relevance-gated. A well-built title about a DIFFERENT subject than the one researched is held near zero however well it reads, because scoring it would tell a creator an off-topic title is a strong one. Also fixed: an ordinal window ("the first hour") now counts as the duration it is. Cardinals were in the number list and ordinals were not, so "the first hour" scored nothing where "one hour" scored. The Click Appeal floor is unchanged: below 10 of 30 an overall is capped at 69, below 15 at 79. Calibrated before release so the 30-point scale is actually used: generic bait sits at 2, a bare subject label at 5, a useful title with a concrete condition around 16, and a title with relevance, a gap, stakes, specificity and a reversal reaches 26. The upper range needs several independent drivers -- repeating one structure cannot get there, because every component is capped.

2026-09-28
Scoring

Title Opportunity Score 3.0.0: Click Appeal takes 30 points, and a floor under it

`titleScoreVersion` moves to 3.0.0. Scores from 2.0.0 are not comparable with it. API_VERSION does not move and SCORING_VERSION does not move: the response gains one field and the topic model is untouched, which the versioning policy covers as a changelog-only release. Weights: coverage 25 to 20, alignment 20 to 15, clickAppeal 20 to 30, differentiation 15, clarity 10 and length 10 unchanged. That makes the split 35 points for being FOUND and 55 for being worth CHOOSING, where 2.0.0 had it even at 45 and 45. Why, given 2.0.0 was three days ago: 2.0.0 made viewer appeal count but not decide. With findability at 45 a rewrite could still win on points by inserting the researched phrase and deleting the hook, because the arithmetic let one half outvote the other. Inserting a keyword is the half a rewrite can always improve mechanically; the reason somebody clicks is the half it deletes by accident. Click Appeal's three components scale with it -- specificity 0-12 from four classes worth 3, viewer pull 0-12 on a 9/6/6/3 ladder, subject anchoring 0-6 -- and nothing about WHAT earns a point changed, only what a point is worth. New: `titleScore.appealFloor`, null on nearly every title. 30 points still leaves 70, so a title that is easy to find and gives a viewer no reason to choose it could read as excellent. Below 10 of 30 Click Appeal an overall score is capped at 69; below 15, at 79; from 15 up there is no cap. It is the one rule in this score that is not a weighted sum. The consequence worth reading if you store or re-render these: `overall` can now be LOWER than `dimensions` add up to. It was always their sum before. When `appealFloor` is present it carries `rawOverall`, `cap` and a sentence explaining the gap -- display `overall` and explain it with `appealFloor` rather than re-adding the breakdown. Every dimension still reports what it actually measured, so a creator sees a real 20 of 20 for coverage and sees that it did not save the title. `clickAppeal` is still NOT predicted click-through and must not be rendered as a percentage or a rate. Capitalisation and punctuation still cannot move it, by construction. Both SDKs are updated.

2026-09-27
Scoring

Title Opportunity Score 2.0.0: a Click Appeal dimension, and the end of promise-word counting

API contract 2.2.0. The Title Opportunity Score gains a sixth dimension, `clickAppeal`, and every weight moved: coverage 30 to 25, alignment 25 to 20, differentiation 20 to 15, clarity 15 to 10, length unchanged at 10, with clickAppeal taking 20. `titleScoreVersion` moves to 2.0.0 and scores from 1.0.0 are not comparable with it. SCORING_VERSION did not move: the topic model is untouched. Why: coverage and alignment were 55 of 100 points between them, so a rewrite could be rewarded for inserting the researched phrase while deleting the only reason anyone would click. Measured on a real run, "Slow Gospel Blues on a Resonator -- One Take, No Pedals" became "Slow Gospel Blues Unplugged | Capturing the Spirit of Resonator Music" and scored HIGHER, because the abstraction contained the phrase and the two concrete constraints did not. Read the six dimensions as two halves: coverage and alignment are whether a video can be FOUND, and differentiation, clickAppeal and clarity are whether it is worth CHOOSING. A rewrite that raises one and lowers the other is making a trade, and the breakdown is where a caller sees which -- showing the total alone hides it. `clickAppeal` is NOT predicted click-through and must not be rendered as a percentage or a rate: it measures textual structures (concrete specificity; viewer pull, taken as the stronger of a plain payoff or an unresolved tension rather than their sum; and whether that pull is anchored to a nameable subject), while real click performance depends on the thumbnail, the audience, the traffic surface and the position on screen. Capitalisation and punctuation cannot move it, by construction rather than by promise -- the matcher never sees them. Also removed: `clarity` counted about fifty "promise words" (secret, ultimate, proven, best) as evidence of a viewer promise, so hype scored as substance. It is renamed "Clarity and credibility" and now reads textual bait risk only. `dimensions[].key` gains the value `clickAppeal`; widening an enum is additive under the deprecation policy. Both SDKs are updated.

2026-09-27
API

API contract 2.1.0: POST /v2/titles, and a new 422 error code

A new endpoint, so API_VERSION moves to 2.1.0. Additive: nothing about /v2/analyze, /v2/suggest or /v2/health changed, and no existing field was renamed, retyped or removed. POST /v2/titles takes a video title and nothing else -- no keyword, no search volume, no competition figure, no brief -- and returns the researched topic it chose, the evidence behind that choice, the search angles it REJECTED and why, how the titles on the selected first page are actually built, a score for the title you sent, and scored alternatives. It costs one title generation from a separate monthly allowance, plus up to three new analyses it reports as `research.analysesRun`. Two consequences for a caller to note. First, the response carries TWO scores and they are different things: `research.selected.opportunityScore` is the HitSonar Score for a topic (the same `score.overall` /v2/analyze reports, versioned by `score.version`) and `titleScore.overall` is the Title Opportunity Score for a title, versioned by a third number, `titleScoreVersion`, which starts at 1.0.0. Neither is a prediction, and neither is produced by a language model -- both are computed, so the same input always gives the same number. Second, `error.code` gained the value `insufficient_context` (422), returned when a title names an occasion rather than a subject ("Episode 11") and HitSonar will not invent one. Adding an enum value is additive under the deprecation policy; removing one would not be. `error.type` is unchanged. The deprecation policy has been clarified to match: only API_VERSION's MAJOR number moves for a breaking change, and a minor bump like this one means new capability that takes nothing away. /v1 is frozen and gains neither the endpoint nor the code. Also corrected: /v2/health's `titles` field was documented as not implying an endpoint to call. It now does imply one -- when `titles` is false, POST /v2/titles answers 503 on that deployment, because HitSonar does not fall back to gluing a keyword into a template.

2026-09-27
API

/v2 responses carry directSuggestions and seedEvidence

Additive, so not a breaking change under the deprecation policy, and API_VERSION does not move. Both /v2/analyze and /v2/suggest now include `directSuggestions` -- the subset of `suggestions` YouTube returned for the bare phrase, those whose `sources` contain "seed" -- and `seedEvidence`, which pairs the bare-phrase completion count with how to read it ("strong" at 10 or more, "thin" at 5 to 9, "very_thin" at 4 or fewer, "unknown" when the lookup failed, matching the scoring model's own bands). Neither is new evidence: both are computed from values already present in the same response, and a caller deriving them by hand gets identical results. They exist because the length of `suggestions` is discovery breadth and reads far too easily as demand -- the expansion fans a seed through roughly forty query shapes, so a meaningless string still returns plenty, with an empty `directSuggestions` beside it. The TypeScript SDK's generated types include both; the Python SDK exposes them as optional, so a client on the new SDK still parses a response from a deployment that predates them. /v1 is unchanged and gains neither field.

2026-09-27
Documentation

Corrected: which response headers are actually sent, and what signals.trends means

Two documentation corrections, no change to the API. First, headers. The 2026-09-22 entry below says every /v2 response carries X-RateLimit-*. That was never true of the runtime and is corrected here rather than edited there: X-Request-Id is on every /v2 response, success and error alike, and is now documented on every status in the OpenAPI document. X-RateLimit-* is sent for organization API keys where a rate-limit state exists -- not for operator-configured keys, which have no organization counter, and not on a 401, which has no identified key to report a state for. X-Usage-* is sent on organization-metered POST /v2/analyze calls only. On a throttle rejection, Retry-After is the authoritative header. Second, signals.trends: "no_data" now covers any answer from Trends with no usable direction, including the all-zero series described in scoring 1.2.0, and trend: null means no usable reading rather than that Google failed -- read signals.trends to tell the two apart.

2026-09-26
Scoring

Scoring 1.2.0: an absent Trends reading is no longer reported as "steady"

Google Trends answers about a phrase too small for its index with a full-length series of zeroes rather than an error. The direction was read as a ratio of two windows, and both comparisons are false when each window averages zero, so the absence fell through to the last branch and was reported as a steady trend. When the recent and prior windows are both zero, signals.trends is now "no_data" and trend is null. The arithmetic behind score.overall is unchanged: a null direction and a "steady" one both carry a zero trend nudge. What can change for the same phrase is score.confidence, score.notes and signals.trends -- a non-null direction was one of the conditions for confidence to reach "medium", so an absence of evidence could previously raise the confidence reported alongside a score. score.version moved because that customer-visible behaviour changed, and because the version is part of the analysis cache key. Every expected value in the new golden file is identical to 1.1.0's.

2026-09-26
Scoring

Scoring 1.1.0: keyword expansion by query shape rather than by genre

The autocomplete expansion behind suggestions and completions carried ten music-specific modifiers -- songs, music, mix, playlist, instrumental, live, cover, beats, lyrics, worship -- appended to every seed in every niche, so a run on a personal-finance phrase asked YouTube about its worship variant. They are replaced by general query shapes: question and intent words as prefixes, narrowing words as suffixes, and the complete alphabet (it previously skipped q, u, v, x, y and z). The scoring arithmetic itself did not change; the evidence feeding it did. Because completions is derived from the suggestions a phrase returns, and completions feeds score.demand where no first page was read, the same phrase can score differently before and after -- so score.version moved, and with it the analysis cache key, so no deployment on 1.1.0 serves a result built from the old expansion. Every expected value in the new golden file is identical to 1.0.0's, which is the evidence that only the measurement moved.

2026-09-25
API

/v2/health reports whether title generation is configured

GET /v2/health gained a boolean `titles`, alongside the existing `ranking`, `accounts` and `youtubeCache`: it says whether a model provider is configured for AI title generation on this deployment. Additive, so it is not a breaking change under the deprecation policy -- a caller reading specific fields is unaffected. The TypeScript SDK's generated types include it; the Python SDK exposes it as an optional field, so a client on the new SDK still parses a response from a deployment that predates it. /v1/health is unchanged and keeps its frozen shape.

2026-09-22
API

The new error envelope moved to /v2; /v1 restored to its original contract

The error-envelope change below was briefly shipped as a change to /v1 itself -- exactly the silent-change-inside-an-existing-prefix this service's own versioning policy exists to prevent. Corrected the same day: /v1/analyze, /v1/suggest and /v1/health now answer precisely as they always have (the plain { error: "..." } shape, no request id, no rate-limit headers, their own frozen OpenAPI document at /v1/openapi.json), with only a Deprecation response header added. The new contract lives at /v2/analyze, /v2/suggest and /v2/health instead. See the deprecation policy's worked example.

2026-09-22
Breaking Change

API contract 2.0.0 (/v2): one error envelope, a request id on every response, visible rate limits

On /v2 only, every error response has the shape { error: { code, message, type, param, requestId } } instead of { error: "some string" }. Every /v2 response, success or error, carries X-Request-Id, X-RateLimit-Limit/Remaining/Reset, and (on /v2/analyze) X-Usage-Limit/Remaining/Reset. This is breaking only for callers who move to /v2; /v1 callers see no change at all. If your code targets /v2 and checks typeof body.error === "string", update it.

2026-09-22
API

API_VERSION separated from the scoring version

/v2's response shape carries its own version, independent of score.version (SCORING_VERSION). A future scoring change will not move this number, and a future contract change will not move score.version. See Versioning on the main reference.

2026-09-22
Documentation

OpenAPI 3.1 generated from one schema source, not hand-written

/v2/openapi.json and /v2/openapi.yaml are now generated directly from the Zod schemas the server validates requests and tests validate responses against, and checked in CI with npm run openapi:validate. The two can no longer drift apart from what the server actually does. /v1 keeps its own separate, hand-frozen spec at /v1/openapi.json, which this generator does not touch.

2026-09-22
Documentation

The developer reference, a live try-it console, and this changelog

Published the first version of this reference at /developers, with copy-paste samples, a panel that sends a real request with your own key, and the OpenAPI spec linked from the page.

2026-09-20
Scoring

Scoring 1.0.0 frozen

The first versioned, calibrated scoring model. Every score carries score.version; a future change to the formula ships as a new version with its own golden tests, never edited in place. See the scoring methodology page.