PlioLive Earnings

API Documentation

Live, attributed earnings-call transcripts delivered as Server-Sent Events. 99% accuracy from Plio 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://pliioai.com/api/live/nvda-q4-fy26/stream

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

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

Python

from plioai_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 plioai-live-earnings

TypeScript

import { LiveEarningsClient } from "@plioai/live-earnings";

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 @plioai/live-earnings

Reference

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

Event types

Every envelope on /stream looks like:

{
  "tenantId": "plio-live",
  "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": "plio-live",
  "text": "We want to take the great opportunity...",
  "phase": "qa"
}

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

unknown— a turn we couldn't attribute confidently

Same shape as attribution 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.

ai_insight — model-surfaced moment

{
  "type": "ai_insight",
  "turnId": "nvda-q4-fy26-211",
  "insight": "Q1 FY27 revenue guide of $43B is +5.5% QoQ — above sell-side consensus of $41.6B. Watch the gross-margin commentary on the next CFO turn.",
  "category": "guidance",
  "frame": "Data-center momentum vs. sequential growth slowdown narrative.",
  "watchFor": "Whether Colette frames this as Hopper transition or Blackwell pull-in."
}

Fires alongside high-signal turns — guidance moves, analyst pressure, CFO numbers, deviation from prior guidance. turnId references the attribution event the insight is anchored to. category is a coarse bucket (e.g. guidance, surprise, pressure). frame and watchFor are optional additional context; consume only the fields you need.

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. Latest amend per turnId wins.

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://pliioai.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://pliioai.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.

Predictions

A separate, non-streaming endpoint returns the latest set of predicted analyst questions for a call. Useful for IR-side dashboards that surface predicted Q&A before and during the event.

Endpoint

GET https://pliioai.com/api/live/{slug}/predictions
Authorization: Bearer ek_xxxxxxxxxxxxxxxxxxxxxxxx

Returns the latest prediction_runs row for drop_number=1 plus its prediction_items ordered by rank ascending. Prefers status="complete" runs; falls back to the latest pending / error run so consumers can render in-flight states.

When predictions fire (drop model)

Predictions are generated at three points relative to the call. Only Drop 1 is live in production today; Drops 2 and 3 layer onto the same response shape and ship as fast-follow.

  • Drop 1 — pre-call. Generated ~25-30 min before broadcast, anchored to the press release plus the per-analyst behavioral corpus. LIVE.
  • Drop 2 — end of prepared remarks. Re-ranks with delta callouts based on what management actually said. NOT YET SHIPPED.
  • Drop 3 — per-Q&A. Re-ranks remaining-analyst predictions after each answered turn. NOT YET SHIPPED.

Response shape

{
  "run": {
    "id": "e1786274-517e-47c2-8e51-78358981877d",
    "tenantId": "plio-live",
    "callSlug": "mrvl-q1-fy27",
    "dropNumber": 1,
    "runSeq": 2,
    "status": "complete",
    "createdAt": "2026-05-27T20:18:42.123Z",
    "errorMessage": null
  },
  "items": [
    {
      "id": "12ab34cd-...",
      "rank": 1,
      "analystName": "Vivek Arya",
      "analystFirm": "Bank of America",
      "predictedQuestion": "Matt, you guided custom to double in FY27...",
      "whyReasoning": "Arya pairs a near-term clarifier with a strategic exclusivity probe...",
      "peerEvidence": [
        { "company": "Marvell", "quarter": "q4-fy26", "question": "..." },
        { "company": "Marvell", "quarter": "q3-fy26", "question": "..." }
      ],
      "triggerLanguage": "custom business reached $1.5 billion in fiscal 2026...",
      "bestAnswerFrame": "Lead with PO coverage on next-gen XPU...",
      "likelyFollowUp": "So if the second customer slips a quarter or two...",
      "confidence": 0.82
    }
  ]
}

Item fields

  • rank — integer 1-N, unique. 1 = highest joint likelihood × strategic importance.
  • analystName / analystFirm — the analyst most likely to ask.
  • predictedQuestion— literal question phrased in the analyst's voice.
  • whyReasoning — why this analyst, why this question, why now.
  • peerEvidence— cross-company precedents where the same analyst (or a peer) asked an analogous question on another company's call.
  • triggerLanguage — verbatim release sentence that motivates the question.
  • bestAnswerFrame — how mgmt should frame the answer.
  • likelyFollowUp — what the analyst likely asks if mgmt is evasive.
  • confidence0.00-1.00 joint probability estimate.

Polling

complete runs ship with Cache-Control: private, max-age=10; pending and error runs ship no-cache. Consumer pattern: poll every ~10 s while the run is pending or the endpoint returns 404; oncecomplete, cache the result.

async function poll(slug, token) {
  const r = await fetch(`https://pliioai.com/api/live/${slug}/predictions`, {
    headers: { Authorization: `Bearer ${token}` },
  });
  if (r.status === 404) return { run: null, items: [] };
  if (!r.ok) throw new Error(`predictions ${r.status}`);
  return r.json();
}

const data = await poll("mrvl-q1-fy27", TOKEN);
if (data.run?.status === "complete") {
  // render data.items, sorted by rank ASC
}

Full field-by-field contract in the OpenAPI spec (PredictionsResponse, PredictionRun, PredictionItem, PeerEvidence schemas).

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.

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 turnIdturn_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-26Auth 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-26New 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-05-21New 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.