DEV Community

Daniel Oliveira
Daniel Oliveira

Posted on

How to download earnings call transcripts as JSON in Python, speaker by speaker

Most earnings call transcript sources hand you one long block of text. That's fine for reading, but as soon as you want to measure something (how long the CEO talks, what analysts asked, how the answers changed from one quarter to the next) you need every turn on its own, with who said it and in what role.

This is the short script I use to pull a call speaker by speaker from the Equibles earnings call transcript API. A free key is enough: the free plan allows 100 requests a day across the MCP server and the REST API, with no credit card, and a call of up to 200 turns comes back in one request. I'll use NVIDIA's May 2026 call as the example.

Setup

pip install requests
export EQUIBLES_API_KEY=eq_your_api_key
Enter fullscreen mode Exit fullscreen mode
import os
from collections import Counter

import requests

API = "https://api.equibles.com/v1"
HEADERS = {"Authorization": f"Bearer {os.environ['EQUIBLES_API_KEY']}"}


def get(path, **params):
    r = requests.get(f"{API}{path}", headers=HEADERS, params=params, timeout=30)
    r.raise_for_status()
    return r.json()
Enter fullscreen mode Exit fullscreen mode

1. Find the call, and watch the fiscal year

Calls are keyed by the company's own fiscal year and quarter, not the calendar. NVIDIA's fiscal year ends in late January, so its May 2026 call is FY2027 Q1, not 2026 Q2. Listing the company's earnings calls first saves the guessing:

events = get("/stocks/NVDA/investor-events", eventType="EarningsCall", limit=4)
for e in events["data"]:
    print(e["callDate"][:10], f"FY{e['fiscalYear']} Q{e['fiscalQuarter']}", e["hasTranscript"])
Enter fullscreen mode Exit fullscreen mode
2026-11-17 FY2027 Q3 False
2026-08-26 FY2027 Q2 True
2026-05-20 FY2027 Q1 True
2026-02-25 FY2026 Q4 True
Enter fullscreen mode Exit fullscreen mode

The top row is the November call, which hasn't happened yet, so hasTranscript is False. Check that flag before you ask for a transcript.

2. Download every turn

def get_transcript(ticker, fiscal_year, fiscal_quarter):
    path = f"/stocks/{ticker}/earnings-calls/{fiscal_year}/{fiscal_quarter}/speakers"
    turns, offset = [], 0
    while True:
        page = get(path, limit=200, offset=offset)
        turns += page["data"]
        if not page["hasMore"]:
            return page["eventTitle"], turns
        offset += page["turnCount"]


title, turns = get_transcript("NVDA", 2027, 1)
print(title, len(turns), "turns")
Enter fullscreen mode Exit fullscreen mode
Nvidia Corp Q1 FY2027 Earnings Call 31 turns
Enter fullscreen mode Exit fullscreen mode

turnCount is the number of turns in this page and hasMore says whether more remain, so the loop moves the offset forward until it's done. Each turn has speakerName, speakerRole, text, and startSeconds and endSeconds, its position in the call audio.

One detail that matters for analysis: speakerName is filled only when the speaker's identity is verified. Otherwise it's None, and you get a numbered speakerIndex plus a role label when one is known. That's why the next step groups by role.

3. Who did the talking?

words = Counter()
for t in turns:
    # Analyst roles carry the firm after a dash, so keep the part before it
    role = (t["speakerRole"] or "Unknown").split(" \u2014 ")[0]
    words[role] += len(t["text"].split())
total = sum(words.values())
for role, n in words.most_common():
    print(f"{n / total:>4.0%}  {n:>6,} words  {role}")
Enter fullscreen mode Exit fullscreen mode
 53%   4,244 words  CEO
 29%   2,331 words  CFO
 10%     830 words  Analyst
  4%     335 words  Head of Investor Relations
  4%     283 words  Operator
Enter fullscreen mode Exit fullscreen mode

The CEO and CFO spoke 82% of the words on the call. The eight analysts who asked questions got 10% between them.

4. The analyst Q&A

A second endpoint groups the Q&A by analyst: everything one analyst said in an exchange, their firm, and who answered.

qa = get("/stocks/NVDA/earnings-calls/2027/1/questions")
for q in qa["data"]:
    firm = q["firm"]["name"] if q["firm"] else "unknown firm"
    answered_by = ", ".join(a["name"] for a in q["answerers"])
    print(f"{q['number']}. {q['analyst']['name']} ({firm}) -> {answered_by}")
Enter fullscreen mode Exit fullscreen mode
1. Joseph Moore (Morgan Stanley) -> Jensen Huang
...
6. Stacy Rasgon (Bernstein Research) -> Jensen Huang
7. Jim Schneider (Goldman Sachs) -> Jensen Huang
Enter fullscreen mode Exit fullscreen mode

Jensen Huang answered seven of the eight questions alone. Only the last one went to the CFO and the head of investor relations as well.

5. Read one full exchange

startTurnIndex and endTurnIndex (exclusive) are positions in the call's turn list, so they slice the transcript you already downloaded:

first = qa["data"][0]
for t in turns[first["startTurnIndex"]:first["endTurnIndex"]]:
    who = t["speakerName"] or t["speakerRole"]
    print(f"[{who}] {t['text'][:120]}")
Enter fullscreen mode Exit fullscreen mode
[Operator] Thank you. At this time, I would like to remind everyone, in order to ask a question, press star, then the number one on
[Joseph Moore] Great. Thank you for letting me ask the question. I guess I'd like to ask, what drove the change in segmentation? What's
[Jensen Huang] Yeah, thanks, Joe. First of all, Colette meant to say we're increasing our quarterly dividends from one cent to 25 cents
Enter fullscreen mode Exit fullscreen mode

If you haven't downloaded the whole call, pass startTurnIndex as the offset of the speakers request instead.

A few things worth knowing

  • The speakers response also has a corrections list: figures spoken on the call that the company's filings contradict. The transcript text itself is never edited, so a conflict travels next to it. This call has none.
  • Conferences and investor days have no fiscal quarter. For those, take the event id from the investor-events list and request /v1/investor-events/{id}/speakers.
  • Every request above counts once against the daily limit, so this whole walkthrough costs three.

What do you measure on earnings calls? I'm curious what people track besides sentiment scores.

Top comments (0)