diff --git a/content/docs/en/apis/stacks-blockchain-api/meta.json b/content/docs/en/apis/stacks-blockchain-api/meta.json
index 643bedfbd..1233034b6 100644
--- a/content/docs/en/apis/stacks-blockchain-api/meta.json
+++ b/content/docs/en/apis/stacks-blockchain-api/meta.json
@@ -8,6 +8,7 @@
"architecture",
"pagination",
"none-handling",
+ "websockets",
"---Reference---",
"...reference"
]
diff --git a/content/docs/en/apis/stacks-blockchain-api/websockets.mdx b/content/docs/en/apis/stacks-blockchain-api/websockets.mdx
new file mode 100644
index 000000000..56286a37c
--- /dev/null
+++ b/content/docs/en/apis/stacks-blockchain-api/websockets.mdx
@@ -0,0 +1,178 @@
+---
+title: WebSockets and Socket.IO
+sidebarTitle: WebSockets
+description: Subscribe to real-time blockchain events over WebSockets or Socket.IO.
+---
+
+## Overview
+
+Instead of polling REST endpoints, you can subscribe to blockchain events and have the API push updates to you as they happen. The Stacks Blockchain API exposes two real-time channels:
+
+- **WebSockets**: A standard WebSocket connection speaking JSON-RPC 2.0, available at the `/extended/v1/ws` path.
+- **Socket.IO**: A [Socket.IO](https://socket.io/) server on the API's root URL, which adds automatic reconnection and fallback transports on top of WebSockets.
+
+Both channels deliver the same events: new blocks, microblocks, mempool transactions, transaction status updates, address activity, and NFT events.
+
+| Network | WebSockets | Socket.IO |
+|---------|------------|-----------|
+| Mainnet | `wss://api.mainnet.hiro.so/extended/v1/ws` | `https://api.mainnet.hiro.so` |
+| Testnet | `wss://api.testnet.hiro.so/extended/v1/ws` | `https://api.testnet.hiro.so` |
+
+The easiest way to use either channel is through the [`@stacks/blockchain-api-client`](https://github.com/stx-labs/stacks-blockchain-api/tree/master/client) package, which wraps both protocols in a typed interface. You can also connect directly with any WebSocket or Socket.IO implementation — both approaches are covered below.
+
+
+ Events reflect the canonical chain as it stands when they are sent, and they are susceptible to re-orgs: a block you were notified about (and the transactions it anchored) can later be orphaned when a new canonical chain fork takes its place. Don't treat a pushed event as final — keep watching subsequent block events, and confirm anything critical against the REST API (for example, a transaction's `canonical` flag and its number of confirmations) before acting on it.
+
+
+## Using the API client library
+
+Install the client package:
+
+```terminal
+$ npm install @stacks/blockchain-api-client
+```
+
+### WebSockets
+
+Use `connectWebSocketClient` to open a connection, then call a `subscribe*` method for each event stream you want:
+
+```js
+import { connectWebSocketClient } from '@stacks/blockchain-api-client';
+
+const client = await connectWebSocketClient('wss://api.mainnet.hiro.so/');
+
+const sub = await client.subscribeAddressTransactions(
+ 'ST3GQB6WGCWKDNFNPSQRV8DY93JN06XPZ2ZE9EVMA',
+ event => console.log(event)
+);
+```
+
+Each subscription returns an object you can use to stop receiving that event stream:
+
+```js
+await sub.unsubscribe();
+```
+
+### Socket.IO
+
+Create a `StacksApiSocketClient` pointed at the API's root URL:
+
+```js
+import { StacksApiSocketClient } from '@stacks/blockchain-api-client';
+
+const client = new StacksApiSocketClient({ url: 'https://api.mainnet.hiro.so' });
+
+client.subscribeBlocks(block => console.log(block));
+client.subscribeMempool(tx => console.log(tx));
+```
+
+### Subscription methods
+
+Both clients cover the same events:
+
+| Event | WebSocket client | Socket.IO client |
+|-------|------------------|------------------|
+| Blocks | `subscribeBlocks(handler)` | `subscribeBlocks(handler)` |
+| Microblocks | `subscribeMicroblocks(handler)` | `subscribeMicroblocks(handler)` |
+| Mempool transactions | `subscribeMempool(handler)` | `subscribeMempool(handler)` |
+| Transaction updates | `subscribeTxUpdates(txId, handler)` | `subscribeTransaction(txId, handler)` |
+| Address transactions | `subscribeAddressTransactions(address, handler)` | `subscribeAddressTransactions(address, handler)` |
+| Address STX balance | `subscribeAddressBalanceUpdates(address, handler)` | `subscribeAddressStxBalance(address, handler)` |
+| NFT events (all) | `subscribeNftEventUpdates(handler)` | `subscribeNftEvent(handler)` |
+| NFT asset events | `subscribeNftAssetEventUpdates(assetId, value, handler)` | `subscribeNftAssetEvent(assetId, value, handler)` |
+| NFT collection events | `subscribeNftCollectionEventUpdates(assetId, handler)` | `subscribeNftCollectionEvent(assetId, handler)` |
+
+## Connecting directly
+
+The client library is a convenience, not a requirement. Both channels speak documented protocols that any language or runtime can implement.
+
+### WebSockets (JSON-RPC 2.0)
+
+Open a WebSocket to `/extended/v1/ws` and send JSON-RPC 2.0 messages with the `subscribe` and `unsubscribe` methods. The event to subscribe to goes in `params.event`:
+
+| `event` | Additional params |
+|---------|-------------------|
+| `block` | — |
+| `microblock` | — |
+| `mempool` | — |
+| `tx_update` | `tx_id` |
+| `address_tx_update` | `address` |
+| `address_balance_update` | `address` |
+| `nft_event` | — |
+| `nft_asset_event` | `asset_identifier`, `value` |
+| `nft_collection_event` | `asset_identifier` |
+
+The server pushes matching events as JSON-RPC notifications whose `method` equals the event name and whose `params` contain the payload:
+
+```js
+const ws = new WebSocket('wss://api.mainnet.hiro.so/extended/v1/ws');
+
+ws.addEventListener('open', () => {
+ ws.send(
+ JSON.stringify({
+ jsonrpc: '2.0',
+ id: 1,
+ method: 'subscribe',
+ params: { event: 'address_tx_update', address: 'ST3GQB6WGCWKDNFNPSQRV8DY93JN06XPZ2ZE9EVMA' },
+ })
+ );
+});
+
+ws.addEventListener('message', message => {
+ const data = JSON.parse(message.data);
+ // Responses to your requests have an `id`; pushed events are
+ // notifications with a `method` and `params`.
+ if (data.method === 'address_tx_update') {
+ console.log(data.params);
+ }
+});
+```
+
+To stop receiving an event, send the same message with `method: 'unsubscribe'`.
+
+
+ Raw WebSocket connections do not reconnect on their own. If you connect directly, handle the socket's `close` and `error` events and re-subscribe after reconnecting.
+
+
+### Socket.IO
+
+Connect any standard Socket.IO client to the API's root URL. Subscriptions are managed with topic strings, in one of two ways:
+
+1. Pass initial topics in the `subscriptions` query parameter (comma-separated) when connecting.
+2. Emit `subscribe` and `unsubscribe` events with topic names after connecting.
+
+| Topic | Description |
+|-------|-------------|
+| `block` | New blocks |
+| `microblock` | New microblocks |
+| `mempool` | New mempool transactions |
+| `transaction:{txId}` | Updates for a specific transaction |
+| `address-transaction:{address}` | Transactions involving an address |
+| `address-stx-balance:{address}` | STX balance changes for an address |
+| `nft-event` | All NFT events |
+| `nft-asset-event:{assetIdentifier}+{value}` | Events for a specific NFT asset |
+| `nft-collection-event:{assetIdentifier}` | Events for an NFT collection |
+
+The server emits events named after the topic, including the dynamic segment:
+
+```js
+import { io } from 'socket.io-client';
+
+const address = 'ST3GQB6WGCWKDNFNPSQRV8DY93JN06XPZ2ZE9EVMA';
+
+const socket = io('https://api.mainnet.hiro.so', {
+ query: { subscriptions: `block,address-transaction:${address}` },
+});
+
+socket.on('block', block => {
+ console.log(block);
+});
+
+socket.on(`address-transaction:${address}`, (addr, tx) => {
+ console.log(addr, tx);
+});
+
+// Add or remove topics at any time:
+socket.emit('subscribe', 'mempool');
+socket.emit('unsubscribe', 'mempool');
+```