GlimpseEngine · API Reference · V1

Eros Live API Reference

Technical reference for live video contribution, official match events, and commentary delivery as timed audio or multilingual subtitle text.

HTTPS + JSONSRT + MPEG-TSUTC + media PTSUpdated 2026-09-04

1. Integration overview

Eros receives one continuous encoded live video stream over SRT. Official match facts arrive independently over HTTPS. Commentary audio is available either as timed callback instructions or as continuous low-latency frames over WebSocket.

Receive credentials
Provisioning supplies the applicable HTTPS and WebSocket endpoints, role-specific tokens, SRT publish URL, and callback HMAC secret.
Publish video and events
Start the SRT contribution, then post official events against the same match and media timeline.
Play commentary audio
Use the provisioned callback or WebSocket surface and align playback to the active video timeline.
Base URL. Use the HTTPS API base URL supplied during onboarding. Examples below use the non-production placeholder https://api.example.com; the hostname serving this documentation is not automatically the API hostname. Credentials shown are placeholders.

GET/health/ready

No authentication. A 200 response with {"status":"ready"} means the integration endpoint is ready; 503 means retry later.

2. Authentication

HTTPS requests use a role-specific Bearer token. Never put tokens in query strings, browser code, screenshots, or logs.

Authorization: Bearer <token-issued-for-this-endpoint>
Content-Type: application/json
CredentialUsed forHandling
Event tokenOfficial match event submissionServer-to-server only
Receipt tokenAudio playback receiptsServer-to-server only
Commentary stream tokenContinuous WebSocket audioRead-only; server-side WebSocket clients only
SRT publish URLLive video contributionContains a secret stream ID; treat the complete URL as a password
Callback HMAC secretVerify requests sent by ErosStore in a secret manager; do not send it over the API

3. SRT live video

SRT carries the encoded live video stream, not individual decoded frames. The contribution is normally MPEG-TS containing H.264/H.265 video and optional audio. Eros preserves source timestamps and extracts frames downstream for AI processing.

Recommended wording: “We receive the encoded live video stream over SRT and extract timestamped video frames downstream.”

FFmpeg example

ffmpeg -re -i input.ts \
  -map 0:v:0 -map 0:a? -c copy \
  -f mpegts \
  'srt://<host>:<port>?streamid=<issued-stream-id>&pkt_size=1316'

Use the issued URL verbatim. Do not rewrite or log the streamid. Keep source PTS monotonic. A reconnect may create a new stream epoch; event and playback integrations must use the active epoch supplied in Eros messages.

ItemContract
TransportSRT caller/publisher mode
ContainerMPEG-TS
VideoH.264 recommended; confirm H.265 support before use
TimestampsContinuous source PTS in milliseconds after normalization
FramesNo per-frame HTTP upload is required from the source video publisher

4. Official match events

POST/v1/events

curl -X POST 'https://api.example.com/v1/events' \
  -H 'Authorization: Bearer <event-token>' \
  -H 'Content-Type: application/json' \
  -d '{
    "match_id":"bundesliga-2026-001",
    "event_id":"provider-shot-88421",
    "revision":1,
    "canonical_id":"attack-991-shot-1",
    "source_pts_ms":4382000,
    "received_pts_ms":4382600,
    "period":"2",
    "event_type":"SHOT",
    "team_id":"M05",
    "player_id":"M05:10",
    "player_name":"Amiri",
    "outcome":null,
    "status":"confirmed",
    "temporal_scope":"live",
    "authority":"official",
    "source":"provider_name",
    "stage":"attempt",
    "payload":{"match_clock":"73:02"}
  }'

Accepted events return 202:

{"accepted":true,"duplicate":false,"fact_ref":"match-event:provider-shot-88421:r1"}

Commentary profile

Request output.commentary_profile at match creation: "betting" for event-focused factual lines or "tv" for richer objective play descriptions and verified background. Neither promises continuous speech. The output setting overrides match_package.commentary_profile, then the service default applies (source default betting; deployments may override it). Confirm the effective profile with the operator configuring the match. Output settings are fixed at creation; package preferences can change only before arming.

Idempotency and corrections

  • Retry the exact same body with the same event_id and revision; the response returns duplicate:true.
  • To correct or retract a fact, keep event_id, increment revision, and set status to corrected or retracted.
  • Attempt and result messages share canonical_id but use different event_id values.
  • received_pts_ms must be greater than or equal to source_pts_ms.
  • Events are accepted only while the corresponding match is live; otherwise the API returns 409.
  • authority: send "official" for your data feed. In a betting-profile match an event without authority is treated as official; in a tv-profile match it is treated as candidate. A candidate event is stored but its announcement is not guaranteed; the response then carries "delivery":"not_guaranteed_candidate_authority".
  • In a betting-profile match only betting-relevant event types are announced: GOAL, OWN_GOAL, PENALTY, PENALTY_AWARDED, PENALTY_MISSED, YELLOW_CARD, RED_CARD, SECOND_YELLOW, YELLOW_RED_CARD, SUBSTITUTION, OFFSIDE, FOUL, FREE_KICK, VAR, VAR_CHECK, VAR_DECISION, and — at most one every few seconds — CORNER, SHOT, SHOT_ON_TARGET, SAVE, DANGEROUS_ATTACK, KICK_OFF. Any other type is stored but not announced; the response then carries "delivery":"not_narrated_in_betting_profile" and a reason. A tv-profile match can consider other event types; confirmation, relevance, timing and delivery checks still apply. Acceptance is not a guarantee of narration.
  • A provisional goal must be sent with "status":"candidate" (not only authority): a confirmed goal is announced as a goal immediately.

5. Timed audio — callback transport

You choose the audio transport per match, on the create request. This section is the callback one; §6 is websocket. Declare it in output:
"output": { "mode": "audio", "languages": ["zh-CN"],
            "audio_transport": "callback",
            "audio_callback_url": "https://your-host/eros/audio" }
audio_transport has no default — omitting it in audio mode is a 400. The full integration guide for both transports is at /docs/audio.

Your service exposes an HTTPS callback URL and gives it to us on each create-match request. Eros sends JSON instructions using POST. Return any 2xx only after the instruction is durably accepted.

Changed 2026-09-11: the deployment-wide callback address (EROS_AUDIO_CALLBACK_URL) is retired. A callback match that does not carry its own audio_callback_url is refused — there is no fallback address any more. The URL must be https, must not carry credentials, and must resolve to a public address; we re-check it immediately before every send.

Headers and signature

Content-Type: application/json
User-Agent: eros-live-engine/1
X-Eros-Timestamp: 1787068800
X-Eros-Signature: sha256=<64-lowercase-hex>
Idempotency-Key: <match>:<epoch>:<segment>:r<revision>:<action>

Before parsing JSON, compute HMAC-SHA256 over the exact raw bytes:

signed_bytes = utf8(X-Eros-Timestamp + ".") + raw_request_body
expected = HMAC-SHA256(callback_secret, signed_bytes)

Compare signatures in constant time and reject stale timestamps. The standard integration window is five minutes.

upsert example

{
  "schema_version":"eros.audio-delivery.v1",
  "segment_schema_version":"eros.timed-audio.v1",
  "delivery_sequence":81,
  "action":"upsert",
  "match_id":"bundesliga-2026-001",
  "stream_epoch":"epoch-...",
  "segment_id":"speech-acde1234",
  "revision":1,
  "source_pts_ms":51600,
  "target_start_pts_ms":51600,
  "expires_pts_ms":61600,
  "duration_ms":4500,
  "priority":2,
  "codec":"wav",
  "sample_rate":24000,
  "channels":1,
  "trace_id":"frame-52",
  "payload_bytes":216044,
  "content_sha256":"<64-lowercase-hex>",
  "idempotency_key":"bundesliga-2026-001:epoch-...:speech-acde1234:r1",
  "audio_object_key":"timed-audio/.../speech-acde1234-r1.wav",
  "audio_url":"https://objects.example.com/...signed...",
  "commentary_text":"The ball reaches the left side of the box.",
  "emotion":"Calm"
}
  • Download audio_url immediately and verify both payload_bytes and content_sha256.
  • Schedule against target_start_pts_ms; do not play immediately based on HTTP arrival time.
  • Drop audio that reaches expires_pts_ms before playback and report failed.
  • action=cancel withdraws an unplayed revision and has no audio_url.
  • emotion is the delivery mood of the line, always one of Calm, Tension, Nervous, Drama, Excited (goal); replays are always Calm. The synthesized voice already follows it, and the matching subtitle line carries the same value.
  • Apply instructions for the same match, epoch, segment, and revision in delivery_sequence order. Persist the highest applied sequence for that identity and ignore a lower sequence that arrives later, so a retried upsert cannot undo a newer cancel.
  • Deduplicate Callback retries using the five-part Idempotency-Key header.
Two different keys. The Callback header key includes the action. Playback receipts must use the four-part idempotency_key from the JSON body.

6. Timed audio — websocket transport

The counterpart to §5: the same timed-audio segments, but you connect to us and the WAV bytes are inlined in the message. There is no inbound leg, so you do not need to host a public endpoint or configure object storage.

"output": { "mode": "audio", "languages": ["zh-CN"], "audio_transport": "websocket" }

wss://<host>/v1/matches/{match_id}/audio/stream?after_sequence=0
Authorization: Bearer <audio receipt token>

One delivery is one self-contained JSON message: every field from §5 plus delivery_sequence, with audio_url replaced by audio_base64. Receipts (§8) go back up the same socket and are confirmed with a receipt_ack.

Raise your client’s maximum message size to 32 MiB. A delivery is a whole WAV segment in base64. Most WebSocket libraries default to 1 MiB, and exceeding it does not drop one message — it closes the whole connection with 1009. Also treat close code 1008 as fatal: a bad token does not fail the handshake, the connection is established and then closed.

Details, reconnection and the full field list: /docs/audio.

7. Continuous PCM frames (v2 lane)

This is not the audio delivery transport. §5 and §6 carry finished, timed WAV segments. This section is a separate, lower-level lane that emits raw PCM frames for a deployment running the v2 playout mode — if you are integrating timed audio, you want §5 or §6.

The WebSocket surface emits continuous, low-latency commentary frames. Each binary WebSocket message contains one MessagePack-encoded eros.commentary-frame.v2 envelope with a 20 ms PCM payload.

Connect

wss://<stream-host>/v2/commentary/stream?stream_epoch=<active-epoch>&after_sequence=<last-processed-sequence>

Authorization: Bearer <commentary-stream-token>

The Authorization header is required during the WebSocket handshake. Use a server-side WebSocket client that supports custom handshake headers. On the first connection, set after_sequence=-1; the server starts with the latest retained batch rather than replaying the entire retention window.

Decoded MessagePack envelope

{
  "type":"commentary_frame",
  "schema_version":"eros.commentary-frame.v2",
  "match_id":"bundesliga-2026-001",
  "stream_epoch":"epoch-...",
  "sequence":2581,
  "source_pts_ms":51600,
  "duration_ms":20,
  "codec":"pcm_s16le",
  "sample_rate":24000,
  "channels":1,
  "speaker_id":"voice-main",
  "lane_id":"commentary.main",
  "trace_id":"frame-52",
  "voice_active":true,
  "payload":"<MessagePack binary: 960 bytes>"
}

Audio is signed 16-bit little-endian PCM at 24 kHz, mono. A normal frame contains 480 samples and 960 payload bytes. Play frames in increasing sequence order and keep the highest fully processed sequence.

Reconnect and discontinuities

  • Reconnect with the active stream_epoch and the highest processed after_sequence; the server resumes from the next retained frame.
  • A discontinuity message with reason stream_epoch_changed identifies a new active epoch. Clear buffered audio and reconnect using that epoch.
  • A discontinuity message with reason retention_window_exceeded includes first_available_sequence. Clear stale buffers and reconnect with after_sequence = first_available_sequence - 1; the cursor is exclusive, so using the boundary itself would skip one frame.
  • Close code 1012 follows a discontinuity. Close code 1013 means the consumer fell too far behind; reconnect from the latest known sequence.
  • Close code 1008 indicates an invalid credential or query parameter and must not be blindly retried.
Delivery mode. When WebSocket streaming is enabled alongside timed callbacks, callbacks remain the authoritative delivery path unless the environment is explicitly provisioned as WebSocket-only.

8. Playback receipts

POST/v1/audio/receipts

curl -X POST 'https://api.example.com/v1/audio/receipts' \
  -H 'Authorization: Bearer <receipt-token>' \
  -H 'Content-Type: application/json' \
  -d '{
    "idempotency_key":"bundesliga-2026-001:epoch-...:speech-acde1234:r1",
    "status":"played"
  }'
StatusWhen to use it
playedAudio actually finished playing. Only this status records the commentary fact as spoken.
cancelledThe player successfully cancelled the audio before playback.
failedDownload, integrity check, decode, expiry, scheduling, or playback failed.

Success returns 202. Retrying the same key and status is safe and returns duplicate:true; changing the status for an existing key returns 400.

9. Subtitle delivery

Subtitle mode delivers commentary as text lines instead of audio. Each line is one JSON envelope, matching eros.subtitle.v2, delivered once per configured language. There are two transports — poll and WebSocket — and they share one sequence space per match and stream epoch, so a client may mix transports without losing or duplicating lines.

Envelope

{
  "schema_version":"eros.subtitle.v1",
  "match_id":"bundesliga-2026-001",
  "stream_epoch":"epoch-...",
  "sequence":2581,
  "line_id":"line-acde1234",
  "source_pts_ms":51600,
  "priority":1,
  "language":"en",
  "text":"Amiri shoots.",
  "trace_id":"frame-52",
  "emitted_unix_ms":1787068800000,
  "latency_ms":5100,
  "audio_idempotency_key":null,
  "target_start_pts_ms":null,
  "duration_ms":null,
  "emotion":"Tension"
}

emotion is the delivery mood of the line, always exactly one of Calm (routine play, substitutions, replays, background), Tension (shot, foul, free kick, corner, yellow card), Nervous (outcome still open: VAR check, penalty about to be taken, a chance developing), Drama (penalty awarded, red card, big save, goal ruled out) or Excited (goal). It is set from the confirmed match situation the line reports, never empty; treat an unrecognised value as Calm.

Client-side interruption rule (the one rule to implement). When a new line arrives whose priority is numerically lower than the line currently being spoken or displayed, fade that line out and speak/show the new one immediately. Otherwise, queue the new line behind what is currently active. Lower numbers are more important.
priorityMeaning
0Official event (from §4 match events)
1Corroborated shot, save, or goal
2Other action and replay
3Tactical, evaluative, or background commentary

GET/v1/matches/{match_id}/subtitles

curl 'https://api.example.com/v1/matches/bundesliga-2026-001/subtitles?after_sequence=2580&language=en' \
  -H 'Authorization: Bearer <commentary-stream-token>'

Returns lines with sequence > after_sequence for the requested language:

{
  "match_id":"bundesliga-2026-001",
  "stream_epoch":"epoch-...",
  "language":"en",
  "subtitles":[ { "schema_version":"eros.subtitle.v2", "sequence":2581, "...":"..." } ]
}

language is required; there is no default. after_sequence defaults to 0 on first poll.

WebSocket

wss://<stream-host>/v1/matches/{match_id}/subtitles/stream?language=en&after_sequence=2580

Authorization: Bearer <commentary-stream-token>

Uses the same commentary stream token as §7. The server pushes each subsequent line as one JSON text frame — not MessagePack, since the envelope is already JSON and there is no reason to add a second encoding for this transport. Close code 1008 at handshake means a missing or wrong token, a missing language, or an invalid after_sequence; it must not be blindly retried.

Sequence gaps are not packet loss

after_sequence is an exclusive cursor: requesting after_sequence=2580 returns sequence 2581 and above. sequence is shared across languages — the same commentary line carries the same sequence number in every language — but a client may legitimately observe gaps in its own language's stream. When a line fails translation into one language, that language simply does not receive that sequence number; the line still exists (and was delivered) in every language where translation succeeded. A missing sequence number is evidence of a translation failure for that language, not evidence of a dropped delivery, and must not trigger a retry against the poll or WebSocket surface — there is nothing to retry.

Append-only: what does not exist

Unlike timed audio (§5), the subtitle stream has no revision protocol. A delivered line is never corrected, retracted, or withdrawn. Concretely, the envelope omits:

  • No action field — there is only one kind of delivery.
  • No revision — a line is delivered exactly once per language and never re-delivered with different text.
  • No cancellation — there is no equivalent of the audio callback's action=cancel.
  • No playback receipt — there is no equivalent of §8. Delivery bookkeeping on the Eros side completes the moment a line is admitted onto the stream; the client is not required to report anything back.
⚠️ The customer's own text-to-speech time is not inside our 8-second budget. Our measured latency (p50 5.1s / p95 7s) is measured to the moment a line's text is emitted by Eros — not to the moment audio reaches a listener. If a client synthesizes speech from these lines, total audience-facing latency is our latency + the client's TTS synthesis time + the client's own playback scheduling. Concretely: if the client does not add a matching delay to its video, the synthesized commentary will trail the picture by roughly 6–9 seconds. This is a property of the architecture — text delivery and audio synthesis are two separate, sequential steps — and not a defect in either side. It must be accounted for during integration planning, typically by delaying video playback by an amount that absorbs the client's own TTS and scheduling time, so that video and synthesized audio arrive together.

10. HTTP and retry rules

HTTPMeaningRecommended action
200 / 202Request acceptedContinue; preserve the idempotency key
400Invalid JSON, fields, timestamps, or conflicting receiptFix the request; do not blind-retry
401Missing or invalid credentialStop and rotate/check the assigned credential
404Route or match not foundConfirm environment and match identity
409Match is not live or state conflictRefresh live state before retrying
429 / 5xxTemporary capacity or service failureRetry the identical request with capped exponential backoff and jitter

All JSON timestamps are UTC ISO 8601 unless a field is explicitly named *_unix_ms. All *_pts_ms values are milliseconds on the active source-media timeline, not Unix time.