Sending Proactive Notifications to a Customer with History via Genesys Cloud APIs
What You Will Build
- You will build a service that retrieves historical web message transcripts for a specific customer and uses that context to send a proactive outbound web message or email notification.
- This tutorial uses the Genesys Cloud CX Analytics API to retrieve conversation details and the Engage API (or Email API) to dispatch the proactive message.
- The implementation is provided in Python using the
purecloud-platform-client-v2SDK and therequestslibrary for raw API calls where the SDK lacks specific proactive messaging helpers.
Prerequisites
- OAuth Client Type: Confidential Client (Client Credentials Grant) or Public Client (Authorization Code Grant with PKCE). For backend services, Confidential Client is recommended.
- Required Scopes:
analytics:conversation:read(to retrieve historical transcript data)message:outbound:create(to send proactive web messages via Engage)email:outbound:send(if falling back to email)user:read(to identify the agent or service account sending the message)
- SDK Version:
purecloud-platform-client-v2>= 164.0.0 (Python) - Runtime: Python 3.9+
- Dependencies:
pip install purecloud-platform-client-v2pip install requestspip install python-dotenv(for secure credential management)
Authentication Setup
Genesys Cloud uses OAuth 2.0 for all API access. For a backend service sending proactive notifications, you will typically use the Client Credentials Grant flow. This flow requires a registered OAuth Client in the Genesys Cloud Admin Console with the appropriate scopes granted.
Step 1: Configure Environment Variables
Create a .env file in your project root. Never hardcode credentials.
# .env
GENESYS_CLOUD_REGION=us-east-1 # e.g., eu-west-1, au-southeast-2
GENESYS_CLOUD_CLIENT_ID=your_client_id_here
GENESYS_CLOUD_CLIENT_SECRET=your_client_secret_here
Step 2: Initialize the SDK Client
The Genesys Cloud Python SDK handles token acquisition and refresh automatically when initialized with a client ID and secret.
import os
from dotenv import load_dotenv
from purecloud_platform_client_v2 import PlatformClient, Configuration
load_dotenv()
def get_platform_client() -> PlatformClient:
"""
Initializes and returns a configured Genesys Cloud Platform Client.
"""
region = os.getenv("GENESYS_CLOUD_REGION", "us-east-1")
client_id = os.getenv("GENESYS_CLOUD_CLIENT_ID")
client_secret = os.getenv("GENESYS_CLOUD_CLIENT_SECRET")
if not client_id or not client_secret:
raise ValueError("GENESYS_CLOUD_CLIENT_ID and GENESYS_CLOUD_CLIENT_SECRET are required.")
# Configure the API client
config = Configuration()
config.host = f"https://api.{region}.mypurecloud.com"
config.client_id = client_id
config.client_secret = client_secret
# Create the platform client
platform_client = PlatformClient(config)
# Explicitly set the authentication method for client credentials
platform_client.set_auth(
client_id=client_id,
client_secret=client_secret
)
return platform_client
Implementation
The workflow consists of three distinct phases:
- Retrieve History: Query the Analytics API for past web message conversations associated with the customer.
- Enrich Context: Parse the transcript to extract relevant context (e.g., previous issue, name, sentiment).
- Send Proactive Message: Construct and dispatch the new message using the Engage API.
Step 1: Retrieve Historical Conversation Details
To send a context-aware proactive notification, you first need to prove the customer exists in your system and retrieve their last interaction. The POST /api/v2/analytics/conversations/details/query endpoint is the primary tool for this.
Required Scope: analytics:conversation:read
from purecloud_platform_client_v2 import AnalyticsApi, ConversationDetailQueryRequest
from purecloud_platform_client_v2.api_exception import ApiException
import json
def get_last_web_message_history(platform_client: PlatformClient, email_address: str, max_records: int = 5):
"""
Retrieves the last N web message conversations for a specific email address.
"""
analytics_api = AnalyticsApi(platform_client)
# Construct the query body
# We filter by communication type 'webchat' and the participant's email
query_body = ConversationDetailQueryRequest(
interval="2023-01-01T00:00:00Z/2023-12-31T23:59:59Z", # Adjust interval as needed
group_by=["participant"],
metrics=["durationSeconds"],
filter=[
{
"type": "equals",
"field": "conversation.communicationType",
"value": "webchat"
},
{
"type": "contains",
"field": "conversation.participants.email",
"value": email_address
}
],
size=max_records,
sort=[{"field": "conversation.startTime", "direction": "desc"}]
)
try:
# Execute the query
response = analytics_api.post_analytics_conversations_details_query(body=query_body)
if not response.entity:
print("No historical conversations found.")
return None
# The response contains summary data. To get the full transcript,
# we need the conversation IDs.
conversation_ids = [item.id for item in response.entity]
# Fetch full details for the most recent conversation
if conversation_ids:
latest_conv_id = conversation_ids[0]
return fetch_full_transcript(platform_client, latest_conv_id)
return None
except ApiError as e:
print(f"Analytics API Error: {e.status} - {e.reason}")
raise
def fetch_full_transcript(platform_client: PlatformClient, conversation_id: str):
"""
Fetches the full conversation transcript including messages.
"""
analytics_api = AnalyticsApi(platform_client)
# Query for the specific conversation details
query_body = ConversationDetailQueryRequest(
interval="2023-01-01T00:00:00Z/2023-12-31T23:59:59Z",
group_by=[], # No grouping to get individual conversation rows
metrics=[],
filter=[
{
"type": "equals",
"field": "conversation.id",
"value": conversation_id
}
],
size=1
)
try:
response = analytics_api.post_analytics_conversations_details_query(body=query_body)
if response.entity and len(response.entity) > 0:
return response.entity[0]
return None
except ApiError as e:
print(f"Failed to fetch transcript: {e.reason}")
return None
Step 2: Extract Context from Transcript
The Analytics API returns a structured object. The interactions field contains the sequence of messages. We need to parse this to find the customer’s last issue or intent.
from typing import Dict, Optional
def extract_customer_context(conversation_data: Dict) -> Optional[Dict]:
"""
Parses the conversation data to extract the customer's last message and name.
"""
if not conversation_data or not conversation_data.interactions:
return None
customer_name = "Valued Customer"
last_customer_message = ""
# Iterate through interactions to find customer messages
# Interactions are ordered chronologically
for interaction in conversation_data.interactions:
if interaction.direction == "inbound": # Message from customer to agent/system
if interaction.sender and interaction.sender.name:
customer_name = interaction.sender.name
if interaction.text:
last_customer_message = interaction.text
return {
"name": customer_name,
"last_message": last_customer_message,
"conversation_id": conversation_data.id
}
Step 3: Send Proactive Web Message via Engage API
Genesys Cloud’s “Proactive Messaging” feature allows you to send a web message to a user who has previously chatted with you, provided they have the chat widget open or are on a tracked page. This is done via the Engage API.
Required Scope: message:outbound:create
Endpoint: POST /api/v2/engage/messages
Note: The SDK does not always have a direct method for every Engage endpoint. Using the requests library with the SDK’s token is often cleaner for newer Engage features.
import requests
from purecloud_platform_client_v2 import PlatformClient
def send_proactive_web_message(platform_client: PlatformClient, customer_email: str, context: Dict):
"""
Sends a proactive web message to a customer using the Engage API.
Args:
platform_client: The initialized Genesys Cloud client.
customer_email: The email address of the customer.
context: Dictionary containing 'name' and 'last_message'.
"""
# Get the current access token from the platform client
# The SDK caches this token. We extract it for the raw HTTP request.
auth_client = platform_client.get_auth_client()
access_token = auth_client.get_access_token()
if not access_token:
raise Exception("Failed to retrieve access token.")
base_url = platform_client.get_config().host.replace("api.", "") # Engage API might use different subdomain
engage_url = f"https://engage.{base_url}/api/v2/engage/messages"
# Construct the message payload
# The 'to' field requires a specific structure for web messaging
payload = {
"to": {
"type": "email",
"address": customer_email
},
"from": {
"type": "email",
"address": "support@yourcompany.com", # Must be a verified sender in Genesys
"name": "Customer Support Bot"
},
"subject": "Follow up on your recent inquiry",
"body": {
"contentType": "text/html",
"content": f"""
<h2>Hello {context.get('name', 'there')}</h2>
<p>We noticed you recently asked about: <strong>{context.get('last_message', 'an issue')}</strong>.</p>
<p>Did we resolve your issue? Click below to chat with an agent if you need more help.</p>
"""
},
"messageType": "proactive" # Critical: Marks this as a proactive notification
}
headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json",
"Accept": "application/json"
}
try:
response = requests.post(engage_url, json=payload, headers=headers)
if response.status_code == 202: # Accepted
print(f"Proactive message sent successfully. ID: {response.json().get('id')}")
return response.json()
elif response.status_code == 400:
print(f"Bad Request: {response.text}")
raise ValueError(f"Invalid payload: {response.text}")
elif response.status_code == 429:
print("Rate limited. Please retry later.")
raise Exception("Rate limited")
else:
print(f"Error sending message: {response.status_code} - {response.text}")
raise Exception(f"Engage API Error: {response.text}")
except requests.exceptions.RequestException as e:
print(f"Network error: {e}")
raise
Step 4: Fallback to Email (Optional but Recommended)
If the customer is not currently active on the web widget, the proactive web message may fail or not be delivered immediately. A robust system often sends an email as a fallback.
def send_fallback_email(platform_client: PlatformClient, customer_email: str, context: Dict):
"""
Sends a standard email using the Email API.
"""
email_api = platform_client.email_api # Assuming EmailApi is imported from SDK
# Construct the email body object
email_body = {
"from": {
"address": "support@yourcompany.com",
"name": "Support Team"
},
"to": [
{
"address": customer_email,
"name": context.get('name', 'Customer')
}
],
"subject": "Checking in on your recent support request",
"body": {
"contentType": "text/html",
"content": f"<p>Hi {context.get('name', 'there')},</p><p>Following up on: {context.get('last_message')}</p>"
}
}
try:
response = email_api.post_email_outbound_send(body=email_body)
print(f"Email sent. ID: {response.id}")
return response
except Exception as e:
print(f"Email failed: {e}")
return None
Complete Working Example
Below is the consolidated script. Save this as proactive_notification.py.
import os
import sys
from dotenv import load_dotenv
from purecloud_platform_client_v2 import PlatformClient, Configuration, AnalyticsApi, ConversationDetailQueryRequest, ApiError
import requests
from typing import Dict, Optional
# Load environment variables
load_dotenv()
def get_platform_client() -> PlatformClient:
region = os.getenv("GENESYS_CLOUD_REGION", "us-east-1")
client_id = os.getenv("GENESYS_CLOUD_CLIENT_ID")
client_secret = os.getenv("GENESYS_CLOUD_CLIENT_SECRET")
if not client_id or not client_secret:
raise ValueError("Missing credentials in .env file.")
config = Configuration()
config.host = f"https://api.{region}.mypurecloud.com"
config.client_id = client_id
config.client_secret = client_secret
platform_client = PlatformClient(config)
platform_client.set_auth(client_id=client_id, client_secret=client_secret)
return platform_client
def get_last_web_message_history(platform_client: PlatformClient, email_address: str) -> Optional[Dict]:
analytics_api = AnalyticsApi(platform_client)
# Define a wide interval to catch recent history
query_body = ConversationDetailQueryRequest(
interval="2023-01-01T00:00:00Z/2024-12-31T23:59:59Z",
group_by=["participant"],
metrics=["durationSeconds"],
filter=[
{"type": "equals", "field": "conversation.communicationType", "value": "webchat"},
{"type": "contains", "field": "conversation.participants.email", "value": email_address}
],
size=1,
sort=[{"field": "conversation.startTime", "direction": "desc"}]
)
try:
response = analytics_api.post_analytics_conversations_details_query(body=query_body)
if not response.entity or len(response.entity) == 0:
return None
latest_conv_id = response.entity[0].id
# Fetch full transcript
transcript_query = ConversationDetailQueryRequest(
interval="2023-01-01T00:00:00Z/2024-12-31T23:59:59Z",
group_by=[],
metrics=[],
filter=[{"type": "equals", "field": "conversation.id", "value": latest_conv_id}],
size=1
)
transcript_response = analytics_api.post_analytics_conversations_details_query(body=transcript_query)
if transcript_response.entity and len(transcript_response.entity) > 0:
return transcript_response.entity[0]
return None
except ApiError as e:
print(f"Analytics Error: {e.reason}")
return None
def extract_context(conversation_data: Dict) -> Dict:
customer_name = "Valued Customer"
last_msg = "unknown topic"
if conversation_data.interactions:
for interaction in conversation_data.interactions:
if interaction.direction == "inbound":
if interaction.sender and interaction.sender.name:
customer_name = interaction.sender.name
if interaction.text:
last_msg = interaction.text[:100] # Truncate for safety
return {"name": customer_name, "last_message": last_msg}
def send_proactive_message(platform_client: PlatformClient, email: str, context: Dict):
auth_client = platform_client.get_auth_client()
token = auth_client.get_access_token()
if not token:
raise Exception("No auth token")
host = platform_client.get_config().host
region = host.split(".")[1] # Extract region from api.us-east-1.mypurecloud.com
engage_url = f"https://engage.{region}.mypurecloud.com/api/v2/engage/messages"
payload = {
"to": {"type": "email", "address": email},
"from": {"type": "email", "address": "noreply@yourdomain.com", "name": "Support Bot"},
"subject": "Quick Check-In",
"body": {
"contentType": "text/html",
"content": f"<p>Hi {context['name']},</p><p>We saw you asked about <b>{context['last_message']}</b>. Need more help?</p>"
},
"messageType": "proactive"
}
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json"
}
try:
res = requests.post(engage_url, json=payload, headers=headers)
res.raise_for_status()
print(f"Success: Message sent. ID: {res.json().get('id')}")
except requests.exceptions.HTTPError as e:
print(f"HTTP Error: {e.response.status_code} - {e.response.text}")
except Exception as e:
print(f"Request Error: {e}")
def main():
target_email = os.getenv("TARGET_EMAIL", "customer@example.com")
print(f"Initiating proactive notification flow for {target_email}...")
client = get_platform_client()
# 1. Get History
hist = get_last_web_message_history(client, target_email)
if not hist:
print("No historical web messages found. Aborting.")
return
# 2. Extract Context
context = extract_context(hist)
print(f"Context extracted: Name={context['name']}, Topic={context['last_message']}")
# 3. Send Message
send_proactive_message(client, target_email, context)
if __name__ == "__main__":
main()
Common Errors & Debugging
Error: 401 Unauthorized
Cause: The OAuth token is expired or invalid.
Fix: Ensure the PlatformClient is initialized correctly. The SDK auto-refreshes tokens, but if you are using the raw requests library, you must fetch the token immediately before the call using auth_client.get_access_token(). Do not cache the token string manually across long-running processes.
Error: 403 Forbidden
Cause: The OAuth Client lacks the required scopes.
Fix:
- Log in to Genesys Cloud Admin.
- Navigate to Admin > Security > OAuth Clients.
- Select your client.
- Ensure
analytics:conversation:readandmessage:outbound:createare checked under Scopes. - Save changes. Note: Scope changes may take up to 15 minutes to propagate.
Error: 400 Bad Request - “Invalid recipient type”
Cause: The to field in the Engage API payload is malformed.
Fix: For proactive web messages triggered by email history, the to field must specify "type": "email". If you are trying to target a specific session ID, the type would be "webchat", but that requires the active session ID, which is not available from historical analytics queries.
Error: 429 Too Many Requests
Cause: You have exceeded the API rate limit.
Fix: Implement exponential backoff. The Genesys Cloud API returns Retry-After headers.
import time
def retry_with_backoff(func, *args, max_retries=3, **kwargs):
for attempt in range(max_retries):
try:
return func(*args, **kwargs)
except requests.exceptions.HTTPError as e:
if e.response.status_code == 429:
retry_after = int(e.response.headers.get('Retry-After', 2 ** attempt))
print(f"Rate limited. Waiting {retry_after} seconds...")
time.sleep(retry_after)
else:
raise
raise Exception("Max retries exceeded")