WebSocket and SSE updates
Distinguish dataset publication notices from network and SCP observation streams.
Publication notifications
wss://api.stellaratlas.io/v1/stream/data reports newly published dataset batches. It does not repeatedly send entire tables or stream every contract event.
Browser connections currently accept only the https://stellaratlas.io origin. This is the Explorer’s first-party update feed, not a general cross-origin subscription API.
A connection starts with {"version":1,"event":"ready"}. No subscribe message is required. Subsequent messages have this shape:
{
"version": 1,
"event": "datasets.published",
"batchId": "00000000-0000-4000-8000-000000000001",
"minLedger": 65239200,
"maxLedger": 65239263,
"publishedAt": "2026-10-31T21:30:00.000Z",
"datasets": ["history_contract_events", "history_transactions"]
}The values above illustrate the message format. minLedger and maxLedger are inclusive. datasets identifies affected tables; the notice does not contain their rows.
Refresh the affected view
From a StellarAtlas page, refresh one bounded event page after a relevant notice. This uses the shared historical example: only publications overlapping that example’s ledger window cause a read.
const eventUrl = 'https://api.stellaratlas.io/v1/analytics/contracts/CA526Y2NQWGWVVQ7RFFPGAZMU66PSYJ3UC2MTVAV4ZU7OM5BOPHDXUSG/events?limit=10&min_ledger=64877635&max_ledger=64877698&view=typed';
const feed = new WebSocket('wss://api.stellaratlas.io/v1/stream/data');
const seen = new Set();
let timer = null, request = null, pending = false, stopped = false;
function queueRefresh() {
if (stopped || timer !== null || request !== null) return;
timer = setTimeout(async () => {
timer = null;
if (stopped) return;
pending = false;
request = new AbortController();
const deadline = setTimeout(() => request?.abort(), 10_000);
try {
const response = await fetch(eventUrl, { signal: request.signal });
if (stopped) return;
if (!response.ok) throw new Error('HTTP ' + response.status);
const page = await response.json();
if (stopped) return;
console.log('Refreshed bounded event page:', page);
} catch (error) {
if (!stopped) console.error('Refresh failed; retain the previous page:', error);
} finally {
clearTimeout(deadline);
request = null;
if (pending) queueRefresh();
}
}, 250);
}
feed.onmessage = ({ data }) => {
if (stopped || typeof data !== 'string' || data.length > 4096) return;
let notice;
try { notice = JSON.parse(data); } catch { return; }
if (notice?.version !== 1 || notice.event !== 'datasets.published' ||
typeof notice.batchId !== 'string' || !Array.isArray(notice.datasets) ||
!notice.datasets.includes('history_contract_events')) return;
if (!Number.isSafeInteger(notice.minLedger) || !Number.isSafeInteger(notice.maxLedger) ||
notice.minLedger > notice.maxLedger) return;
if (notice.maxLedger < 64877635 || notice.minLedger > 64877698) return;
if (seen.has(notice.batchId)) return;
seen.add(notice.batchId);
while (seen.size > 256) seen.delete(seen.values().next().value);
pending = true;
queueRefresh();
};
feed.onclose = () => {
if (!stopped) console.info('Disconnected; recheck coverage before reconnecting.');
};
function stopUpdates() {
stopped = true;
if (timer !== null) clearTimeout(timer);
timer = null;
pending = false;
request?.abort();
feed.close();
seen.clear();
}
// Call stopUpdates() when the view closes.Notices coalesce for 250 milliseconds, with one request in flight and at most 256 remembered batch IDs. Each read has a ten-second deadline. This single-session example does not retry failed reads or reconnect automatically; call stopUpdates() on cleanup. A queued new notice may schedule the next bounded read.
Use the contract event REST or GraphQL query to retrieve actual events. Keep the contract, topic, order and page-size filters. Coalesce notices and deduplicate by batchId; do not download a complete dataset after every notice.
This JSON message protocol is separate from GraphQL requests. Do not send graphql-ws connection or subscription messages to this endpoint. Address arrays and protocol-specific event subscriptions are not filters on this publication feed.
Reconnect without losing query coverage
These notices are freshness hints, not a replayable event log. After reconnecting, recheck coverage and refresh the visible query. Continue historical event pagination using nextCursor, with unchanged filters.
Share one connection across active views, close it when no longer needed, and reconnect with capped backoff rather than a tight loop. A slow client may be closed with code 1013. WebSocket ping/pong heartbeats are handled by the browser; silence does not mean there are missing dataset rows.
Network and SCP Server-Sent Events
These separate HTTP feeds use text/event-stream, not the publication WebSocket protocol. Paths below use the same API base. Listen for named events with addEventListener; onmessage alone does not receive them.
| GET path | Named events | Data |
|---|---|---|
/v1/live | network, error | Network snapshot or an unavailable-snapshot message. |
/v1/scp-statements/live | scp-metadata, scp, error | Observation source/freshness metadata and SCP statement batches. |
The network feed attempts an initial snapshot, then polls every five seconds without overlapping reads. That interval does not prove the snapshot is fresh. HTTP 200 can still carry a named error event; it is not an empty successful snapshot. SCP connections at capacity return HTTP 503 before starting the stream. Check scp-metadata source and freshness before treating observations as current.
From a StellarAtlas page, this example retains the response as JSON text, closes on the first server or transport error, and leaves previous data available. Call its cleanup function when leaving the view. Native EventSource can reconnect automatically; closing it here prevents that retry. Browser HTTP CORS rules still apply.
const networkEvents = new EventSource('https://api.stellaratlas.io/v1/live');
let networkUpdatesStopped = false;
networkEvents.addEventListener('network', ({ data }) => {
if (networkUpdatesStopped) return;
console.log('Network snapshot JSON:', data);
});
networkEvents.addEventListener('error', (event) => {
if (networkUpdatesStopped) return;
stopNetworkUpdates();
const detail = 'data' in event ? event.data : 'SSE connection interrupted';
console.error('Network refresh unavailable; retain saved data:', detail);
});
function stopNetworkUpdates() {
if (networkUpdatesStopped) return;
networkUpdatesStopped = true;
networkEvents.close();
}
// Call stopNetworkUpdates() when the view closes.These streams do not publish wallet balances, asset holders or contract-event rows. They are not a failover replacement for unavailable analytics, and they do not implement Last-Event-ID replay. A reconnect is a new observation session, not proof of complete history. Keep historical pagination and coverage checks with the relevant REST or GraphQL query.
Network and SCP WebSocket channels
wss://api.stellaratlas.io/v1/live/ws is a separate observation protocol. Select comma-separated channels in the connection URL; do not send a JSON subscribe message or the publication feed’s ready/dataset protocol.
| Channel | Message types |
|---|---|
summary | summary and latestLedger |
network | network and latestLedger |
scp | scp, or scpGraph with scpMode=graph |
Messages use a type and payload; all connections may also receive heartbeat and error. Omitting channels selects network and full SCP, without summary. Omit scpMode for full SCP; scpMode=full is not accepted. Unknown/duplicate channels, repeated selectors or graph mode without SCP fail upgrade validation with HTTP 400.
SCP frames include source, freshness, freshnessMs, observedAt, cursor and truncated. For an explicit reconnect, pass both cursor fields as afterObservedAtMs and URL-encoded afterStatementHash, exactly once each. The timestamp is a nonnegative safe integer in milliseconds, not a ledger number. This resumes within retained observations ordered by observation time and statement hash; it is not arbitrary historical replay. The cursor affects SCP only; it does not rewind network, summary or latest-ledger snapshots. truncated=true means the bounded update may have more data; false does not prove complete network history.
Check source/freshness even after a successful connection. A latest-ledger message may identify a Horizon fallback; that is not live SCP sourcing from public RPC or analytics failover. A heartbeat indicates connection activity, not fresh or complete data. Server errors can arrive in an open connection; capacity/backpressure can close it with 1013, and send failures with 1011.
Unlike the publication feed, this retained network/SCP handler has no application-level Origin allowlist. HTTP CORS is not WebSocket authorization, and deployment/edge restrictions may still apply; this is not a live cross-origin acceptance claim.
The single-session example below selects summary and compact SCP. It inspects the envelope and logs original JSON text without converting string identifiers, slot indices or values to Number. It does not validate the full payload, automatically reconnect or advance a resume cursor. The frontend’s existing subscribeToLiveNetworkStream helper handles validation, shared subscriptions and bounded reconnects; its cursor resets when SCP mode changes or a hidden tab becomes visible again.
const networkSocket = new WebSocket('wss://api.stellaratlas.io/v1/live/ws?channels=summary,scp&scpMode=graph');
let networkSocketStopped = false;
networkSocket.onmessage = ({ data }) => {
if (networkSocketStopped || typeof data !== 'string') return;
let envelope;
try { envelope = JSON.parse(data); } catch { return; }
if (!envelope ||
typeof envelope !== 'object' ||
Array.isArray(envelope) ||
!('type' in envelope) ||
typeof envelope.type !== 'string') return;
if (envelope.type === 'error') {
console.error('Network/SCP read unavailable; retain saved data:', data);
return;
}
if (!['summary', 'latestLedger', 'scpGraph', 'heartbeat'].includes(envelope.type)) return;
if (envelope.type === 'scpGraph') {
if ('truncated' in envelope && envelope.truncated === true)
console.warn('Bounded SCP update; do not infer complete history.');
if ('freshness' in envelope && envelope.freshness !== 'fresh')
console.warn('SCP observations are not fresh; inspect metadata.');
}
console.log('Network/SCP message JSON:', data);
};
networkSocket.onerror = () => {
if (networkSocketStopped) return;
console.error('Network connection unavailable; retain saved data.');
stopNetworkSocket();
};
networkSocket.onclose = () => {
if (networkSocketStopped) return;
networkSocketStopped = true;
console.info('Network stream closed; recheck source/freshness before reconnecting.');
};
function stopNetworkSocket() {
if (networkSocketStopped) return;
networkSocketStopped = true;
networkSocket.close();
}
// Single session: call stopNetworkSocket() when the view closes.