diff --git a/README.md b/README.md
index e683a1a0d8..5b44b1df93 100644
--- a/README.md
+++ b/README.md
@@ -467,8 +467,6 @@
Ntfy.sh
-
-

@@ -481,6 +479,8 @@
Resend
|
+
+

@@ -499,6 +499,18 @@
SMTP
|
+
+
+ 
+ Telegram
+
+ |
+
+
+ 
+ Twilio
+
+ |

@@ -507,22 +519,16 @@
|
- 
+ 
Zoom
|
-
- 
- Telegram
-
- |
-
-
- 
- Twilio
+
+ 
+ Zoom Chat
|
diff --git a/docs/images/zoom_chat-provider1.png b/docs/images/zoom_chat-provider1.png
new file mode 100644
index 0000000000..3fedb4f11e
Binary files /dev/null and b/docs/images/zoom_chat-provider1.png differ
diff --git a/docs/images/zoom_chat-provider10.png b/docs/images/zoom_chat-provider10.png
new file mode 100644
index 0000000000..83a1af63e0
Binary files /dev/null and b/docs/images/zoom_chat-provider10.png differ
diff --git a/docs/images/zoom_chat-provider2.png b/docs/images/zoom_chat-provider2.png
new file mode 100644
index 0000000000..a315f5b573
Binary files /dev/null and b/docs/images/zoom_chat-provider2.png differ
diff --git a/docs/images/zoom_chat-provider3.png b/docs/images/zoom_chat-provider3.png
new file mode 100644
index 0000000000..44af6d78bb
Binary files /dev/null and b/docs/images/zoom_chat-provider3.png differ
diff --git a/docs/images/zoom_chat-provider4.png b/docs/images/zoom_chat-provider4.png
new file mode 100644
index 0000000000..c073ae4f75
Binary files /dev/null and b/docs/images/zoom_chat-provider4.png differ
diff --git a/docs/images/zoom_chat-provider5.png b/docs/images/zoom_chat-provider5.png
new file mode 100644
index 0000000000..56ae5e5531
Binary files /dev/null and b/docs/images/zoom_chat-provider5.png differ
diff --git a/docs/images/zoom_chat-provider6.png b/docs/images/zoom_chat-provider6.png
new file mode 100644
index 0000000000..b217320a3c
Binary files /dev/null and b/docs/images/zoom_chat-provider6.png differ
diff --git a/docs/images/zoom_chat-provider7.png b/docs/images/zoom_chat-provider7.png
new file mode 100644
index 0000000000..fb34a190b2
Binary files /dev/null and b/docs/images/zoom_chat-provider7.png differ
diff --git a/docs/images/zoom_chat-provider8.png b/docs/images/zoom_chat-provider8.png
new file mode 100644
index 0000000000..812fa24346
Binary files /dev/null and b/docs/images/zoom_chat-provider8.png differ
diff --git a/docs/images/zoom_chat-provider9.png b/docs/images/zoom_chat-provider9.png
new file mode 100644
index 0000000000..4c2aa7c5bf
Binary files /dev/null and b/docs/images/zoom_chat-provider9.png differ
diff --git a/docs/mint.json b/docs/mint.json
index 07a6c3ce4c..625116c134 100644
--- a/docs/mint.json
+++ b/docs/mint.json
@@ -266,7 +266,8 @@
"providers/documentation/youtrack-provider",
"providers/documentation/zabbix-provider",
"providers/documentation/zenduty-provider",
- "providers/documentation/zoom-provider"
+ "providers/documentation/zoom-provider",
+ "providers/documentation/zoom_chat-provider"
]
},
"providers/adding-a-new-provider"
diff --git a/docs/providers/documentation/zoom_chat-provider.mdx b/docs/providers/documentation/zoom_chat-provider.mdx
new file mode 100644
index 0000000000..6240726beb
--- /dev/null
+++ b/docs/providers/documentation/zoom_chat-provider.mdx
@@ -0,0 +1,82 @@
+---
+title: "Zoom Chat"
+sidebarTitle: "Zoom Chat Provider"
+description: "Zoom Chat provider allows you to send Zoom Chats using the Incoming Webhook Zoom application."
+---
+import AutoGeneratedSnippet from '/snippets/providers/zoom_chat-snippet-autogenerated.mdx';
+
+
+For this integration, you will need to add and configure the Incoming Webhook application from the Zoom App Marketplace: https://marketplace.zoom.us/apps/eH_dLuquRd-VYcOsNGy-hQ
+
+
+
+
+## Connecting with the Provider
+
+### Enable the Incoming Webhook Application
+
+The Incoming Webhook application is available in the Zoom App Marketplace.
+
+
+
+
+
+
+
+
+
+### Create Team Chat Channel:
+
+This channel will be the recipient of the Keep notifications.
+
+
+
+
+
+
+
+
+
+### Enable the Incoming Webhook Application
+
+Send `/inc connect ` to the channel to enable a webhook with authorization code. The app will respond with the webhook url and authorization code.
+
+
+You should use the "Full Format" Incoming Webhook Url, which ends in `?format=full`.
+
+
+
+
+
+
+
+
+
+
+## (Optional) Enabling User JID Lookup
+
+Messages can optionally include Zoom user JIDs, which are used to tag a particular Zoom user in a message.
+This is useful, for example, if a team subscribes to a chat channel but members only wish to be notified when they are explicitly tagged.
+
+### Create a Zoom Application
+
+User lookup requires authorization. Create an internal only, Zoom Server to Server OAuth application.
+
+
+
+
+
+
+
+
+
+### Assign Required Scopes
+
+
+
+
+
+
+
+
+
diff --git a/docs/providers/overview.md b/docs/providers/overview.md
index 3ef8a0c7c6..13cb496b62 100644
--- a/docs/providers/overview.md
+++ b/docs/providers/overview.md
@@ -129,3 +129,4 @@ By leveraging Keep Providers, users are able to deeply integrate Keep with the t
- [Zabbix](/providers/documentation/zabbix-provider)
- [Zenduty](/providers/documentation/zenduty-provider)
- [Zoom](/providers/documentation/zoom-provider)
+- [Zoom Chat](/providers/documentation/zoom_chat-provider)
diff --git a/docs/providers/overview.mdx b/docs/providers/overview.mdx
index dd6ebeef36..37d08694e6 100644
--- a/docs/providers/overview.mdx
+++ b/docs/providers/overview.mdx
@@ -976,3 +976,10 @@ By leveraging Keep Providers, users are able to deeply integrate Keep with the t
icon={
}
>
+
+ }
+>
+
diff --git a/docs/snippets/providers/zoom_chat-snippet-autogenerated.mdx b/docs/snippets/providers/zoom_chat-snippet-autogenerated.mdx
new file mode 100644
index 0000000000..497a542be1
--- /dev/null
+++ b/docs/snippets/providers/zoom_chat-snippet-autogenerated.mdx
@@ -0,0 +1,42 @@
+{/* This snippet is automatically generated using scripts/docs_render_provider_snippets.py
+Do not edit it manually, as it will be overwritten */}
+
+## Authentication
+This provider requires authentication.
+- **webhook_url**: Zoom Incoming Webhook Full Format Url (required: True, sensitive: True)
+- **authorization_token**: Incoming Webhook Authorization Token (required: True, sensitive: True)
+- **account_id**: Zoom Account ID (required: False, sensitive: True)
+- **client_id**: Zoom Client ID (required: False, sensitive: True)
+- **client_secret**: Zoom Client Secret (required: False, sensitive: True)
+
+Certain scopes may be required to perform specific actions or queries via the provider. Below is a summary of relevant scopes and their use cases:
+- **user:read:user:admin**: View a Zoom user's details
+- **user:read:list_users:admin**: List Zoom users
+
+
+
+## In workflows
+
+This provider can be used in workflows.
+
+
+
+As "action" to make changes or update data, example:
+```yaml
+actions:
+ - name: Query zoom_chat
+ provider: zoom_chat
+ config: "{{ provider.my_provider_name }}"
+ with:
+ severity: {value} # The severity of the alert.
+ title: {value} # The title to use for the message. (optional)
+ message: {value} # The text message to send. Supports Markdown formatting.
+ tagged_users: {value} # A list of Zoom user email addresses to tag. (optional)
+ details_url: {value} # A URL linking to more information. (optional)
+```
+
+
+
+
+Check the following workflow example:
+- [zoom_chat_example.yml](https://github.com/keephq/keep/blob/main/examples/workflows/zoom_chat_example.yml)
diff --git a/examples/workflows/zoom_chat_example.yml b/examples/workflows/zoom_chat_example.yml
new file mode 100644
index 0000000000..55bd11213e
--- /dev/null
+++ b/examples/workflows/zoom_chat_example.yml
@@ -0,0 +1,17 @@
+workflow:
+ id: zoom_chat-message
+ name: Zoom Chat Message
+ description: Sends a notification to a Zoom Chat channel via the Incoming Webhook application.
+ triggers:
+ - type: manual
+ actions:
+ - name: zoom_chat-action
+ provider:
+ type: zoom_chat
+ config: "{{ providers.zoom_chat }}"
+ with:
+ message: test message from keep
+ severity: critical
+ title: critical test message
+ tagged_users: joesmith@mail.com
+ details_url: https://www.github.com/keep
\ No newline at end of file
diff --git a/keep-ui/public/icons/zoom_chat-icon.png b/keep-ui/public/icons/zoom_chat-icon.png
new file mode 100644
index 0000000000..c5d6f985f3
Binary files /dev/null and b/keep-ui/public/icons/zoom_chat-icon.png differ
diff --git a/keep/providers/zoom_chat_provider/__init__.py b/keep/providers/zoom_chat_provider/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/keep/providers/zoom_chat_provider/zoom_chat_provider.py b/keep/providers/zoom_chat_provider/zoom_chat_provider.py
new file mode 100644
index 0000000000..375672fbf2
--- /dev/null
+++ b/keep/providers/zoom_chat_provider/zoom_chat_provider.py
@@ -0,0 +1,368 @@
+"""
+ZoomChatProvider is a class that provides a way to send Zoom Chats programmatically using the Incoming Webhook Zoom application.
+"""
+
+import dataclasses
+import http
+import os
+import time
+from typing import Optional
+
+import pydantic
+import requests
+from requests.auth import HTTPBasicAuth
+
+from keep.contextmanager.contextmanager import ContextManager
+from keep.exceptions.provider_exception import ProviderException
+from keep.providers.base.base_provider import BaseProvider
+from keep.providers.models.provider_config import ProviderConfig, ProviderScope
+from keep.validation.fields import HttpsUrl
+
+
+@pydantic.dataclasses.dataclass
+class ZoomChatProviderAuthConfig:
+ """
+ ZoomChatProviderAuthConfig holds the authentication information for the ZoomChatProvider.
+ """
+
+ webhook_url: HttpsUrl = dataclasses.field(
+ metadata={
+ "name": "webhook_url",
+ "description": "Zoom Incoming Webhook Full Format Url",
+ "required": True,
+ "sensitive": True,
+ "validation": "https_url",
+ },
+ )
+ authorization_token: str = dataclasses.field(
+ metadata={
+ "name": "authorization_token",
+ "description": "Incoming Webhook Authorization Token",
+ "required": True,
+ "sensitive": True,
+ },
+ )
+ account_id: Optional[str] = dataclasses.field(
+ default="zoom_account_id",
+ metadata={
+ "required": False,
+ "description": "Zoom Account ID",
+ "sensitive": True,
+ }
+ )
+ client_id: Optional[str] = dataclasses.field(
+ default="zoom_client_id",
+ metadata={
+ "required": False,
+ "description": "Zoom Client ID",
+ "sensitive": True,
+ }
+ )
+ client_secret: Optional[str] = dataclasses.field(
+ default="zoom_client_secret",
+ metadata={
+ "required": False,
+ "description": "Zoom Client Secret",
+ "sensitive": True,
+ }
+ )
+
+
+class ZoomChatProvider(BaseProvider):
+ """Send alert message to Zoom Chat using the Incoming Webhook application."""
+
+ PROVIDER_DISPLAY_NAME = "Zoom Chat"
+ PROVIDER_TAGS = ["messaging"]
+ PROVIDER_CATEGORY = ["Communication"]
+ BASE_URL = "https://api.zoom.us/v2"
+
+ PROVIDER_SCOPES = [
+ ProviderScope(
+ name="user:read:user:admin",
+ description="View a Zoom user's details",
+ mandatory=False,
+ alias="View a Zoom user",
+ ),
+ ProviderScope(
+ name="user:read:list_users:admin",
+ description="List Zoom users",
+ mandatory=False,
+ alias="List Zoom users",
+ ),
+ ]
+
+ def __init__(
+ self,
+ context_manager: ContextManager,
+ provider_id: str,
+ config: ProviderConfig,
+ ):
+ super().__init__(context_manager, provider_id, config)
+ self.access_token = None
+
+ def validate_config(self):
+ """Validates required configuration for Zoom Chat provider."""
+ self.authentication_config = ZoomChatProviderAuthConfig(
+ **self.config.authentication
+ )
+ if (
+ not self.authentication_config.webhook_url
+ and not self.authentication_config.authorization_token
+ ):
+ raise Exception(
+ "Zoom Incoming Webhook URL and authorization token are required."
+ )
+
+ def _get_access_token(self) -> str:
+ """
+ Get OAuth access token from Zoom.
+ Returns:
+ str: Access token
+ """
+ try:
+ token_url = "https://zoom.us/oauth/token"
+ auth = HTTPBasicAuth(
+ self.authentication_config.client_id,
+ self.authentication_config.client_secret,
+ )
+ data = {
+ "grant_type": "account_credentials",
+ "account_id": self.authentication_config.account_id,
+ }
+ response = requests.post(token_url, auth=auth, data=data)
+ if response.status_code != 200:
+ raise ProviderException(
+ f"Failed to get access token: {response.json()}"
+ )
+ return response.json()["access_token"]
+ except Exception as e:
+ raise ProviderException(f"Failed to get access token: {str(e)}")
+
+ def _get_headers(self) -> dict:
+ """
+ Get headers for API requests.
+ Returns:
+ dict: Headers including authorization
+ """
+ if not self.access_token:
+ self.access_token = self._get_access_token()
+ return {
+ "Authorization": f"Bearer {self.access_token}",
+ "Content-Type": "application/json",
+ }
+
+ def validate_scopes(self) -> dict[str, bool | str]:
+ """Validate scopes for the provider."""
+ if not all(
+ [
+ self.authentication_config.account_id,
+ self.authentication_config.client_id,
+ self.authentication_config.client_secret,
+ ]
+ ):
+ return {
+ "user:read:user:admin": "OAuth credentials not configured",
+ "user:read:list_users:admin": "OAuth credentials not configured",
+ }
+ try:
+ # Test API access by listing users
+ response = requests.get(
+ f"{self.BASE_URL}/users", headers=self._get_headers()
+ )
+ if response.status_code != 200:
+ raise Exception(f"Failed to validate scopes: {response.json()}")
+ return {
+ "user:read:user:admin": True,
+ "user:read:list_users:admin": True,
+ }
+ except Exception as e:
+ self.logger.exception("Failed to validate scopes")
+ return {
+ "user:read:user:admin": str(e),
+ "user:read:list_users:admin": str(e),
+ }
+
+ def dispose(self):
+ """Clean up resources."""
+ self.access_token = None
+ pass
+
+ def _get_zoom_userinfo(self, email: str) -> dict:
+ """Get a user's information from Zoom API using email address."""
+ try:
+ response = requests.get(
+ f"{self.BASE_URL}/users/{email}",
+ headers=self._get_headers(),
+ )
+ if response.status_code == 200:
+ self.logger.info("User details retrieved successfully")
+ return response.json()
+ else:
+ raise ProviderException(
+ f"Failed to retrieve user info for {email}: {response.status_code} - {response.text}"
+ )
+ except requests.exceptions.RequestException as e:
+ raise ProviderException(f"Failed to retrieve user info: {str(e)}")
+
+ def _notify(
+ self,
+ severity: str = "info",
+ title: Optional[str] = "",
+ message: str = "",
+ tagged_users: Optional[str] = "",
+ details_url: Optional[str] = "",
+ **kwargs: dict,
+ ) -> str:
+ """
+ Send a message to Zoom Chat using a Incoming Webhook URL.
+ Args:
+ title (str): The title to use for the message. (optional)
+ message (str): The text message to send. Supports Markdown formatting.
+ tagged_users (list): A list of Zoom user email addresses to tag. (optional)
+ severity (str): The severity of the alert.
+ details_url (str): A URL linking to more information. (optional)
+ Raises:
+ ProviderException: If the message could not be sent successfully.
+ """
+ self.logger.debug("Sending message to Zoom Chat Incoming Webhook")
+ webhook_url = self.authentication_config.webhook_url
+ authorization_token = self.authentication_config.authorization_token
+ if not message:
+ raise ProviderException("Message is required")
+
+ def __send_message(url, body, headers, retries=3):
+ for attempt in range(retries):
+ try:
+ resp = requests.post(url, json=body, headers=headers)
+ if resp.status_code == http.HTTPStatus.OK:
+ return resp
+ self.logger.warning(
+ f"Attempt {attempt + 1} failed with status code {resp.status_code}"
+ )
+ except requests.exceptions.RequestException as e:
+ self.logger.error(f"Attempt {attempt + 1} failed: {e}")
+ if attempt < retries - 1:
+ time.sleep(1)
+ raise requests.exceptions.RequestException(
+ f"Failed to notify message after {retries} attempts"
+ )
+
+ payload = {
+ "content": {
+ "settings": {
+ "default_sidebar_color": (
+ "#EF4444"
+ if severity == "critical"
+ else (
+ "#F97316"
+ if severity == "high"
+ else (
+ "#EAB308"
+ if severity == "warning"
+ else "#10B981" if severity == "low" else "#3B82F6"
+ )
+ )
+ )
+ },
+ "body": [
+ {
+ "type": "message",
+ "is_markdown_support": "true",
+ "text": message,
+ }
+ ],
+ }
+ }
+
+ # Conditionally add a title entry
+ if title:
+ payload["content"]["head"] = {
+ "text": title,
+ "style": {"bold": "true"},
+ }
+
+ # Conditionally add the "View More Details" entry
+ if details_url:
+ payload["content"]["body"].append(
+ {"type": "message", "text": "View More Details", "link": details_url}
+ )
+
+ # Conditionally add tagged users
+ if tagged_users:
+ tagged_users_list = [user.strip() for user in tagged_users.split(",")]
+ tagged_user_jid_list = []
+
+ for user in tagged_users_list:
+ try:
+ user_data = self._get_zoom_userinfo(user)
+ jid = user_data.get("jid")
+ display_name = user_data.get("display_name")
+ if jid and display_name:
+ tagged_user_jid_list.append(f"")
+ except ProviderException as e:
+ self.logger.warning(f"Failed to get info for user {user}: {e}")
+ continue
+
+ if tagged_user_jid_list:
+ tagged_user_string = " ".join(tagged_user_jid_list)
+ payload["content"]["body"].insert(
+ 0,
+ {
+ "type": "message",
+ "is_markdown_support": True,
+ "text": tagged_user_string,
+ },
+ )
+
+ request_headers = {
+ "Authorization": authorization_token,
+ "Content-Type": "application/json",
+ }
+ response = __send_message(webhook_url, body=payload, headers=request_headers)
+ if response.status_code != http.HTTPStatus.OK:
+ raise ProviderException(
+ f"Failed to send message to Zoom Chat: {response.text}"
+ )
+ self.logger.debug("Alert message sent to Zoom Chat successfully")
+ return "Alert message sent to Zoom Chat successfully"
+
+
+if __name__ == "__main__":
+ import logging
+
+ # Set up logging
+ logging.basicConfig(level=logging.INFO, handlers=[logging.StreamHandler()])
+
+ # Get webhook details from environment
+ webhook_url = os.environ.get("ZOOM_WEBHOOK_URL")
+ webhook_auth_token = os.environ.get("ZOOM_WEBHOOK_AUTH_TOKEN")
+
+ if not all([webhook_url, webhook_auth_token]):
+ raise Exception(
+ "ZOOM_WEBHOOK_URL and ZOOM_WEBHOOK_AUTH_TOKEN are required"
+ )
+
+ # Create context manager
+ context_manager = ContextManager(
+ tenant_id="singletenant",
+ workflow_id="test",
+ )
+
+ # Initialize the provider and provider config
+ config = ProviderConfig(
+ name="Zoom Chat",
+ description="Zoom Chat Output Provider",
+ authentication={
+ "webhook_url": webhook_url,
+ "authorization_token": webhook_auth_token,
+ },
+ )
+
+ # Initialize provider
+ provider = ZoomChatProvider(
+ context_manager=context_manager,
+ provider_id="zoom_chat_provider",
+ config=config,
+ )
+
+ provider.notify(message="Simple alert to Zoom chat.")