# Stellar WebSocket subscriptions

Receive new Stellar ledgers, contract events, transactions, and classic payments over one WebSocket connection. You can subscribe to mainnet or testnet and narrow notifications with topic-specific filters.

## Connect with your API key

Create an API key in the [Alchemy Dashboard](https://dashboard.alchemy.com/). Choose the endpoint for your network and replace `YOUR_API_KEY`:

| Network | WebSocket URL |
| --- | --- |
| Mainnet | `wss://stellar-mainnet.g.alchemy.com/v2/YOUR_API_KEY` |
| Testnet | `wss://stellar-testnet.g.alchemy.com/v2/YOUR_API_KEY` |

Send the messages below over the WebSocket after it opens. These methods use positional JSON-RPC `params` arrays. They are WebSocket methods, not HTTP JSON-RPC calls, and do not use `eth_subscribe`.

This JavaScript example uses the built-in WebSocket client in Node.js 22 or later. Set `ALCHEMY_API_KEY` in your environment before running it:

```javascript
const apiKey = process.env.ALCHEMY_API_KEY;
if (!apiKey) throw new Error('Set ALCHEMY_API_KEY');

const ws = new WebSocket(
  `wss://stellar-mainnet.g.alchemy.com/v2/${encodeURIComponent(apiKey)}`,
);

ws.addEventListener('open', () => {
  ws.send(JSON.stringify({
    jsonrpc: '2.0',
    id: 1,
    method: 'stellar_subscribe',
    params: ['newLedgers'],
  }));
});

let subscriptionId;
ws.addEventListener('message', ({ data }) => {
  const message = JSON.parse(data);
  if (message.error) {
    console.error(message.error);
  } else if (message.id === 1) {
    subscriptionId = message.result;
    console.log('Subscribed:', subscriptionId);
  } else if (message.method === 'stellar_subscription') {
    console.log(message.params.subscription, message.params.result);
  }
});
ws.addEventListener('error', () => console.error('WebSocket connection failed'));
ws.addEventListener('close', ({ code, reason }) => console.log('Closed:', code, reason));

// When finished, call this while the connection is still open.
function unsubscribe() {
  if (subscriptionId && ws.readyState === WebSocket.OPEN) {
    ws.send(JSON.stringify({
      jsonrpc: '2.0', id: 2, method: 'stellar_unsubscribe',
      params: [subscriptionId],
    }));
  }
}
```

The example shows one connection lifecycle. Add reconnect, backfill, and deduplication for applications that need complete records; see [Recover after a disconnect](#recover-after-a-disconnect).

## Subscribe and receive notifications

Send `stellar_subscribe` with `[topic]` or `[topic, filter]`. An omitted filter or `{}` matches all eligible records. `null` is not a filter object. Use a unique request ID to associate each acknowledgement with its request.

```json
{"jsonrpc":"2.0","id":1,"method":"stellar_subscribe","params":["newLedgers"]}
```

The response contains an opaque subscription ID. Keep it to route notifications and unsubscribe on the same connection:

```json
{"jsonrpc":"2.0","id":1,"result":"0x0123456789abcdef0123456789abcdef"}
```

Notifications have no request `id`. `params.result` contains one record, not an array or an RPC pagination envelope:

```json
{
  "jsonrpc": "2.0",
  "method": "stellar_subscription",
  "params": {
    "subscription": "0x0123456789abcdef0123456789abcdef",
    "result": {
      "ledger": 5193687,
      "txHash": "de3197681ae894f6256ec97c9b2e472bd139f59194c40de0b4b92de603369a84",
      "createdAt": 1791992022,
      "operationIndex": 0,
      "type": "payment",
      "from": "GBM2QDS4TARZZJ7LLFP74UMMALNRD7GHO4UXTM3KERGANOWFHBVUV3ST",
      "to": "GAZEXNDQ2WYQOQRCCDJ5WXCTEB3XTRNBUVCIDC6UJMPHOFV3K2IGENKR",
      "operation": {
        "destination": "GAZEXNDQ2WYQOQRCCDJ5WXCTEB3XTRNBUVCIDC6UJMPHOFV3K2IGENKR",
        "asset": "native",
        "amount": "1"
      }
    }
  }
}
```

This is a `payments` notification. Keep the mapping between each subscription ID and its topic; the notification envelope does not repeat the topic name.

| Topic | One notification contains | Optional filters |
| --- | --- | --- |
| `newLedgers` | One closed ledger | None |
| `events` | One Stellar event, including Soroban events | `type`, `contractIds`, `topics` |
| `transactions` | One included transaction, successful or failed | `status`, `accounts` |
| `payments` | One classic payment or path-payment operation from a successful transaction | `accounts`, `from`, `to` |

Different subscriptions can overlap. The same transaction can produce transaction, event, and payment notifications. Do not treat notifications across subscriptions as a globally ordered stream.

## New ledgers

```json
{"jsonrpc":"2.0","id":10,"method":"stellar_subscribe","params":["newLedgers"]}
```

The result fields are:

| Field | JSON type | Meaning |
| --- | --- | --- |
| `sequence` | integer | Ledger sequence number |
| `hash` | string | Ledger hash as hexadecimal |
| `ledgerCloseTime` | string | Unix timestamp in seconds, encoded as a decimal string |
| `headerXdr` | string | Base64-encoded ledger header XDR |
| `metadataXdr` | string | Base64-encoded ledger metadata XDR |

Example result (XDR strings are abbreviated placeholders):

```json
{
  "sequence": 5193684,
  "hash": "0255a52580756425bdf01702907832873c1afdb574f843411dd31429e329540a",
  "ledgerCloseTime": "1791992007",
  "headerXdr": "<base64 ledger header XDR>",
  "metadataXdr": "<base64 ledger metadata XDR>"
}
```

There is no ledger-range, starting-ledger, or replay filter. Ledger payloads can be large; configure your WebSocket client's incoming message size appropriately.

## Events and Soroban transfers

Use `events` for contract events, including Soroban token transfers:

```json
{"jsonrpc":"2.0","id":20,"method":"stellar_subscribe","params":["events",{"type":"contract","contractIds":["CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC"]}]}
```

| Filter | Shape | Meaning |
| --- | --- | --- |
| `type` | `"contract"` or `"system"` | Match the exact event type; omit to receive all returned event types |
| `contractIds` | Array of 1–100 C StrKeys | Match any listed contract |
| `topics` | Array of 1–5 topic patterns | Match any pattern; each pattern contains 1–4 segments |

Within a pattern, each position must match. `"*"` matches exactly one segment. Other segments are base64 XDR values, not decoded text or addresses. Each encoded segment is limited to 4,096 characters. A pattern must have the same number of segments as the event.

To match common three-segment and four-segment transfer event formats, include both patterns:

```json
{
  "jsonrpc": "2.0",
  "id": 21,
  "method": "stellar_subscribe",
  "params": ["events", {
    "type": "contract",
    "topics": [
      ["AAAADwAAAAh0cmFuc2Zlcg==", "*", "*"],
      ["AAAADwAAAAh0cmFuc2Zlcg==", "*", "*", "*"]
    ]
  }]
}
```

`AAAADwAAAAh0cmFuc2Zlcg==` encodes the XDR symbol `transfer`. Custom contracts can emit different event formats. Add `contractIds` to limit the stream to the token contracts you want. The stream does not normalize token transfers or convert their amounts for you.

Example result:

```json
{
  "type": "contract",
  "ledger": 5193684,
  "ledgerClosedAt": "2026-10-14T15:33:27Z",
  "contractId": "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC",
  "id": "0022306702925758464-0000000000",
  "operationIndex": 0,
  "transactionIndex": 0,
  "txHash": "57d3b82f30a8d85c9937a163264e278db814fbb9f5b4bbe7464f82d883a5f783",
  "inSuccessfulContractCall": false,
  "topic": ["AAAADwAAAANmZWUA", "AAAAEgAAAAAAAAAAA+0oimKONzC713z+Cl8orRHR8/s0JY+JMothA0fY7bY="],
  "value": "AAAACgAAAAAAAAAAAAAAAAAAAGQ="
}
```

This example is a fee event, not a transfer. The result field is singular `topic`; the filter field is plural `topics`. `topic` entries and `value` remain base64 XDR. `ledgerClosedAt` is an ISO-8601 timestamp. `contractId` can be absent or empty for events without an associated contract; tolerate additional fields and event types.

A contract event is not by itself proof that a transfer succeeded. Check `inSuccessfulContractCall` and, when your application requires transaction success, the transaction result. There is no subscription filter for `inSuccessfulContractCall`, transfer amount, or decoded event value; apply those checks in your client.

## Transactions

```json
{"jsonrpc":"2.0","id":30,"method":"stellar_subscribe","params":["transactions",{"status":"SUCCESS","accounts":["GBM2QDS4TARZZJ7LLFP74UMMALNRD7GHO4UXTM3KERGANOWFHBVUV3ST"]}]}
```

`status` accepts `SUCCESS` or `FAILED`. Omit it for both. `accounts` accepts 1–100 G or M StrKeys and matches any listed account in the transaction source, fee-bump fee payer, operation sources, or direct operation counterparties: destination, trustor, source (`from`), sponsored account, and account-merge destination.

This is not an account balance or full state-change subscription. It does not match asset issuers, claimants, memo text, or arbitrary smart-contract arguments. There is no pending-transaction subscription.

| Field | JSON type | Meaning |
| --- | --- | --- |
| `status` | string | `SUCCESS` or `FAILED` |
| `txHash` | string | Transaction hash as hexadecimal |
| `ledger` | integer | Ledger containing the transaction |
| `createdAt` | integer | Ledger close time as Unix seconds |
| `applicationOrder` | integer | Transaction application order within the ledger |
| `feeBump` | boolean | Whether the transaction uses a fee-bump envelope |
| `envelopeJson` | object | Transaction envelope in Stellar XDR-JSON format |
| `resultJson` | object | Transaction result in XDR-JSON format |
| `resultMetaJson` | object | Transaction result metadata in XDR-JSON format |
| `events` | object | Transaction event data, when provided |

Example selected fields (the three XDR-JSON objects and `events` are omitted here):

```json
{
  "status": "FAILED",
  "txHash": "57d3b82f30a8d85c9937a163264e278db814fbb9f5b4bbe7464f82d883a5f783",
  "applicationOrder": 1,
  "feeBump": false,
  "ledger": 5193684,
  "createdAt": 1791992007
}
```

The full envelope uses a tagged union: regular transactions are under `envelopeJson.tx.tx`, v0 transactions under `envelopeJson.tx_v0.tx`, and fee-bump inner transactions under `envelopeJson.tx_fee_bump.tx.inner_tx.tx.tx`. These objects use Stellar's [XDR-JSON format (SEP-0051)](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0051.md), including decimal strings for 64-bit integers. Keep those values as strings or use arbitrary-precision integers; do not convert them to JavaScript `Number` indiscriminately.

## Classic payments

```json
{"jsonrpc":"2.0","id":40,"method":"stellar_subscribe","params":["payments",{"to":["GAZEXNDQ2WYQOQRCCDJ5WXCTEB3XTRNBUVCIDC6UJMPHOFV3K2IGENKR"]}]}
```

`accounts` matches a payment's sender or recipient. `from` matches its sender; `to` matches its recipient. Each filter accepts an array of 1–100 G or M StrKeys. Combine filters to require all of their conditions.

A result contains `ledger` (integer), `txHash` (string), `createdAt` (Unix seconds as an integer), `operationIndex` (zero-based integer), `type`, `from`, `to`, and an `operation` object. The notification example above shows a complete classic payment result. If an operation has its own source account, `from` is that account.

| `type` | `operation` fields | Amount semantics |
| --- | --- | --- |
| `payment` | `destination`, `asset`, `amount` | Amount transferred |
| `path_payment_strict_receive` | `send_asset`, `send_max`, `destination`, `dest_asset`, `dest_amount`, `path` | Exact destination amount; `send_max` is an upper bound, not the actual amount spent |
| `path_payment_strict_send` | `send_asset`, `send_amount`, `destination`, `dest_asset`, `dest_min`, `path` | Exact source amount; `dest_min` is a lower bound, not the actual amount received |

Amounts are decimal strings in Stellar integer units, not human-readable token balances. For classic Stellar assets, divide by `10^7` to obtain standard units: `"1"` is 0.0000001 XLM when the asset is `"native"`. Assets retain their XDR-JSON representation: native XLM is `"native"`; issued assets use `credit_alphanum4` or `credit_alphanum12` objects containing `asset_code` and `issuer`.

Only successful transactions produce payment notifications. A transaction with multiple payment operations produces multiple records. Account creation, account merge, and Soroban contract transfers do not produce `payments` records. Use `events` for Soroban transfers. Use transaction results if you need the actual executed amounts of path payments.

## Filter rules and errors

Filter fields are ANDed together; addresses within each array are ORed. Addresses are case-sensitive and must be valid StrKeys, including checksums. M addresses match exactly; they do not automatically match the underlying G address. Account filters do not accept C contract addresses.

Unknown topics or filter fields, empty address arrays, malformed addresses, invalid enum values, and unsupported filter shapes are rejected. For example:

```json
{"jsonrpc":"2.0","id":50,"method":"stellar_subscribe","params":["payments",{"unknown":true}]}
```

returns a JSON-RPC error with code `-32602` (`Invalid params`). Inspect the response's `error` object instead of treating its `result` as a subscription ID. Authentication and access errors can also prevent the WebSocket connection from opening.

## Unsubscribe

Send the ID returned by `stellar_subscribe` over the same connection:

```json
{"jsonrpc":"2.0","id":60,"method":"stellar_unsubscribe","params":["0x0123456789abcdef0123456789abcdef"]}
```

```json
{"jsonrpc":"2.0","id":60,"result":true}
```

The result is `true` when the subscription was removed and `false` if the ID was not found on that connection. Already-in-flight notifications may arrive while you unsubscribe. Other subscriptions on the connection remain active. Closing the connection ends all of its subscriptions; IDs cannot be reused after reconnecting.

## Recover after a disconnect

Subscriptions deliver new activity after initialization. The acknowledgement confirms that the subscription was accepted; it does not mean that a record is immediately available or guarantee delivery from the instant you sent the request. There is no `startLedger`, historical replay, resume token, or acknowledgement-based delivery option.

Delivery is best effort. Disconnections and interruptions can cause gaps; there is no durable exactly-once or at-least-once guarantee. There is no fixed notification latency guarantee. A quiet subscription can mean that no new records match its filters.

If your application needs complete history:

1. Persist the last processed ledger and record IDs for each network.
2. Reconnect with backoff and jitter, then recreate your subscriptions with new IDs.
3. Buffer new live notifications while you retrieve an overlapping historical range through the corresponding HTTP RPC methods: `getLedgers`, `getEvents`, or `getTransactions`. Recover classic payments from successful transaction operations.
4. Merge historical and live records and deduplicate by network plus ledger hash/sequence, event `id`, transaction `txHash`, or payment `(txHash, operationIndex)`.

Historical RPC availability is bounded by retained history. Recover promptly; a gap older than retained history may need another archive source. Do not infer that an empty filtered stream is fully caught up or that the last notification's ledger marks the end of all records for that ledger.

Close code `1011` can indicate a service interruption or an unrecoverable delivery gap. Reconnect and recover missing records. Close code `1008` can indicate a policy or slow-consumer limit; address the cause before reconnecting. Process notifications promptly, bound your client queues, and reduce subscription scope if your client cannot keep up. Your plan's connection, subscription, and usage limits still apply.

## Machine-readable contracts

Use the [OpenRPC reference](https://multi-network-ws-api-specs.docs.alchemy.com/stellar-websocket/openrpc.json) for subscribe/unsubscribe method contracts and the [message JSON Schema](https://multi-network-ws-api-specs.docs.alchemy.com/stellar-websocket/messages.schema.json) for positional parameters and notification shapes. Result records may include additional fields; clients should ignore unknown fields for forward compatibility.
