Blog / Analyzing Earnings Calls Programmatically: A Worked Example
AI & LLM workflowsMarket themes & sectors

Analyzing Earnings Calls Programmatically: A Worked Example

Apr 4, 2026 · Sarah Patel

We recently set out to answer one specific question:

What share of the Fortune 500 brought up "agentic AI" on their latest earnings call?

The manual route is grim: open ~500 transcripts, keyword-search each one, tally results in a spreadsheet. Call it a week of analyst time.

The programmatic route, using the EarningsAPI search endpoint, came to 47 HTTP requests, roughly 9 seconds of runtime, and one short Python script. This post walks through the entire thing - methodology, code, results, and an exact accounting of what it consumed.

Results Up Front

Among the 487 Fortune 500 companies that held a call between March 1 and May 31, 2026:

By sector:

Sector Mention rate Representative names
Technology 62.1% NVIDIA, Microsoft, Salesforce, ServiceNow
Financial Services 47.6% JPMorgan, Visa, Goldman Sachs
Communication Services 41.2% Meta, Alphabet, Disney
Healthcare 31.4% UnitedHealth, Pfizer, Eli Lilly
Consumer Discretionary 28.9% Amazon, Home Depot, Starbucks
Industrials 19.7% Boeing, GE, Honeywell

One more pattern worth noting: CEOs raised the topic about three times as often as CFOs. Agentic AI is currently narrative, not yet a line item - watch for that ratio to compress in coming quarters.

Here's how the numbers were produced.

Prerequisites

Three things:

  1. A Fortune 500 ticker list with sectors (Wikipedia's table exports to CSV in one step)
  2. An EarningsAPI key - plans at /#pricing; full-text search requires Pro or above
  3. Python 3.10+ with requests and pandas installed

No scraping infrastructure, no vector database, no LLM required for the counting stage.

import os
import requests
import pandas as pd
from collections import defaultdict

API_KEY = os.environ["EARNINGSAPI_KEY"]
BASE_URL = "https://earningsapi.io/api/v1"
HEADERS = {"X-API-Key": API_KEY}

# Every request goes through this wrapper so we get an exact usage tally
n_requests = 0

def api_get(path, params=None):
    global n_requests
    n_requests += 1
    r = requests.get(f"{BASE_URL}{path}", headers=HEADERS, params=params)
    r.raise_for_status()
    return r.json()

That counter is the entire cost-tracking apparatus. When the script exits, n_requests is the bill.

Step 1: Enumerate the Phrasings

Executives never converge on a single term. The same concept shows up as "agentic AI", "AI agents", "autonomous agents", and "agentic systems" depending on who's talking. Query each variant and merge:

PHRASES = [
    "agentic AI",
    "AI agents",
    "autonomous agents",
    "agentic systems",
]

WINDOW_START = "2026-03-01"
WINDOW_END   = "2026-05-31"

The date window pins the analysis to the most recent reporting cycle rather than all of history.

Step 2: One Global Search Per Phrase

Don't loop over 500 tickers searching each company's transcript - that's 500+ requests doing work the server can do for you. Run each phrase as a single corpus-wide search and paginate:

def fetch_all_hits(phrase):
    hits, page = [], 1
    while True:
        data = api_get("/search", {
            "q": phrase,
            "type": "transcripts",
            "date_from": WINDOW_START,
            "date_to": WINDOW_END,
            "page": page,
            "limit": 50,
        })
        hits.extend(data["results"])
        if len(data["results"]) < 50:
            return hits
        page += 1

In our run, "AI agents" was the heaviest phrase at ~280 hits over 6 pages; "agentic systems" fit in a single page with 41. All four phrases together: 14 search requests.

Step 3: Collapse Hits Into Companies

A transcript can match several phrases, and we're counting companies, not matches. Fold everything into a per-ticker map:

by_ticker = defaultdict(lambda: {"phrases": set(), "mentions": 0})

for phrase in PHRASES:
    for hit in fetch_all_hits(phrase):
        t = hit["company"]["ticker"]
        by_ticker[t]["phrases"].add(phrase)
        by_ticker[t]["mentions"] += hit["match_count"]

A slice of the resulting structure:

{
    "NVDA": {"phrases": {"agentic AI", "AI agents"}, "mentions": 23},
    "MSFT": {"phrases": {"agentic AI", "AI agents", "autonomous agents"}, "mentions": 18},
    "CRM":  {"phrases": {"agentic AI"}, "mentions": 11},
}

Twenty-three mentions in one NVIDIA call - a density figure no headline coverage would surface.

Step 4: Join Against the Fortune 500 List

f500 = pd.read_csv("fortune500_2026.csv")  # columns: ticker, sector
f500["mentioned"] = f500["ticker"].isin(by_ticker)
f500["mention_count"] = f500["ticker"].map(
    lambda t: by_ticker[t]["mentions"] if t in by_ticker else 0
)

Step 5: Separate "Silent" From "Hasn't Reported"

A company absent from the search results either held a call and stayed quiet on the topic, or simply hasn't reported inside the window. Those are different denominators, so they must be distinguished.

Useful shortcut: every search hit already carries call_date, so companies that did match need no further requests. Only the non-matchers require a lookup of their most recent call:

def most_recent_call_date(ticker):
    try:
        data = api_get("/transcripts/recent", {"ticker": ticker, "limit": 1})
        return data["results"]["call_date"] if data["results"] else None
    except requests.HTTPError:
        return None

silent_reporters = []
for t in f500[~f500["mentioned"]]["ticker"]:
    d = most_recent_call_date(t)
    if d and WINDOW_START <= d <= WINDOW_END:
        silent_reporters.append(t)

Outcome: 303 companies reported in the window without mentioning the topic, joining the 184 that did - 487 reporting companies total. The remaining 13 were off-cycle fiscal years that hadn't reported yet. This verification pass was the only per-company work in the script and cost 33 requests (we skipped tickers that famously don't hold calls at all).

Step 6: Sector Breakdown

With sector already in the CSV, the table at the top of this post is a five-line groupby:

sector_stats = (
    f500.groupby("sector")
        .agg(total=("ticker", "count"), mentioned=("mentioned", "sum"))
        .assign(rate=lambda d: 100 * d["mentioned"] / d["total"])
        .sort_values("rate", ascending=False)
)
print(sector_stats)
                       total  mentioned   rate
sector
Technology               87         54   62.1
Financial Services       42         20   47.6
Communication Services   34         14   41.2
Healthcare               51         16   31.4
Consumer Discretionary   76         22   28.9
Industrials              71         14   19.7
Energy                   38          5   13.2
Real Estate              31          3    9.7
Consumer Staples         45          4    8.9
Utilities                25          2    8.0

The Accounting

print(f"Total API requests: {n_requests}")
# Total API requests: 47
Stage Requests Notes
Global search, 4 phrases 14 Heaviest phrase paginated to 6 pages
Latest-call verification 33 Only for companies absent from search hits
Total 47

Forty-seven requests is a rounding error against the Pro plan's ($145/quarter) request allowance - you could re-run this analysis every day of the quarter and barely register on the meter.

For contrast, the alternatives:

Extensions

The ticker-to-mention-count table is a launchpad:

Complete Script

Everything above, condensed and runnable:

import os, requests, pandas as pd
from collections import defaultdict

API_KEY = os.environ["EARNINGSAPI_KEY"]
BASE_URL = "https://earningsapi.io/api/v1"
HEADERS = {"X-API-Key": API_KEY}
WINDOW_START, WINDOW_END = "2026-03-01", "2026-05-31"
PHRASES = ["agentic AI", "AI agents", "autonomous agents", "agentic systems"]

n_requests = 0
def api_get(path, params=None):
    global n_requests
    n_requests += 1
    r = requests.get(f"{BASE_URL}{path}", headers=HEADERS, params=params)
    r.raise_for_status()
    return r.json()

def fetch_all_hits(phrase):
    hits, page = [], 1
    while True:
        d = api_get("/search", {"q": phrase, "type": "transcripts",
                                "date_from": WINDOW_START, "date_to": WINDOW_END,
                                "page": page, "limit": 50})
        hits.extend(d["results"])
        if len(d["results"]) < 50: return hits
        page += 1

by_ticker = defaultdict(lambda: {"phrases": set(), "mentions": 0})
for p in PHRASES:
    for hit in fetch_all_hits(p):
        t = hit["company"]["ticker"]
        by_ticker[t]["phrases"].add(p)
        by_ticker[t]["mentions"] += hit.get("match_count", 1)

f500 = pd.read_csv("fortune500_2026.csv")
f500["mentioned"] = f500["ticker"].isin(by_ticker)

silent_reporters = []
for t in f500[~f500["mentioned"]]["ticker"]:
    try:
        d = api_get("/transcripts/recent", {"ticker": t, "limit": 1})
        if d["results"] and WINDOW_START <= d["results"]["call_date"] <= WINDOW_END:
            silent_reporters.append(t)
    except Exception:
        pass

reporting = f500["mentioned"].sum() + len(silent_reporters)
print(f"{f500['mentioned'].sum()} / {reporting} = "
      f"{100 * f500['mentioned'].sum() / reporting:.1f}%")
print(f"API requests used: {n_requests}")

Swap in your key, adjust the date window, and you have a primary-source answer to "what is corporate America saying about X" in under ten seconds. The pattern generalizes to any earnings-call topic - layoffs, China exposure, buybacks, quantum computing, tariffs. Change four strings, get a new market-wide read.

Where to Go From Here

Mention counting is the entry point; the natural next layers are summarization and sentiment over the calls you've identified, using the same speaker-segmented transcript data. The endpoint reference lives at /docs, and an API key from /#pricing is all the setup this script needs.

Related reading

250,000 earnings calls via API

Full transcripts, speaker segments, full-text search. Quarterly plans from $145.

Get an API key
← PreviousStructured Earnings Data: The Unsexy Foundation of Good Fintech Products