Skip to content

Channel Plugins

Channel plugins add communication methods to TeamWeb AI. They are configured per assistant and handle both sending messages out and receiving inbound webhooks.

Directory Layout

my_channel/
    manifest.py
    channel.py

Example: A Simple SMS Channel

manifest.py:

from typing import Any

from app.plugins.base import PluginManifest, PluginType

manifest = PluginManifest(
    name="sms_provider",
    plugin_type=PluginType.CHANNEL,
    channel_type="messaging",
    version="1.0.0",
    description="Send and receive SMS messages",
    author="Your Name",
    config_schema={
        "api_key": {
            "type": "string",
            "required": True,
            "secret": True,
            "label": "API Key",
        },
        "phone_number": {
            "type": "string",
            "required": True,
            "label": "Phone Number",
            "placeholder": "+1234567890",
        },
    },
)


def get_setup_docs(app_config: dict[str, Any], **kwargs: Any) -> str:
    public_url = app_config.get("PUBLIC_URL", "https://yourdomain.com")
    assistant_id = kwargs.get("assistant_id", "<assistant_id>")
    webhook_url = (
        f"{public_url}/webhooks/channels/sms_provider/{assistant_id}/inbound"
    )
    return (
        "<h6>SMS Setup</h6>"
        "<ol>"
        "<li>Configure your SMS provider to send webhooks to:"
        f"<code>{webhook_url}</code></li>"
        "</ol>"
    )

channel.py:

import logging
from typing import Any

from app.plugins.base import Attachment, BaseChannel, InboundMessage

logger = logging.getLogger(__name__)


class SmsChannel(BaseChannel):
    """Send and receive SMS messages.

    Instantiated with config from AssistantPlugin.config_json.
    """

    def __init__(self, api_key: str, phone_number: str, **kwargs: Any) -> None:
        self.api_key = api_key
        self.phone_number = phone_number

    def send_message(
        self,
        to: str,
        subject: str,
        body_html: str,
        body_text: str,
        reply_to: str | None = None,
        headers: dict[str, str] | None = None,
        attachments: list[Attachment] | None = None,
    ) -> dict[str, Any]:
        """Send an SMS message.

        Args:
            to: Recipient phone number.
            subject: Ignored for SMS.
            body_html: Ignored for SMS.
            body_text: Message text content.
            reply_to: Ignored for SMS.
            headers: Optional channel-specific headers.
            attachments: Ignored for SMS.

        Returns:
            Dict with message_id from the SMS provider.
        """
        # Call your SMS provider API here
        # ...
        return {"message_id": "msg_123"}

    def parse_inbound(self, payload: dict[str, Any]) -> InboundMessage:
        """Parse an inbound SMS webhook into a normalised message.

        Args:
            payload: Raw webhook payload from the SMS provider.

        Returns:
            Normalised InboundMessage.
        """
        return InboundMessage(
            sender=payload.get("from", ""),
            recipient=payload.get("to", ""),
            body_text=payload.get("text", ""),
            message_id=payload.get("id"),
        )

    def build_reply_to(self, conversation_reply_token: str) -> str:
        """Build a reply-to identifier with the conversation token.

        For channels that support reply threading, embed the token so
        inbound messages can be routed to the correct conversation.

        Args:
            conversation_reply_token: UUID token for the conversation.

        Returns:
            Reply-to string (e.g. phone number with token suffix).
        """
        return ""

Channel Type Constraint

The channel_type in the manifest must be one of "email", "messaging", "voice", or "browser". Only one channel of each type may be active per assistant. Enabling a second messaging channel automatically disables the first.

Feedback Support

Channel plugins can declare support for TeamWeb AI’s feedback system by setting flags on the manifest:

manifest = PluginManifest(
    name="my_channel",
    plugin_type=PluginType.CHANNEL,
    channel_type="browser",
    # ...
    supports_message_feedback=True,   # Per-message thumbs up/down
    supports_session_feedback=True,   # Post-session rating form
)
FlagDefaultDescription
supports_message_feedbackFalseThe channel UI can show thumbs up/down on assistant messages
supports_session_feedbackFalseThe channel UI can show a feedback form when a session ends

When these flags are True, the channel configuration page in the admin UI shows additional Feedback Settings where admins can choose which user types see feedback controls.

Only channels with a UI controlled by TeamWeb AI should set these flags (e.g., browser_internal and browser_external). External platforms like Slack, Telegram, and email don’t have a natural place for feedback controls.

Constructor

The channel class is instantiated with the plugin configuration unpacked as keyword arguments from AssistantPlugin.config_json. Define your __init__ parameters to match the keys in your config_schema. Always include **kwargs to handle any extra fields gracefully.

Webhook URL

Inbound messages are received at:

/webhooks/channels/<plugin_name>/<assistant_id>/inbound

TeamWeb AI automatically routes the payload to your parse_inbound() method. Use get_setup_docs() in your manifest to display the correct webhook URL in the configuration UI.

InboundMessage Fields

FieldDescription
senderSender identifier (email, phone, user ID)
recipientRecipient identifier
subjectMessage subject (if applicable)
body_textPlain text message body
body_htmlHTML body (if available)
message_idChannel-specific message ID
reply_tokenToken for routing to an existing conversation
attachmentsList of attachment dicts with filename, content_type, data

send_message Parameters

ParameterDescription
toRecipient address (email, channel ID, phone number)
subjectMessage subject (ignore if not applicable to your channel)
body_htmlHTML body content
body_textPlain text body content
reply_toReply-to address for threading
headersChannel-specific headers dict (e.g. slack_thread_ts)
attachmentsList of Attachment objects with filename, data (bytes), and content_type

Return a dict with at least a message_id key.

Reply Threading

For channels that support reply threading, implement build_reply_to() to embed a conversation token into a reply address. TeamWeb AI calls this when sending the first message in a conversation so that subsequent inbound messages can be routed back to the same conversation.

For example, the Postmark email channel embeds the token in the local part of a reply-to email address: reply+{token}@yourdomain.com.

Request Verification

For channels that support request signing (e.g. Slack’s HMAC signatures), implement a verify_request class method that validates the webhook signature. TeamWeb AI calls this before processing the payload:

@classmethod
def verify_request(
    cls,
    signing_secret: str,
    timestamp: str,
    body: bytes,
    signature: str,
) -> bool:
    """Verify the webhook request signature.

    Args:
        signing_secret: The app's signing secret.
        timestamp: Request timestamp header value.
        body: Raw request body bytes.
        signature: Signature header value.

    Returns:
        True if the signature is valid.
    """
    # Implement your verification logic here
    return True