How to Send a Canned Response During a Chat Interaction via the Conversations API
What You Will Build
- You will build a script that sends a pre-defined text message into an active Genesys Cloud chat conversation using the Conversations API.
- This tutorial uses the Genesys Cloud
POST /api/v2/conversations/{conversationId}/eventsendpoint. - The implementation covers Python (using
requests) and JavaScript (usingaxios), demonstrating how to construct the correct JSON payload for a chat message event.
Prerequisites
OAuth Configuration
- Client Type: You must use a Client Credentials flow for server-to-server integration, or a JWT Bearer flow if acting on behalf of a specific user (e.g., a supervisor injecting a message).
- Required Scopes:
conversation:chat:write(Required to send messages in chat conversations).conversation:write(General write access, often required depending on your org’s specific permission sets).user:read(Optional, but useful if you need to resolve user IDs before sending).
Environment Setup
- Python: Python 3.8+ with
requestslibrary installed (pip install requests). - JavaScript: Node.js 14+ with
axiosinstalled (npm install axios). - Genesys Cloud Account: An active account with a valid Organization ID and API credentials (Client ID and Client Secret).
Key Concepts
- Conversation ID: The unique identifier for the specific chat session. This is typically obtained when the chat is initiated or via the Conversations API list/query endpoints.
- Event Type: The Conversations API treats all communication as “events.” For chat text, the event type is
chat. - From User: The message must be associated with a specific user ID (the agent or system user sending the canned response).
Authentication Setup
Before sending any data, you must obtain a valid OAuth access token. Genesys Cloud uses standard OAuth 2.0.
Python Authentication Helper
import requests
import time
class GenesysAuth:
def __init__(self, org_id: str, client_id: str, client_secret: str):
self.org_id = org_id
self.client_id = client_id
self.client_secret = client_secret
self.token_url = f"https://{org_id}.mypurecloud.com/oauth/token"
self.access_token = None
self.token_expiry = 0
def get_token(self) -> str:
"""
Retrieves a new OAuth token if the current one is expired or missing.
Implements basic caching to avoid unnecessary token requests.
"""
if self.access_token and time.time() < self.token_expiry:
return self.access_token
headers = {
"Content-Type": "application/x-www-form-urlencoded"
}
data = {
"grant_type": "client_credentials",
"client_id": self.client_id,
"client_secret": self.client_secret
}
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"]
# Expires in is usually 3600 seconds. Subtract 60s for buffer.
self.token_expiry = time.time() + (token_data["expires_in"] - 60)
return self.access_token
except requests.exceptions.HTTPError as http_err:
raise Exception(f"OAuth Authentication Failed: {http_err.response.text}")
except Exception as err:
raise Exception(f"Unexpected Error during Auth: {err}")
JavaScript Authentication Helper
const axios = require('axios');
class GenesysAuth {
constructor(orgId, clientId, clientSecret) {
this.orgId = orgId;
this.clientId = clientId;
this.clientSecret = clientSecret;
this.tokenUrl = `https://${orgId}.mypurecloud.com/oauth/token`;
this.accessToken = null;
this.tokenExpiry = 0;
}
async getToken() {
// Check if token is still valid (buffer of 60 seconds)
if (this.accessToken && Date.now() < this.tokenExpiry) {
return this.accessToken;
}
try {
const formData = new URLSearchParams();
formData.append('grant_type', 'client_credentials');
formData.append('client_id', this.clientId);
formData.append('client_secret', this.clientSecret);
const response = await axios.post(this.tokenUrl, formData, {
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
}
});
this.accessToken = response.data.access_token;
// Store expiry time in milliseconds
this.tokenExpiry = Date.now() + (response.data.expires_in * 1000) - 60000;
return this.accessToken;
} catch (error) {
if (error.response) {
throw new Error(`OAuth Authentication Failed: ${error.response.data}`);
}
throw error;
}
}
}
Implementation
Step 1: Identify the Conversation and User IDs
You cannot send a message without a conversationId and a from user ID. In a real production scenario, you would listen for Webhooks (e.g., conversation.chat.started) to get these IDs. For this tutorial, we assume you have these values.
Critical Note: The from user must be an agent or a user with permissions to participate in chats. If you are using Client Credentials, you are acting as the application. You often need to map the application action to a specific “System User” or the actual Agent ID if the agent is currently in the conversation.
For this example, we will assume you have:
conversationId: The UUID of the active chat.userId: The UUID of the user (Agent) sending the canned response.
Step 2: Construct the Chat Event Payload
The Genesys Cloud Conversations API uses a generic event structure. To send a chat message, you must POST to /api/v2/conversations/{conversationId}/events.
Required Fields in the Body:
eventType: Must be"chat".from: An object containing theidof the user sending the message.text: The actual content of the canned response.
Optional but Recommended Fields:
timestamp: ISO 8601 format. If omitted, the server uses the current time.metadata: Can be used to tag the message as a “canned response” for analytics.
Python Implementation
def send_canned_response(auth: GenesysAuth, conversation_id: str, user_id: str, message_text: str):
"""
Sends a chat message event to a specific conversation.
Args:
auth: GenesysAuth instance with valid token.
conversation_id: UUID of the chat conversation.
user_id: UUID of the user sending the message.
message_text: The string content of the message.
"""
base_url = f"https://{auth.org_id}.mypurecloud.com"
endpoint = f"/api/v2/conversations/{conversation_id}/events"
url = f"{base_url}{endpoint}"
headers = {
"Authorization": f"Bearer {auth.get_token()}",
"Content-Type": "application/json"
}
# Construct the event payload
payload = {
"eventType": "chat",
"from": {
"id": user_id
},
"text": message_text,
"metadata": {
"source": "canned_response_bot"
}
}
try:
response = requests.post(url, json=payload, headers=headers)
# Check for success (201 Created)
if response.status_code == 201:
print(f"Message sent successfully to conversation {conversation_id}")
return response.json()
elif response.status_code == 403:
raise PermissionError(f"User {user_id} does not have permission to send messages in this conversation.")
elif response.status_code == 404:
raise ValueError(f"Conversation {conversation_id} not found.")
else:
# Raise generic error for other status codes
response.raise_for_status()
except requests.exceptions.RequestException as e:
print(f"Error sending message: {e}")
if hasattr(e, 'response') and e.response is not None:
print(f"Response Body: {e.response.text}")
raise
JavaScript Implementation
async function sendCannedResponse(auth, conversationId, userId, messageText) {
const baseUrl = `https://${auth.orgId}.mypurecloud.com`;
const endpoint = `/api/v2/conversations/${conversationId}/events`;
const url = `${baseUrl}${endpoint}`;
const token = await auth.getToken();
const payload = {
eventType: "chat",
from: {
id: userId
},
text: messageText,
metadata: {
source: "canned_response_bot"
}
};
try {
const response = await axios.post(url, payload, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
if (response.status === 201) {
console.log(`Message sent successfully to conversation ${conversationId}`);
return response.data;
} else {
throw new Error(`Unexpected status code: ${response.status}`);
}
} catch (error) {
if (error.response) {
if (error.response.status === 403) {
throw new Error(`User ${userId} does not have permission to send messages.`);
} else if (error.response.status === 404) {
throw new Error(`Conversation ${conversationId} not found.`);
} else {
throw new Error(`API Error: ${error.response.data}`);
}
}
throw error;
}
}
Step 3: Handling Real-World Edge Cases
Case 1: The “From” User Must Be In The Conversation
If you attempt to send a message from a userId that is not currently a participant in the chat conversation, Genesys Cloud will return a 403 Forbidden or 400 Bad Request.
Solution: Before sending, verify the user is in the conversation. You can do this by fetching the conversation details.
Python Verification Code:
def verify_user_in_conversation(auth: GenesysAuth, conversation_id: str, user_id: str) -> bool:
"""
Checks if the specified user is currently a participant in the conversation.
"""
base_url = f"https://{auth.org_id}.mypurecloud.com"
url = f"{base_url}/api/v2/conversations/{conversation_id}"
headers = {
"Authorization": f"Bearer {auth.get_token()}",
"Content-Type": "application/json"
}
try:
response = requests.get(url, headers=headers)
response.raise_for_status()
conversation_data = response.json()
participants = conversation_data.get("participants", [])
for participant in participants:
if participant.get("id") == user_id:
return True
return False
except Exception as e:
print(f"Error verifying conversation: {e}")
return False
Case 2: Rate Limiting (429 Too Many Requests)
If you are sending canned responses in a loop (e.g., auto-responses), you may hit rate limits. Genesys Cloud returns a Retry-After header in 429 responses.
Python Retry Logic:
import time
def send_with_retry(auth, conversation_id, user_id, message_text, max_retries=3):
for attempt in range(max_retries):
try:
return send_canned_response(auth, conversation_id, user_id, message_text)
except requests.exceptions.HTTPError as e:
if e.response.status_code == 429:
retry_after = int(e.response.headers.get('Retry-After', 5))
print(f"Rate limited. Waiting {retry_after} seconds before retry {attempt + 1}...")
time.sleep(retry_after)
else:
raise
raise Exception("Max retries exceeded for sending canned response.")
Complete Working Example
This is a complete Python script that ties authentication, verification, and sending together.
import requests
import time
import sys
# Configuration
ORG_ID = "your-org-id"
CLIENT_ID = "your-client-id"
CLIENT_SECRET = "your-client-secret"
CONVERSATION_ID = "a1b2c3d4-e5f6-7890-g1h2-i3j4k5l6m7n8" # Replace with real ID
USER_ID = "u1v2w3x4-y5z6-7890-a1b2-c3d4e5f6g7h8" # Replace with Agent/System User ID
CANNED_MESSAGE = "Thank you for your patience. Your request has been escalated to our specialist team."
class GenesysAuth:
def __init__(self, org_id, client_id, client_secret):
self.org_id = org_id
self.client_id = client_id
self.client_secret = client_secret
self.token_url = f"https://{org_id}.mypurecloud.com/oauth/token"
self.access_token = None
self.token_expiry = 0
def get_token(self):
if self.access_token and time.time() < self.token_expiry:
return self.access_token
headers = {"Content-Type": "application/x-www-form-urlencoded"}
data = {
"grant_type": "client_credentials",
"client_id": self.client_id,
"client_secret": self.client_secret
}
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"] - 60)
return self.access_token
def verify_participation(auth, conv_id, user_id):
url = f"https://{auth.org_id}.mypurecloud.com/api/v2/conversations/{conv_id}"
headers = {"Authorization": f"Bearer {auth.get_token()}"}
resp = requests.get(url, headers=headers)
resp.raise_for_status()
data = resp.json()
for p in data.get("participants", []):
if p.get("id") == user_id:
return True
return False
def send_chat_message(auth, conv_id, user_id, text):
url = f"https://{auth.org_id}.mypurecloud.com/api/v2/conversations/{conv_id}/events"
headers = {
"Authorization": f"Bearer {auth.get_token()}",
"Content-Type": "application/json"
}
payload = {
"eventType": "chat",
"from": {"id": user_id},
"text": text
}
resp = requests.post(url, json=payload, headers=headers)
resp.raise_for_status()
return resp.json()
def main():
auth = GenesysAuth(ORG_ID, CLIENT_ID, CLIENT_SECRET)
try:
print(f"Verifying user {USER_ID} is in conversation {CONVERSATION_ID}...")
if not verify_participation(auth, CONVERSATION_ID, USER_ID):
print("Error: User is not a participant in this conversation. Cannot send message.")
sys.exit(1)
print("Sending canned response...")
result = send_chat_message(auth, CONVERSATION_ID, USER_ID, CANNED_MESSAGE)
print(f"Success! Event ID: {result.get('id')}")
except Exception as e:
print(f"Failed: {e}")
sys.exit(1)
if __name__ == "__main__":
main()
Common Errors & Debugging
Error: 403 Forbidden
Cause:
- The
userIdprovided in thefromobject is not a participant in the conversation. - The OAuth token lacks the
conversation:chat:writescope. - The user is an agent, but the conversation type is not Chat (e.g., it is a Voice or Callback conversation).
Fix:
- Verify the scope in your OAuth client configuration.
- Use the
GET /api/v2/conversations/{conversationId}endpoint to inspect thetypefield. It must bechat. - Ensure the
userIdexists in theparticipantsarray of the conversation details.
Error: 400 Bad Request
Cause:
- The
eventTypeis incorrect. It must be lowercase"chat". - The
textfield is missing or empty. - The
fromobject is malformed or missing theid.
Fix:
- Validate your JSON payload structure.
- Ensure
textis a non-empty string.
Error: 401 Unauthorized
Cause:
- The access token is expired.
- The Client ID/Secret is incorrect.
- The token does not have the required scopes.
Fix:
- Implement token refresh logic (as shown in the
GenesysAuthclass). - Check your API credentials in the Genesys Cloud Admin console.
Error: 404 Not Found
Cause:
- The
conversationIdis invalid or the conversation has ended and been archived (depending on retention settings). - The Organization ID in the URL is incorrect.
Fix:
- Confirm the conversation is active.
- Verify the
org_idused in the base URL matches your organization.