brightbean — Python SDK
Official Python SDK for the BrightBean API. YouTube creator analytics: score titles and thumbnails for click-through-rate, analyze video hooks, benchmark channels and videos against their niche, and surface ranked content opportunities.
Install
pip install brightbean
Requires Python 3.10+.
Quickstart
from brightbean import BrightBean
with BrightBean(api_key="bb_...") as client:
result = client.score(
title="10 Tips to Boost Your Coding Productivity",
thumbnail_url="https://i.ytimg.com/vi/abc123/maxresdefault.jpg",
)
print(f"score={result.score:.2f} percentile={result.percentile}")
Async
import asyncio
from brightbean import AsyncBrightBean
async def main():
async with AsyncBrightBean(api_key="bb_...") as client:
return await client.score(title="My epic video")
asyncio.run(main())
Every method documented below has the same signature on AsyncBrightBean; just
await the call.
API reference
Response objects are typed dataclasses. Field names match the wire format
(snake_case). Nullable fields are explicitly annotated … | None.
client.score(...) — packaging CTR
Score a YouTube packaging (title and/or thumbnail) for predicted click-through rate. The server auto-detects the mode from your inputs and charges credits accordingly.
result = client.score(
title="10 Tips to Boost Your Coding Productivity",
thumbnail_url="https://i.ytimg.com/vi/abc123/maxresdefault.jpg",
)
print(result.score, result.percentile, result.niche_label)
Parameters
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
title |
str | None |
conditional* | None |
Max 250 chars. |
thumbnail_url |
str | None |
conditional* | None |
Public URL. Max 1024 chars. Mutually exclusive with thumbnail_base64. |
thumbnail_base64 |
str | None |
conditional* | None |
Base64-encoded image. Max 12.5 MB encoded. Mutually exclusive with thumbnail_url. |
channel_url |
str | None |
no | None |
Reserved — currently accepted but not consumed by the model. |
* At least one of title, thumbnail_url, or thumbnail_base64 is required.
Returns PackagingScoreResponse
| Field | Type | Description |
|---|---|---|
score_id |
UUID |
Opaque identifier for this scoring call. |
score |
float (0–1) |
Calibrated CTR percentile rank. |
percentile |
int (0–100) |
round(score * 100). |
raw_score |
float | None |
Continuous pre-calibration sigmoid output. Useful for fine-grained comparisons that the percentile-ranked score would round into the same plateau. |
mode |
"title" | "thumbnail" | "combined" |
Server-detected scoring mode. |
niche_slug |
str |
Canonical niche slug. Same taxonomy as /v1/research/* and /v1/benchmark/*. |
niche_label |
str |
Human-readable niche name. |
niche_confidence |
float |
Confidence (cosine similarity) of the niche assignment. |
Cost: 1 credit for title-only, 2 credits for thumbnail-only, 3 credits for combined (the combined mode captures interaction effects the single-input modes miss).
client.video_hook(...) — hook quality of the first ~6 s
Score the first ~6 seconds of a YouTube video for hook quality. Returns a structured analysis with archetype classification, five dimension scores, and actionable strengths / weaknesses / suggestions.
result = client.video_hook(
youtube_url="https://www.youtube.com/watch?v=dQw4w9WgXcQ",
)
print(result.primary_archetype, result.overall_score)
print(result.scores.clarity, result.scores.tension)
Parameters
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
youtube_url |
str |
yes | — | Public YouTube URL, max 1024 chars. Accepts youtube.com/watch?v=…, youtu.be/…, youtube.com/shorts/…, youtube.com/embed/…. |
Returns VideoHookScoreResponse
| Field | Type | Description |
|---|---|---|
score_id |
UUID |
Opaque identifier for this scoring call. |
primary_archetype |
str |
One of 13 archetypes (curiosity_gap, direct_question, bold_claim, in_medias_res, stakes_statement, promise, pattern_interrupt, authority, demonstration, empathy, negative_framing, cold_open, greetings). |
secondary_archetype |
str | None |
Optional second archetype if the hook blends two. |
scores |
Scores (nested) |
Five 0–10 dimension scores. See below. |
overall_score |
int (0–100) |
Holistic hook-quality score. |
transcript |
str |
First-6-seconds transcript. May be blank for music-only or visual-only openings. |
visual_summary |
str |
Short description of the visual content. |
strengths |
list[str] (2–4 items) |
What the hook does well. |
weaknesses |
list[str] (2–4 items) |
What the hook does poorly. |
suggestions |
list[str] (2–4 items) |
Concrete edits the creator could make. |
delta_vs_niche_top |
int |
Difference between this hook's overall_score and the niche's top performers. |
key_differences_vs_top |
list[str] (1–3 items) |
Specific ways this hook differs from niche-top hooks. |
Nested scores:
| Field | Type | Description |
|---|---|---|
clarity |
int (0–10) |
How quickly the viewer understands the topic. |
specificity |
int (0–10) |
How concrete the promise is. |
tension |
int (0–10) |
Stakes / curiosity gap. |
visual_energy |
int (0–10) |
Movement, cuts, visual contrast. |
pace |
int (0–10) |
Words / cuts per second. |
Cost: 10 credits per call.
client.benchmark_channel(...) — channel vs. niche
Benchmark a YouTube channel against its niche distribution. Fetches the channel's recent videos, classifies the niche, and percentile-ranks engagement and title patterns.
result = client.benchmark_channel(
url="https://www.youtube.com/@MarquesBrownlee",
)
print(result.niche.slug, result.niche.match_strength)
print(result.channel.engagement_percentiles.overall)
Parameters
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
url |
str |
yes | — | Channel URL or @handle, max 1024 chars. Accepts youtube.com/@handle, /channel/UC…, /c/<name>, /user/<legacy>. |
Returns BenchmarkChannelResponse — two top-level objects, channel and niche.
channel
| Field | Type | Description |
|---|---|---|
channel_id |
str |
YouTube channel ID (UC…). |
title |
str |
Channel display name. |
subscriber_count |
int |
Public subscriber count at fetch time. |
video_count |
int |
Total public videos. |
engagement |
object | Raw ratios. view_to_sub_ratio, like_to_view_ratio, comment_to_view_ratio — each float | None. |
engagement_percentiles |
object | The same three ratios plus overall, each ranked into int (0–100) | None against the niche distribution. |
sample_window_days |
int |
Date span (in days) of the fetched sample, so callers can weight the signal against the channel's posting cadence. |
title_patterns |
object | Aggregate stats: mean_length_chars, mean_length_words, share_with_question_mark, share_with_number, median_uppercase_ratio, share_with_emoji. Each float | None (or int | None for the length fields). |
niche
| Field | Type | Description |
|---|---|---|
slug |
str |
Canonical niche slug. |
name |
str |
Human-readable niche name. |
match_score |
float |
Cosine similarity of the channel to the niche centroid. |
match_strength |
"strong" | "moderate" | "weak" |
Categorized confidence. |
engagement |
object | Three ratio-keyed distribution objects (view_to_sub_ratio, like_to_view_ratio, comment_to_view_ratio). Each carries p10, p25, p50, p75, p90, p95 — float | None. |
title_patterns |
object | Same fields as channel.title_patterns plus common_niche_phrases: list of {phrase: str, frequency: float | None, used_by_channel: bool}. |
exemplar_channels |
list (up to 5) |
{title: str | None, subscriber_count: int | None}. |
Cost: 5 credits per call.
client.benchmark_video(...) — single video vs. niche
Benchmark one YouTube video against its niche distribution.
result = client.benchmark_video(
url="https://www.youtube.com/watch?v=dQw4w9WgXcQ",
)
print(result.video.engagement_percentiles.overall)
print(result.video.title_patterns.fits_niche_patterns)
Parameters
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
url |
str |
yes | — | YouTube video URL, max 1024 chars. Accepts youtube.com/watch?v=…, youtu.be/…, youtube.com/shorts/…, youtube.com/embed/…. |
Returns BenchmarkVideoResponse — two top-level objects, video and niche.
video
| Field | Type | Description |
|---|---|---|
video_id |
str |
11-character YouTube video ID. |
title |
str |
Video title. May be blank. |
channel_title |
str |
Owning channel's display name. May be blank. |
published_at |
datetime |
UTC publish timestamp. |
view_count |
int |
Public view count at fetch time. |
like_count |
int | None |
None when likes are hidden on the video. |
comment_count |
int | None |
None when comments are disabled. |
engagement |
object | view_to_sub_ratio, like_to_view_ratio, comment_to_view_ratio — each float | None. |
engagement_percentiles |
object | Same three ratios plus overall, each int (0–100) | None. |
title_patterns |
object | length_chars (int | None), has_question (bool), has_number (bool), has_emoji (bool), fits_niche_patterns (bool). |
niche
Same shape as benchmark_channel's niche — slug, name, match_score,
match_strength, ratio-keyed engagement distributions, title_patterns with
common_niche_phrases. The used_by_channel flag on each common phrase here
means "did this single video's title use the phrase"; the serializer field name
stays the same across both benchmark endpoints.
Cost: 3 credits per call.
client.niches() — list catalogued niches
List every catalogued YouTube niche for the active research run. Use the
slug values as the niche argument to client.content_gaps(...).
result = client.niches()
for n in result.niches:
print(n.slug, n.gap_count)
Parameters: none.
Returns NicheListResponse
| Field | Type | Description |
|---|---|---|
niches |
list[NicheSummary] |
Flat array of catalogued niches. |
Each NicheSummary:
| Field | Type | Description |
|---|---|---|
slug |
str |
Canonical niche slug. |
name |
str |
Human-readable name. |
gap_count |
int |
Number of content gaps catalogued for that niche in the active run, for the default gap types. 0 means the niche exists in the taxonomy but no gaps have been ranked yet. |
Cost: 1 credit per call.
client.content_gaps(...) — ranked content opportunities
Return ranked content opportunities (gaps) for a YouTube niche.
result = client.content_gaps(
niche="python_programming_tutorials",
gap_type=["underserved", "stale"], # or just "underserved"
limit=10,
min_score=60,
)
for gap in result.gaps:
print(gap.opportunity_score, gap.canonical_title)
Parameters
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
niche |
str |
yes | — | Slug from client.niches(), max 100 chars. |
gap_type |
str | list[str] | None |
no | None (server defaults to ["underserved", "stale"]) |
One of "underserved", "stale", "competitive", or a list of those. "competitive" is opt-in (omit from the list to exclude it). |
limit |
int | None |
no | None (server default: 20) |
1–50. |
min_score |
int | None |
no | None (server default: 0) |
Minimum opportunity_score (0–100) to include. |
Returns ContentGapsResponse
| Field | Type | Description |
|---|---|---|
niche |
object | {slug: str, name: str}. |
gaps |
list[ContentGapsItem] |
Ranked gaps (see below). |
Each gap (ContentGapsItem):
| Field | Type | Description |
|---|---|---|
canonical_title |
str |
Suggested canonical title for the gap. |
opportunity_score |
int (0–100) |
Composite ranking score. |
gap_type |
"underserved" | "stale" | "competitive" |
Which category the gap falls into. |
components |
object | {demand: float, supply: float, recency: float} — the three signals that make up opportunity_score. |
explanation |
str |
Short rationale for why this gap exists. May be blank. |
suggested_angles |
list[str] |
Concrete video angles the creator could take. |
evidence |
object | Supporting signals. See below. |
Nested evidence:
| Field | Type | Description |
|---|---|---|
newest_quality_video_age_days |
int | None |
Age (in days) of the newest high-quality video in this gap; None if no qualifying video exists. |
trends_appearance_count |
int |
How many times this topic appeared in trends signals. |
autocomplete_rank |
int | None |
YouTube autocomplete rank, if observed. |
residual_outlier_count |
int |
Number of outlier signals contributing to the score. |
top_competitors |
list (up to 5) |
{title: str, channel_title: str, subscriber_count: int | None, view_count: int, age_days: int, published_at: datetime}. |
related_queries |
list[str] (up to 5) |
Adjacent search queries. |
Cost: 5 credits per call.
client.me() — account info
Return account info and plan details for the authenticated API key. Free to call.
me = client.me()
print(me.email, me.plan.name, me.credits_remaining)
Parameters: none.
Returns MeResponse
| Field | Type | Description |
|---|---|---|
email |
str |
Account email. |
full_name |
str |
May be blank if the user hasn't set it. |
plan |
Plan (nested) |
{name: str, slug: str, monthly_credits: int, rate_limit_per_minute: int}. |
credits_remaining |
int |
Credits left in the current billing period. |
period_end |
datetime |
When the current billing period ends and credits reset. |
Cost: free (no credits deducted).
client.usage() — credit balance + recent activity
Return the credit balance and recent API activity for the authenticated API key. Free to call.
usage = client.usage()
print(usage.credits_remaining, "/", usage.credits_total)
for entry in usage.recent_activity:
print(entry.created_at, entry.endpoint, entry.delta)
Parameters: none.
Returns UsageResponse
| Field | Type | Description |
|---|---|---|
credits_remaining |
int |
Credits left in the current billing period. |
credits_total |
int |
Total credits for the current plan (resets at period_end). |
period_end |
datetime |
When the current billing period ends. |
recent_activity |
list[CreditLedgerEntry] |
Up to 20 most-recent credit-ledger entries. |
Each CreditLedgerEntry:
| Field | Type | Description |
|---|---|---|
delta |
int |
Signed change. Negative for API charges. |
balance_after |
int |
Credit balance immediately after this entry. |
reason |
str |
Free-form reason (e.g. "api_call", "monthly_reset"). |
endpoint |
str |
API endpoint path that triggered the entry. May be blank for non-API entries. |
created_at |
datetime |
UTC timestamp. |
Cost: free (no credits deducted).
Errors
The SDK raises typed exceptions for each documented status code:
| Status | Exception | Notes |
|---|---|---|
| 400 | BadRequestError |
Validation failed. |
| 401 | AuthenticationError |
Missing, invalid, or expired API key. |
| 402 | InsufficientCreditsError |
Account is out of credits. |
| 429 | RateLimitError |
Too many requests. retry_after (int seconds). |
| 503 | ServiceUnavailableError |
Scoring service down. retry_after (int seconds). |
| 5xx | ServerError |
Other server-side failure. |
| network | NetworkError |
DNS, connection refused, TLS, timeout. |
All API errors inherit BrightBeanAPIError. Network failures raise
NetworkError. Both inherit BrightBeanError for catch-all handling.
Retries
Idempotent GETs (me(), usage()) retry automatically on 5xx and network
errors with exponential backoff + jitter. Tune via max_retries:
client = BrightBean(api_key="bb_...", max_retries=5)
POST endpoints and paid GETs are never auto-retried because the server may
have charged credits before the network response was lost. Catch
RateLimitError or ServiceUnavailableError and back off using retry_after
if you need retry semantics.
Configuration
BrightBean(
api_key="bb_...", # required
base_url="https://api.brightbean.xyz",
timeout=30.0, # seconds, or httpx.Timeout(...)
max_retries=3, # idempotent GETs only
user_agent=None, # override the default UA string
)
base_url defaults to https://api.brightbean.xyz. Override for staging or
local testing.
Development
pip install -e .[dev]
openapi-python-client generate \
--path ../../openapi.yaml \
--config generator.yaml \
--meta=none \
--output-path ./brightbean/_generated \
--overwrite
pytest && ruff check . && mypy brightbean
The brightbean/_generated/ directory is regenerated from the upstream
OpenAPI spec. The hand-written facade (brightbean/_client.py,
brightbean/errors.py, brightbean/__init__.py) wraps it to give the
ergonomic BrightBean import shape.
Metadata
Release files for brightbean 1.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| brightbean-1.0.0.tar.gz | 38.4 kB | With provenance | Details for brightbean-1.0.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| brightbean-1.0.0-py3-none-any.whl | Python 3 | none | any | Details for brightbean-1.0.0-py3-none-any.whl |
Total release size: 119.1 kB
Release files / brightbean-1.0.0.tar.gz
| Download URL | brightbean-1.0.0.tar.gz |
|---|---|
| Size | 38.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
9c5a2714de5a4f42d424a528bde1bbe8e376a32b98bb716cde883ad57fc9ecf9
|
|
BLAKE2b-256 checksum How to use checksums |
6f675d3411fba07a4daeff135288792dbe3253550835e3574cb7560e8eb75e38
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on May 15, 2026.
Transparency logRelease files / brightbean-1.0.0-py3-none-any.whl
| Download URL | brightbean-1.0.0-py3-none-any.whl |
|---|---|
| Size | 80.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
1662bb1bbacceeee7e0a9980e7084f553c90e6f44942fd6412732a77cc1b34f8
|
|
BLAKE2b-256 checksum How to use checksums |
1415215c43a95420515d2681bbab03009a02caf07980890f71d3e8da659e22a7
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on May 15, 2026.
Transparency logSecurity for brightbean 1.0.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for brightbean-1.0.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on May 15, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.