Live Earnings

API Documentation

Live earnings-call transcripts as Server-Sent Events: each turn's text as it is spoken, then who said it — the executive or the analyst and their firm. It is the same wire format our own subscriber app reads. Transcription and speaker naming are powered by Meta's Muse models by default.

Getting started

1. Two hosts

  • REST — https://live-earnings.com: /meta, /events, /sections, /report, /session-token.
  • Streams — https://stream.live-earnings.com: /stream, /partial-stream, /audio-stream. These hold a connection open for the whole call, so they are served separately; calling one on the REST host answers 503 stateful_route_wrong_origin.

Every path is /api/live/{slug}/…, where the slug names the call (e.g. nvda-q2-fy27).

2. Authenticate

Your API key (ek_…) grants access to one call: open the call at live-earnings.com/live, sign in, and copy it from the call's API Access tab. From a server, send it as a header on any endpoint, streams included:

Authorization: Bearer ek_xxxxxxxxxxxxxxxxxxxxxxxx

In a browser, EventSource cannot send headers, and a key in a URL ends up in logs and history. Mint a short-lived session token on your server and open the stream with it instead:

# On your server: exchange the key for a 2-hour stream token
curl -X POST -H "Authorization: Bearer $LE_KEY" \
  https://live-earnings.com/api/live/$SLUG/session-token
# → { "sessionToken": "…", "expiresIn": 7200 }
// In the browser
const es = new EventSource(
  `https://stream.live-earnings.com/api/live/${slug}/stream?t=${sessionToken}`
);
es.addEventListener("envelope", (e) => handle(JSON.parse(e.data)));

A session token covers only that call and is checked when a stream opens: a stream already open keeps running until it closes (at 60 minutes at the latest — see Reconnecting), and after 2 hours a new connection with the same token answers 401. So before the 2 hours are up, mint a fresh token and reconnect with it — EventSource's own automatic reconnect reuses the old URL. An invalid ?t= never falls back to any other credential. A browser page on your own domain can stream from https://stream.live-earnings.com only once we have added your origin; until then, stream from your server.

3. Read turns: the text first, the name after

A turn's text is published as soon as it is final; who said it follows seconds later. Keep one row per turnId and fold three events into it:

  • unknown — the text, with the name still to come (naming: "pending"). Show it unnamed.
  • turn_speaker_amend — the name for an earlier turn. newName: "unknown" means it stays unnamed. An amend without by, or with by: "operator", is a human correction and beats any later by: "model" amend. basis: "early" marks a provisional name.
  • attribution — text and name together. Some calls publish this way instead, and replays of older calls do.
const turns = new Map(); // turnId → { text, name, by, provisional }

function handle(env) {
  const ev = env.event;
  if (ev.type === "unknown" || ev.type === "attribution") {
    const t = turns.get(ev.turnId) ?? {};
    t.text = ev.text;
    if (ev.type === "attribution" && t.by !== "operator") {
      t.name = ev.name;
      t.provisional = false;
    }
    turns.set(ev.turnId, t);
  } else if (ev.type === "turn_speaker_amend") {
    const t = turns.get(ev.turnId) ?? {};
    const human = ev.by !== "model";
    if (human || t.by !== "operator") {        // a correction beats the model
      t.name = ev.newName === "unknown" ? undefined : ev.newName;
      t.by = human ? "operator" : "model";
      t.provisional = ev.basis === "early";
    }
    turns.set(ev.turnId, t);
  }
  render(turns);
}

Try it

export LE_KEY=ek_xxxxxxxxxxxxxxxxxxxxxxxx SLUG=nvda-q2-fy27

# The call and its roster
curl -H "Authorization: Bearer $LE_KEY" https://live-earnings.com/api/live/$SLUG/meta

# Everything so far — up to 1000 events per page; follow nextFrom
curl -H "Authorization: Bearer $LE_KEY" https://live-earnings.com/api/live/$SLUG/events

# Live, until the call ends (an ended call like this one answers 204:
# use the slug of a call that is live or about to start)
curl -N -H "Authorization: Bearer $LE_KEY" https://stream.live-earnings.com/api/live/$SLUG/stream

SDKs

TypeScript (Node 18+) and Python (3.9+) clients do the paging, the SSE framing and the three-event fold above (Transcript). Each quickstart prints the call so far, then each new turn as it is spoken — text first, the name when it arrives. SLUG is the call you copied the key from: a key opens that call only.

TypeScript

1. Install, in a new folder:

mkdir -p le-quickstart && cd le-quickstart
npm i live-earnings-sdk

2. Save this as quickstart.mjs in that folder:

import { LiveEarningsClient, LiveEarningsError, Transcript } from "live-earnings-sdk";

const client = new LiveEarningsClient({ token: process.env.LE_KEY });
const slug = process.env.SLUG;
const transcript = new Transcript();
const show = (turn) => turn && console.log((turn.name ?? "…") + ": " + (turn.text ?? ""));

// Everything so far…
let last; // the last seq seen; undefined = start the stream from now
let ended = false;
for await (const env of client.eventsAll(slug)) {
  show(transcript.apply(env));
  last = env.seq;
  if (env.event.type === "done") ended = true;
}
// …then live until the call ends. Connections drop (and are cut after an
// hour), so reconnect from the last seq seen; the server replays the gap.
while (!ended) {
  try {
    for await (const env of client.stream(slug, { lastEventId: last })) {
      if (env.event.type === "done") { ended = true; break; }
      show(transcript.apply(env));
      last = env.seq;
    }
  } catch (err) {
    // A 4xx (bad key, wrong call) will not fix itself; anything else might.
    if (err instanceof LiveEarningsError && err.status >= 400 && err.status < 500 && err.status !== 429) throw err;
  }
  if (!ended) await new Promise((r) => setTimeout(r, 2000));
}

3. Run it with your key and your call's slug in place of the placeholders:

LE_KEY=ek_YOUR_KEY SLUG=your-call-slug node quickstart.mjs

Python

1. Install, in a new folder:

mkdir -p le-quickstart && cd le-quickstart
python3 -m venv venv && . venv/bin/activate
pip install live-earnings

2. Save this as quickstart.py in that folder:

import os
import time

from live_earnings import LiveEarningsClient, LiveEarningsError, Transcript

client = LiveEarningsClient(token=os.environ["LE_KEY"])
slug = os.environ["SLUG"]
transcript = Transcript()


def show(turn):
    if turn:
        print(f"{turn.name or '…'}: {turn.text or ''}")


# Everything so far…
last = None  # the last seq seen; None = start the stream from now
ended = False
for env in client.events(slug):
    show(transcript.apply(env))
    last = env.seq
    ended = ended or env.event.type == "done"
# …then live until the call ends. Connections drop (and are cut after an
# hour), so reconnect from the last seq seen; the server replays the gap.
while not ended:
    try:
        for env in client.stream(slug, last_event_id=last):
            if env.event.type == "done":
                ended = True
                break
            show(transcript.apply(env))
            last = env.seq
    except LiveEarningsError as err:
        # A 4xx (bad key, wrong call) will not fix itself; anything else might.
        if 400 <= err.status < 500 and err.status != 429:
            raise
    if not ended:
        time.sleep(2)

3. Run it with your key and your call's slug in place of the placeholders:

LE_KEY=ek_YOUR_KEY SLUG=your-call-slug python quickstart.py

The SDKs send the key as a header, so they need no session token — that is for browsers. They do not reconnect by themselves: a stream that drops raises or ends before done (every stream is cut after an hour), so the quickstarts reconnect with the last seq seen (lastEventId / last_event_id) and the server replays what they missed. Package pages: npm, PyPI.

Endpoints

  • GET /meta — the call: ticker, fiscal period, status, and its roster of executives and analysts.
  • GET /events?from=&to=&limit= — stored envelopes by seq, oldest first. from defaults to 0, to to the last event, limit to 1000 (max 5000). When a page is capped, nextFrom is the from for the next request; it is null when you have everything.
  • GET /sections — the call's phase_transitionevents: where the prepared remarks and the Q&A begin.
  • GET /report — the post-call summary: runtime, turns, latency.
  • POST /session-token — a 2-hour token for streams (see Getting started).
  • GET /stream (stream host) — every envelope, live.
  • GET /partial-stream (stream host) — the turn in progress, word by word (see below).
  • GET /audio-stream (stream host) — the call audio as base64 16 kHz PCM chunks (pcm_base64).

Full contracts: OpenAPI 3.1 (REST) and AsyncAPI 2.6 (streams).

Event reference

Every envelope, on /stream and from /events:

{
  "tenantId": "live-earnings",
  "callSlug": "nvda-q2-fy27",
  "seq": 184,
  "publishedAt": "2026-11-19T22:14:03.142Z",
  "event": { "type": "…", … }
}

seq only ever increases within a call, but numbers are skipped (some events are internal) — a missing number is not a lost event. On /stream each envelope is an SSE message with event: envelope and id: <seq>. Lines starting with : are heartbeats (every 25 s) — ignore them.

unknown — text, name to follow

{
  "type": "unknown",
  "turnId": "nvda-q2-fy27-61",
  "text": "Thanks, Toshiya. Data center revenue was a record.",
  "phase": "cfo_remarks",
  "sttSpeakerChange": "new",
  "naming": "pending"
}

naming: "pending" means a turn_speaker_amend for this turnId will follow. Without it, no name is coming from us — though a person may still correct the turn later with an amend.

turn_speaker_amend — a turn's name

{
  "type": "turn_speaker_amend",
  "turnId": "nvda-q2-fy27-61",
  "newName": "Colette Kress",
  "by": "model"
}
  • by: "model" — the name we worked out. newName: "unknown" if we could not.
  • by absent or "operator"— a person named it. It always wins over a model amend; our app shows it as "Verified by a person" unless reason is "sticky" or "bulk" (assigned to many turns at once).
  • basis: "early"— named from the turn's first sentence only. Show it as provisional.
  • Apply it in place: the turn keeps its position in the transcript.

attribution — text and name together

{
  "type": "attribution",
  "turnId": "nvda-q2-fy27-61",
  "name": "Colette Kress",
  "source": "live-earnings",
  "text": "Thanks, Toshiya. Data center revenue was a record.",
  "phase": "cfo_remarks",
  "sttSpeakerChange": "new"
}

source is always "live-earnings". An amend may still follow and correct the name.

phase_transition — a new section of the call

{
  "type": "phase_transition",
  "fromPhase": "ir_intro",
  "toPhase": "cfo_remarks",
  "triggerTurnId": "nvda-q2-fy27-12"
}

Phases: operator_opening, ir_intro, cfo_remarks, ceo_remarks, qa, closing, and unknown. The CEO's and CFO's prepared remarks come in either order — most calls are CEO-first.

done — the call ended

Close the connection when it arrives.

sttSpeakerChange

Every unknown and attribution carries it: whether the speech engine heard the "same" speaker as the turn before, a "new" one, or could not tell ("unknown"). It is the engine's observation, never a name, and never changes. A same mark refers to the turn numbered one lower — if you did not receive that turn, treat it as unknown. On our measurements a same mark has been reliable; a new mark on a very short turn (five words or fewer) is often wrong.

The turn in progress (/partial-stream)

/partial-stream sends the turn being spoken, growing word by word, before it is final — for live captions. Each SSE message is event: partial:

event: partial
data: {"turnOrder":62,"turnId":"nvda-q2-fy27-62","elapsedSec":1841,
       "transcript":"We expect gross margins to","speakerName":"Colette Kress","naming":"early"}

transcript is the whole text so far, not a delta. speakerName with naming: "early" is a provisional name from the turn's first sentence. The final text and name arrive on /stream under the same turnId; nothing here is stored, so there is no replay. A first event: hello message may list enabled features.

Reconnecting

Connections drop. EventSource reconnects by itself (the SDKs do not — see SDKs above) and sends Last-Event-ID (the last seq it saw); /stream then replays everything after it before going live, so nothing is lost — an event near the reconnect can arrive twice, so keep envelopes by seq. Other SSE clients: send the header yourself. To fetch history instead, page through /events:

let from = 0;
for (;;) {
  const r = await fetch(`https://live-earnings.com/api/live/${slug}/events?from=${from}`, {
    headers: { Authorization: `Bearer ${key}` },
  });
  const { events, nextFrom } = await r.json();
  events.forEach(handle);
  if (nextFrom === null) break;
  from = nextFrom;
}

A call that is over has no live stream. Once a call has ended and its last turn is 30 minutes old (or a scheduled call is 6 hours past its start and never ran), /stream and /partial-stream and /audio-stream answer 204 No Content; the SDKs in sdks/raise an error with status 204. Don't reconnect on a 204 — read the transcript from /events.

The server also closes a stream itself. When the call has ended, an event: ended message comes first: stop, since a reconnect gets 204. When the service moves to a new server process (a restart or deploy), and at 60 minutes (the platform's request limit), it just closes. Reconnect, and you land on the new process with the gap replayed. Reconnecting after 2 hours needs a freshly minted session token.

MCP server

Connect an AI assistant (Claude or any MCP client over Streamable HTTP) and ask about earnings calls in plain language — during a call (“who just asked about margins, and what did the CFO say?”) or after it (“summarize the Q&A by analyst”). Read-only.

Not a programmer? The step-by-step guide, with questions to ask: Use Live Earnings in Claude or ChatGPT.

Sign in with your Live Earnings account (recommended). One endpoint for every published call, no key. Use exactly this URL:

https://live-earnings.com/api/mcp

In claude.ai: Settings → Connectors → Add custom connector, paste the URL, sign in and Allow — the guide walks through it and its common errors.

You can ask about the listed calls: list_calls shows them, and every other tool takes the call as a call argument, except the company key-metrics tools, which take company (list_companies takes none), and list_analysts and get_analyst_history, which read across the calls you can see. Removing the connector stops that client; to end every connection to your account at once, contact support.

One call with its key (advanced). For scripts, headless clients or a call that is not listed: https://live-earnings.com/api/mcp/<slug> with Authorization: Bearer ek_…. Your call page's MCP Access tab has it: Copy, or Reveal key, gives it with your key filled in. For Claude Code:

claude mcp add --transport http live-earnings \
  https://live-earnings.com/api/mcp/<slug> \
  --header "Authorization: Bearer ek_…"

A client that takes only a URL can use https://live-earnings.com/api/mcp/<slug>/<key>; the key is then in the URL — treat it like a password. A wrong, revoked or other call's key is refused with 403.

  • list_calls — the calls you can ask about (sign-in endpoint only).
  • get_call — the ticker, period, status and named roster.
  • get_new_turns — follow a live call: call it again with since set to the cursor it returned (one cursor per call); a pending turn's name follows in seconds.
  • get_transcript — the whole call under the names in force; filter by phase or a seq range.
  • get_sections — where each section starts.
  • get_qa_index — the Q&A's operator handoffs with seq ranges, to summarize it by analyst.
  • get_live_link — the call's page (no key in it; sign in).
  • get_release — the company's earnings release: the sentences it leads with, verbatim, and a download link.
  • get_release_metrics — the company's key metrics as the call's Key metrics tab shows them: actuals, every guide, and our calculations, each figure with its source.
  • list_companies, get_company_metrics, get_metric_history, get_focus_sets — a company's key metrics across its calls, as of any moment (sign-in endpoint only; they take company).
  • list_analysts — analysts who asked questions on the calls you can see, with the companies where they asked and how many of their exchanges a person has checked (sign-in endpoint only). The rest is machine-generated.
  • get_analyst_history — one analyst's questions across companies, newest first, with automatic checks and a coverage note saying which calls are indexed (sign-in endpoint only). Read the full exchange with get_transcript.

More in docs/api.md.

Errors

  • 401 unauthorized — no key or token, a revoked key, a key for another call, or an expired session token.
  • 400 invalid_range — from is greater than to on /events.
  • 404 not_found — no call with that slug.
  • 503 stateful_route_wrong_origin — a stream requested on the REST host; use https://stream.live-earnings.com.
  • 5xx — retry with exponential backoff.

Versioning & stability

The wire format is the published OpenAPI and AsyncAPI specs. New event types, new optional fields and new endpoints can appear at any time: ignore what you do not recognise. Breaking changes are announced to subscribers at least 30 days ahead and ship behind a ?version= parameter for at least 90 days.

Changelog

  • 2026-09-27 — Streams have their own host, https://stream.live-earnings.com. Session tokens are documented; the raw key in a ?token= query is deprecated (use the header, or a session token). GET /events without to works (it returned 500). AI insights and predictions are withdrawn while we rebuild them: insight events are no longer sent and /predictions answers 404 feature_disabled. TypeScript and Python SDKs 0.4.0 are on npm (live-earnings-sdk) and PyPI (live-earnings).
  • 2026-09-25 — A model amend may carry basis: "early": a name from the turn's first sentence, kept when naming from the full text failed. /partial-stream may carry a provisional speakerName (naming: "early") and opens with a hello message.
  • 2026-09-24 — Text first, name after: turns arrive as unknown with naming: "pending" and are named by a turn_speaker_amend with by. This is now how most calls publish.
  • 2026-09-23 — Meta Muse became the default speech engine; sttSpeakerChange is on every turn.
  • 2026-09-19 — POST /session-token: short-lived stream tokens (?t=).
  • 2026-05-26 — GET /report requires a key, like /meta and /events.
  • 2026-05-21 — turn_speaker_amend replaced a second attribution as the way names are corrected. Calls from before then may still carry the old form: the latest name per turnId wins.