How to Build an Earnings Briefing Engine Using the FMP API
A repeatable pipeline that turns earnings prep into one-page briefs

Earnings weeks are a compression problem. Many companies report within a short window, yet the preparation work is scattered across multiple sources.
The workflow is repetitive and time-sensitive, especially when you follow more than a few tickers. When you assemble earnings context one company at a time, you repeat the same steps for every symbol. Over time, briefings become inconsistent because each run follows a slightly different process.
This is where an earnings briefing engine becomes useful. It converts an ad hoc workflow into a repeatable pipeline, producing a consistent one-page brief for each ticker. It also makes the process easier to audit and extend over time.
In this article, we will build a minimal earnings briefing engine using Financial Modeling Prep’s stable endpoints.
Curious about it? Let’s get into it.
Foundation
An earnings briefing engine is a compact workflow that summarizes the key information you need before an earnings event. It does not attempt to forecast returns, and it does not replace deep research. However, the purpose is more operational as it creates a briefing document we can store and reuse.
The Earnings Briefing Engine that we will build comprises three things:
Fetch upcoming earnings events for a date window using the earnings calendar endpoint.
Build a standardized per-ticker bundle that captures event context, expectations, and recent financial performance.
Generate a consistent one-page briefing in a simple format.
The Data Source
This project uses Financial Modeling Prep (FMP) as the primary data source. FMP publishes an extensive catalog of financial datasets through its Stable API. The platform provides over 100 documented endpoints. It also offers additional delivery options, including WebSocket streaming and bulk downloads for selected datasets.
FMP Stable uses a simple base URL, and authentication is handled via an API key passed as a query parameter.
Base URL: https://financialmodelingprep.com/stable/
Auth: apikey=<YOUR_KEY>This briefing engine is built around a small set of Stable endpoints. Each endpoint maps to a section in the final one-page brief:
Earnings Calendar (
earnings-calendar) provides upcoming and past earnings events. It includes the announcement date and EPS fields when available.Analyst Estimates (
analyst-estimates) provides forecasted revenue and EPS. This supports the market expectations section.Company Profile (
profile) provides a company snapshot such as sector, price, and market capitalization.Income Statement (
income-statement) provides historical statement rows for trend context.Key Metrics (
key-metrics) provides common KPIs used for compact metric blocks.
These are the data we will retrieve from the FMP API, and we will build the system based on it.
What the Earnings Briefing Engine Does
Pull upcoming earnings events for a date window
The engine queries the Stable Earnings Calendar endpoint withfromandto. It returns upcoming announcements and may include EPS fields when available.Extract symbols and de-duplicate
From the calendar response, the engine extractssymbolvalues. It keeps first occurrence order and removes duplicates.Fetch a fixed dataset per symbol
For each ticker, the engine calls a small and explicit set of endpoints: >Company Profile for the snapshot context.
>Analyst Estimates for revenue and EPS expectations.
>Optional fundamentals endpoints, such as Income Statement and Key Metrics, when you want trend and KPI blocks.Normalize responses into a stable ticker bundle
Each API response is mapped into a predictable internal schema. Missing datasets become empty objects or empty lists.Render a one-page briefing from the bundle
A single renderer transforms the bundle into a consistent Markdown brief.Save outputs to disk for reuse
Each ticker corresponds to a Markdown file in the output folder.Repeat the workflow with different inputs
We can rerun the engine with a different date window or a watchlist.
Project Architecture
This project stays intentionally small. The goal is a single, clear pipeline with two entry points, rather than spreading behavior across many scripts.
earnings_briefing_engine/
├─ app/
│ ├─ __init__.py
│ ├─ config.py # loads API key and stable base URL once
│ ├─ fmp_client.py # HTTP wrapper, apikey injection, error handling
│ ├─ engine.py # calendar or watchlist → bundle → briefing orchestration
│ └─ render_markdown.py # one-page Markdown template renderer
├─ output/
│ └─ briefings/ # generated files, one per ticker
├─ .env # local configuration
├─ requirements.txt
├─ run.py # upcoming earnings window mode
├─ run_watchlist.py # fixed watchlist mode
└─ output.txt # optional run log or notesHere are explanations for each of the scripts’ purposes:
app/config.py
Stores the single source of truth for configuration. This includes FMP_BASE_URL=https://financialmodelingprep.com/stable and your API key. FMP authenticates requests by appending apikey=... to each request.
app/fmp_client.py
A thin client that constructs URLs, attaches apikey, sets timeouts, and normalizes errors. This keeps API details out of the business logic. The calling pattern follows FMP’s Stable base URL and query authentication.
app/engine.py
The orchestration layer. It runs the numbered flow defined earlier:
In calendar mode, it calls
earnings-calendarwithfromandto.It extracts and de-duplicates symbols.
It fetches a fixed set of per-ticker datasets, then normalizes them into a stable bundle.
In watchlist mode, it can populate the event context with the per-company earnings endpoint
earnings.It then calls the renderer and writes output files.
app/render_markdown.py
Converts the normalized bundle into a one-page briefing with consistent headings. Markdown is used because it is portable, diffable, and easy to store. You can add HTML or PDF later without changing the data pipeline.
output/briefings/
Holds the generated artifacts. A practical convention is one file per ticker per event date, for example AAPL_2026-02-06.md. This creates a durable record you can re-run and compare over time.
Building the Earnings Briefing Engine
Let’s start to build our engine. We will break it down step-by-step.
Step 1: Create the environment
Start with a virtual environment and install only what you need by filling the requirements.txt.
requests>=2.31.0
python-dotenv>=1.0.0We will using the requests for API calls and python-dotenv to load secrets from .env
Step 2: Add a .env file for configuration
Create .env at the project root and store:
FMP_API_KEY=YOUR_API_KEY
FMP_BASE_URL=https://financialmodelingprep.com/stableThe Stable base URL is the canonical starting point for the endpoints used in this tutorial.
Step 3: Load settings once in app/config.py
Keep configuration in one place. The engine should not read environment variables inside business logic. It should receive a settings object.
The config.py will have the following code:
import os
from dataclasses import dataclass
from dotenv import load_dotenv
load_dotenv()
@dataclass(frozen=True)
class Settings:
api_key: str
base_url: str
out_dir: str = “output/briefings”
def get_settings() -> Settings:
“”“
This project targets FMP Stable endpoints:
https://financialmodelingprep.com/stable/...
“”“
api_key = os.getenv(”FMP_API_KEY”, “”).strip()
base_url = os.getenv(”FMP_BASE_URL”, “”).strip()
if not api_key:
raise RuntimeError(”Missing FMP_API_KEY. Set it in your environment or in a .env file.”)
# Default to Stable API.
if not base_url:
base_url = “https://financialmodelingprep.com/stable”
# Auto-correct common misconfiguration.
if “/api/v3” in base_url:
base_url = “https://financialmodelingprep.com/stable”
return Settings(api_key=api_key, base_url=base_url)In this step, we define:
api_keybase_urlout_dir
This aligns with the Stable API pattern and ensures consistent requests.
Step 4: Build a small in app/fmp_client.py
Next, we will build our FMP Client using the following code:
from __future__ import annotations
from dataclasses import dataclass
from typing import Any, Dict, Optional
import requests
def _redact_apikey(url: str) -> str:
if “apikey=” not in url:
return url
return url.split(”apikey=”)[0] + “apikey=REDACTED”
@dataclass(frozen=True)
class FmpClient:
“”“
Minimal HTTP client for FMP Stable endpoints.
“”“
api_key: str
base_url: str = “https://financialmodelingprep.com/stable”
timeout_s: int = 30
max_retries: int = 2 # only for 429
def get_json(
self,
path: str,
params: Optional[Dict[str, Any]] = None,
*,
allow_plan_errors: bool = True,
) -> Any:
“”“
If allow_plan_errors is True:
- 402 (Payment Required) -> None
- 403 (Forbidden) -> None
“”“
base = self.base_url.rstrip(”/”)
url = f”{base}/{path.lstrip(’/’)}”
q = dict(params or {})
q[”apikey”] = self.api_key
attempts = 0
while True:
attempts += 1
resp = requests.get(url, params=q, timeout=self.timeout_s)
if allow_plan_errors and resp.status_code in (402, 403):
return None
if resp.status_code == 429 and attempts <= self.max_retries:
retry_after = resp.headers.get(”Retry-After”)
wait_s = int(retry_after) if (retry_after and retry_after.isdigit()) else (1 + attempts)
import time
time.sleep(wait_s)
continue
if resp.status_code == 401:
raise requests.HTTPError(f”Unauthorized (401) for {_redact_apikey(resp.url)}”, response=resp)
resp.raise_for_status()
return resp.json()Our client should do only four things:
Construct
base_url + pathAttach
apikeyto query parametersSet timeouts
Normalize common errors
FMP documents API key usage via query parameters, and also notes header-based auth as an alternative.
5. Implement the full pipeline in app/engine.py
We will implement the whole Earnings Briefing within the engine.py with the code below:
from __future__ import annotations
from datetime import date, timedelta
from pathlib import Path
from typing import Any, Dict, List, Optional
from app.fmp_client import FmpClient
from app.render_markdown import render_markdown
def _dedupe_keep_order(items: List[str]) -> List[str]:
seen = set()
out: List[str] = []
for s in items:
s = (s or “”).strip().upper()
if s and s not in seen:
seen.add(s)
out.append(s)
return out
def _first_dict(x: Any) -> Dict[str, Any]:
return x[0] if isinstance(x, list) and x and isinstance(x[0], dict) else {}
def fetch_earnings_calendar(client: FmpClient, start: date, end: date) -> List[Dict[str, Any]]:
“”“
Earnings Calendar (stable):
GET /earnings-calendar?from=YYYY-MM-DD&to=YYYY-MM-DD
Docs: https://financialmodelingprep.com/stable/earnings-calendar
“”“
data = client.get_json(
“earnings-calendar”,
{”from”: start.isoformat(), “to”: end.isoformat()},
allow_plan_errors=True,
)
return data or []
def fetch_profile(client: FmpClient, symbol: str) -> Dict[str, Any]:
“”“
Company Profile (stable):
GET /profile?symbol=SYMBOL
Docs: https://financialmodelingprep.com/stable/profile?symbol=AAPL
“”“
data = client.get_json(”profile”, {”symbol”: symbol}, allow_plan_errors=True)
return _first_dict(data)
# Optional (may be plan-limited depending on account)
def fetch_analyst_estimates(client: FmpClient, symbol: str, *, period: str = “quarter”, limit: int = 8, page: int = 0) -> List[Dict[str, Any]]:
data = client.get_json(
“analyst-estimates”,
{”symbol”: symbol, “period”: period, “page”: page, “limit”: limit},
allow_plan_errors=True,
)
return data or []
def fetch_income_statement(client: FmpClient, symbol: str, *, period: str = “quarter”, limit: int = 8) -> List[Dict[str, Any]]:
data = client.get_json(
“income-statement”,
{”symbol”: symbol, “period”: period, “limit”: limit},
allow_plan_errors=True,
)
return data or []
def fetch_key_metrics(client: FmpClient, symbol: str, *, period: str = “quarter”, limit: int = 8) -> List[Dict[str, Any]]:
data = client.get_json(
“key-metrics”,
{”symbol”: symbol, “period”: period, “limit”: limit},
allow_plan_errors=True,
)
return data or []
def fetch_stock_news(client: FmpClient, symbol: str, *, limit: int = 20) -> List[Dict[str, Any]]:
data = client.get_json(”news/stock”, {”symbols”: symbol, “limit”: limit}, allow_plan_errors=True)
return data or []
def fetch_press_releases(client: FmpClient, symbol: str, *, limit: int = 20) -> List[Dict[str, Any]]:
data = client.get_json(”news/press-releases”, {”symbols”: symbol, “limit”: limit}, allow_plan_errors=True)
return data or []
def build_bundle(
client: FmpClient,
symbol: str,
*,
event: Optional[Dict[str, Any]] = None,
include_estimates: bool = False,
include_financials: bool = False,
include_news: bool = False,
statements_period: str = “quarter”,
statements_limit: int = 8,
) -> Dict[str, Any]:
profile = fetch_profile(client, symbol)
estimates: List[Dict[str, Any]] = []
income: List[Dict[str, Any]] = []
key_metrics: List[Dict[str, Any]] = []
news: List[Dict[str, Any]] = []
press: List[Dict[str, Any]] = []
if include_estimates:
estimates = fetch_analyst_estimates(client, symbol, period=statements_period, limit=statements_limit)
if include_financials:
income = fetch_income_statement(client, symbol, period=statements_period, limit=statements_limit)
key_metrics = fetch_key_metrics(client, symbol, period=statements_period, limit=statements_limit)
if include_news:
news = fetch_stock_news(client, symbol)
press = fetch_press_releases(client, symbol)
return {
“symbol”: symbol,
“event”: event or {},
“profile”: profile,
“estimates”: estimates,
“income”: income,
“key_metrics”: key_metrics,
“news”: news,
“press”: press,
}
def run(
settings: Any,
*,
days_ahead: int = 7,
limit: int = 10,
symbols: Optional[List[str]] = None,
include_estimates: bool = False,
include_financials: bool = False,
include_news: bool = False,
) -> None:
“”“
Two modes:
1) Calendar mode (default): pull upcoming earnings, then build briefs.
2) Watchlist mode: pass symbols=[...].
“”“
client = FmpClient(api_key=settings.api_key, base_url=settings.base_url)
out_dir = Path(getattr(settings, “out_dir”, “output/briefings”))
out_dir.mkdir(parents=True, exist_ok=True)
events_by_symbol: Dict[str, Dict[str, Any]] = {}
if symbols:
target_symbols = _dedupe_keep_order(symbols)
else:
start = date.today()
end = start + timedelta(days=days_ahead)
events = fetch_earnings_calendar(client, start, end)
for e in events:
sym = (e.get(”symbol”) or “”).strip().upper()
if sym:
events_by_symbol.setdefault(sym, e)
target_symbols = list(events_by_symbol.keys())[:limit]
if not target_symbols:
print(
“No symbols returned.\n”
“Confirm your base URL is https://financialmodelingprep.com/stable and your API key is valid.\n”
“If you are on the free tier, some datasets may be restricted.”
)
return
for i, sym in enumerate(target_symbols, start=1):
bundle = build_bundle(
client,
sym,
event=events_by_symbol.get(sym),
include_estimates=include_estimates,
include_financials=include_financials,
include_news=include_news,
)
md = render_markdown(bundle)
out_path = out_dir / f”{sym}.md”
out_path.write_text(md, encoding=”utf-8”)
print(f”[{i}/{len(target_symbols)}] wrote {out_path}”)This is where the engine becomes a repeatable workflow. The code above basically does the following actions:
Pull a calendar window. Call
earnings-calendarwithfromandto. This yields upcoming and past earnings events, including EPS fields when available.Extract symbols and de-duplicate. Read the
symbolfield from the calendar results. De-duplicate while preserving order. Apply a smalllimitso runs remain predictable.Fetch a fixed dataset set per ticker. Use the same calls for every symbol. Start with the essentials, then treat deeper fundamentals as optional.
profile?symbol=...for sector and market cap style snapshot fields.analyst-estimates?symbol=...&period=...&page=...&limit=...for revenue and EPS expectations. Optionallyincome-statementfor trend context andkey-metricsfor compact KPI blocks.Normalize into a stable bundle schema. Map responses into one predictable shape, then pass that shape downstream. Missing datasets are represented as
{}or[]. This keeps rendering stable even when some endpoints return no data on a given plan.Write one artifact per ticker. For each bundle, call the renderer and save the Markdown into
output/briefings/.
If you also support watchlists, you can populate event context using the per-company earnings endpoint, then reuse the same bundle and rendering path.
Step 6: Render the one-page brief in app/render_markdown.py
Next, we set up the render_markdown.py with the following code:
from __future__ import annotations
from datetime import datetime
from typing import Any, Dict, List, Optional
Json = Dict[str, Any]
def _first_dict(x: Any) -> Json:
if isinstance(x, list) and x and isinstance(x[0], dict):
return x[0]
if isinstance(x, dict):
return x
return {}
def _as_list_of_dicts(x: Any) -> List[Json]:
if isinstance(x, list):
return [i for i in x if isinstance(i, dict)]
return []
def _get_first_present(d: Json, keys: List[str], default: Any = “N/A”) -> Any:
for k in keys:
v = d.get(k)
if v is not None and v != “”:
return v
return default
def _fmt_num(x: Any) -> str:
if x is None:
return “N/A”
try:
if isinstance(x, bool):
return “N/A”
if isinstance(x, (int, float)):
if abs(x) >= 1_000_000_000:
return f”{x/1_000_000_000:.2f}B”
if abs(x) >= 1_000_000:
return f”{x/1_000_000:.2f}M”
if abs(x) >= 1_000:
return f”{x:,.0f}”
return f”{x:.4g}”
xf = float(str(x).replace(”,”, “”))
return _fmt_num(xf)
except Exception:
return str(x)
def render_markdown(bundle: Json) -> str:
sym = bundle.get(”symbol”, “N/A”)
event = bundle.get(”event”) or {}
profile = bundle.get(”profile”) or {}
estimates = _as_list_of_dicts(bundle.get(”estimates”))
key_metrics = _as_list_of_dicts(bundle.get(”key_metrics”))
income = _as_list_of_dicts(bundle.get(”income”))
news = _as_list_of_dicts(bundle.get(”news”))
press = _as_list_of_dicts(bundle.get(”press”))
est0 = _first_dict(estimates)
km0 = _first_dict(key_metrics)
company = _get_first_present(profile, [”companyName”, “name”], sym)
sector = _get_first_present(profile, [”sector”], “N/A”)
# FIX: market cap key is commonly “marketCap” on profile payloads.
mcap = _get_first_present(profile, [”marketCap”, “mktCap”, “marketCapitalization”], None)
price = _get_first_present(profile, [”price”], None)
event_date = _get_first_present(event, [”date”, “earningDate”], “N/A”)
event_time = _get_first_present(event, [”time”, “timeEstimated”], “N/A”)
# Prefer analyst estimates if present, otherwise fall back to the calendar row.
eps_est = _get_first_present(est0, [”estimatedEps”, “epsEstimated”], None)
if eps_est in (None, “N/A”):
eps_est = _get_first_present(event, [”epsEstimated”, “estimatedEps”], None)
rev_est = _get_first_present(est0, [”estimatedRevenue”, “revenueEstimated”], None)
if rev_est in (None, “N/A”):
rev_est = _get_first_present(event, [”revenueEstimated”, “estimatedRevenue”], None)
lines: List[str] = []
lines.append(f”# Earnings Briefing: {company} ({sym})”)
lines.append(”“)
lines.append(”## Event”)
lines.append(f”- Date: {event_date}”)
lines.append(f”- Time: {event_time}”)
lines.append(”“)
lines.append(”## Snapshot”)
lines.append(f”- Sector: {sector}”)
lines.append(f”- Price: {_fmt_num(price)}”)
lines.append(f”- Market cap: {_fmt_num(mcap)}”)
lines.append(”“)
lines.append(”## Expectations”)
lines.append(f”- Estimated EPS: {_fmt_num(eps_est)}”)
lines.append(f”- Estimated revenue: {_fmt_num(rev_est)}”)
lines.append(”“)
# Only show these sections if you enabled them (or if your plan returns data).
if km0:
lines.append(”## Key metrics (latest)”)
lines.append(f”- P/E: {_fmt_num(km0.get(’peRatio’))}”)
lines.append(f”- Net margin: {_fmt_num(km0.get(’netProfitMargin’))}”)
lines.append(”“)
if income:
lines.append(”## Trend context”)
lines.append(”- Financial statements were fetched (see JSON bundle for details).”)
lines.append(”“)
if news or press:
lines.append(”## Recent context”)
if news:
lines.append(”- Stock news:”)
for n in news[:3]:
title = _get_first_present(n, [”title”], None)
pub = _get_first_present(n, [”publishedDate”, “date”], None)
if title:
lines.append(f” - {title}” + (f” ({pub})” if pub else “”))
if press:
lines.append(”- Press releases:”)
for p in press[:3]:
title = _get_first_present(p, [”title”], None)
pub = _get_first_present(p, [”date”, “publishedDate”], None)
if title:
lines.append(f” - {title}” + (f” ({pub})” if pub else “”))
lines.append(”“)
lines.append(”## Questions to listen for”)
lines.append(”- What changed in demand, pricing, or volume versus last quarter?”)
lines.append(”- What is driving margin movement?”)
lines.append(”- What guidance signals matter most for the next two quarters?”)
lines.append(”“)
lines.append(f”_Generated at {datetime.utcnow().strftime(’%Y-%m-%d %H:%M UTC’)}. Not financial advice._”)
return “\n”.join(lines)The renderer code above takes the bundle and produces a consistent Markdown page:
Event section uses the calendar row
Snapshot section uses profile fields
Expectations use analyst estimates, with optional fallback to calendar fields
Optional sections appear only when data exists
Step 7: Add entry points for two run modes
We keep the entry points thin:
run.pyfor “upcoming earnings” mode. It runs theearnings-calendarwindow and generates briefings for symbols in that window. We can tweak the cide as below:
from app.config import get_settings
from app.engine import run
if __name__ == “__main__”:
settings = get_settings()
run(settings, days_ahead=7, limit=10)run_watchlist.pyfor “watchlist” mode. It runs the same bundle and renderer, but starts from a fixed list of symbols:
from app.config import get_settings
from app.engine import run
WATCHLIST = [”AAPL”, “MSFT”, “NVDA”, “TSLA”]
if __name__ == “__main__”:
settings = get_settings()
run(settings, symbols=WATCHLIST)If you want watchlist mode to always show an earnings context, you can enrich it with the per-company earnings endpoint.
Step 8: Verify outputs and iterate safely
A successful run should produce one Markdown file per ticker under output/briefings/. For example, the result is shown below:
# Earnings Briefing: Shopify Inc. (SHOP)
## Event
- Date: 2026-02-11
- Time: N/A
## Snapshot
- Sector: Technology
- Price: 112.9
- Market cap: 147.38B
## Expectations
- Estimated EPS: 0.5
- Estimated revenue: 3.59B
## Questions to listen for
- What changed in demand, pricing, or volume versus last quarter?
- What is driving margin movement?
- What guidance signals matter most for the next two quarters?
_Generated at 2026-02-05 17:11 UTC. Not financial advice._If you see missing event dates, expand the calendar window. If you see missing expectations, confirm that estimates are enabled and available for those symbols. If you hit request limits, reduce the batch size or add caching. The Basic plan call limit is published in FMP’s plan comparison
That’s all you need to know on how to build an Earnings Briefing Engine using the FMP API.
Conclusion
In this article, we have learn on how to build an earnings briefing engine that reduces manual effort during earnings weeks by enforcing a repeatable workflow.
Using Financial Modeling Prep (FMP) as the primary data source, the process relies on a stable API to retrieve earnings events and selected supporting context, then we summarize the results into a standardized one-page briefing format that can be stored and reused.
In practice, this system will beuseful for maintaining a disciplined pre-earnings routine, supporting watchlist management during busy reporting weeks, and creating a written record of what to review before each announcement.
I hope it has helped!

