How to Send a Canned Response During a Chat Interaction via the Conversations API

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}/events endpoint.
  • The implementation covers Python (using requests) and JavaScript (using axios), 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 requests library installed (pip install requests).
  • JavaScript: Node.js 14+ with axios installed (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:

  1. conversationId: The UUID of the active chat.
  2. 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 the id of 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:

  1. The userId provided in the from object is not a participant in the conversation.
  2. The OAuth token lacks the conversation:chat:write scope.
  3. 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 the type field. It must be chat.
  • Ensure the userId exists in the participants array of the conversation details.

Error: 400 Bad Request

Cause:

  1. The eventType is incorrect. It must be lowercase "chat".
  2. The text field is missing or empty.
  3. The from object is malformed or missing the id.

Fix:

  • Validate your JSON payload structure.
  • Ensure text is a non-empty string.

Error: 401 Unauthorized

Cause:

  1. The access token is expired.
  2. The Client ID/Secret is incorrect.
  3. The token does not have the required scopes.

Fix:

  • Implement token refresh logic (as shown in the GenesysAuth class).
  • Check your API credentials in the Genesys Cloud Admin console.

Error: 404 Not Found

Cause:

  1. The conversationId is invalid or the conversation has ended and been archived (depending on retention settings).
  2. The Organization ID in the URL is incorrect.

Fix:

  • Confirm the conversation is active.
  • Verify the org_id used in the base URL matches your organization.

Official References