Mesh API

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.

PoolWhat countsReset
Transcript pullsGET /v1/events/{event_id}/segments, deduped by event per UTC dayDaily and monthly
SearchesGET /v1/segments/searchDaily
Other endpointsMetadata, usage, topics, speakers, saved analysesUnmetered
PlanDaily transcript pullsDaily searchesMonthly transcript cap
Free3/day10/day3/month
Trial (14 days)5/day50/day150/month
Pro15/day150/day450/month
Max30/day300/day900/month
EnterpriseContract-definedContract-definedContract-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.

Rate Limits

In addition to quota caps, all API-key requests are subject to a per-key request-per-minute rate limit:

PlanRequests per minute
Free / Trial / Pro / Max60
Enterprise300

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" }
StatusMeaning
400Invalid parameter or request body
401Missing, invalid, or expired credentials
403Account disabled, resource outside key scope, or Free-plan event not unlocked
404Resource not found
409Conflict, such as max active keys reached
422Validation error
429Daily/monthly quota exceeded

Quota errors include useful reset headers when applicable:

  • Retry-After
  • X-Pull-Budget-Reset

Health

GET /health

Public health check.

{ "status": "ok" }

Events

GET /v1/events

List events. Use has_transcript=true to browse events with transcript data.

Common query params:

ParamTypeDescription
qstringFull-text search on event title
event_typestringEvent type such as remarks, press_conference, rally, interview
speaker_idUUIDFilter by primary speaker
speaker_idsstringComma-separated UUIDs (max 20). Returns events where ANY listed speaker is primary. Mutually exclusive with speaker_id.
topic_slugstringFilter by topic
citystringPartial city match
state_codestringExact state code
start_datedateEvents on or after this date
end_datedateEvents on or before this date
limitintPage size, default 50
cursorstringCursor from previous response

GET /v1/events/{event_id}

Get one event.

GET /v1/events/{event_id}/segments

Get transcript segments for one event.

Common query params:

ParamTypeDescription
limitintPage size
cursorstringCursor from previous response
speaker_idUUIDFilter to one resolved speaker
topic_slugstringFilter to one topic
unlockbooleanFree-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
}
FieldTypeDescription
event_idsUUID[]Event UUIDs to pull transcripts for. Max 25 (enterprise) or 10 (other plans).
limit_per_eventintMax 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.

Segment Search

GET /v1/segments/search

Search transcript segments across events.

Common query params:

ParamTypeDescription
qstringFull-text segment search
speaker_idUUIDFilter by resolved speaker
topic_slugstringFilter by topic
event_typestringFilter by event type
start_datedateEvent start date lower bound
end_datedateEvent start date upper bound
limitintPage size
cursorstringCursor from previous response

Free-plan search returns only preview segments plus events you have unlocked that day. Paid plans search the full corpus.

Speakers

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.

GET /v1/speakers/{speaker_id}/stats

Get speaker-level statistics when available.

Topics

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.

Usage

GET /v1/usage

Return your current plan, daily transcript pulls, daily searches, monthly transcript usage, and trial metadata. Does not consume quota.

Important fields:

FieldDescription
planCurrent effective plan
daily_transcript_pulls_used / daily_transcript_pulls_capTranscript pull usage today
daily_searches_used / daily_searches_capSearch usage today
monthly_transcript_pulls_used / monthly_transcript_pulls_capMonthly transcript ceiling
trial_started_at / trial_ends_atTrial metadata, if applicable

Pull History

GET /v1/pulls

List transcripts the authenticated user has fetched and whether Mesh has newer transcript data available.

Common query params:

ParamTypeDescription
limitintPage size
offsetintOffset pagination value
api_key_idUUIDOptional key-specific history filter

Use has_updates to show stale transcript pulls in a user dashboard.

API Keys

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.

Analyses

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.

Browse and Discover Events

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 speaker

curl "https://api.meshapi.app/v1/events?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.

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?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.

Pull Transcripts

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?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.

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.

Search Across Events

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 speaker's transcripts

curl "https://api.meshapi.app/v1/segments/search?q=immigration&speaker_id=381194fe-...&limit=50" \
  -H "X-API-Key: mesh_your_key_here"

Combines full-text search with a speaker filter. Only segments from events where that speaker is primary are returned.

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.

Monitor Usage and Changes

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.

Combined Workflows

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.

  • Search results are ordered by transcript position, not relevance.
  • Free-plan unlocks reset at 00:00 UTC.
  • The API key value is only visible once at creation. Store it immediately.
  • Caps are account-wide across all your API keys.