How to set up a Genesys Cloud webhook that sends Slack notifications on queue SLA breach
What You Will Build
- This tutorial builds a Python microservice that listens for Genesys Cloud Queue SLA breach events via Webhooks and posts formatted alerts to a Slack channel.
- The solution uses the Genesys Cloud Platform API for webhook registration and the Python
requestslibrary for the event listener and Slack integration. - The programming language covered is Python 3.9+.
Prerequisites
- OAuth Client: A Genesys Cloud OAuth Client with the
webhook:writeandwebhook:readscopes. - Slack App: A Slack Application with an Incoming Webhook installed in your target channel, providing a Webhook URL.
- SDK Version:
genesys-cloud-purecloud-platform-clientv2.10.0 or later. - Language/Runtime: Python 3.9+.
- External Dependencies:
requests,genesys-cloud-purecloud-platform-client,pydantic(for optional validation).
Authentication Setup
Genesys Cloud uses OAuth 2.0 for API access. For this tutorial, you will use the Client Credentials grant type. This is appropriate for server-to-server communication where no user context is required.
You must obtain the following from the Genesys Cloud Admin Console:
- Client ID
- Client Secret
- Domain (e.g.,
api.mypurecloud.comorapi.us-gov-purecloud.com)
The following Python code demonstrates how to acquire an access token. In production, you should implement token caching and refresh logic to avoid hitting rate limits.
import requests
import base64
import time
class GenesysAuth:
def __init__(self, client_id: str, client_secret: str, domain: str):
self.client_id = client_id
self.client_secret = client_secret
self.domain = domain
self.token_url = f"https://{domain}/oauth/token"
self.access_token = None
self.token_expiry = 0
def get_token(self) -> str:
"""
Retrieves an OAuth2 access token using Client Credentials flow.
Implements basic caching to avoid unnecessary requests.
"""
# Check if token is still valid (subtract 60s for safety buffer)
if self.access_token and time.time() < self.token_expiry - 60:
return self.access_token
# Prepare basic auth header
credentials = f"{self.client_id}:{self.client_secret}"
encoded_credentials = base64.b64encode(credentials.encode('utf-8')).decode('utf-8')
headers = {
"Content-Type": "application/x-www-form-urlencoded",
"Authorization": f"Basic {encoded_credentials}"
}
data = {
"grant_type": "client_credentials"
}
try:
response = requests.post(self.token_url, headers=headers, data=data)
response.raise_for_status()
token_data = response.json()
self.access_token = token_data["access_token"]
self.token_expiry = time.time() + token_data["expires_in"]
return self.access_token
except requests.exceptions.HTTPError as e:
if response.status_code == 401:
raise Exception("Invalid Client ID or Secret.")
elif response.status_code == 429:
# Implement exponential backoff in production
raise Exception("Rate limited. Wait before retrying.")
else:
raise Exception(f"OAuth Error: {e}")
Implementation
Step 1: Register the Webhook via Genesys Cloud API
Before the listener can receive events, you must register a webhook endpoint with Genesys Cloud. The webhook must target a public HTTPS endpoint that can accept POST requests.
Required Scope: webhook:write
The following function uses the Genesys Cloud Python SDK to create the webhook.
from genesyscloud.rest import Configuration, ApiClient
from genesyscloud.webhooks.api import WebhooksApi
from genesyscloud.webhooks.model import Webhook, WebhookConfig
def create_sla_webhook(auth: GenesysAuth, webhook_url: str) -> str:
"""
Registers a webhook in Genesys Cloud to listen for Queue SLA breach events.
Args:
auth: GenesysAuth instance containing valid credentials.
webhook_url: The public HTTPS URL where Genesys will POST events.
Returns:
The ID of the created webhook.
"""
# Initialize SDK client
configuration = Configuration(
host=f"https://{auth.domain}",
access_token=auth.get_token()
)
api_client = ApiClient(configuration)
webhooks_api = WebhooksApi(api_client)
# Define the webhook configuration
# Event: queue:member:sla:breach
# This event fires when a queue member's wait time exceeds the defined SLA threshold.
webhook = Webhook(
name="Slack SLA Breach Notifier",
description="Sends alerts to Slack when queue SLA is breached.",
url=webhook_url,
events=["queue:member:sla:breach"],
config=WebhookConfig(
headers={}, # Optional: Add custom headers here
method="POST"
)
)
try:
# Create the webhook
response = webhooks_api.post_webhooks(body=webhook)
print(f"Webhook created successfully with ID: {response.id}")
return response.id
except Exception as e:
print(f"Error creating webhook: {e}")
raise
Important Note on Webhook Events:
The event queue:member:sla:breach is specific. It triggers when an individual interaction (conversation) in a queue exceeds the Service Level Agreement (SLA) defined for that queue. If you want to monitor overall queue SLA performance, you might consider queue:metrics:summary with a scheduled interval, but for real-time breach alerts, queue:member:sla:breach is the correct choice.
Step 2: Build the Event Listener Service
You need a service that exposes an HTTPS endpoint to receive the webhook payload. For this tutorial, we will use Flask for simplicity, but in production, you should use a robust framework like FastAPI or deploy this logic in a serverless function (AWS Lambda, Azure Functions).
The listener must:
- Verify the request signature (optional but recommended for security).
- Parse the JSON payload.
- Format the message for Slack.
- Post the message to the Slack Webhook URL.
- Return a
200 OKresponse to acknowledge receipt.
Slack Block Kit Formatting:
Slack uses the Block Kit for rich messaging. We will construct a JSON payload with a header, fields for Queue Name, Agent Name, and Wait Time.
import json
import requests
from flask import Flask, request, jsonify
app = Flask(__name__)
# Configuration
SLACK_WEBHOOK_URL = "https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX"
GENESYS_DOMAIN = "api.mypurecloud.com" # Replace with your domain
def post_to_slack(text: str, queue_name: str, wait_time_seconds: int, agent_name: str = "N/A"):
"""
Posts a formatted alert to Slack using Incoming Webhooks.
"""
blocks = [
{
"type": "header",
"text": {
"type": "plain_text",
"text": "🚨 Queue SLA Breach Alert",
"emoji": True
}
},
{
"type": "section",
"fields": [
{
"type": "mrkdwn",
"text": f"*Queue:*\n{queue_name}"
},
{
"type": "mrkdwn",
"text": f"*Wait Time:*\n{wait_time_seconds}s"
},
{
"type": "mrkdwn",
"text": f"*Agent/Resource:*\n{agent_name}"
},
{
"type": "mrkdwn",
"text": f"*Event ID:*\n{request.json.get('event', {}).get('id', 'N/A')[:8]}..."
}
]
}
]
payload = {
"blocks": blocks,
"text": f"SLA Breach: {queue_name} - {wait_time_seconds}s wait"
}
try:
headers = {
"Content-Type": "application/json"
}
response = requests.post(SLACK_WEBHOOK_URL, json=payload, headers=headers)
response.raise_for_status()
return True
except requests.exceptions.RequestException as e:
print(f"Error posting to Slack: {e}")
return False
@app.route('/webhook/genesys', methods=['POST'])
def handle_genesys_webhook():
"""
Endpoint to receive Genesys Cloud webhook events.
"""
# 1. Verify Content-Type
if request.content_type != 'application/json':
return jsonify({"error": "Unsupported Media Type"}), 415
try:
data = request.get_json()
except Exception:
return jsonify({"error": "Invalid JSON"}), 400
# 2. Check for specific event type
event_type = data.get('event', {}).get('type')
if event_type != 'queue:member:sla:breach':
# Ignore other events if this endpoint is dedicated to SLA breaches
return jsonify({"status": "ignored"}), 200
# 3. Extract relevant data from the payload
# The structure of the event payload varies slightly by event type.
# For queue:member:sla:breach, we look at the 'data' field.
event_data = data.get('data', {})
queue_id = event_data.get('queueId')
conversation_id = event_data.get('conversationId')
wait_time = event_data.get('waitTimeSeconds', 0)
# The agent/resource ID might be present if the interaction was assigned
resource_id = event_data.get('resourceId')
# We need the Queue Name. The webhook payload often contains IDs only.
# For a complete solution, you would look up the queue name using the Queue API.
# For this tutorial, we will use the ID or a placeholder if name is missing.
queue_name = event_data.get('queueName', f"Queue ID: {queue_id}")
agent_name = event_data.get('resourceName', f"Resource ID: {resource_id}" if resource_id else "Unassigned")
# 4. Post to Slack
success = post_to_slack(
text=f"SLA Breach in {queue_name}",
queue_name=queue_name,
wait_time_seconds=wait_time,
agent_name=agent_name
)
if success:
return jsonify({"status": "processed"}), 200
else:
return jsonify({"status": "error"}), 500
if __name__ == '__main__':
# Run the Flask app
# In production, use gunicorn or uwsgi
app.run(port=5000, debug=True)
Step 3: Enriching Data with Queue API Lookup
The webhook payload provides IDs (queueId, resourceId). To provide meaningful context in Slack (e.g., “Support Queue” instead of “a1b2c3d4-…”), you should resolve these IDs.
Adding a synchronous API call inside the webhook handler can add latency. If your webhook endpoint times out, Genesys Cloud will retry. To avoid blocking, consider asynchronous processing (e.g., push the event to a queue like RabbitMQ or AWS SQS, then process it).
For this tutorial, we will add a synchronous lookup function to demonstrate the API call.
from genesyscloud.routing.api import QueuesApi
from genesyscloud.routing.model import Queue
def get_queue_name_by_id(queue_id: str, auth: GenesysAuth) -> str:
"""
Retrieves the human-readable name of a queue by its ID.
"""
configuration = Configuration(
host=f"https://{auth.domain}",
access_token=auth.get_token()
)
api_client = ApiClient(configuration)
queues_api = QueuesApi(api_client)
try:
# Get queue by ID
queue = queues_api.get_routing_queue(queue_id=queue_id)
return queue.name if queue else f"Queue ID: {queue_id}"
except Exception as e:
print(f"Error fetching queue details: {e}")
return f"Queue ID: {queue_id}"
Integration into Handler:
Modify the handle_genesys_webhook function to call get_queue_name_by_id if queueName is not present in the payload. Note that this requires passing the auth object into the Flask app context or using a global variable for development.
Complete Working Example
Below is the consolidated code for the Flask application. Ensure you have installed the dependencies:
pip install flask requests genesys-cloud-purecloud-platform-client
import os
import time
import base64
import requests
from flask import Flask, request, jsonify
from genesyscloud.rest import Configuration, ApiClient
from genesyscloud.routing.api import QueuesApi
# --- Configuration ---
CLIENT_ID = os.environ.get("GENESYS_CLIENT_ID")
CLIENT_SECRET = os.environ.get("GENESYS_CLIENT_SECRET")
DOMAIN = os.environ.get("GENESYS_DOMAIN", "api.mypurecloud.com")
SLACK_WEBHOOK_URL = os.environ.get("SLACK_WEBHOOK_URL")
app = Flask(__name__)
class GenesysAuth:
def __init__(self, client_id: str, client_secret: str, domain: str):
self.client_id = client_id
self.client_secret = client_secret
self.domain = domain
self.token_url = f"https://{domain}/oauth/token"
self.access_token = None
self.token_expiry = 0
def get_token(self) -> str:
if self.access_token and time.time() < self.token_expiry - 60:
return self.access_token
credentials = f"{self.client_id}:{self.client_secret}"
encoded_credentials = base64.b64encode(credentials.encode('utf-8')).decode('utf-8')
headers = {
"Content-Type": "application/x-www-form-urlencoded",
"Authorization": f"Basic {encoded_credentials}"
}
data = {"grant_type": "client_credentials"}
try:
response = requests.post(self.token_url, headers=headers, data=data)
response.raise_for_status()
token_data = response.json()
self.access_token = token_data["access_token"]
self.token_expiry = time.time() + token_data["expires_in"]
return self.access_token
except requests.exceptions.HTTPError as e:
raise Exception(f"OAuth Error: {e}")
# Initialize Auth
auth = GenesysAuth(CLIENT_ID, CLIENT_SECRET, DOMAIN)
def post_to_slack(queue_name: str, wait_time_seconds: int, agent_name: str):
blocks = [
{
"type": "header",
"text": {"type": "plain_text", "text": "🚨 Queue SLA Breach Alert", "emoji": True}
},
{
"type": "section",
"fields": [
{"type": "mrkdwn", "text": f"*Queue:*\n{queue_name}"},
{"type": "mrkdwn", "text": f"*Wait Time:*\n{wait_time_seconds}s"},
{"type": "mrkdwn", "text": f"*Agent/Resource:*\n{agent_name}"},
]
}
]
payload = {"blocks": blocks, "text": f"SLA Breach: {queue_name}"}
try:
headers = {"Content-Type": "application/json"}
response = requests.post(SLACK_WEBHOOK_URL, json=payload, headers=headers)
response.raise_for_status()
return True
except requests.exceptions.RequestException as e:
print(f"Error posting to Slack: {e}")
return False
def get_queue_name_by_id(queue_id: str) -> str:
"""
Retrieves the human-readable name of a queue by its ID.
"""
configuration = Configuration(
host=f"https://{auth.domain}",
access_token=auth.get_token()
)
api_client = ApiClient(configuration)
queues_api = QueuesApi(api_client)
try:
queue = queues_api.get_routing_queue(queue_id=queue_id)
return queue.name if queue else f"Queue ID: {queue_id}"
except Exception as e:
print(f"Error fetching queue details: {e}")
return f"Queue ID: {queue_id}"
@app.route('/webhook/genesys', methods=['POST'])
def handle_genesys_webhook():
if request.content_type != 'application/json':
return jsonify({"error": "Unsupported Media Type"}), 415
try:
data = request.get_json()
except Exception:
return jsonify({"error": "Invalid JSON"}), 400
event_type = data.get('event', {}).get('type')
if event_type != 'queue:member:sla:breach':
return jsonify({"status": "ignored"}), 200
event_data = data.get('data', {})
queue_id = event_data.get('queueId')
wait_time = event_data.get('waitTimeSeconds', 0)
resource_id = event_data.get('resourceId')
# Resolve Queue Name
queue_name = event_data.get('queueName')
if not queue_name and queue_id:
queue_name = get_queue_name_by_id(queue_id)
# Resolve Agent Name (Simplified: In prod, use Users API to lookup resourceId)
agent_name = event_data.get('resourceName', f"Resource ID: {resource_id}" if resource_id else "Unassigned")
success = post_to_slack(queue_name, wait_time, agent_name)
if success:
return jsonify({"status": "processed"}), 200
else:
return jsonify({"status": "error"}), 500
if __name__ == '__main__':
if not all([CLIENT_ID, CLIENT_SECRET, SLACK_WEBHOOK_URL]):
raise Exception("Missing environment variables: GENESYS_CLIENT_ID, GENESYS_CLIENT_SECRET, SLACK_WEBHOOK_URL")
app.run(port=5000, debug=True)
Common Errors & Debugging
Error: 401 Unauthorized on Webhook Registration
- Cause: The OAuth token used to call
post_webhookslacks thewebhook:writescope or has expired. - Fix: Ensure your OAuth Client in Genesys Cloud has
webhook:writeandwebhook:readscopes assigned. Verify thatauth.get_token()returns a valid token.
Error: 400 Bad Request on Webhook Registration
- Cause: The
urlprovided in the webhook configuration is not reachable or does not support HTTPS. - Fix: Genesys Cloud requires the webhook URL to be HTTPS. Ensure your endpoint is publicly accessible. You can test this by sending a simple POST request to your URL from a different machine.
Error: Slack Webhook Returns 403 Forbidden
- Cause: The Slack Incoming Webhook URL is invalid or the channel has been removed.
- Fix: Re-add the Incoming Webhook app to the target Slack channel and copy the new URL. Ensure the URL is stored securely in your environment variables.
Error: Webhook Retries Exceeded
- Cause: Your endpoint is returning a non-2xx status code or timing out.
- Fix: Genesys Cloud retries failed webhooks exponentially. If your endpoint takes longer than 30 seconds to respond, Genesys will consider it a failure. Optimize your code or use asynchronous processing (message queue) to ensure immediate 200 OK responses.
Error: Event Payload Missing Fields
- Cause: The webhook payload structure depends on the event type.
queue:member:sla:breachmay not always includeresourceNameif the interaction is still in the queue and not yet assigned. - Fix: Always check for
Nonevalues before accessing dictionary keys. Use fallback values (e.g., “Unassigned”) to prevent crashes.