Live EarningsLive Earnings

API Documentation

Live, attributed earnings-call transcripts delivered as Server-Sent Events. 99% accuracy from Live Earnings Voice AI. Same wire format powers our own subscriber UI; no internal abstractions.

Quickstart

Every subscriber gets an API key (ek_*). Pass it via the Authorization: Bearer header on REST, or ?token= query on SSE (the browser's native EventSourcecan't set headers).

curl

# Live SSE stream
curl -N \
  -H "Authorization: Bearer ek_xxxxxxxxxxxxxxxxxxxxxxxx" \
  https://live-earnings.com/api/live/nvda-q4-fy26/stream

# Historical range
curl -H "Authorization: Bearer ek_xxxxxxxxxxxxxxxxxxxxxxxx" \
  "https://live-earnings.com/api/live/nvda-q4-fy26/events?from=0&to=100"

# Call metadata
curl -H "Authorization: Bearer ek_xxxxxxxxxxxxxxxxxxxxxxxx" \
  https://live-earnings.com/api/live/nvda-q4-fy26/meta

Python

from live_earnings import LiveEarningsClient

client = LiveEarningsClient(token="ek_xxxxxxxxxxxxxxxxxxxxxxxx")

# Call metadata
meta = client.meta("nvda-q4-fy26")
print(meta.ticker, meta.status)  # "NVDA" "ended"

# Live SSE stream — iterate over envelopes as they arrive
for env in client.stream("nvda-q4-fy26"):
    ev = env.event
    if ev.type == "attribution":
        print(f"[{ev.name}] {ev.text}")
    elif ev.type == "phase_transition":
        print(f"--- phase: {ev.to_phase} ---")
    elif ev.type == "done":
        break

# Historical backfill
for env in client.events("nvda-q4-fy26", from_=0, to=999_999):
    ...

Install: pip install live-earnings

TypeScript

import { LiveEarningsClient } from "live-earnings-sdk";

const client = new LiveEarningsClient({
  token: "ek_xxxxxxxxxxxxxxxxxxxxxxxx",
});

// Call metadata
const meta = await client.meta("nvda-q4-fy26");
console.log(meta.ticker, meta.status); // "NVDA" "ended"

// Live SSE stream — async iterator
for await (const env of client.stream("nvda-q4-fy26")) {
  const ev = env.event;
  if (ev.type === "attribution") {
    console.log(`[${ev.name}] ${ev.text}`);
  } else if (ev.type === "done") {
    break;
  }
}

// Historical backfill
const { events } = await client.events("nvda-q4-fy26", { from: 0, to: 1000 });

Install: npm i live-earnings-sdk

Reference

  • OpenAPI 3.1 spec — REST endpoints (/meta, /events, /sections, /report)
  • AsyncAPI 2.6 spec — Streaming endpoints (/stream, /partial-stream, /audio-stream — cycle 2 tiers only; contact Live Earnings for access)

Event types

Every envelope on /stream looks like:

{
  "tenantId": "live-earnings",
  "callSlug": "nvda-q4-fy26",
  "seq": 184,
  "publishedAt": "2026-05-19T22:14:03.142Z",
  "event": { ... }
}

seq is monotonic per (tenant, call). Use it to detect gaps after a reconnect — fetch the missed range via /events?from=<last_seq> then re-attach.

attribution — a finalized speaker turn

{
  "type": "attribution",
  "turnId": "nvda-q4-fy26-184",
  "name": "Jensen Huang",
  "source": "live-earnings",
  "text": "We want to take the great opportunity...",
  "phase": "qa",
  "sttSpeakerChange": "new"
}

name is the canonical speaker name. source is always "live-earnings" — no internal pipeline labels leak.

sttSpeakerChange — same speaker, or a new one

Every attribution and unknown event carries sttSpeakerChange: the speech engine's observation that this turn's speaker is the "same" as the previous turn's, "new", or "unknown" when it cannot tell.

  • Only calls on the Meta Muse engine carry same / new. Calls on AssemblyAI (a per-call option — Muse is the default) are always unknown. So is the first turn on each new Muse connection (the start, its 55-minute handover, a reconnect) and a turn right after one the engine could not label.
  • It is what the engine heard, not truth, and it never changes the speaker name. An operator's correction does not change it either.
  • samemeans "same as the turn just before". If you did not receive the turn with the previous turn number (turnId <slug>-<n-1>), treat it as unknown.
  • Measured against published transcripts on two calls: every real speaker change was marked new (104 of 104) — counting the changes that fell on a turn boundary; Muse merged 3 of TTWO's 76 into the previous turn, and those carry no mark —, so a same mark was never wrong (176 of 176). new was right about 94% of the time on turns of more than 5 words (101 of 108), but only 3 of 17 times on turns of 5 words or fewer.

unknown— a turn we couldn't attribute confidently

Same shape as attribution (including sttSpeakerChange) but without name. Typically followed by a rethink (same turnId, now with a name) within seconds.

phase_transition — section boundary

{
  "type": "phase_transition",
  "fromPhase": "ir_intro",
  "toPhase": "cfo_remarks",
  "triggerTurnId": "nvda-q4-fy26-37"
}

Phases: operator_opening, ir_intro, cfo_remarks, ceo_remarks, qa, closing.

turn_speaker_amend — late correction

// Seq 184 — original attribution
{ "seq": 184, "event": { "type": "attribution",
    "turnId": "t-200", "name": "Speaker A", "text": "..." } }

// Seq 213 — corrected later (dedicated event type)
{ "seq": 213, "event": { "type": "turn_speaker_amend",
    "turnId": "t-200", "newName": "Stacy Rasgon" } }

Post-publish corrections to a turn's speaker label arrive as their own event type — not as a second attribution. The amend references the original turnId and supplies newName. Apply in-place: the corrected turn keeps its original chronological position. An operator's correction beats the model's (by: "model"); otherwise the latest amend per turnId wins.

Names that arrive after the text.A call may publish each turn's text before its speaker is named: the turn arrives as an unknown with naming: "pending", and the name follows as a turn_speaker_amend with by: "model" (newName "unknown"when it could not be named). Show the text immediately, fill the name in place, and show a "corrected" marker only for operator amends. A model amend with basis: "early" means the full-text naming failed and newNameis the turn's early, first-sentence name, kept: show it as provisional.

Legacy note: calls that ran before 2026-05-21 may carry a second attribution event on the same turnId instead (the old amend path). Treat either pattern as a correction — the latest signal per turnId is canonical.

done — call ended

Close your stream connection on receipt.

Reconnecting mid-call

SSE connections drop. Reverse proxies idle them, browsers background-tab them, mobile networks fail. The wire contract is built so a reconnect never loses data — every envelope has a monotonic seq per (tenant, call), and any range is backfillable via GET /events.

Reconnect pattern

  1. Capture the highest seq you successfully consumed from /stream (the SSE id: line on each envelope, equal to envelope.seq).
  2. On disconnect: paginate GET /events?from=<last+1>&to=999999999 in a loop, following the nextFrom cursor untilnextFrom is null. Each response is capped at limit envelopes (default 1000, max 5000).
  3. Reopen the SSE connection. New envelopes resume from the live edge — there's no need to pass any reconnect token; the gap-fill above is authoritative.

Example

// 1) Track the highest seq we've consumed from /stream
let lastSeq = 0;
const stream = new EventSource(
  `https://live-earnings.com/api/live/${SLUG}/stream?token=${TOKEN}`
);
stream.addEventListener("envelope", (e) => {
  const env = JSON.parse(e.data);
  handle(env);
  lastSeq = env.seq;
});

// 2) On error/close, paginate /events from lastSeq+1
stream.onerror = async () => {
  stream.close();
  let from = lastSeq + 1;
  while (true) {
    const r = await fetch(
      `https://live-earnings.com/api/live/${SLUG}/events?from=${from}&to=999999999`,
      { headers: { Authorization: `Bearer ${TOKEN}` } }
    );
    if (!r.ok) throw new Error(`backfill ${r.status}`);
    const { events, nextFrom } = await r.json();
    for (const env of events) {
      handle(env);
      lastSeq = env.seq;
    }
    if (nextFrom === null) break;  // caught up
    from = nextFrom;               // continue pagination
  }
  // 3) Reopen the live stream from the live edge
  reconnect();
};

The 1000-envelope default is a real limit, not advisory. Long calls (90+ min, Q&A-heavy) commonly cross seq=2000; a single un-paginated fetch will silently truncate. Loop on nextFromor you'll lose turns.

Auth

A token grants access to one call. Cross-call use returns 401. The query-string form (?token=) is provided for browser EventSource; prefer the header form everywhere else.

Errors

  • 401 unauthorized — token missing, revoked, or scoped to a different call.
  • 403 forbidden — cookie-authed user without a subscription row for this call (predictions endpoint only).
  • 400 invalid_range — from > to on /events.
  • 404 not_found — unknown call slug.
  • 404 no_predictions — predictions endpoint only; no prediction_runs row exists for the call yet.
  • 5xx — internal error; retry with exponential backoff.

Operator audio ingest (operator-only)

Not part of the subscriber API: the operator console sends call audio to POST /api/live/{slug}/audio-ingest on the pipeline host, with the operator or ingest token. It answers:

  • 400 empty_chunk — the body had no audio.
  • 429 stt_suspended — the stray-capture guard suspended the call (reason: no_speech or too_long). Restart capture (?start=1) or Stop (DELETE) to clear it.
  • 500 stt_not_configured — no credentials for the call's engine.
  • 502 stt_connect_failed — the engine session could not be opened.
  • 503 stt_engine_unavailable— the call's engine setting could not be read; retried, so a later chunk may succeed.
  • 503 stt_engine_not_available— the stored engine is not in this build; change the call's engine.

The capture rig's WebSocket (/audio-ingest-ws/{slug}, currently unused) closes with 4500 (not configured), 4503 (session setup failed), 4504 (engine setting unreadable — transient), 4505 (engine not available — permanent) or 4506 (capture suspended). Full contract: openapi.yaml and asyncapi.yaml.

Versioning & stability

The wire format is stable per the published OpenAPI / AsyncAPI specs. We treat shipped subscribers' consumers as a contract, not a hypothesis.

  • Additive changes (new event types, new optional fields, new endpoints) ship as minor revisions. Existing consumers ignore unknown fields and unknown event types per the long-standing JSON convention — code defensively.
  • Breaking changes (removed fields, renamed types, shape-incompatible alterations) are pre-announced via your subscriber email at least 30 days in advance and ship behind a ?version= query parameter for the transition window. Example: if we rename turnId → turn_id, the new shape lands at ?version=v1.1 alongside the existing v1.0 for ≥ 90 days before v1.0 is retired.
  • The Changelog below lists every shipped change that affects the wire — additive entries are informational; any breaking change will be called out explicitly with the transition window.

Production integrations should pin the OpenAPI / AsyncAPI spec version they were built against and re-validate when they upgrade. We don't version the URL; we version the wire shape.

Changelog

  • 2026-05-26 — Auth required: GET /api/live/<slug>/report now requires a valid subscriber token (Bearer header or ?token= query) — mirrors /meta + /events. Returns 401 without a token. Endpoint behavior, response shape, and tenant scoping are unchanged. Documented in the OpenAPI spec linked from Reference.
  • 2026-05-26 — New endpoint: GET /api/live/<slug>/predictions. Returns the latest set of predicted analyst questions for a call. See "Predictions" above. V1 ships Drop 1 (pre-call) only; Drops 2 (end of prepared remarks) and 3 (per-Q&A) layer onto the same response shape as fast-follow.
  • 2026-09-24 — Names after text: unknown may carry naming: "pending", and turn_speaker_amend carries by (model | operator). SDKs 0.3.0.
  • 2026-09-25 — Kept early names: a by: "model" amend may carry basis: "early" (the full-text naming failed; the early name was kept). Additive. SDKs 0.3.0.
  • 2026-05-21 — New event type: turn_speaker_amend. Post-publish speaker corrections now ship as a dedicated event rather than a duplicate attribution. See "Late corrections" above. Legacy duplicate-attribution entries remain in historical calls.
  • 2026-05-21— End-of-turn silence threshold tuned from 400 ms → 300 ms. Tighter Q&A handoffs; slightly more turns per call. Wire shape unchanged.
  • 2026-05-21— Per-session insight cap raised from 50 → 200. Long Q&A sections no longer go silent on ai_insight mid-call.

What's next

Cycle-2 hardening. Items below are committed to ship before the next paid pilot (target: early June 2026). Wire shape changes will be additive — no breaking field removals.

  • Connection-state badge on /stream. Subscribers can tell at a glance whether their stream is live, replaying gaps, or stalled. Today this requires watching seqdrift; soon it's an explicit signal.
  • /partial-stream reconnect with Last-Event-ID. The live (provisional) partial-transcript stream gets the same gap-replay semantics /stream already has — no more dropped partials when a reverse proxy idles your connection.
  • Phase-aware silence threshold.The ASR's end-of-turn timer adapts to phase context: tighter during Q&A (analysts cut in fast), looser during prepared remarks (executives pause). Reduces both turn-fusion and over-fragmentation. No wire change; better attribution accuracy.
  • Post-call quality report. Auto- generated within 30 sec of done: attribution latency p50/p95/worst, named-attribution rate, unknown rate, correction rate per call. Reachable via GET /api/live/<slug>/report.
  • Confidence on attribution events. Internal scoring will surface on the public wire as an optional confidencefield (0–1). Existing consumers ignore it; agents that care can gate on a threshold.