Anyone who has pointed retrieval-augmented generation at financial documents knows the usual grind: 10-Ks are dense, filing formats drift, and PDF extraction breaks in creative ways. Earnings call transcripts are the pleasant outlier. They are conversational by nature, cleanly structured by speaker, and full of the forward-looking, management-voiced commentary that analysts actually query for. Few document types are better suited to a RAG system.
This guide shows how to get transcripts into your retrieval stack with earningsapi.io - through the REST API for bulk ingestion into your own vector store, or through the MCP connector when you want an agent to retrieve live at reasoning time. The corpus covers 2020 to today: 250,945 earnings calls from 12,728 companies across all 11 GICS sectors, broken into 11.9M speaker segments. Enough width for cross-market retrieval, enough per-company depth for real longitudinal work.
First, the part that removes the most engineering effort.
Chunking is already done for you
Ask any RAG team what they argue about most and the answer is chunking. Split too fine and you sever thoughts mid-sentence; split too coarse and each embedding smears across unrelated topics. The default coping mechanism is a recursive character splitter plus a week of tuning chunk_size and chunk_overlap.
With earnings transcripts you can skip the argument. Every transcript in the dataset arrives pre-segmented by speaker turn. A CEO's answer to an analyst is one segment. The analyst's follow-up is the next. Each segment is one speaker expressing one coherent thought - which is precisely the unit you want in a vector store.
So the rule is simple: one speaker segment, one embedding chunk. No splitter, no overlap heuristics, no sentence-boundary hacks. The discourse structure of a Q&A call already is your chunking strategy.
Every segment ships with:
speaker_type-executive,analyst,operator,attendee, orshareholderspeaker_name- the named individual (the large majority of segments carry one)text- the verbatim content of the turn
The parent call contributes the filtering and citation context: ticker, sector, country, date.
Two practical caveats. Operator boilerplate ("our next question comes from...") adds nothing to retrieval - drop it or down-weight it at ingest. And the occasional marathon executive answer can exceed an embedding model's window; split those as an exception path, not as your default. For nearly everything else, segment equals chunk.
Two integration paths
The MCP connector suits agentic architectures: Claude or any MCP-capable client calls tools like transcript search or speaker-segment fetch at reasoning time, and no vector store exists anywhere. The LLM decides what to retrieve, when. The server runs at mcp.earningsapi.io and connects with a single URL.
The REST API suits the classic pipeline: pull, embed, index, serve retrieval yourself. You own the embedding model, the vector database, reranking, and latency. It is also the only sane way to backfill five-plus years of history - nobody backfills a quarter million calls one tool call at a time.
Mature systems often combine them - REST builds the index, MCP fronts the agent. But if your own vector store is the destination, start with the API.
The ingestion loop over REST
Base URL https://earningsapi.io/api/v1, auth via X-API-Key header, machine-readable schema at https://earningsapi.io/openapi.json. The loop itself is unremarkable, which is the point: list calls, fetch each call's segments, attach metadata, embed, upsert.
# 1. Page through earnings calls (cursor-based)
curl -s "https://earningsapi.io/api/v1/earnings-calls?limit=100" \
-H "X-API-Key: $EARNINGSAPI_KEY"
# 2. Fetch the speaker segments for one call
curl -s "https://earningsapi.io/api/v1/earnings-calls/{call_id}/segments" \
-H "X-API-Key: $EARNINGSAPI_KEY"
# Conceptual ingest loop
for call in list_calls(cursor=cursor):
for seg in get_segments(call.id):
if seg.speaker_type == "operator":
continue # boilerplate, skip
store.upsert(
id=f"{call.id}:{seg.index}",
vector=embed(seg.text),
payload={
"text": seg.text,
"speaker_type": seg.speaker_type,
"speaker_name": seg.speaker_name,
# call-level metadata copied onto every chunk
"ticker": call.ticker,
"sector": call.sector,
"country": call.country,
"date": call.date,
},
)
The one design decision that matters: denormalize call-level metadata onto every segment at ingest. The call knows ticker, sector, country, and date; the segment knows the speaker. Copying the call fields into each chunk's payload makes every vector independently filterable and citable - no join back to a parent record at query time.
Treat the snippets above as the pattern, not the contract; confirm exact paths and field names against the OpenAPI spec in the docs before wiring anything up.
Why the metadata earns its keep
Attached metadata is what upgrades generic semantic search into a financial retrieval system. It does two jobs.
Scoped retrieval. Real queries are almost never corpus-wide. "What did semiconductor executives say about inventory in 2024?" is a metadata filter - sector, speaker_type = executive, a date range - followed by vector search inside that subset. Filtering first improves relevance and shrinks the candidate set your reranker processes. The speaker_type field is the standout: retrieving only management answers, or only analyst questions, is impossible with undifferentiated filing text.
Grounded citations. In finance, RAG without attribution is unusable. Since each chunk carries ticker, date, speaker_name, and speaker_type, the generation step can emit citations like " - CFO, AAPL Q3 2024 call" straight from the payload. No secondary lookup, no invented sources.
Index ticker, sector, country, date, and speaker_type as native filters in your vector DB rather than post-filtering in application code - that set covers company, thematic, geographic, temporal, and role-based scoping.
Incremental sync
A one-shot backfill starts rotting the day earnings season opens. What you want is incremental sync, not scheduled full re-crawls.
Cursor-based polling handles this. Persist the cursor from your last run; the next run asks only for calls that appeared since. New transcripts get embedded and upserted, everything already indexed stays untouched.
cursor = load_cursor()
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)
Schedule it hourly in earnings season, daily otherwise. Cursors are monotonic, so the job is idempotent and nearly free on quiet days - one empty page and exit.
When to skip the vector store entirely
Running a vector store is genuine operational load: embedding spend, index freshness, retrieval quality - all yours. Sometimes that overhead buys you nothing.
If the use case is agentic - a research copilot, a Claude-based analyst tool, chat over earnings data - let the MCP connector retrieve live instead. The model calls search and segment tools at reasoning time and always gets current data straight from the source. No staleness window, no embedding bill, no infrastructure on call.
Setup takes a minute: generate a connector in the dashboard, get a URL of the form https://earningsapi.io/u/mct_xxx/mcp, paste it into your MCP client, done.
Rule of thumb:
- Own vector store when you need custom embeddings, sub-100ms retrieval at scale, hybrid search, or transcripts embedded alongside proprietary internal documents.
- Live MCP retrieval when the LLM drives, freshness beats latency, and you would rather not operate infrastructure.
A common trajectory: prototype on the connector, migrate to a self-hosted index once retrieval requirements harden. Both read the same 11.9M-segment dataset, so nothing about the migration is lossy.
Start building
The tedious parts of a transcript RAG pipeline - chunking, speaker labeling, filterable metadata - are handled before your first line of code. Segments arrive ready to embed, tagged with role, name, ticker, sector, country, and date.
Pick a plan on the pricing page (Basic $105, Pro $145, Ultra $515 per quarter), grab an API key or MCP connector from the dashboard, check the docs for exact endpoints, and point the output at your embedding model.