Python SDK
hitsonar: Pydantic models mirroring the API contract, typed error classes, timeout handling, and rate-limit/usage-limit awareness, on top of requests.
hitsonar has not been published to PyPI, so pip install hitsonar will not resolve. Until it is, use the single-file Python client — the same request shapes and error handling, with no dependency beyond requests — or install from packages/sdk-python in the source repository. This page is published now so the interface can be reviewed before it is frozen; the samples below are accurate against that source.Install
For reference. This command will work once the package is published; it does not yet.
pip install hitsonarQuickstart
import os
from hitsonar import HitSonar
client = HitSonar(api_key=os.environ["HITSONAR_API_KEY"])
result = client.analyze("how to start a podcast")
print(result.score.overall, result.score.grade)HitSonar holds a secret key. Construct it on a server, never in code a browser runs — see Where a secret key belongs. Every response is a Pydantic model, so fields are typed and validated and your editor autocompletes them.
Titles from a title
generate_titles() is the whole title workflow: HitSonar derives the research topics, measures them, rejects the search angles whose first page belongs to a different kind of video, reads how the titles ranking there are built, scores the title you sent and writes scored alternatives. It is the slowest call in this client, so it uses its own 150-second timeout rather than the constructor’s default.
# A title is the whole request. No keyword, no search volume, no brief.
result = client.generate_titles(
title="No Condemnation - Putting Down the Shame You Were Never Meant to Carry",
)
# TWO different scores. Never show both as "score".
result.research.selected.opportunityScore # HitSonar Score: is the TOPIC worth pursuing
result.recommended.titleScore.overall # Title Opportunity: how well the TITLE is built
# Recommendations with no research behind them must not look like ones with it.
if result.research.status == "no_suitable_topic":
# Every candidate topic was rejected on search intent, or had no analysis.
# The titles are still real; they rest on the brief alone.
...
# The rejections are the useful part.
for c in result.research.considered:
if c.decision == "rejected":
print(c.phrase, c.reason)
# Counted from the selected page's real titles. Prevalence, never a ranking claim.
result.titleStyle.observations # ["6 of 10 titles use a colon.", ...]It costs one title generation from a separate monthly allowance, plus up to three new analyses — research.analysesRun reports how many it actually spent. Full field reference: POST /v2/titles.
Errors
Every non-2xx response raises a subclass of HitSonarError, matching the API’s error.code:
from hitsonar import HitSonar, RateLimitError, UsageLimitError, AuthenticationError, InsufficientContextError
try:
client.analyze(phrase)
except RateLimitError as e:
... # e.retry_after_seconds
except UsageLimitError as e:
... # e.usage.used, e.usage.limit, e.usage.resets_at
except AuthenticationError as e:
... # bad or missing key
except InsufficientContextError as e:
# generate_titles() only: the title names an occasion, not a subject ("Episode 11").
# Retrying will not help -- send a description, or a better working title.
# It subclasses InvalidRequestError, so older except blocks still catch it.
...
# every HitSonarError carries .code, .message, .param, .request_id, .statusA request that never reaches the server (a timeout, DNS failure, connection error) raises HitSonarNetworkError, not a HitSonarError — there is no response envelope to read a code from.
Rate limits and usage
client.last_request_id, client.last_rate_limit, and client.last_usage are updated after every call, success or failure, from the response headers. Quote last_request_id when contacting support about a specific call.
Options
HitSonar(
api_key="hs_...",
base_url="https://app.hitsonar.com", # override for self-hosted or staging
timeout=60, # seconds; a cold /v2/analyze run can take ~20s
# generate_titles() uses its own 150s default instead
session=my_requests_session, # inject your own requests.Session, e.g. for testing
)Field names match the wire format
Response fields keep their JSON name exactly (seedSuggestions, not seed_suggestions) rather than being remapped to snake_case, so what you see in the API reference is what you type here.
Versioning
This package’s version is independent of API_VERSION and score.version. It follows ordinary semver for its own surface. See the versioning and deprecation policy.
Source
Hand-written Pydantic models kept in step with the contract, in the main HitSonar repository at packages/sdk-python. Full install/test/publish instructions are in that package’s README.md.
