@@ -60,6 +60,7 @@ export default class Binance {
6060 combineStreamDemo = `wss://demo-stream.binance.com/stream?streams=` ;
6161 wsApi = `wss://ws-api.binance.${ this . domain } :443/ws-api/v3` ;
6262 wsApiTest = `wss://ws-api.testnet.binance.vision/ws-api/v3` ;
63+ wsApiDemo = `wss://demo-ws-api.binance.com/ws-api/v3` ;
6364
6465 verbose = false ;
6566
@@ -285,6 +286,7 @@ export default class Binance {
285286 }
286287
287288 getWsApiUrl ( ) {
289+ if ( this . Options . demo ) return this . wsApiDemo ;
288290 if ( this . Options . test ) return this . wsApiTest ;
289291 return this . wsApi ;
290292 }
@@ -741,12 +743,10 @@ export default class Binance {
741743 */
742744 async signedRequest ( url : string , data : Dict = { } , method : HttpMethod = 'GET' , noDataInSignature = false ) {
743745 this . requireApiSecret ( 'signedRequest' ) ;
744- const isListenKeyEndpoint = url . includes ( 'v3/userDataStream' ) ;
745-
746746 let query = method === 'POST' && noDataInSignature ? '' : this . makeQueryString ( data ) ;
747747
748748 let signature = undefined ;
749- if ( ! noDataInSignature && ! isListenKeyEndpoint ) {
749+ if ( ! noDataInSignature ) {
750750 data . timestamp = new Date ( ) . getTime ( ) ;
751751
752752 if ( this . timeOffset ) data . timestamp += this . timeOffset ;
@@ -1873,6 +1873,11 @@ export default class Binance {
18731873 throw new Error ( `futuresSubscribe: cannot combine '${ category } ' stream "${ streams [ 0 ] } " with '${ mismatchCategory } ' stream "${ mismatch } " on one connection. Binance routes futures streams to separate /public, /market and /private endpoints; subscribe to each category separately.` ) ;
18741874 }
18751875 const baseUrl = this . getFStreamUrl ( category ) ;
1876+ // Private combined streams use ?listenKey=<k1>&listenKey=<k2> query params
1877+ // instead of ?streams=<a>/<b>
1878+ const wsUrl = category === 'private'
1879+ ? baseUrl . replace ( / \? s t r e a m s = $ / , '?' ) + streams . map ( k => 'listenKey=' + k ) . join ( '&' )
1880+ : baseUrl + queryParams ;
18761881 let ws : any = undefined ;
18771882 if ( socksproxy ) {
18781883 socksproxy = this . proxyReplacewithIp ( socksproxy ) ;
@@ -1882,14 +1887,14 @@ export default class Binance {
18821887 host : this . parseProxy ( socksproxy ) [ 1 ] ,
18831888 port : this . parseProxy ( socksproxy ) [ 2 ]
18841889 } ) ;
1885- ws = new WebSocket ( baseUrl + queryParams , { agent } ) ;
1890+ ws = new WebSocket ( wsUrl , { agent } ) ;
18861891 } else if ( httpsproxy ) {
18871892 if ( this . Options . verbose ) this . Options . log ( `futuresSubscribe: using proxy server ${ httpsproxy } ` ) ;
18881893 const config = url . parse ( httpsproxy ) ;
18891894 const agent = new HttpsProxyAgent ( config ) ;
1890- ws = new WebSocket ( baseUrl + queryParams , { agent } ) ;
1895+ ws = new WebSocket ( wsUrl , { agent } ) ;
18911896 } else {
1892- ws = new WebSocket ( baseUrl + queryParams ) ;
1897+ ws = new WebSocket ( wsUrl ) ;
18931898 }
18941899
18951900 ws . reconnect = this . Options . reconnect ;
@@ -3942,9 +3947,10 @@ export default class Binance {
39423947 /**
39433948 * Ensures a WebSocket API connection is open for the given connectionId
39443949 * @param {string } connectionId - connection identifier
3950+ * @param {function } messageHandler - handler for event messages when a new connection is created
39453951 * @return {promise } - resolves when the connection is open
39463952 */
3947- private ensureWsApiConnection ( connectionId : string ) : Promise < void > {
3953+ private ensureWsApiConnection ( connectionId : string , messageHandler : Callback = ( ) => { } ) : Promise < void > {
39483954 return new Promise ( ( resolve , reject ) => {
39493955 const existing = this . wsApiConnections [ connectionId ] ;
39503956 if ( existing ) {
@@ -3958,7 +3964,7 @@ export default class Binance {
39583964 return ;
39593965 }
39603966 }
3961- const ws = this . connectWsApi ( connectionId , ( ) => { } , ( ) => { } ) ;
3967+ const ws = this . connectWsApi ( connectionId , messageHandler , ( ) => { } ) ;
39623968 ws . once ( 'open' , ( ) => resolve ( ) ) ;
39633969 ws . once ( 'error' , ( err : Error ) => reject ( err ) ) ;
39643970 } ) ;
@@ -4378,20 +4384,68 @@ export default class Binance {
43784384 return res ;
43794385 }
43804386
4387+ /**
4388+ * Opens the spot user data stream by subscribing over the WebSocket API.
4389+ * POST /api/v3/userDataStream was removed by Binance on 2026-02-20; user data
4390+ * streams are now started with the userDataStream.subscribe.signature WS-API method.
4391+ * Events are routed to the callbacks configured via userData()/Options.
4392+ * @return {promise } - resolves with { subscriptionId }
4393+ */
43814394 async spotGetDataStream ( params : Dict = { } ) {
4382- return await this . privateSpotRequest ( 'v3/userDataStream' , params , 'POST' , true ) ;
4395+ const connectionId = 'userData' ;
4396+ await this . ensureWsApiConnection ( connectionId , this . userDataHandler . bind ( this ) ) ;
4397+ const timestamp = Date . now ( ) ;
4398+ const query = `apiKey=${ this . APIKEY } ×tamp=${ timestamp } ` ;
4399+ const signature = this . generateSignature ( query ) ;
4400+ const result = await this . sendWsApiRequest ( connectionId , 'userDataStream.subscribe.signature' , {
4401+ apiKey : this . APIKEY ,
4402+ timestamp : timestamp ,
4403+ signature : signature ,
4404+ ...params
4405+ } ) ;
4406+ this . Options . userDataSubscriptionId = result . subscriptionId ;
4407+ return result ;
43834408 }
43844409
4385- async spotKeepDataStream ( listenKey : string | undefined = undefined , params : Dict = { } ) {
4386- listenKey = listenKey || this . Options . listenKey ;
4387- if ( ! listenKey ) throw new Error ( 'A listenKey is required, either as an argument or in this.Options.listenKey' ) ;
4388- return await this . privateSpotRequest ( 'v3/userDataStream' , { listenKey, ...params } , 'PUT' ) ;
4410+ /**
4411+ * Kept for backwards compatibility: PUT /api/v3/userDataStream was removed by
4412+ * Binance on 2026-02-20 and WS-API subscriptions need no keepalive — they live as
4413+ * long as the connection. This only verifies the connection is still open.
4414+ */
4415+ async spotKeepDataStream ( ) {
4416+ const ws = this . wsApiConnections [ 'userData' ] ;
4417+ if ( ! ws || ws . readyState !== WebSocket . OPEN ) {
4418+ throw new Error ( 'spotKeepDataStream: no open user data stream connection, start one with userData() or spotGetDataStream()' ) ;
4419+ }
4420+ if ( this . Options . verbose ) this . Options . log ( 'spotKeepDataStream: keepalive is no longer required, WS-API subscriptions live as long as the connection' ) ;
4421+ return { } ;
43894422 }
43904423
4391- async spotCloseDataStream ( listenKey : string | undefined = undefined , params : Dict = { } ) {
4392- listenKey = listenKey || this . Options . listenKey ;
4393- if ( ! listenKey ) throw new Error ( 'A listenKey is required, either as an argument or in this.Options.listenKey' ) ;
4394- return await this . privateSpotRequest ( 'v3/userDataStream' , { listenKey, ...params } , 'DELETE' ) ;
4424+ /**
4425+ * Closes the spot user data stream by unsubscribing over the WebSocket API.
4426+ * DELETE /api/v3/userDataStream was removed by Binance on 2026-02-20; user data
4427+ * streams are now closed with the userDataStream.unsubscribe WS-API method.
4428+ * @param {number } subscriptionId - optional subscription to close; defaults to the
4429+ * subscription created by userData(). When neither is available, all subscriptions
4430+ * on the connection are closed. The WebSocket connection itself is terminated once
4431+ * the tracked subscription (or all subscriptions) has been closed.
4432+ */
4433+ async spotCloseDataStream ( subscriptionId : number | undefined = undefined , params : Dict = { } ) {
4434+ const connectionId = 'userData' ;
4435+ const ws = this . wsApiConnections [ connectionId ] ;
4436+ if ( ! ws || ws . readyState !== WebSocket . OPEN ) {
4437+ throw new Error ( 'spotCloseDataStream: no open user data stream connection, start one with userData()' ) ;
4438+ }
4439+ const tracked = this . Options . userDataSubscriptionId ;
4440+ subscriptionId = subscriptionId ?? tracked ;
4441+ const requestParams : Dict = { ...params } ;
4442+ if ( subscriptionId !== undefined ) requestParams . subscriptionId = subscriptionId ;
4443+ const result = await this . sendWsApiRequest ( connectionId , 'userDataStream.unsubscribe' , requestParams ) ;
4444+ if ( subscriptionId === undefined || subscriptionId === tracked ) {
4445+ this . Options . userDataSubscriptionId = undefined ;
4446+ this . terminateWsApi ( connectionId , false ) ;
4447+ }
4448+ return result ;
43954449 }
43964450
43974451 // /**
0 commit comments