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.
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).
# 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
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
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
/meta, /events, /sections, /report)/stream, /partial-stream, /audio-stream — cycle 2 tiers only; contact Live Earnings for access)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 oneEvery 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.
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.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.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 confidentlySame 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 endedClose your stream connection on receipt.
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.
seq you successfully consumed from /stream (the SSE id: line on each envelope, equal to envelope.seq).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).// 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.
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.
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.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.
The wire format is stable per the published OpenAPI / AsyncAPI specs. We treat shipped subscribers' consumers as a contract, not a hypothesis.
?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.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.
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.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.
/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.done: attribution latency p50/p95/worst, named-attribution rate, unknown rate, correction rate per call. Reachable via GET /api/live/<slug>/report.confidencefield (0–1). Existing consumers ignore it; agents that care can gate on a threshold.