Public REST API for querying political speech, transcript segments, speakers, topics, usage, API keys, and saved analyses.
Base URL: https://api.meshapi.app
Interactive reference: https://api.meshapi.app/docs
Use this as the source for public API documentation on the Mesh frontend.
curl "https://api.meshapi.app/v1/events?limit=10&has_transcript=true" \
-H "X-API-Key: mesh_your_key_here"
All /v1/* endpoints require authentication except GET /health.
Pass your API key in the X-API-Key header:
X-API-Key: mesh_your_key_here
Create and manage API keys from your account dashboard on the Mesh web app.
| Pool | What counts | Reset |
|---|---|---|
| Transcript pulls | GET /v1/events/{event_id}/segments, deduped by event per UTC day | Daily and monthly |
| Searches | GET /v1/segments/search, GET /v1/search/events | Daily |
| Other endpoints | Metadata, usage, topics, speakers, saved analyses | Unmetered |
| Plan | Daily transcript pulls | Daily searches | Monthly transcript cap |
|---|---|---|---|
| Free | 3/day | 10/day | 3/month |
| Trial (14 days) | 5/day | 50/day | 150/month |
| Pro | 15/day | 150/day | 450/month |
| Max | 30/day | 300/day | 900/month |
| Enterprise | Contract-defined | Contract-defined | Contract-defined |
Caps are account-wide. All of your API keys share the same daily and monthly budget. Caps reset at 00:00 UTC daily and on the first of each month.
In addition to quota caps, all API-key requests are subject to a per-key request-per-minute rate limit:
| Plan | Requests per minute |
|---|---|
| Free / Trial / Pro / Max | 60 |
| Enterprise | 300 |
JWT (web) callers have a separate 120 req/min limit per user. Rate-limited requests return 429 with a Retry-After header.
List endpoints use a paginated envelope:
{
"data": [],
"next_cursor": "dGVzdA==",
"count": 50
}
Single-resource endpoints return the object directly. Errors return:
{ "detail": "Message describing the error" }
| Status | Meaning |
|---|---|
| 400 | Invalid parameter or request body |
| 401 | Missing, invalid, or expired credentials |
| 403 | Account disabled, resource outside key scope, or Free-plan event not unlocked |
| 404 | Resource not found |
| 409 | Conflict, such as max active keys reached |
| 422 | Validation error |
| 429 | Daily/monthly quota exceeded |
Quota errors include useful reset headers when applicable:
Retry-AfterX-Pull-Budget-ResetSpeaker identity has two layers:
Fox World Cup Announcers. Use primary_speaker_id on event endpoints.segment_speaker_id on segment endpoints and segment search.The legacy speaker_id parameter remains supported for compatibility, but its meaning depends on the endpoint: on GET /v1/events it aliases primary_speaker_id; on segment endpoints it aliases segment_speaker_id.
GET /health
Public health check.
{ "status": "ok" }
GET /v1/events
List events. Use has_transcript=true to browse events with transcript data.
Common query params:
| Param | Type | Description |
|---|---|---|
q | string | Optional full-text search on event title |
event_type | string | Event type such as remarks, press_conference, rally, interview |
primary_speaker_id | UUID | Filter by event attribution / primary speaker |
primary_speaker_ids | string | Comma-separated UUIDs (max 20). Returns events where ANY listed speaker is primary. |
segment_speaker_id | UUID | Filter to events where this resolved speaker appears in transcript segments |
segment_speaker_ids | string | Comma-separated resolved segment speaker UUIDs |
speaker_id | UUID | Legacy alias for primary_speaker_id |
speaker_ids | string | Legacy alias for primary_speaker_ids |
topic_slug | string | Filter by topic |
city | string | Partial city match |
state_code | string | Exact state code |
start_date | date | Events on or after this date |
end_date | date | Events on or before this date |
updated_after | datetime | Incremental sync watermark. Returns only events with updated_at > updated_after. |
sort | string | event_date_desc (default), event_date_asc, speaker_asc, event_title_asc, relevance |
include | string | Optional expansions. Use include=segment_speakers to include resolved segment speaker summaries on event-list rows. |
limit | int | Page size, default 50 |
cursor | string | Cursor from previous response |
Event rows include updated_at (RFC 3339 timestamp). Use this field to checkpoint incremental syncs.
When updated_after is omitted, pagination uses sort-aware opaque keyset cursors. A cursor is valid only with the same filters and sort value; changing either returns 422.
When updated_after is provided, the endpoint switches to stable keyset pagination ordered by (updated_at, event_id) ascending. Reuse the same updated_after value for every page in that sync run.
sort is not supported with updated_after sync mode.
GET /v1/events/{event_id}
Get one event. The response includes primary_speaker, segment_speakers, and speaker_summary so clients can distinguish event attribution from who actually spoke.
GET /v1/events/{event_id}/segments
Get transcript segments for one event.
Common query params:
| Param | Type | Description |
|---|---|---|
limit | int | Page size |
cursor | string | Cursor from previous response |
segment_speaker_id | UUID | Filter to one resolved segment speaker |
speaker_id | UUID | Legacy alias for segment_speaker_id |
topic_slug | string | Filter to one topic |
unlock | boolean | Free-plan users can set unlock=true to spend one daily unlock |
Free-plan users may receive a preview response with unlock_required: true. Paid users and already-unlocked events return full access with unlock_required: false.
POST /v1/events/batch-segments
Pull transcripts for multiple events in a single request. Designed for bulk data collection workflows. Requires an API key (not available via JWT).
Request body:
{
"event_ids": ["uuid1", "uuid2", "uuid3"],
"limit_per_event": 500
}
| Field | Type | Description |
|---|---|---|
event_ids | UUID[] | Event UUIDs to pull transcripts for. Max 25 (enterprise) or 10 (other plans). |
limit_per_event | int | Max segments per event, 1–500, default 500. |
Response:
{
"results": [
{
"event_id": "uuid1",
"segments": [...],
"next_cursor": null,
"count": 142
}
],
"skipped": [
{ "event_id": "uuid3", "reason": "daily_cap_exceeded" }
],
"pulls_used": 2,
"pulls_remaining_daily": 13,
"pulls_remaining_monthly": 448
}
Each unique event counts as one transcript pull, deduped by event per UTC day. Events that would exceed the daily or monthly cap are returned in skipped with the reason — the request never 429s partially. If an event needs more than limit_per_event segments, the entry includes a next_cursor that can be passed to GET /v1/events/{event_id}/segments?cursor=...&limit=500 to continue paginating that event.
Not available on the Free plan. Possible skip reasons: daily_cap_exceeded, monthly_cap_exceeded, not_found, speaker_not_in_scope, before_scope_date_range, after_scope_date_range.
GET /v1/events/{event_id}/topics
List topics attached to an event.
GET /v1/events/{event_id}/resolutions
List Kalshi market resolutions linked to an event when available.
GET /v1/segments/search
Search transcript segments across events.
Common query params:
| Param | Type | Description |
|---|---|---|
q | string | Full-text segment search |
segment_speaker_id | UUID | Filter by resolved segment speaker |
primary_speaker_id | UUID | Constrain search to events attributed to this primary speaker |
speaker_expand | string | Set to children to expand segment_speaker_id through speaker relationships |
speaker_id | UUID | Legacy alias for segment_speaker_id |
topic_slug | string | Filter by topic |
event_type | string | Filter by event type |
start_date | date | Event start date lower bound |
end_date | date | Event start date upper bound |
sort | string | relevance (default), event_date_desc, event_date_asc, speaker_asc, event_title_asc |
limit | int | Page size |
cursor | string | Cursor from previous response |
Free-plan search returns only preview segments plus events you have unlocked that day. Paid plans search the full corpus.
Segment search cursors are opaque keyset snapshots. A cursor is valid only with the same q + filter + sort values; changing any of those returns 422.
GET /v1/search/events
Unified event-level search for frontend "All" mode. Combines title matches and transcript-segment matches, deduplicates by event_id, applies sort server-side, then paginates with one keyset cursor.
Common query params:
| Param | Type | Description |
|---|---|---|
q | string | Optional search text used for title and transcript matching |
event_id | UUID | Optional single-event scope |
event_type | string | Filter by event type |
primary_speaker_id | UUID | Filter by event primary speaker |
segment_speaker_id | UUID | Filter by resolved segment speaker |
speaker_expand | string | Set to children to expand speaker/group filters |
speaker_id | UUID | Legacy alias for segment_speaker_id |
topic_slug | string | Filter by topic slug |
start_date | date | Event start date lower bound |
end_date | date | Event start date upper bound |
has_transcript | boolean | true returns events with transcript data only; use this for transcript-backed All-mode results |
sort | string | relevance (default), event_date_desc, event_date_asc, speaker_asc, event_title_asc |
limit | int | Page size |
cursor | string | Cursor from previous response |
Speaker filtering uses union semantics: primary_speaker_id selects the event/title branch by event attribution, while segment_speaker_id selects the transcript-match branch by resolved speaker (with speaker_expand=children applied to that resolved-speaker branch). Supplying both does not require an event to satisfy both conditions.
Each result row includes event card fields plus match metadata:
primary_speaker_id and primary_speaker_name identify the event attribution speaker (not per-segment resolved speaker)title_matchmatching_segment_count (visibility-filtered for Free plan)best_snippet, best_snippet_start_time_ms, best_snippet_idx, best_snippet_segment_idtopics: string[] of canonical event-topic slugsspeakers: string[] of canonical event-attribution speaker namesmatched_segment_speakers: string[] of canonical resolved-speaker names on visible transcript matchesnum_questions, starts_at, event_typeEvent-search cursors are opaque keyset snapshots. A cursor is valid only with the same q + filters + sort; reusing it with different parameters returns 422.
Free-plan visibility restrictions are applied before transcript counts/snippets are computed, so restricted transcript rows do not leak through match metadata.
GET /v1/speakers
List speakers.
GET /v1/speakers/{speaker_id}
Get one speaker.
GET /v1/speakers/{speaker_id}/events
List events where the speaker is primary or appears in resolved transcript segments.
GET /v1/speakers/{speaker_id}/children
List direct child speakers for an umbrella/group speaker.
GET /v1/speakers/{speaker_id}/parents
List umbrella/group speakers that include this speaker.
GET /v1/speakers/{speaker_id}/stats
Get speaker-level statistics when available.
GET /v1/topics
List topics. Add hierarchy=true to return parent/child structure.
GET /v1/topics/{slug}
Get one topic.
GET /v1/topics/{slug}/segments
Browse transcript segments attached to one topic.
GET /v1/usage
Return your current plan, daily transcript pulls, daily searches, monthly transcript usage, and trial metadata. Does not consume quota.
Important fields:
| Field | Description |
|---|---|
plan | Current effective plan |
daily_transcript_pulls_used / daily_transcript_pulls_cap | Transcript pull usage today |
daily_searches_used / daily_searches_cap | Search usage today |
monthly_transcript_pulls_used / monthly_transcript_pulls_cap | Monthly transcript ceiling |
trial_started_at / trial_ends_at | Trial metadata, if applicable |
GET /v1/pulls
List transcripts the authenticated user has fetched and whether Mesh has newer transcript data available.
Common query params:
| Param | Type | Description |
|---|---|---|
limit | int | Page size |
offset | int | Offset pagination value |
api_key_id | UUID | Optional key-specific history filter |
Use has_updates to show stale transcript pulls in a user dashboard.
Manage your API keys from the Mesh web app. Keys can be created, listed, and revoked from your account dashboard.
The raw key value is shown once at creation. Store it securely. It cannot be retrieved again.
POST /v1/analyses/run
Run supported analysis jobs over the transcript corpus.
POST /v1/analyses/word-streaks
Run word-streak analysis.
GET /v1/analyses
List saved analyses.
POST /v1/analyses
Save an analysis configuration.
GET /v1/analyses/{analysis_id}
Get one saved analysis.
PUT /v1/analyses/{analysis_id}
Update a saved analysis.
DELETE /v1/analyses/{analysis_id}
Delete a saved analysis.
List all transcribed events
curl "https://api.meshapi.app/v1/events?has_transcript=true&limit=50" \
-H "X-API-Key: mesh_your_key_here"
Results are paginated. Pass the next_cursor value from the response as ?cursor=... to fetch the next page. Repeat until next_cursor is null.
Filter events by primary speaker
curl "https://api.meshapi.app/v1/events?primary_speaker_id=381194fe-4856-4945-9e49-9809be82e924&has_transcript=true" \
-H "X-API-Key: mesh_your_key_here"
Get speaker UUIDs from GET /v1/speakers. speaker_id still works here as a legacy alias, but new clients should use primary_speaker_id.
Filter events by segment speaker
curl "https://api.meshapi.app/v1/events?segment_speaker_id=381194fe-4856-4945-9e49-9809be82e924&has_transcript=true" \
-H "X-API-Key: mesh_your_key_here"
This returns events where that speaker appears in resolved transcript segments, even if the event's primary speaker is an umbrella/group speaker.
Filter events by topic
curl "https://api.meshapi.app/v1/events?topic_slug=nato&has_transcript=true" \
-H "X-API-Key: mesh_your_key_here"
Browse available topic slugs with GET /v1/topics.
Filter events by date range
curl "https://api.meshapi.app/v1/events?start_date=2026-01-01&end_date=2026-03-31&has_transcript=true" \
-H "X-API-Key: mesh_your_key_here"
Dates are ISO format. Both start_date and end_date are optional and can be used independently.
Combine multiple filters
curl "https://api.meshapi.app/v1/events?primary_speaker_id=381194fe-...&event_type=press_conference&start_date=2026-01-01&has_transcript=true" \
-H "X-API-Key: mesh_your_key_here"
All filters use AND logic. This returns only press conferences by that speaker from 2026 onward that have transcripts.
Incrementally sync newly changed events
curl "https://api.meshapi.app/v1/events?updated_after=2026-01-01T00:00:00Z&limit=200" \
-H "X-API-Key: mesh_your_key_here"
Store the highest updated_at you have fully processed as your next watermark.
For paginated incremental sync:
updated_after fixed across pages.next_cursor until it is null.event_id client-side within a run.If updated_after and cursor don’t match (for example, using a cursor from a different watermark), the API returns 422.
Pull a full transcript
curl "https://api.meshapi.app/v1/events/70d5bc85-cd9b-4670-94fb-6d5a7433b1d0/segments?limit=500" \
-H "X-API-Key: mesh_your_key_here"
Use limit=500 (the maximum) to minimize round trips. If next_cursor is not null, pass it as ?cursor=...&limit=500 to get the next page. Each call counts as one transcript pull for quota purposes, deduped by event per UTC day.
Pull segments by speaker within a transcript
curl "https://api.meshapi.app/v1/events/70d5bc85-.../segments?segment_speaker_id=381194fe-...&limit=500" \
-H "X-API-Key: mesh_your_key_here"
Returns only segments from that resolved speaker within the event. Useful for isolating one person's remarks in a multi-speaker transcript. speaker_id remains a legacy alias on segment endpoints.
Pull segments by topic within a transcript
curl "https://api.meshapi.app/v1/events/70d5bc85-.../segments?topic_slug=nato&limit=500" \
-H "X-API-Key: mesh_your_key_here"
Returns only segments tagged with that topic within the event.
Full-text search
curl "https://api.meshapi.app/v1/segments/search?q=tariffs&limit=50" \
-H "X-API-Key: mesh_your_key_here"
Multi-word queries use AND semantics: q=trade+tariffs matches segments containing both words. Each successful search call counts against your daily search quota.
Search within one segment speaker's transcripts
curl "https://api.meshapi.app/v1/segments/search?q=immigration&segment_speaker_id=381194fe-...&limit=50" \
-H "X-API-Key: mesh_your_key_here"
Combines full-text search with a resolved segment speaker filter.
Search inside an umbrella speaker's events
curl "https://api.meshapi.app/v1/segments/search?q=goal&primary_speaker_id=381194fe-...&limit=50" \
-H "X-API-Key: mesh_your_key_here"
This is the right filter when the selected speaker is an event collection or broadcast group, such as Fox World Cup Announcers.
Search within a topic
curl "https://api.meshapi.app/v1/segments/search?q=sanctions&topic_slug=nato&limit=50" \
-H "X-API-Key: mesh_your_key_here"
Returns segments that match the text query and are tagged with the specified topic.
Search within event type and date range
curl "https://api.meshapi.app/v1/segments/search?q=economy&event_type=remarks&start_date=2026-01-01&end_date=2026-06-01&limit=50" \
-H "X-API-Key: mesh_your_key_here"
Narrows search to a specific event type and time window. All search filters use AND logic.
Check quota usage
curl "https://api.meshapi.app/v1/usage" \
-H "X-API-Key: mesh_your_key_here"
Key fields to watch: daily_transcript_pulls_used vs daily_transcript_pulls_cap, daily_searches_used vs daily_searches_cap, and monthly_transcript_pulls_used vs monthly_transcript_pulls_cap. A _cap of null means unlimited. This endpoint never consumes quota.
Detect updated transcripts
curl "https://api.meshapi.app/v1/pulls?limit=100" \
-H "X-API-Key: mesh_your_key_here"
Returns every transcript you have previously pulled. Check the has_updates field on each entry: true means Mesh has newer transcript data since your last pull (re-diarized, speaker resolved, etc.). Re-fetch those transcripts to get the latest version. This endpoint never consumes quota.
These examples chain multiple endpoints together for real-world data collection tasks. They are written in Python for readability but the logic applies to any HTTP client. Enterprise users with contract-defined caps can adjust concurrency and batch sizes to match their quotas.
Bulk-pull all transcripts for a speaker (using batch endpoint)
Collect a speaker's event IDs, then use the batch endpoint to pull transcripts in chunks of up to 25 (enterprise) or 10 (other plans) per request.
import requests
BASE = "https://api.meshapi.app"
HEADERS = {"X-API-Key": "mesh_your_key_here"}
SPEAKER_ID = "381194fe-4856-4945-9e49-9809be82e924"
# 1. Collect every event ID for this speaker
event_ids = []
cursor = None
while True:
params = {
"speaker_id": SPEAKER_ID,
"has_transcript": "true",
"limit": 200,
}
if cursor:
params["cursor"] = cursor
resp = requests.get(f"{BASE}/v1/events", headers=HEADERS, params=params).json()
event_ids.extend(e["event_id"] for e in resp["data"])
cursor = resp.get("next_cursor")
if not cursor:
break
# 2. Batch-pull transcripts (25 at a time for enterprise, 10 for other plans)
BATCH_SIZE = 25
for i in range(0, len(event_ids), BATCH_SIZE):
batch = event_ids[i : i + BATCH_SIZE]
resp = requests.post(
f"{BASE}/v1/events/batch-segments",
headers=HEADERS,
json={"event_ids": batch, "limit_per_event": 500},
).json()
for entry in resp["results"]:
print(f"{entry['event_id']}: {entry['count']} segments")
for skip in resp["skipped"]:
print(f"Skipped {skip['event_id']}: {skip['reason']}")
if resp["pulls_remaining_daily"] == 0:
print("Daily cap reached — resume tomorrow")
break
Each unique event counts as one transcript pull per UTC day. Re-fetching the same event later in the day is free, so retries and incremental runs are safe. The batch response tells you exactly how many pulls remain so you can stop gracefully.
Pull transcripts for two speakers and merge by date
Use speaker_ids to fetch events from both speakers in a single paginated call, then batch-pull the transcripts.
SPEAKER_A = "381194fe-4856-4945-9e49-9809be82e924"
SPEAKER_B = "f47ac10b-58cc-4372-a567-0e02b2c3d479"
# 1. Fetch events for both speakers in one call using speaker_ids
events = []
cursor = None
while True:
params = {
"speaker_ids": f"{SPEAKER_A},{SPEAKER_B}",
"has_transcript": "true",
"limit": 200,
}
if cursor:
params["cursor"] = cursor
resp = requests.get(f"{BASE}/v1/events", headers=HEADERS, params=params).json()
events.extend(resp["data"])
cursor = resp.get("next_cursor")
if not cursor:
break
# Events come sorted by date (newest first) — already interleaved
event_ids = [e["event_id"] for e in events]
# 2. Batch-pull transcripts
BATCH_SIZE = 25
for i in range(0, len(event_ids), BATCH_SIZE):
batch = event_ids[i : i + BATCH_SIZE]
resp = requests.post(
f"{BASE}/v1/events/batch-segments",
headers=HEADERS,
json={"event_ids": batch, "limit_per_event": 500},
).json()
for entry in resp["results"]:
event = next(e for e in events if e["event_id"] == entry["event_id"])
speaker = event.get("primary_speaker_name", "Unknown")
print(f"[{speaker}] {event['canonical_title']}: {entry['count']} segments")
if resp["pulls_remaining_daily"] == 0:
break
Quota-aware bulk pulling
The batch endpoint returns pulls_remaining_daily and pulls_remaining_monthly in every response, so you can make cap-aware decisions without extra API calls. For finer control, check GET /v1/usage before starting.
def get_remaining_pulls():
usage = requests.get(f"{BASE}/v1/usage", headers=HEADERS).json()
daily_cap = usage["daily_transcript_pulls_cap"] # null = unlimited
monthly_cap = usage["monthly_transcript_pulls_cap"]
daily_left = (
daily_cap - usage["daily_transcript_pulls_used"]
if daily_cap is not None else float("inf")
)
monthly_left = (
monthly_cap - usage["monthly_transcript_pulls_used"]
if monthly_cap is not None else float("inf")
)
return min(daily_left, monthly_left)
budget = get_remaining_pulls()
print(f"Transcript pulls remaining: {budget}")
# Batch-pull only what fits in the remaining budget
BATCH_SIZE = 25
for i in range(0, len(event_ids), BATCH_SIZE):
batch = event_ids[i : i + BATCH_SIZE]
resp = requests.post(
f"{BASE}/v1/events/batch-segments",
headers=HEADERS,
json={"event_ids": batch},
).json()
for entry in resp["results"]:
print(f"{entry['event_id']}: {entry['count']} segments")
# The batch endpoint gracefully skips events over cap instead of 429-ing
if resp["skipped"]:
for skip in resp["skipped"]:
print(f"Skipped {skip['event_id']}: {skip['reason']}")
break
The usage endpoint never consumes quota. The batch endpoint never returns a 429 for partial over-cap — it processes what it can and skips the rest.
Build a topic corpus from search, then batch-pull full transcripts
Search for segments matching a topic across all speakers, collect the distinct event IDs from the results, then batch-pull the full transcript for each.
# 1. Search for all segments mentioning "tariffs" in press conferences
matched_event_ids = set()
cursor = None
while True:
params = {
"q": "tariffs",
"event_type": "press_conference",
"start_date": "2026-01-01",
"limit": 200,
}
if cursor:
params["cursor"] = cursor
resp = requests.get(
f"{BASE}/v1/segments/search", headers=HEADERS, params=params
).json()
for seg in resp["data"]:
matched_event_ids.add(seg["event_id"])
cursor = resp.get("next_cursor")
if not cursor:
break
print(f"Found segments in {len(matched_event_ids)} events")
# 2. Batch-pull the full transcript for each discovered event
event_list = list(matched_event_ids)
BATCH_SIZE = 25
for i in range(0, len(event_list), BATCH_SIZE):
batch = event_list[i : i + BATCH_SIZE]
resp = requests.post(
f"{BASE}/v1/events/batch-segments",
headers=HEADERS,
json={"event_ids": batch, "limit_per_event": 500},
).json()
for entry in resp["results"]:
print(f"{entry['event_id']}: {entry['count']} segments")
if resp["pulls_remaining_daily"] == 0:
break
Search calls count against the daily search quota, and each new event pulled counts against the transcript pull quota.
Detect and re-pull updated transcripts
Poll GET /v1/pulls to find transcripts that Mesh has re-processed since your last fetch, then batch-pull fresh copies.
# 1. Check for stale pulls
resp = requests.get(f"{BASE}/v1/pulls?limit=100", headers=HEADERS).json()
stale_ids = [p["event_id"] for p in resp["data"] if p.get("has_updates")]
print(f"{len(stale_ids)} transcripts have newer data")
# 2. Batch re-pull only the updated ones
BATCH_SIZE = 25
for i in range(0, len(stale_ids), BATCH_SIZE):
batch = stale_ids[i : i + BATCH_SIZE]
resp = requests.post(
f"{BASE}/v1/events/batch-segments",
headers=HEADERS,
json={"event_ids": batch, "limit_per_event": 500},
).json()
for entry in resp["results"]:
print(f"Refreshed {entry['event_id']}: {entry['count']} segments")
Re-pulling a previously fetched event on the same UTC day does not cost an additional transcript pull.
Extract Q&A from press conferences
Transcript segments include is_question and question_group_id fields. Pull a press conference transcript and separate questions from answers to analyze what journalists are asking a speaker about.
# 1. Find all press conferences for a speaker
events = []
cursor = None
while True:
params = {
"speaker_id": SPEAKER_ID,
"event_type": "press_conference",
"has_transcript": "true",
"limit": 200,
}
if cursor:
params["cursor"] = cursor
resp = requests.get(f"{BASE}/v1/events", headers=HEADERS, params=params).json()
events.extend(resp["data"])
cursor = resp.get("next_cursor")
if not cursor:
break
# 2. Pull each transcript and extract Q&A pairs
for event in events:
resp = requests.post(
f"{BASE}/v1/events/batch-segments",
headers=HEADERS,
json={"event_ids": [event["event_id"]], "limit_per_event": 500},
).json()
if not resp["results"]:
continue
segments = resp["results"][0]["segments"]
questions = [s for s in segments if s["is_question"]]
answers = [s for s in segments if not s["is_question"]]
print(f"{event['canonical_title']}: {len(questions)} questions, {len(answers)} answer segments")
# Group questions by question_group_id to see multi-sentence questions
from collections import defaultdict
groups = defaultdict(list)
for q in questions:
gid = q.get("question_group_id") or q["idx"]
groups[gid].append(q["sentence_txt"])
for gid, texts in groups.items():
print(f" Q: {' '.join(texts)}")
The question_group_id links multi-sentence questions into a single logical question. Segments without is_question in the same region are typically the speaker's answer.
Compare two speakers on a topic
Use the topic segments endpoint (GET /v1/topics/{slug}/segments) with different speaker filters to compare what two speakers say about the same subject. This endpoint is unmetered — it does not count as a transcript pull.
SPEAKER_A = "381194fe-4856-4945-9e49-9809be82e924"
SPEAKER_B = "f47ac10b-58cc-4372-a567-0e02b2c3d479"
TOPIC = "nato"
def get_topic_segments(topic_slug, speaker_id):
segments = []
cursor = None
while True:
params = {"speaker_id": speaker_id, "limit": 200}
if cursor:
params["cursor"] = cursor
resp = requests.get(
f"{BASE}/v1/topics/{topic_slug}/segments",
headers=HEADERS, params=params,
).json()
segments.extend(resp["data"])
cursor = resp.get("next_cursor")
if not cursor:
break
return segments
segs_a = get_topic_segments(TOPIC, SPEAKER_A)
segs_b = get_topic_segments(TOPIC, SPEAKER_B)
print(f"Speaker A on {TOPIC}: {len(segs_a)} segments across {len(set(s['event_id'] for s in segs_a))} events")
print(f"Speaker B on {TOPIC}: {len(segs_b)} segments across {len(set(s['event_id'] for s in segs_b))} events")
# Organize by event date for chronological comparison
for seg in sorted(segs_a, key=lambda s: s.get("event_date", "")):
print(f" [{seg.get('event_date', '')[:10]}] {seg['sentence_txt'][:120]}")
Since topic segment browsing is unmetered, this workflow consumes zero transcript pulls and zero searches — useful for exploratory analysis before deciding what to pull in full.
Correlate transcripts with prediction market outcomes
Pull events that have Kalshi market resolutions, then fetch both the transcript and the resolution data to analyze how speech correlates with market outcomes.
# 1. Find events with transcripts (market-linked events have kalshi_tickers)
events = []
cursor = None
while True:
params = {"has_transcript": "true", "limit": 200}
if cursor:
params["cursor"] = cursor
resp = requests.get(f"{BASE}/v1/events", headers=HEADERS, params=params).json()
events.extend(resp["data"])
cursor = resp.get("next_cursor")
if not cursor:
break
market_events = [e for e in events if e.get("kalshi_tickers")]
print(f"{len(market_events)} events have linked prediction markets")
# 2. For each market-linked event, fetch resolutions and transcript
for event in market_events:
eid = event["event_id"]
# Get market resolutions (unmetered)
resolutions = requests.get(
f"{BASE}/v1/events/{eid}/resolutions", headers=HEADERS
).json()
# Get transcript topics (unmetered)
topics = requests.get(
f"{BASE}/v1/events/{eid}/topics", headers=HEADERS
).json()
print(f"\n{event['canonical_title']}")
print(f" Topics: {', '.join(t['slug'] for t in topics)}")
for r in resolutions.get("resolutions", []):
print(f" Market: {r['market_ticker']} → {r['resolution']}")
# 3. Batch-pull the full transcripts for deeper analysis
event_ids = [e["event_id"] for e in market_events]
BATCH_SIZE = 25
for i in range(0, len(event_ids), BATCH_SIZE):
batch = event_ids[i : i + BATCH_SIZE]
resp = requests.post(
f"{BASE}/v1/events/batch-segments",
headers=HEADERS,
json={"event_ids": batch, "limit_per_event": 500},
).json()
for entry in resp["results"]:
print(f" {entry['event_id']}: {entry['count']} segments pulled")
Resolutions and topics are unmetered. Only the batch-segments call at the end consumes transcript pulls.
Build a speaker profile
Combine speaker details, aggregated stats, event history, and top topics into a comprehensive profile — useful for building speaker pages or research dossiers.
# 1. Speaker identity and stats (unmetered)
speaker = requests.get(
f"{BASE}/v1/speakers/{SPEAKER_ID}", headers=HEADERS
).json()
stats = requests.get(
f"{BASE}/v1/speakers/{SPEAKER_ID}/stats", headers=HEADERS
).json()
print(f"{speaker['canonical_name']} ({speaker['event_count']} events)")
speaking = stats["speaking_time"]
qa = stats["as_primary_speaker"]
print(f" Total speaking time: {speaking['total_speaking_ms'] / 60000:.0f} minutes")
print(f" Events appeared in: {speaking['events_appeared_in']}")
print(f" Q&A events: {qa['qa_event_count']}, avg questions: {qa['avg_questions_per_event']:.1f}")
# 2. All their events (unmetered)
events = requests.get(
f"{BASE}/v1/speakers/{SPEAKER_ID}/events", headers=HEADERS
).json()
# 3. Discover which topics this speaker covers most
from collections import Counter
topic_counts = Counter()
for event in events:
topics = requests.get(
f"{BASE}/v1/events/{event['event_id']}/topics", headers=HEADERS
).json()
for t in topics:
topic_counts[t["slug"]] += 1
print(f"\nTop topics:")
for slug, count in topic_counts.most_common(10):
print(f" {slug}: {count} events")
Every call in this workflow is unmetered — speaker details, stats, events, and event topics all fall outside the transcript pull and search quotas.
Track a topic over time
Use the topic segments endpoint with date ranges to measure how much a topic is discussed week-over-week or month-over-month.
from datetime import date, timedelta
TOPIC = "tariffs"
start = date(2026, 1, 1)
end = date(2026, 6, 1)
# Walk month by month
current = start
while current < end:
month_end = (current.replace(day=28) + timedelta(days=4)).replace(day=1)
if month_end > end:
month_end = end
segments = []
cursor = None
while True:
params = {
"start_date": current.isoformat(),
"end_date": month_end.isoformat(),
"limit": 200,
}
if cursor:
params["cursor"] = cursor
resp = requests.get(
f"{BASE}/v1/topics/{TOPIC}/segments",
headers=HEADERS, params=params,
).json()
segments.extend(resp["data"])
cursor = resp.get("next_cursor")
if not cursor:
break
event_ids = set(s["event_id"] for s in segments)
print(f"{current.strftime('%Y-%m')}: {len(segments)} segments across {len(event_ids)} events")
current = month_end
This workflow is entirely unmetered — topic segment browsing does not consume transcript pulls or searches. Useful for building dashboards or tracking topic salience before committing pull budget.