SimpleAPI WebSocket

WebSocket APIs in Canvas let you define channels that clients can connect to. These APIs support one-way, server-to-client communication, and are designed for use cases such as real-time notifications.

To use websockets in a Canvas plugin, you must define the WebSocketAPI auth handler to manage access to your channel, use the Broadcast effect to publish messages to that channel, and create client code to manage the connection, handle new messages, and gracefully reconnect.

Defining a WebSocket API #

To define a WebSocket handler, subclass WebSocketAPI. You must implement an authenticate method that determines whether the connection should be accepted.

from canvas_sdk.handlers.simple_api.websocket import WebSocketAPI

class MyWebSocketAPI(WebSocketAPI):
    def authenticate(self) -> bool:
        ...

If authenticate() returns True, the connection is accepted. Otherwise, it is denied.

Clients should connect using a URL that maps to your plugin and channel name:

wss://<instance>.canvasmedical.com/plugin-io/ws/<plugin_name>/<channel_name>/

Connecting from a patient portal page #

With family portal access, a family member can have different charts open in different browser tabs. A browser can’t set headers on a WebSocket, so a portal page names the chart it shows in the URL, with the patient’s key in acting_for:

wss://<instance>.canvasmedical.com/plugin-io/ws/<plugin_name>/<channel_name>/?acting_for=<patient_key>

The connection’s logged_in_user and portal_access then describe that chart, as the X-Portal-Acting-For header does for the page’s SimpleAPI requests. Without acting_for, the connection acts on the chart the session last chose.

WebSocket Object #

When a handler is invoked, the websocket object is available as an attribute on the handler. This object provides details about the connection, including:

  • channel: The channel name from the connection URL
  • headers: A dictionary of headers associated with the connection
  • api_key: The api key, if present
  • logged_in_user: A dictionary like { "id": ..., "type": ... } if a logged-in user is present
  • portal_access: For a connection from the patient portal, who is using the chart and their access level in each area. It has the same attributes as SessionCredentials.portal_access on SimpleAPI requests.

You can access this object within your handler methods using self.websocket.

Authentication #

You must implement the authenticate() method in your handler class. Two authentication methods are supported:

Session-Based (Internal Clients) #

For connections initiated from within the Canvas browser UI by a logged-in user:

def authenticate(self) -> bool:
    logged_in_user = self.websocket.logged_in_user
    # Structure looks like:
    # {
    #     "id": "abc123",
    #     "type": "Staff"
    # }
    # Where "type" is "Staff" or "Patient"
    # You could authenticate based on type or check to see if the
    # individual is in a particular group or team.
    ...

For a connection from the patient portal, id is the key of the patient whose chart the portal is open on. With family portal access, that patient can be someone other than the person signed in.

To accept connections from a family member or representative, set PORTAL_ACCESS on your handler to the lowest access level it needs in each area, as on a SimpleAPI. They connect when authenticate() accepts them and they have at least that level in every area it names:

from canvas_sdk.handlers.simple_api import PortalAccessLevel
from canvas_sdk.handlers.simple_api.websocket import WebSocketAPI


class CopayWebSocket(WebSocketAPI):
    PORTAL_ACCESS = {
        "appointments": PortalAccessLevel.VIEW,
        "billing": PortalAccessLevel.VIEW,
    }

    def authenticate(self) -> bool:
        user = self.websocket.logged_in_user
        return user is not None and user["type"] == "Patient"

APIKey-Based (External Clients) #

For external tools or scripts, pass an auth key as a query parameter:

wss://<instance>.canvasmedical.com/plugin-io/ws/<plugin>/<channel>?api_key=<key>

The key is made available in your handler via self.websocket.api_key. You can store this key as a secret.

def authenticate(self) -> bool:
  provided_token = self.websocket.api_key

  # Validate provided key against an API key in self.secrets
  ...

Broadcasting Messages #

To send a message to all connected clients on a WebSocket channel, use the Broadcast effect. This effect can be returned from any handler.

The Broadcast effect takes the following parameters:

  • message: A JSON-serializable Python dictionary or value.
  • channel: The target channel name as a string.

Here’s an example using a SimpleAPI HTTP POST route to trigger a broadcast:

from http import HTTPStatus
from canvas_sdk.effects import Effect
from canvas_sdk.effects.simple_api import Broadcast, JSONResponse, Response
from canvas_sdk.handlers.simple_api import Credentials, SimpleAPI, api

class WebhookAPI(SimpleAPI):
    @api.post("/callback")
    def broadcast_message(self) -> list[Response | Effect]:
        body = self.request.json()

        return [
            Broadcast(message=body, channel="notifications").apply(),
            JSONResponse({"status": "ok"}, status_code=HTTPStatus.ACCEPTED),
        ]

In this example, when the /callback endpoint receives a POST request, it broadcasts the request body as a message to all clients subscribed to the notifications channel.