StellarKit API exposes two real-time streaming transports: Server-Sent Events (SSE) for account transactions, account payments, and live ledgers, and a WebSocket channel for live ledger updates. This guide explains how each transport works, what events you will receive, how to reconnect gracefully, and how to close streams cleanly.
- Choosing a Transport
- SSE —
/stream/transactions/:id - SSE —
/stream/payments/:id - SSE —
/stream/ledgers(SSE) - WebSocket —
/stream/ledgers - Reconnection Guidance
- Clean Close Handling
- Error Reference
| Endpoint | Transport | Best for |
|---|---|---|
GET /stream/transactions/:id |
SSE | Watching a specific account for new transactions |
GET /stream/payments/:id |
SSE | Watching a specific account for incoming/outgoing payments |
GET /stream/ledgers |
SSE or WebSocket | Live ledger heartbeat, dashboards, block explorers |
Both SSE and WebSocket deliver the same ledger payload on /stream/ledgers. Use SSE when you want a fire-and-forget stream that works through standard fetch/EventSource APIs and HTTP/2 multiplexing. Use WebSocket when your application already manages a WS connection, needs bidirectional messaging later, or runs in an environment where SSE is unsupported.
Streams new transactions for a Stellar account in real time, starting from the moment the connection is opened (historical transactions are not replayed).
GET /stream/transactions/:id
Path parameter:
| Name | Type | Description |
|---|---|---|
id |
string | Stellar account public key (G..., 56 characters) |
Response headers:
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
Access-Control-Allow-Origin: *
| Event name | When emitted | Payload |
|---|---|---|
connected |
Immediately after the account is validated | { account, timestamp } |
transaction |
Each time a new transaction is confirmed for the account | Normalised transaction object (see below) |
heartbeat |
Every 25 seconds to keep the connection alive through proxies | { timestamp } |
error |
On a fatal stream error before the connection is closed | { code, message } |
connected
event: connected
data: {"account":"GAAZI4TCR3TY5OJHCTJC2A4QSY6CJWJH5IAJTGKIN2ER7LBNVKOCCWN","timestamp":"2026-07-28T10:00:00.000Z"}
transaction
event: transaction
data: {
"id": "3389e9f0f1a65f19736cacf544c2e825313e8447f569233bb8db39aa607c1234",
"hash": "3389e9f0f1a65f19736cacf544c2e825313e8447f569233bb8db39aa607c1234",
"ledger": 45123456,
"createdAt": "2026-07-28T10:00:05.000Z",
"sourceAccount": "GAAZI4TCR3TY5OJHCTJC2A4QSY6CJWJH5IAJTGKIN2ER7LBNVKOCCWN",
"fee": {
"charged": 100,
"chargedInXLM": "0.0000100"
},
"operationCount": 1,
"successful": true
}
heartbeat
event: heartbeat
data: {"timestamp":"2026-07-28T10:00:25.000Z"}
error
event: error
data: {"code":"STREAM_ERROR","message":"Transaction stream encountered an error"}
| Code | Description |
|---|---|
STREAM_ERROR |
The underlying Horizon stream emitted an error |
HORIZON_UNAVAILABLE |
Could not establish the Horizon stream during setup |
const EventSource = require("eventsource"); // npm install eventsource
const ACCOUNT = "GAAZI4TCR3TY5OJHCTJC2A4QSY6CJWJH5IAJTGKIN2ER7LBNVKOCCWN";
const url = `http://localhost:3000/stream/transactions/${ACCOUNT}`;
const es = new EventSource(url);
es.addEventListener("connected", (e) => {
const { account, timestamp } = JSON.parse(e.data);
console.log(`Stream opened for ${account} at ${timestamp}`);
});
es.addEventListener("transaction", (e) => {
const tx = JSON.parse(e.data);
console.log(`New transaction: ${tx.hash} (ledger ${tx.ledger})`);
});
es.addEventListener("heartbeat", (e) => {
// Optionally log keep-alive ticks
});
es.addEventListener("error", (e) => {
if (e.data) {
const { code, message } = JSON.parse(e.data);
console.error(`Stream error [${code}]: ${message}`);
}
es.close();
});
// Close when done
// es.close();<script>
const ACCOUNT = "GAAZI4TCR3TY5OJHCTJC2A4QSY6CJWJH5IAJTGKIN2ER7LBNVKOCCWN";
const es = new EventSource(`/stream/transactions/${ACCOUNT}`);
es.addEventListener("connected", (e) => {
const { account } = JSON.parse(e.data);
console.log("Watching account:", account);
});
es.addEventListener("transaction", (e) => {
const tx = JSON.parse(e.data);
console.log("New tx:", tx.hash);
// Update your UI here
});
es.addEventListener("heartbeat", () => {
// Connection is alive — no action needed
});
es.addEventListener("error", (e) => {
if (e.data) {
console.error("Fatal stream error:", JSON.parse(e.data));
}
es.close();
});
// Clean up when the user navigates away
window.addEventListener("beforeunload", () => es.close());
</script>Streams incoming and outgoing payment events for a Stellar account. Only payment and create_account operation types are forwarded — all other operation types are filtered server-side.
GET /stream/payments/:id
Path parameter:
| Name | Type | Description |
|---|---|---|
id |
string | Stellar account public key (G..., 56 characters) |
| Event name | When emitted | Payload |
|---|---|---|
payment |
Each time a qualifying payment operation is confirmed | { type, amount, asset, from, to, timestamp } |
: ping (comment) |
Every 30 seconds | SSE keep-alive comment — no named event, no data |
The periodic : ping comment is an SSE comment line, not a named event. The EventSource API ignores it automatically; you do not need to handle it.
payment — incoming XLM
event: payment
data: {
"type": "payment",
"amount": "10.0000000",
"asset": {
"code": "XLM",
"issuer": null,
"type": "native"
},
"from": "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5",
"to": "GAAZI4TCR3TY5OJHCTJC2A4QSY6CJWJH5IAJTGKIN2ER7LBNVKOCCWN",
"timestamp": "2026-07-28T10:01:00.000Z"
}
payment — incoming USDC
event: payment
data: {
"type": "payment",
"amount": "25.0000000",
"asset": {
"code": "USDC",
"issuer": "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN",
"type": "credit_alphanum4"
},
"from": "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5",
"to": "GAAZI4TCR3TY5OJHCTJC2A4QSY6CJWJH5IAJTGKIN2ER7LBNVKOCCWN",
"timestamp": "2026-07-28T10:01:05.000Z"
}
payment — account creation
event: payment
data: {
"type": "create_account",
"amount": "1.0000000",
"asset": {
"code": "XLM",
"issuer": null,
"type": "native"
},
"from": "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5",
"to": "GAAZI4TCR3TY5OJHCTJC2A4QSY6CJWJH5IAJTGKIN2ER7LBNVKOCCWN",
"timestamp": "2026-07-28T10:01:10.000Z"
}
The asset field always uses the normalised object form:
| Field | Type | Notes |
|---|---|---|
code |
string | "XLM" for native; otherwise the asset code |
issuer |
string | null | null for native XLM |
type |
string | "native", "credit_alphanum4", or "credit_alphanum12" |
const EventSource = require("eventsource");
const ACCOUNT = "GAAZI4TCR3TY5OJHCTJC2A4QSY6CJWJH5IAJTGKIN2ER7LBNVKOCCWN";
const es = new EventSource(`http://localhost:3000/stream/payments/${ACCOUNT}`);
es.addEventListener("payment", (e) => {
const { type, amount, asset, from, to, timestamp } = JSON.parse(e.data);
console.log(`[${timestamp}] ${type}: ${amount} ${asset.code} from ${from} → ${to}`);
});
es.onerror = () => {
console.error("Payment stream disconnected");
es.close();
};
// Close when done
// es.close();<script>
const ACCOUNT = "GAAZI4TCR3TY5OJHCTJC2A4QSY6CJWJH5IAJTGKIN2ER7LBNVKOCCWN";
const es = new EventSource(`/stream/payments/${ACCOUNT}`);
es.addEventListener("payment", (e) => {
const p = JSON.parse(e.data);
const label = p.type === "create_account" ? "Account created" : "Payment";
console.log(`${label}: ${p.amount} ${p.asset.code} from ${p.from}`);
});
es.onerror = () => {
console.warn("Payment stream lost. Will auto-reconnect...");
// EventSource reconnects automatically — see Reconnection section
};
window.addEventListener("beforeunload", () => es.close());
</script>Streams live Stellar ledger closes. A new ledger closes approximately every 5 seconds. Each message carries ledger metadata — no named event type is set, so messages arrive on the default message event.
GET /stream/ledgers
No path parameters or query parameters.
| Event name | When emitted | Payload |
|---|---|---|
(default message) |
Each time a new ledger closes | { sequence, closedAt, baseFee, transactionCount, operationCount } |
: keep-alive (comment) |
Every 15 seconds | SSE comment, no data — ignored by EventSource automatically |
data: {
"sequence": 52430100,
"closedAt": "2026-07-28T10:02:00.000Z",
"baseFee": 100,
"transactionCount": 14,
"operationCount": 23
}
| Field | Type | Description |
|---|---|---|
sequence |
number | Ledger sequence number, monotonically increasing |
closedAt |
string | ISO 8601 UTC timestamp of ledger close |
baseFee |
number | Network base fee in stroops (1 XLM = 10,000,000 stroops) |
transactionCount |
number | Successful transactions in this ledger |
operationCount |
number | Total operations across all transactions |
const EventSource = require("eventsource");
const es = new EventSource("http://localhost:3000/stream/ledgers");
es.onmessage = (e) => {
const ledger = JSON.parse(e.data);
console.log(
`Ledger #${ledger.sequence} closed at ${ledger.closedAt} ` +
`| ${ledger.transactionCount} txs | base fee ${ledger.baseFee} stroops`
);
};
es.onerror = () => {
console.error("Ledger SSE stream disconnected");
// EventSource reconnects automatically
};<script>
const es = new EventSource("/stream/ledgers");
es.onmessage = (e) => {
const ledger = JSON.parse(e.data);
document.getElementById("sequence").textContent = ledger.sequence;
document.getElementById("closedAt").textContent = ledger.closedAt;
document.getElementById("txCount").textContent = ledger.transactionCount;
};
es.onerror = () => {
console.warn("Ledger stream interrupted. Reconnecting...");
};
window.addEventListener("beforeunload", () => es.close());
</script>The same live ledger data is also available as a WebSocket stream. Messages are plain JSON text frames — no subprotocol is required.
Upgrade an HTTP connection to WebSocket on the /stream/ledgers path:
ws://localhost:3000/stream/ledgers
wss://your-host.example.com/stream/ledgers (TLS)
Connecting to any other WebSocket path will result in the socket being destroyed immediately.
Each message is a JSON text frame:
{
"sequence": 52430100,
"closedAt": "2026-07-28T10:02:00.000Z",
"baseFee": 100,
"transactionCount": 14
}| Field | Type | Description |
|---|---|---|
sequence |
number | Ledger sequence number |
closedAt |
string | ISO 8601 UTC close timestamp |
baseFee |
number | Network base fee in stroops |
transactionCount |
number | Successful transactions in this ledger |
Note: The WebSocket payload omits
operationCount. If you need that field, use the SSE/stream/ledgersendpoint instead.
- Client sends an HTTP
Upgrade: websocketrequest to/stream/ledgers. - Server upgrades the connection and begins forwarding live ledger events immediately.
- Server sends JSON frames as ledgers close (~every 5 seconds).
- Either party can close the connection at any time using a standard WebSocket close frame.
- On client disconnect the server automatically cancels its Horizon subscription to avoid background resource leaks.
The server sends a close frame with code 1011 (Internal Error) if the Horizon stream subscription fails during setup. All other disconnects use standard close code 1000 (Normal Closure) or are initiated by the client.
const WebSocket = require("ws"); // npm install ws
const ws = new WebSocket("ws://localhost:3000/stream/ledgers");
ws.on("open", () => {
console.log("WebSocket connected to /stream/ledgers");
});
ws.on("message", (data) => {
const ledger = JSON.parse(data.toString());
console.log(
`Ledger #${ledger.sequence} | ` +
`${ledger.transactionCount} txs | ` +
`base fee ${ledger.baseFee} stroops`
);
});
ws.on("close", (code, reason) => {
console.log(`Connection closed — code ${code}: ${reason}`);
});
ws.on("error", (err) => {
console.error("WebSocket error:", err.message);
});
// Clean close
// ws.close(1000, "Done");<script>
const ws = new WebSocket("wss://your-host.example.com/stream/ledgers");
ws.addEventListener("open", () => {
console.log("Connected to ledger stream");
});
ws.addEventListener("message", (e) => {
const ledger = JSON.parse(e.data);
console.log(`Ledger #${ledger.sequence} closed: ${ledger.closedAt}`);
// Update your UI
});
ws.addEventListener("close", (e) => {
console.warn(`WebSocket closed (code ${e.code}). Scheduling reconnect...`);
scheduleReconnect();
});
ws.addEventListener("error", () => {
// "error" always fires before "close" for abnormal closures
console.error("WebSocket connection error");
});
// Clean close on page unload
window.addEventListener("beforeunload", () => ws.close(1000, "page unload"));
</script>The browser EventSource API reconnects automatically when the connection drops. It waits a short interval (typically 3 seconds, or the value from the retry: SSE field if set) and then opens a fresh connection.
You do not need to implement reconnection logic for EventSource — it is built in. You do need to handle reconnection yourself when using Node.js eventsource or a custom HTTP client.
Node.js SSE reconnection example (transactions)
const EventSource = require("eventsource");
const ACCOUNT = "GAAZI4TCR3TY5OJHCTJC2A4QSY6CJWJH5IAJTGKIN2ER7LBNVKOCCWN";
const BASE_URL = "http://localhost:3000";
let es;
let retryDelay = 1000; // Start at 1 second
const MAX_DELAY = 30_000; // Cap at 30 seconds
function connect() {
es = new EventSource(`${BASE_URL}/stream/transactions/${ACCOUNT}`);
es.addEventListener("connected", () => {
retryDelay = 1000; // Reset backoff on successful connection
console.log("Stream connected");
});
es.addEventListener("transaction", (e) => {
const tx = JSON.parse(e.data);
console.log("New tx:", tx.hash);
});
es.onerror = () => {
es.close();
console.warn(`Stream lost. Reconnecting in ${retryDelay}ms...`);
setTimeout(() => {
retryDelay = Math.min(retryDelay * 2, MAX_DELAY); // Exponential backoff
connect();
}, retryDelay);
};
}
connect();The WebSocket API does not reconnect automatically. You must handle the close event and schedule a new WebSocket constructor call.
Node.js WebSocket reconnection example
const WebSocket = require("ws");
const WS_URL = "ws://localhost:3000/stream/ledgers";
let ws;
let retryDelay = 1000;
const MAX_DELAY = 30_000;
function connect() {
ws = new WebSocket(WS_URL);
ws.on("open", () => {
retryDelay = 1000; // Reset on success
console.log("WebSocket connected");
});
ws.on("message", (data) => {
const ledger = JSON.parse(data.toString());
console.log(`Ledger #${ledger.sequence}`);
});
ws.on("close", (code) => {
// Don't reconnect on a deliberate client-side close (1000)
if (code === 1000) {
console.log("Closed cleanly. Not reconnecting.");
return;
}
console.warn(`WS closed (code ${code}). Reconnecting in ${retryDelay}ms...`);
setTimeout(() => {
retryDelay = Math.min(retryDelay * 2, MAX_DELAY);
connect();
}, retryDelay);
});
ws.on("error", (err) => {
console.error("WS error:", err.message);
// "close" will fire after "error" — reconnect logic lives there
});
}
connect();Browser WebSocket reconnection example
<script>
const WS_URL = "wss://your-host.example.com/stream/ledgers";
let ws;
let retryDelay = 1000;
const MAX_DELAY = 30_000;
let intentionalClose = false;
function connect() {
ws = new WebSocket(WS_URL);
ws.addEventListener("open", () => {
retryDelay = 1000;
console.log("Connected");
});
ws.addEventListener("message", (e) => {
const ledger = JSON.parse(e.data);
console.log(`Ledger #${ledger.sequence}`);
});
ws.addEventListener("close", (e) => {
if (intentionalClose) return;
console.warn(`Reconnecting in ${retryDelay}ms...`);
setTimeout(() => {
retryDelay = Math.min(retryDelay * 2, MAX_DELAY);
connect();
}, retryDelay);
});
ws.addEventListener("error", () => {
// "close" fires next — reconnection is handled there
});
}
function closeStream() {
intentionalClose = true;
ws.close(1000, "user action");
}
connect();
window.addEventListener("beforeunload", closeStream);
</script>Call EventSource.close() to terminate the connection. The browser will not attempt to reconnect after an explicit close.
// Node.js
es.close();
// Browser
es.close();The server detects the client disconnect via the req.on("close") event and immediately cancels the underlying Horizon subscription and any heartbeat intervals. No further data is sent and no resources are leaked on the server side.
Call ws.close(code, reason) with close code 1000 to signal a normal closure. Pass a short reason string to help with server-side logging.
// Node.js
ws.close(1000, "client shutting down");
// Browser
ws.close(1000, "user navigated away");The server handles the close event on the socket, cancels its Horizon stream subscription, and logs the disconnection. The isClosed guard in the server prevents the cleanup function from running twice if both close and error fire for the same disconnection event.
Before SSE headers are sent, the server returns standard HTTP JSON errors if validation fails. These are not SSE events — they are plain HTTP responses.
| Status | Error type | Cause |
|---|---|---|
400 |
ValidationError |
id is not a valid G... Stellar public key |
404 |
NotFound |
No account exists on the network for that public key |
{
"success": false,
"error": {
"type": "ValidationError",
"message": "Invalid Stellar account ID",
"detail": "Must be a valid G... address"
}
}Once SSE headers have been sent, errors are delivered as error events rather than HTTP status codes.
| Code | Cause |
|---|---|
STREAM_ERROR |
Horizon stream emitted an error mid-connection |
HORIZON_UNAVAILABLE |
Failed to set up the Horizon stream during connection |
The server sends close code 1011 (Internal Error) when it cannot subscribe to the Horizon ledger stream. Implement a reconnect with backoff when you receive this code.
- Getting Started Guide — Initial setup and environment variables
- Response Format Guide — Normalised response shapes used across the API
- Rate Limiting — Request limits and retry strategies
- Error Reference — Full error type catalogue