# SonicAlpha API *Base URL `https://api.sonicalpha.ai` · Machine-readable schema: `/openapi.json`* In long-form podcasts, investors, analysts, and operators say what they actually think — often before it shows up anywhere else. At SonicAlpha, we identify the episodes that are relevant to public-market investors and process them into structured, citable context for AI agents. --- ## 1. Getting started ```bash export SONICALPHA_KEY="sk_..." ``` Every request authenticates with `Authorization: Bearer $SONICALPHA_KEY` (or `X-API-Key`). Keys are server-side credentials. Test your access with one call — hand the API a research question: ```bash curl -s https://api.sonicalpha.ai/v1/insights/ask \ -H "Authorization: Bearer $SONICALPHA_KEY" -H "Content-Type: application/json" \ -d '{"query": "Who do investors think captures the value from AV adoption: autonomous fleet operators or platforms like Uber?"}' ``` In ~10–30 seconds you get back an agent-curated evidence set: `sources` — ranked insights with speaker, role, companies, sentiment, publish time, and audio pointers. If you got a list of insights about Uber, Waymo, and Tesla with named speakers, your key works. --- ## 2. Two ways to consume this corpus **Agentic search (`POST /v1/insights/ask`).** Send a research question; our agent decomposes it into sub-queries, runs them against the corpus, and curates the evidence. It resolves sources by name — "what has *BG2* said about OpenAI?", "on *Invest Like the Best*…" — scoping retrieval to that show (across all of its feeds), and finds the episodes where a topic was actually discussed before reading them. By default the response is **evidence-only** — `sources` with `answer: null`, ~10–30s — the natural mode when you're combining our evidence with other data in your own generation. Pass `"answer": true` to also get a written, cited answer (every `[n]` maps to a source object); it adds ~15–25s, so expect ~25–45s end-to-end. `POST /v1/insights/ask/stream` delivers the same over SSE — sub-query events as retrieval progresses, then answer deltas when the written answer is on. **Individual search (`POST /v1/insights/search`).** If your platform has its own decomposer and orchestrator, this is your endpoint: deterministic, filterable, fast (~1s filter-only; semantic queries take a few seconds — they include a relevance rerank), and every parameter is a hard guarantee rather than an agent's judgment call. Your agents compose the sub-queries; you own ranking fusion and synthesis. --- ## 3. The data model There are three entities. A **podcast** is a show; it contains **episodes**; each episode yields **insights**. Every insight carries its `episode_id` and `podcast_id`, and every id is resolvable through the API — no dead ends. One naming rule everywhere: an entity's **own** fields are bare (`id`, `title`, `text`, `name`); fields that point at **another** entity are prefixed (`episode_id`, `podcast_name`, `episode_audio_url`). Most integrations start with insights: they're the objects you search, cite, and ground — so we take the entities in that order. ### Insight — the atomic unit One discrete, investor-relevant claim extracted from a conversation: ```json { "id": "in-q0c1xa53", // stable id — GET /v1/insights/{id}, or search's ids filter "title": "Uber at 17x Forward Earnings Is Mispriced Because AV Fleet Value Is in the App, Not the Cars", "text": "One-paragraph extracted claim.", "type": "thesis", // thesis | observation | data_point "speaker_name": "Josh Brown", // null when unresolved "speaker_title": "CEO, Ritholtz Wealth Management", "speaker_role": "investor", // operator | investor | researcher | commentator | government | unknown "mentions": [ // tracked public companies on this insight {"company": "Uber Technologies", "ticker": "UBER", "sentiment": "bullish"}, {"company": "Waymo", "ticker": "GOOGL", "sentiment": "neutral"} ], "published_at": "2026-05-28T17:27:59Z", // episode publish time — the point-in-time anchor "start_seconds": 2509.26, // position of the claim inside the episode audio "end_seconds": 2698.54, "quote": "...", // verbatim transcript, when include_quotes: true "episode_id": "hnwhkobq", "episode_title": "Brad Gerstner on the Market, Tech and the Flood of Mega IPOs", "episode_audio_url": "https://...mp3", // seek to start_seconds to play the claim "episode_url": "https://...", // publisher's episode page "episode_image_url": "https://...", "podcast_id": "5rbu6953", "podcast_name": "Halftime Report", "podcast_image_url": "https://..." } ``` Field notes: - Every insight object is self-contained: the speaker, the companies, the episode and show identity, and the audio pointer are all on it — displaying, citing, or playing an insight never requires a follow-up call. - `text` is our extracted paraphrase of the claim; `quote` is the verbatim transcript passage it came from. Cite `text`, ground with `quote`. - `type`: `thesis` = forward-looking argument, `observation` = qualitative read on the present, `data_point` = a concrete number. - `speaker_role` is what the speaker does professionally: `operator` (runs a business), `investor` (deploys capital), `researcher` (sell-side/academic), `commentator` (journalist/host), `government`. "CEO of a VC firm" = investor, not operator. `unknown` when the profession can't be determined; the field is always present. - `mentions[].ticker` is the canonical symbol for a tracked public company. Private and untracked names still appear in the insight text and are reachable through semantic queries. - `start_seconds`/`end_seconds` are seconds into `episode_audio_url` — pass them straight to a player or a `#t=` fragment. - Results are ordered most-relevant first (newest first when no query); ordering is the relevance contract. - Timestamps are ISO 8601 UTC with a `Z` suffix; request dates accept bare days, `date_to` inclusive. - `published_at` is when the episode aired. New insights typically become searchable within hours of airing, so a "published today" query run at breakfast will fill in over the day. ### Episode One recording of a show — the conversation the insights were extracted from. An episode exists in the API only if it produced at least one searchable insight. ```json { "id": "hnwhkobq", "title": "Brad Gerstner on the Market, Tech and the Flood of Mega IPOs", "published_at": "2026-05-28T17:27:59Z", "duration_seconds": 3141, "insight_count": 26, "episode_audio_url": "https://...mp3", "episode_url": "https://...", // publisher's episode page "episode_image_url": "https://...", "podcast_id": "5rbu6953", "podcast_name": "Halftime Report" } ``` Field notes: - `id` is the join key: every insight from this conversation carries it as `episode_id`, and search accepts it in the `episode_ids` filter to scope retrieval to one conversation. - `published_at` is the point-in-time anchor its insights inherit — an insight is knowable from the moment its episode aired. - `insight_count` is how much searchable content the conversation yielded — a 26-insight episode is a dense discussion, a 2-insight one a passing segment. - `episode_audio_url` is the full recording; insights carry the same URL plus `start_seconds`/`end_seconds`, so any claim can be played at the moment it was said. ### Podcast The show — the ongoing feed episodes belong to. One directory entry per show: mirror and regional feeds are merged, and `podcast_ids` lists every feed id the show publishes under. A podcast exists in the API only if it has searchable insights; absence means *not covered*, not "not found." ```json { "id": "idl2iow9", "podcast_ids": ["idl2iow9"], "name": "The Real Eisman Playbook", "description": "...", "image_url": "https://...", "insight_count": 478, "episode_count": 34, "latest_episode_at": "2026-08-17T16:06:00Z" } ``` Field notes: - `id` scopes work to one voice: search accepts it in the `podcast_ids` filter, so "everything this show has said about X" is one query. Any id in `podcast_ids` works too — the filter covers the whole show whichever feed id you hold. - `insight_count` / `episode_count` together read as coverage depth — how much of this show's thinking the corpus holds. - `latest_episode_at` is the freshness signal: how recently this show last produced searchable content. --- ## 4. The core endpoints ### POST /v1/insights/ask · /v1/insights/ask/stream `query` is free-form natural language, 8–1000 characters. Two optional flags: `answer` (see §2 for behavior and timings) and `include_quotes`, which attaches the verbatim transcript passage to each source so the agentic path is verbatim-grounded like search. The agent picks its own filters from the question's wording: name a show or an episode and it scopes to that source, name an audience ("what do *investors* think…") and it filters by speaker role, give a window ("since June") and it dates the search. The stream variant emits SSE events: `stage`, `sub_query`, `sub_query_result`, `source_resolved`, `retrieval_done`, `sources`, `answer_delta` (when the answer is on), `done`, `ping`, `error`. Each event is a `data:` line whose JSON carries a `type` field — there are no named SSE `event:` frames, so parse the JSON rather than using `addEventListener("stage", …)`. `done` repeats the complete answer — treat it as the canonical text; `answer_delta` events are for display. Evidence only (default, ~10–30s): ```json {"query": "Where do investors split on hyperscaler ASICs versus NVIDIA's GPUs — and who's making the strongest case on each side?"} ``` Full written answer (~25–45s): ```json {"query": "Where do investors split on hyperscaler ASICs versus NVIDIA's GPUs — and who's making the strongest case on each side?", "answer": true} ``` Scoped to a source by name and tracked over time — no ids needed: ```json {"query": "How have the All-In podcast's views on AI capex evolved over time?"} ``` Every response carries `note_type` and `note`: null when evidence was found; `"source_not_covered"` when a named show or episode isn't in the corpus (absence means not covered, not that it said nothing); and `"no_evidence"` when covered ground simply had nothing on the question. Branch on `note_type` — *not covered* is never a silent blank. On the stream, the pair rides on the `done` event. Responses also include `took_ms` and a small timing `debug` object. ### POST /v1/insights/search Search returns the insights that match exactly what you specify — the same request always returns the same results. Every parameter is optional; combine them freely. - `query` — natural language, up to 2000 chars. Phrase it as a claim or question, not keywords. Omit it to browse newest-first with just filters. - `speaker_roles` — one or more of `operator`, `investor`, `researcher`, `commentator`, `government`, `unknown`. - `tickers` — canonical symbols, e.g. `["NVDA", "UBER"]`. - `sentiments` — one or more of `bullish`, `bearish`, `neutral`. - `mentions` — precise (ticker, sentiment) pairs, e.g. `[{"ticker": "NVDA", "sentiment": "bearish"}]`. Each pair must hold on a single mention; omit `sentiment` for a ticker-only pair. - `types` — one or more of `thesis`, `observation`, `data_point`. - `date_from` / `date_to` — ISO dates; when it was said (`published_at`). - `podcast_ids` / `episode_ids` / `ids` — scope to a show, an episode, or specific insights. Any feed id of a show covers the whole show. - `top_k` — results per page, 1–25. - `cursor` — the previous response's `next_cursor`, sent **alone**, for the next page. - `include_quotes` — `true` attaches the verbatim transcript passage to each insight (~+1s per 25). How sentiment filtering works — three rules: 1. `sentiments` together with `tickers` matches as a pair on one mention: "bearish *about NVDA*", never bearish-about-something-else while mentioning NVDA. 2. `sentiments` alone means "has that sentiment about *anything*" — a browse of bearish takes, not a claim about a specific company. 3. Lists combine as any-of. For different stances on different companies — bullish NVDA *or* bearish AMD in one call — use `mentions` pairs: ```json { "query": "AI chip competition", "mentions": [ {"ticker": "NVDA", "sentiment": "bullish"}, {"ticker": "AMD", "sentiment": "bearish"} ] } ``` Evidence pack — bearish takes on NVDA from professional investors, with verbatim grounding: ```json { "query": "NVIDIA downside risks", "tickers": ["NVDA"], "sentiments": ["bearish"], "speaker_roles": ["investor"], "date_from": "2026-07-18", "include_quotes": true } ``` Follow one voice — everything a show has said on a topic (`idl2iow9` is The Real Eisman Playbook, Steve Eisman's show — resolved via the directory below): ```json {"query": "regional banks credit risk", "podcast_ids": ["idl2iow9"]} ``` The response envelope is always `{"results": [...insights...], "next_cursor", "pagination_end", "took_ms"}` — one page per call, with `pagination_end` null while more pages exist. For the next page, send `next_cursor` back by itself: ```json {"cursor": "cznqOucaYJWs9XCc..."} ``` Pages never overlap or reshuffle; cursors expire after ~15 minutes. When `next_cursor` is null, `pagination_end` says why: `end_of_results` (a filter-only browse fully paged the matching window) or `ranking_exhausted` (a relevance query reached its quality boundary at ~150 results — narrow the query if you need more depth). ### GET /v1/insights/{id} Resolve one insight id directly — the `id` every hit carries. Returns the full insight object. Two optional query params: `include_quotes=true` attaches the verbatim transcript passage, and `include_similar=true` embeds up to 10 related insights under `similar` as full insight objects, ordered most-similar first (ordering is the similarity contract; there are no scores). Store an id today, re-hydrate it any time: ```bash curl -s "https://api.sonicalpha.ai/v1/insights/in-q0c1xa53?include_quotes=true&include_similar=true" \ -H "Authorization: Bearer $SONICALPHA_KEY" ``` ### GET /v1/insights/{id}/quote The verbatim grounding for one insight: the transcript passage it was extracted from, as rendered text (`quote`) plus structured `segments[]` with per-segment timestamps — the surrounding conversational turns, not just the clipped sentence. Same content `include_quotes` attaches inline; use this route when you want the full context for one citation. ### Podcasts — GET /v1/podcasts · /{id} · /{id}/episodes Resolve show names to the ids search filters on. `/v1/podcasts?query=` is typo-tolerant name lookup (`eisamn`, `oshaughnessy` both resolve); omit `query` to browse by coverage. One entry per show — mirror feeds are merged. `/v1/podcasts/{id}/episodes` lists one show's episodes, newest first. All lists take `limit` + `cursor` and return `next_cursor`. Find a show, then scope search to it: ```bash curl -s "https://api.sonicalpha.ai/v1/podcasts?query=eisman" \ -H "Authorization: Bearer $SONICALPHA_KEY" # → id "idl2iow9" → search {"query": "...", "podcast_ids": ["idl2iow9"]} ``` ### Episodes — GET /v1/episodes · /{id} `/v1/episodes?query=` searches episode titles, scopable with `podcast_id` and a date window; `/v1/episodes/{id}` resolves one episode id directly. One show's recent episodes, newest first: ```bash curl -s "https://api.sonicalpha.ai/v1/episodes?podcast_id=5rbu6953&date_from=2026-08-01" \ -H "Authorization: Bearer $SONICALPHA_KEY" ``` --- ## 5. Essentials - **Errors**: one envelope — `{"error": {"type", "message"}}` — with standard HTTP codes; 429s include `Retry-After`. Unknown or outdated parameter names are a 400 that names the supported ones — nothing is silently ignored. - **Rate limits**: every response carries live `X-RateLimit-*` headers — they, plus `Retry-After` on 429s, are the reference. Both per-minute limits and per-day quotas exist; on a 429, sleep exactly `Retry-After` seconds (it can be long when a daily quota is exhausted — quotas reset at 00:00 UTC), don't retry-loop against the wall. - **Client**: send a `User-Agent` header; our CDN blocks the default Python `urllib` agent (requests/httpx/node defaults all pass). - **Versioning**: the surface is `/v1`; fields are added, never renamed or removed. That's the entire surface. If you encounter any issues, message support@sonicalpha.ai — we will get back to you asap.