Sending proactive notifications to a customer who previously had a web messaging session
What You Will Build
- You will build a Python script that identifies a customer based on their previous web messaging session ID and sends a proactive outbound message to their browser.
- This tutorial uses the Genesys Cloud CX Messaging API (
/api/v2/messaging/messages) and the Conversations Search API to locate the target user. - The implementation is written in Python 3.9+ using the
requestslibrary andhttpxfor asynchronous operations where appropriate.
Prerequisites
- OAuth Client: A Genesys Cloud OAuth client with the
messaging:outbound:sendandanalytics:conversation:searchscopes. - SDK Version: This tutorial uses the REST API directly via
requestsfor maximum transparency, but the logic applies to thePureCloudPlatformClientV2Python SDK (v130.0.0+). - Runtime: Python 3.9 or higher.
- Dependencies:
requests(for HTTP calls)pydantic(for data validation, optional but recommended)uuid(standard library, for generating message IDs)
Authentication Setup
Genesys Cloud uses OAuth 2.0 for authentication. For server-to-server integrations (like this proactive notification service), you will use the Client Credentials grant type.
You must store your Client ID and Client Secret securely. Never hardcode these in your application. Use environment variables or a secrets manager.
Step 1: Obtain an Access Token
The following function handles the token retrieval. It caches the token to avoid unnecessary API calls, as tokens are valid for 3600 seconds.
import os
import time
import requests
from typing import Optional
class GenesysAuth:
def __init__(self, client_id: str, client_secret: str, env: str = "mypurecloud.com"):
self.client_id = client_id
self.client_secret = client_secret
self.env = env
self.access_token: Optional[str] = None
self.token_expiry: float = 0
self.base_url = f"https://api.{env}"
def get_token(self) -> str:
"""
Retrieves an OAuth access token.
Returns the token string.
Raises ValueError if authentication fails.
"""
# Check if we have a valid cached token
if self.access_token and time.time() < self.token_expiry:
return self.access_token
token_url = f"{self.base_url}/oauth/token"
payload = {
"grant_type": "client_credentials",
"client_id": self.client_id,
"client_secret": self.client_secret
}
headers = {
"Content-Type": "application/json"
}
response = requests.post(token_url, json=payload, headers=headers)
if response.status_code != 200:
raise ValueError(f"Authentication failed: {response.status_code} - {response.text}")
token_data = response.json()
self.access_token = token_data["access_token"]
self.token_expiry = time.time() + token_data["expires_in"] - 60 # Buffer of 60 seconds
return self.access_token
def get_headers(self) -> dict:
"""Returns headers with the current Bearer token."""
return {
"Authorization": f"Bearer {self.get_token()}",
"Content-Type": "application/json"
}
Implementation
Step 1: Locate the Customer’s Session
To send a proactive message, you must target a specific user session. Genesys Cloud Web Messaging sessions are identified by a sessionId (often stored as a cookie on the client side) or by linking the conversation to a specific userId if the user was authenticated.
In most proactive scenarios, you do not have the active sessionId because the user is currently offline or idle. However, if you are targeting a user who is currently on your site but idle, or if you have stored their userId from a previous authenticated session, you can search for their recent conversations.
If you are sending a message to a user who is not currently online, Web Messaging cannot deliver it directly to the browser. You must use Outbound Messaging to a phone number or email, or use the Engagement API to push a notification to a mobile app.
Note: True “proactive web messaging” to a browser requires the browser to have an open WebSocket connection. If the page is closed, the connection is dead. This tutorial assumes you are sending an outbound message to a user who has previously opted in, using their contact information linked to their previous web session.
For this tutorial, we will assume you have the userId or email of the customer from your CRM, and you want to send them a message via the Outbound Messaging API, which can trigger a web message if they are online, or fall back to SMS/Email depending on your routing configuration.
However, the most robust way to “proactively” message a web user who is currently idle but connected is to use the Conversations API to find their active conversation and inject a message.
Let us focus on the scenario where you have the Conversation ID or Session ID from a previous interaction, and you want to send a message to that specific channel.
Critical Constraint: You cannot send a message to a closed conversation. You must verify the conversation is still open or initiate a new outbound engagement.
For this tutorial, we will use the Outbound Messaging API to send a message to a user identified by their email, which is linked to their previous web session. This is the standard pattern for “re-engagement.”
Step 2: Construct the Outbound Message Payload
The endpoint /api/v2/messaging/messages accepts a JSON payload. The structure depends on the channel. For web messaging re-engagement, we typically use the email or sms channel if the web socket is closed, or we use the userId if we are pushing to a registered client.
To send a message to a user via their previous web channel, you must use the Engagement API or Outbound Messaging with the web channel type, targeting the userId.
Here is the payload structure for an outbound web message:
{
"type": "web",
"to": {
"userId": "string",
"email": "string"
},
"from": {
"userId": "string"
},
"body": {
"text": "Hello, we noticed you were looking at our pricing page. Do you have any questions?"
}
}
Step 3: Send the Message via API
We will create a function that sends this payload.
import json
from typing import Dict, Any
class GenesysMessaging:
def __init__(self, auth: GenesysAuth):
self.auth = auth
self.base_url = auth.base_url
def send_proactive_web_message(self, user_id: str, email: str, from_user_id: str, message_text: str) -> Dict[str, Any]:
"""
Sends a proactive outbound web message to a user.
Args:
user_id: The Genesys Cloud userId of the recipient.
email: The email address of the recipient (for fallback).
from_user_id: The Genesys Cloud userId of the sender (agent or bot).
message_text: The content of the message.
Returns:
The API response dictionary.
"""
url = f"{self.base_url}/api/v2/messaging/messages"
payload = {
"type": "web",
"to": {
"userId": user_id,
"email": email
},
"from": {
"userId": from_user_id
},
"body": {
"text": message_text
}
}
headers = self.auth.get_headers()
try:
response = requests.post(url, json=payload, headers=headers)
# Handle specific errors
if response.status_code == 401:
raise Exception("Authentication failed. Token may be expired.")
elif response.status_code == 403:
raise Exception("Forbidden. Check OAuth scopes: messaging:outbound:send")
elif response.status_code == 429:
raise Exception("Rate limit exceeded. Implement retry logic.")
elif response.status_code >= 400:
raise Exception(f"API Error: {response.status_code} - {response.text}")
return response.json()
except requests.exceptions.RequestException as e:
raise Exception(f"Network error: {str(e)}")
Step 4: Handling Rate Limits and Retries
Genesys Cloud APIs enforce rate limits. A 429 status code indicates you have exceeded the limit. You must implement exponential backoff.
import time
import random
def send_with_retry(messaging: GenesysMessaging, user_id: str, email: str, from_user_id: str, message_text: str, max_retries: int = 3) -> Dict[str, Any]:
"""
Sends a message with exponential backoff retry logic for 429 errors.
"""
attempt = 0
while attempt < max_retries:
try:
return messaging.send_proactive_web_message(user_id, email, from_user_id, message_text)
except Exception as e:
if "429" in str(e):
wait_time = (2 ** attempt) + random.uniform(0, 1)
print(f"Rate limited. Retrying in {wait_time:.2f} seconds...")
time.sleep(wait_time)
attempt += 1
else:
raise e
raise Exception("Max retries exceeded for 429 error.")
Complete Working Example
This script ties together authentication, message construction, and sending with error handling.
import os
import sys
import json
import time
import requests
from typing import Dict, Any, Optional
# --- Configuration ---
CLIENT_ID = os.environ.get("GENESYS_CLIENT_ID")
CLIENT_SECRET = os.environ.get("GENESYS_CLIENT_SECRET")
ENVIRONMENT = os.environ.get("GENESYS_ENV", "mypurecloud.com")
# --- Authentication Class ---
class GenesysAuth:
def __init__(self, client_id: str, client_secret: str, env: str):
self.client_id = client_id
self.client_secret = client_secret
self.env = env
self.access_token: Optional[str] = None
self.token_expiry: float = 0
self.base_url = f"https://api.{env}"
def get_token(self) -> str:
if self.access_token and time.time() < self.token_expiry:
return self.access_token
token_url = f"{self.base_url}/oauth/token"
payload = {
"grant_type": "client_credentials",
"client_id": self.client_id,
"client_secret": self.client_secret
}
headers = {"Content-Type": "application/json"}
response = requests.post(token_url, json=payload, headers=headers)
if response.status_code != 200:
raise ValueError(f"Auth failed: {response.status_code} - {response.text}")
data = response.json()
self.access_token = data["access_token"]
self.token_expiry = time.time() + data["expires_in"] - 60
return self.access_token
def get_headers(self) -> dict:
return {
"Authorization": f"Bearer {self.get_token()}",
"Content-Type": "application/json"
}
# --- Messaging Service Class ---
class ProactiveMessagingService:
def __init__(self, auth: GenesysAuth):
self.auth = auth
self.base_url = auth.base_url
def send_message(self, user_id: str, email: str, from_user_id: str, text: str) -> Dict[str, Any]:
url = f"{self.base_url}/api/v2/messaging/messages"
payload = {
"type": "web",
"to": {
"userId": user_id,
"email": email
},
"from": {
"userId": from_user_id
},
"body": {
"text": text
}
}
response = requests.post(url, json=payload, headers=self.auth.get_headers())
if response.status_code == 200:
return {"success": True, "data": response.json()}
elif response.status_code == 429:
raise Exception("Rate Limit 429")
else:
raise Exception(f"Failed: {response.status_code} - {response.text}")
# --- Main Execution ---
def main():
if not CLIENT_ID or not CLIENT_SECRET:
print("Error: Set GENESYS_CLIENT_ID and GENESYS_CLIENT_SECRET environment variables.")
sys.exit(1)
auth = GenesysAuth(CLIENT_ID, CLIENT_SECRET, ENVIRONMENT)
service = ProactiveMessagingService(auth)
# Example Usage
target_user_id = "12345678-1234-1234-1234-123456789012" # Replace with actual userId
target_email = "customer@example.com"
sender_user_id = "87654321-4321-4321-4321-210987654321" # Replace with actual agent/bot userId
message_content = "Hi there! We noticed you left an item in your cart. Would you like help completing your purchase?"
try:
result = service.send_message(target_user_id, target_email, sender_user_id, message_content)
print("Message sent successfully:")
print(json.dumps(result, indent=2))
except Exception as e:
print(f"Error sending message: {e}")
if __name__ == "__main__":
main()
Common Errors & Debugging
Error: 401 Unauthorized
- Cause: The OAuth token is expired, invalid, or the client credentials are incorrect.
- Fix: Ensure
GENESYS_CLIENT_IDandGENESYS_CLIENT_SECRETare correct. Verify the token caching logic inGenesysAuthis not returning a stale token.
Error: 403 Forbidden
- Cause: The OAuth client lacks the required scope
messaging:outbound:send. - Fix: Go to Genesys Cloud Admin > Security > OAuth. Edit your client application. Add
messaging:outbound:sendto the scopes. If you are usinganalytics:conversation:search, add that scope as well.
Error: 422 Unprocessable Entity
- Cause: The payload structure is invalid. For example, missing
userIdin thetoobject or invalidtype. - Fix: Validate the JSON payload against the Genesys Cloud API specification. Ensure
userIdis a valid UUID string.
Error: 429 Too Many Requests
- Cause: You have exceeded the rate limit for the messaging API.
- Fix: Implement exponential backoff as shown in the
send_with_retryfunction. Reduce the frequency of proactive messages.