Twilio SMS/MMS
Description #
Plugin that provides a SimpleAPI for sending and receiving SMS/MMS messages using the Canvas SDK’s Twilio client. It supports listing phone numbers, sending messages, retrieving message history and media, managing inbound webhooks, handling delivery status callbacks, and auto-replying to inbound messages. Includes a chart application that renders a form interface for SMS management directly from the chart.
Configuration #
This example plugin defines the following “secrets” in the manifest file:
"secrets": [
"TwilioAccountSID",
"TwilioAPIKey",
"TwilioAPISecret"
],
Once defined in the MANIFEST.json, set the secrets for your plugin in the Admin UI of your Canvas EMR. Read more
TwilioAccountSID #
Your Twilio Account SID.
TwilioAPIKey #
Your Twilio API Key SID.
TwilioAPISecret #
Your Twilio API Key Secret.
CANVAS_MANIFEST.json #
{
"sdk_version": "0.85.0",
"plugin_version": "0.0.1",
"name": "twilio_sms_mms",
"description": "use Twillio to send and receive SMS/MMS",
"components": {
"protocols": [
{
"class": "twilio_sms_mms.handlers.sms_manip:SmsManip",
"description": "SMS/MMS with Twilio"
}
],
"applications": [
{
"class": "twilio_sms_mms.handlers.sms_form_app:SmsFormApp",
"name": "SMS Twilio",
"description": "SMS/MMS with Twilio",
"icon": "static/twilio_sms_mms.png",
"scope": "patient_specific",
"show_in_panel": false
}
],
"commands": [],
"content": [],
"effects": [],
"views": []
},
"secrets": [
"TwilioAccountSID",
"TwilioAPIKey",
"TwilioAPISecret"
],
"tags": {},
"references": [],
"license": "",
"diagram": false,
"readme": "./README.md"
}
handlers/ #
sms_manip.py #
Purpose
This code defines a SimpleAPI handler that exposes REST endpoints for managing SMS/MMS operations via the Canvas SDK’s Twilio client.
Class Overview
- The main class,
SmsManip, extendsStaffSessionAuthMixinandSimpleAPI. - It creates a Twilio
SmsClientusing credentials stored in plugin secrets (Account SID, API Key, API Secret).
Main Workflow
GET /phone_list— Retrieves all phone numbers associated with the Twilio account, including capabilities and webhook configuration.GET /message_list/<number>/<direction>— Lists messages for a specific phone number filtered by direction (from/to).GET /message/<message_sid>— Retrieves details of a specific message.GET /medias/<message_sid>— Retrieves all media attachments for a specific message.DELETE /message_delete/<message_sid>— Deletes a specific message.POST /sms_send— Sends an SMS message with optional status callback URL.POST /inbound_webhook/<phone_sid>— Configures the inbound webhook URL for a Twilio phone number.POST /outbound_api_status— Handles status callbacks for outbound SMS messages.POST /inbound_treatment— Processes incoming messages and sends automatic replies with optional media.
Twilio Client Integration
- The
_twillio_clientmethod creates anSmsClientinstance fromcanvas_sdk.clients.twilio.libraries. - Error handling uses the
RequestFailedexception from the Twilio client structures. - Inbound message handling generates TwiML responses for automatic replies.
from http import HTTPStatus
from canvas_sdk.clients.twilio.constants import DateOperation, HttpMethod
from canvas_sdk.clients.twilio.libraries import SmsClient
from canvas_sdk.clients.twilio.structures import (
RequestFailed,
Settings,
SmsMms,
StatusInbound,
StatusOutboundApi,
)
from canvas_sdk.effects import Effect
from canvas_sdk.effects.simple_api import HTMLResponse, JSONResponse, Response
from canvas_sdk.handlers.simple_api import SimpleAPI, StaffSessionAuthMixin, api
from logger import log
from ..constants.constants import Constants
class SmsManip(StaffSessionAuthMixin, SimpleAPI):
"""API handler for Twilio SMS/MMS operations."""
PREFIX = None
def _twillio_client(self) -> SmsClient:
"""Create and configure a Twilio SMS client."""
settings = Settings(
account_sid=self.secrets[Constants.twillio_account_sid],
key=self.secrets[Constants.twillio_api_key],
secret=self.secrets[Constants.twillio_api_secret],
)
return SmsClient(settings)
@api.get("/phone_list")
def phone_list(self) -> list[Response | Effect]:
"""Retrieve the list of phone numbers associated with the Twilio account."""
client = self._twillio_client()
try:
result = [
JSONResponse(
[
{
"sid": p.sid,
"phoneNumber": p.phone_number,
"label": p.friendly_name,
"capabilities": p.capabilities.to_dict(),
"statusCallback": p.status_callback,
"status": p.status,
"inboundWebhook": {
"url": p.sms_url,
"method": p.sms_method.value,
},
}
for p in client.account_phone_numbers()
],
status_code=HTTPStatus(HTTPStatus.OK),
),
]
except RequestFailed as e:
result = [
JSONResponse({"information": e.message}, status_code=HTTPStatus(e.status_code))
]
return result
@api.get("/message_list/<number>/<direction>")
def message_list(self) -> list[Response | Effect]:
"""Retrieve a list of messages for a specific phone number."""
number = self.request.path_params["number"]
direction = self.request.path_params["direction"]
number_from = number if direction == "from" else ""
number_to = number if direction == "to" else ""
client = self._twillio_client()
try:
result = [
JSONResponse(
[
{
"sid": p.sid,
"sent": p.date_sent.isoformat(),
"status": p.status.value,
"mediaCount": p.count_media,
}
for p in client.retrieve_all_sms(
number_to, number_from, "", DateOperation.ON_EXACTLY
)
],
status_code=HTTPStatus(HTTPStatus.OK),
),
]
except RequestFailed as e:
result = [
JSONResponse({"information": e.message}, status_code=HTTPStatus(e.status_code))
]
return result
@api.get("/message/<message_sid>")
def message(self) -> list[Response | Effect]:
"""Retrieve details of a specific message."""
message_sid = self.request.path_params["message_sid"]
client = self._twillio_client()
try:
result = [
JSONResponse(
client.retrieve_sms(message_sid).to_dict(),
status_code=HTTPStatus(HTTPStatus.OK),
),
]
except RequestFailed as e:
result = [
JSONResponse({"information": e.message}, status_code=HTTPStatus(e.status_code))
]
return result
@api.get("/medias/<message_sid>")
def media_list(self) -> list[Response | Effect]:
"""Retrieve all media attachments for a specific message."""
message_sid = self.request.path_params["message_sid"]
client = self._twillio_client()
try:
result = [
Response(
content_type=p.content_type,
content=client.retrieve_raw_media(message_sid, p.sid),
)
for p in client.retrieve_media_list(message_sid)
]
except RequestFailed as e:
result = [
JSONResponse({"information": e.message}, status_code=HTTPStatus(e.status_code))
]
return result
@api.delete("/message_delete/<message_sid>")
def message_delete(self) -> list[Response | Effect]:
"""Delete a specific message from Twilio."""
message_sid = self.request.path_params["message_sid"]
client = self._twillio_client()
try:
result = [
JSONResponse(
{
"sid": message_sid,
"deleted": client.delete_sms(message_sid),
},
status_code=HTTPStatus(HTTPStatus.OK),
)
]
except RequestFailed as e:
result = [
JSONResponse({"information": e.message}, status_code=HTTPStatus(e.status_code))
]
return result
@api.post("/inbound_webhook/<phone_sid>")
def inbound_webhook(self) -> list[Response | Effect]:
"""Configure the inbound webhook URL for a Twilio phone number."""
phone_sid = self.request.path_params["phone_sid"]
content = self.request.json()
webhook_url = content["url"]
method = HttpMethod(content["method"])
client = self._twillio_client()
try:
response = client.set_inbound_webhook(phone_sid, webhook_url, method)
result = [JSONResponse({"result": response}, status_code=HTTPStatus(HTTPStatus.OK))]
except RequestFailed as e:
result = [
JSONResponse({"information": e.message}, status_code=HTTPStatus(e.status_code))
]
return result
@api.post("/sms_send")
def sms_send(self) -> list[Response | Effect]:
"""Send an SMS message via Twilio."""
content = self.request.json()
number_from = content.get("numberFrom")
number_from_id = content.get("numberFromSid")
number_to = content.get("numberTo")
callback_url = content.get("callbackUrl")
text = content.get("text")
client = self._twillio_client()
try:
sms_mms = SmsMms(
number_from=number_from,
number_from_sid=number_from_id,
number_to=number_to,
message=text,
media_url="",
status_callback_url=callback_url,
)
response = client.send_sms_mms(sms_mms)
result = [JSONResponse(response.to_dict(), status_code=HTTPStatus(HTTPStatus.OK))]
except RequestFailed as e:
result = [
JSONResponse({"information": e.message}, status_code=HTTPStatus(e.status_code))
]
return result
@api.post("/outbound_api_status")
def outbound_api_status(self) -> list[Response | Effect]:
"""Handle status callbacks for outbound SMS messages."""
status = StatusOutboundApi.callback_outbound_api(self.request.text())
log.info(f"sms_extern: sid/status: {status.sms_sid}/{status.sms_status}")
return [Response(status_code=HTTPStatus(HTTPStatus.OK))]
@api.post("/inbound_treatment")
def inbound_treatment(self) -> list[Response | Effect]:
"""Handle inbound SMS messages with automatic replies."""
inbound = StatusInbound.callback_inbound(self.request.text())
if "hello" in inbound.body.lower():
reply = "Hello!"
image = "<Media>https://img.freepik.com/free-psd/hand-drawn-summer-frame-illustration_23-2151631028.jpg</Media>"
else:
reply = "Say hello!"
image = ""
response = (
'<?xml version="1.0" encoding="UTF-8"?>\n'
"<Response>"
f"<Message><Body>{reply}</Body>{image}</Message>"
"</Response>"
)
return [
HTMLResponse(
content=response,
status_code=HTTPStatus(HTTPStatus.OK),
)
]
sms_form_app.py #
Purpose
This code defines an Application handler that launches a modal form in the right chart pane for interacting with the Twilio SMS/MMS API endpoints.
from canvas_sdk.effects import Effect
from canvas_sdk.effects.launch_modal import LaunchModalEffect
from canvas_sdk.handlers.application import Application
from canvas_sdk.templates import render_to_string
from ..constants.constants import Constants
class SmsFormApp(Application):
"""Application handler for the SMS/MMS form interface."""
def on_open(self) -> Effect:
"""Render and launch the SMS form modal in the right chart pane."""
host = f"https://{self.environment[Constants.customer_identifier]}.canvasmedical.com"
content = render_to_string(
"templates/sms_form.html",
{
"smsSendURL": f"{Constants.plugin_api_base_route}/sms_send",
"phoneListURL": f"{Constants.plugin_api_base_route}/phone_list",
"setInboundWebhookURL": f"{Constants.plugin_api_base_route}/inbound_webhook",
"messageListURL": f"{Constants.plugin_api_base_route}/message_list",
"messageURL": f"{Constants.plugin_api_base_route}/message",
"mediaURL": f"{Constants.plugin_api_base_route}/medias",
"deleteMessageURL": f"{Constants.plugin_api_base_route}/message_delete",
"defaultCallbackURL": f"{host}{Constants.plugin_api_base_route}/outbound_api_status",
},
)
return LaunchModalEffect(
content=content,
target=LaunchModalEffect.TargetType.RIGHT_CHART_PANE,
).apply()