If you have ever tried to build retrieval-augmented generation over financial documents, you already know the pain: 10-Ks are dense, filings are inconsistent, and PDFs fight your parser at every turn. Earnings call transcripts are a refreshing exception. They are conversational, they are structured, and they are packed with exactly the kind of forward-looking, management-voiced signal that analysts actually want to query. That makes them near-ideal source material for an earnings call RAG pipeline.
This post walks through how to feed earnings transcripts into a RAG system using earningscalls.dev — either through the MCP connector for agentic, on-demand retrieval, or through the REST API for bulk ingestion into your own vector store. The dataset covers 2020 to present (5+ years), 246,000+ earnings call transcripts across 12,000+ companies, 70 countries, 170+ exchanges, all 11 GICS sectors, and 11M+ speaker segments. That is enough breadth to build cross-market retrieval, and enough depth per company to do meaningful longitudinal analysis.
Let me start with the part that saves you the most engineering time.
Speaker segments are natural chunks
The hardest, most fiddly part of any RAG pipeline is chunking. Get it wrong and you either shred sentences mid-thought (too small) or dilute your embeddings with unrelated topics (too big). Most teams reach for a recursive character splitter, tune chunk_size and chunk_overlap, and then spend a week arguing about whether 512 or 800 tokens is "right."
Earnings transcripts let you skip that entirely. Every transcript in the dataset is pre-split into segments by speaker. When the CEO answers an analyst question, that answer is one segment. When the analyst asks the next question, that is another segment. Each segment is a self-contained turn in the conversation — a single speaker, a single coherent thought.
That maps one-to-one onto what you want in a RAG store: one segment = one embedding chunk. No recursive splitting, no overlap heuristics, no sentence-boundary regex. The natural discourse structure of a Q&A call is already the chunking strategy.
Each segment carries:
speaker_type— one ofexecutive,analyst,operator,attendee,shareholderspeaker_name— the named individual (about 93% of segments carry a named speaker)text— the verbatim segment content
And the parent call carries the context you need for filtering and citations: ticker, sector, country, and date.
A few practical notes. Operator segments ("Our next question comes from...") are usually noise for retrieval — filter them out or down-weight them at ingest. Some executive answers run long; if a single segment blows past your embedding model's context window, split it, but do it as a fallback, not a default. The whole point is that for the vast majority of segments, one segment is exactly one chunk.
Two ways in: MCP for agentic retrieval vs REST API for bulk ingestion
There are two integration paths, and they serve different architectures.
The MCP connector is the right choice when you want an agent (Claude, or any MCP-capable client) to retrieve transcript data live, at reasoning time. The model calls tools like search_transcripts or get_speaker_segments when it needs them, and you never stand up a vector store at all. This is agentic retrieval: the LLM decides what to fetch. See MCP vs REST API for earnings call data for the full comparison.
The REST API is the right choice when you are building a classic RAG pipeline: pull the data, embed it, index it, and serve retrieval from your own store. This gives you control over the embedding model, the vector database, chunking fallbacks, reranking, and latency. It is also how you do bulk historical backfill — you cannot backfill five years of transcripts one agent tool call at a time.
Most serious systems end up using both: REST for the bulk index, MCP for the agent front-end. But if you are ingesting into your own vector store, start with the API.
Bulk ingest with the REST API
The API base is https://earningscalls.dev/api/v1, authenticated with an X-API-Key header. The full schema lives at https://earningscalls.dev/openapi.json. The ingestion loop is straightforward: list calls, pull segments per call, attach metadata, embed, upsert.
Here is the shape of it in pseudocode:
# 1. List earnings calls (paged via cursor)
curl -s "https://earningscalls.dev/api/v1/earnings-calls?limit=100" \
-H "X-API-Key: $EARNINGSCALLS_API_KEY"
# 2. For each call, pull its speaker segments
curl -s "https://earningscalls.dev/api/v1/earnings-calls/{call_id}/segments" \
-H "X-API-Key: $EARNINGSCALLS_API_KEY"
# Ingest loop (conceptual)
for call in list_calls(cursor=cursor):
for seg in get_segments(call.id):
if seg.speaker_type == "operator":
continue # skip boilerplate
vector = embed(seg.text)
store.upsert(
id=f"{call.id}:{seg.index}",
vector=vector,
payload={
"text": seg.text,
"speaker_type": seg.speaker_type, # executive / analyst / ...
"speaker_name": seg.speaker_name,
# parent-call metadata attached to every chunk
"ticker": call.ticker,
"sector": call.sector,
"country": call.country,
"date": call.date,
},
)
The key move is denormalizing the parent-call metadata onto every segment. The call knows the ticker, sector, country, and date; the segment knows the speaker. Copy the call-level fields into each chunk's payload at ingest time so every vector is independently filterable and citable without a join back to the parent.
Check the exact field names and endpoint paths against openapi.json before you wire this up — treat the pseudocode above as the pattern, not the literal spec.
Metadata for filtering and citations
The metadata you attach is what turns a generic semantic search into a useful financial retrieval system. Two jobs it does:
Pre-filtering. Most real queries are scoped. "What did semiconductor executives say about inventory in 2024?" is a metadata filter (sector = Information Technology, speaker_type = executive, date in 2024) followed by a vector search over that subset. Filtering before you rank both improves relevance and cuts the candidate set your reranker has to chew through. speaker_type is especially powerful — being able to retrieve only management answers, or only analyst questions, is something you simply cannot do with unstructured filings.
Citations. RAG without citations is not trustworthy for finance. Because every chunk carries ticker, date, speaker_name, and speaker_type, your generation step can produce grounded attributions like "— CFO, AAPL Q3 2024 call" directly from the payload. No separate lookup, no hallucinated sources.
A good filter schema to index on: ticker, sector, country, date, speaker_type. That set covers company-level, thematic, geographic, temporal, and role-based retrieval. If your vector DB supports it, index these as native metadata filters rather than post-filtering in application code.
Keeping the store fresh
A one-time backfill goes stale fast — new earnings calls land continuously through every reporting season. You want incremental sync, not a periodic full re-crawl.
The API supports cursor-based polling for exactly this. You persist the cursor from your last sync, and on the next run you ask only for calls that have appeared since. New transcripts get embedded and upserted; nothing already in your store gets re-fetched.
cursor = load_cursor() # from last run
while True:
page = list_calls(cursor=cursor, limit=100)
for call in page.items:
ingest(call) # embed + upsert new segments
cursor = page.next_cursor
if not page.has_more:
break
save_cursor(cursor)
Run this on a schedule — hourly during earnings season, daily otherwise. Because cursors are monotonic, the job is idempotent and cheap: on a quiet day it fetches one empty page and exits. This is the same incremental pattern you would use to keep a screening agent current — see screen earnings calls across the market with MCP tools.
When to skip your own vector store and let the MCP connector retrieve live
Standing up and maintaining a vector store is real operational work: embeddings cost money, the index needs freshness, and you own the retrieval quality. Sometimes it is not worth it.
If your use case is agentic — a research assistant, a Claude-based analyst tool, a chat interface over earnings data — you can often skip the vector store entirely and let the MCP connector retrieve on demand. The model calls search_transcripts, get_speaker_segments, or get_earnings_call at reasoning time, gets fresh data straight from the source, and never touches an index you have to keep in sync. No staleness, no embedding bill, no infrastructure.
Set it up in the dashboard: Dashboard → Connectors → Generate, which gives you a URL like https://earningscalls.dev/u/mct_xxxx/mcp. Drop that into your MCP client and the tools are available immediately. For a full walkthrough, see build an earnings research agent with MCP and Claude.
The rule of thumb:
- Build your own vector store when you need custom embeddings, sub-100ms retrieval at scale, hybrid search, or you are embedding transcripts alongside proprietary internal documents.
- Use the MCP connector live when the LLM can drive retrieval, freshness matters more than latency, and you would rather not run infrastructure.
Many teams start with the connector to prototype, then graduate to a self-hosted store once retrieval requirements harden. Both read from the same underlying 11M+ segment dataset, so the migration path is clean.
Get started
Whichever path you take, the segments are already chunked, already tagged with speaker roles, and already carrying the ticker, sector, country, and date you need to filter and cite. That is most of the annoying part of a RAG pipeline handled before you write a line of code.
Grab an API key or generate an MCP connector from the dashboard, read the docs for exact endpoints, and pick a plan on the pricing page — Pro is $24.99/mo (5,000 requests/mo) and Ultra is $39.99/mo (25,000 requests/mo). Point it at your embedding model and start retrieving.