You can call the ic_websocket_cdk::ws_send function in any update call in your canister. You can also expose expose an update method to call the ws_send (e.g. from another canister using inter-canister calls). We did the same for the ic_websocket_cdk’s integration test canister. You can have a look at it here.
I think here you were just confusing the ws_message with the ws_send. The ws_message should just be used in the ws_message update method exposed by the canister.
You get the client_key for the first time when the on_open callback is called. The on_open callback that you pass to the CDK must accept an argument of type OnOpenCallbackArgs, which is a struct that contains the client_key. In the ic_websocket_example you can see it:
So, in the on_open callback you can for example save the client_key in a map and read it from that map later when in your logic you need to call the ws_send.
That’s what for us seems to be a bug. Reinstalling the canister may solve the issue, as I mentioned here:
Have you tried it?
On the frontend client, are you getting any errors? Are you getting this sequence of log messages in the console?
[init] Generating new secret key
[onWsOpen] WebSocket opened, sending first service message
[onWsOpen] First service message sent
[onWsMessage] First service message received