WebSocket Guest API handshake fails with 4003 after successful token exchange

I’m building a custom web messaging UI to replace the standard widget. The goal is to handle the entire lifecycle via the WebSocket-based Guest API rather than the iframe approach.

The initial step works fine. I call /api/v2/oauth/token with my client credentials and get a valid access token. I then use that token to fetch the guest connection details from /api/v2/webmessaging/guests. The response gives me the gatewayUrl and the accessToken required for the socket connection.

Here is the setup code:

const wsUrl = response.data.gatewayUrl;
const token = response.data.accessToken;
const socket = new WebSocket(wsUrl, ['NICE-WebSocket-Auth', token]);

The connection opens, but immediately after the handshake, the server sends a close frame with code 4003 and the reason Invalid authentication token.

I’ve verified the token isn’t expired. It was generated seconds ago. I can use the same access token to successfully post a message via the REST endpoint /api/v2/webmessaging/guests/messages without any issues. The REST API accepts it, but the WebSocket gateway rejects it.

I tried adding the token as a query parameter instead of a subprotocol header, like this:

const socket = new WebSocket(`${wsUrl}?token=${token}`);

That results in a 401 Unauthorized at the HTTP upgrade level. The docs are sparse on the exact WebSocket handshake requirements for the Guest API. They mention using the accessToken from the guest resource, but don’t specify if it needs to be signed or if there’s a specific header format beyond the standard Sec-WebSocket-Protocol.

Has anyone successfully connected to the NICE-WebSocket-Auth gateway with a custom client? What’s the exact header format expected? The token format looks like a standard JWT, but maybe the gateway expects a different encoding or a specific claim structure that the REST API doesn’t care about.

I’m stuck on this handshake. Any pointers on debugging the WebSocket auth layer would be helpful.