Sending Structured Messages via the Genesys Cloud Open Messaging API
What You Will Build
- This tutorial demonstrates how to programmatically send rich, structured messages including quick replies and interactive cards using the Genesys Cloud Open Messaging API.
- The implementation relies on the
POST /api/v2/conversations/messaging/contacts/{contactId}/messagesendpoint to deliver payload data to an active messaging conversation. - The code examples are provided in Python 3.9+ using the
requestslibrary and the official Genesys Cloud Python SDK.
Prerequisites
- OAuth Client: A Genesys Cloud OAuth client configured with the Confidential client type (Client Credentials Grant) or Public client type (if using PKCE, though less common for server-side messaging).
- Required Scopes:
messages:send(Required to send messages)conversations:read(Required to verify contact existence and conversation status)analytics:conversations:query(Optional, for debugging conversation history)
- SDK Version:
genesys-cloud-python-sdkversion 100.0.0 or later. - Runtime: Python 3.9 or higher.
- Dependencies:
requestsgenesys-cloud-python-sdkpython-dotenv(for managing credentials)
Authentication Setup
Genesys Cloud uses OAuth 2.0. For server-side integrations that send messages on behalf of a bot or system, the Client Credentials Grant is the standard flow. This flow exchanges your client ID and secret for an access token.
The token is valid for 1 hour. Your application must handle token expiration by catching 401 Unauthorized responses or implementing a time-based cache.
import os
import requests
from typing import Dict, Optional
import time
class GenesysAuth:
def __init__(self, client_id: str, client_secret: str, base_url: str = "https://api.mypurecloud.com"):
self.client_id = client_id
self.client_secret = client_secret
self.base_url = base_url.rstrip('/')
self.token_url = f"{self.base_url}/oauth/token"
self.access_token: Optional[str] = None
self.token_expiry: float = 0
def get_token(self) -> str:
"""
Retrieves an OAuth access token.
Uses cached token if valid, otherwise fetches a new one.
"""
if self.access_token and time.time() < self.token_expiry - 60:
return self.access_token
payload = {
'grant_type': 'client_credentials',
'client_id': self.client_id,
'client_secret': self.client_secret
}
headers = {
'Content-Type': 'application/x-www-form-urlencoded'
}
response = requests.post(self.token_url, data=payload, headers=headers)
if response.status_code != 200:
raise Exception(f"Failed to obtain token: {response.status_code} {response.text}")
data = response.json()
self.access_token = data['access_token']
self.token_expiry = time.time() + data['expires_in']
return self.access_token
def get_headers(self) -> Dict[str, str]:
"""Returns headers ready for API calls."""
return {
'Authorization': f"Bearer {self.get_token()}",
'Content-Type': 'application/json'
}
# Usage Example
# auth = GenesysAuth(os.getenv('GENESYS_CLIENT_ID'), os.getenv('GENESYS_CLIENT_SECRET'))
# headers = auth.get_headers()
Implementation
To send structured messages, you must first have an active conversation and a valid contact ID. The messaging API does not create conversations; it injects messages into existing ones.
Step 1: Validate the Contact and Conversation
Before sending a rich message, you must ensure the contact ID is valid and the conversation is in a state that accepts messages (e.g., active or queued). Sending to a closed conversation will result in a 400 Bad Request.
Endpoint: GET /api/v2/conversations/messaging/contacts/{contactId}
Scope: conversations:read
import requests
import sys
def validate_contact(auth: GenesysAuth, contact_id: str) -> dict:
"""
Fetches contact details to ensure the conversation exists and is active.
"""
url = f"{auth.base_url}/api/v2/conversations/messaging/contacts/{contact_id}"
headers = auth.get_headers()
try:
response = requests.get(url, headers=headers)
if response.status_code == 401:
# Token might have expired during the check
auth.access_token = None
headers = auth.get_headers()
response = requests.get(url, headers=headers)
if response.status_code == 404:
print(f"Error: Contact {contact_id} not found.", file=sys.stderr)
sys.exit(1)
if response.status_code == 403:
print(f"Error: Insufficient permissions. Check scopes.", file=sys.stderr)
sys.exit(1)
if response.status_code != 200:
print(f"Error fetching contact: {response.status_code} {response.text}", file=sys.stderr)
sys.exit(1)
contact_data = response.json()
# Check if conversation is still active
# Note: The contact object contains conversationId.
# We assume if we can fetch the contact, the channel is open.
return contact_data
except requests.exceptions.RequestException as e:
print(f"Network error: {e}", file=sys.stderr)
sys.exit(1)
Step 2: Construct the Structured Message Payload
Genesys Cloud supports several message types within the Open Messaging API. The two most common structured types are:
- Quick Reply: A set of buttons where the user selects one, sending back a text payload.
- Card (Carousel): A rich card with an image, title, subtitle, and buttons.
The payload structure follows the Message schema. The content field is critical. It must be a JSON object that defines the structure.
Quick Reply Payload
For quick replies, the content type is quick-reply. You define a title and an array of choices. Each choice has a label (what the user sees) and a value (what is sent back to the webhook/platform).
def create_quick_reply_payload(title: str, choices: list) -> dict:
"""
Creates a payload for a Quick Reply message.
Args:
title: The text above the buttons.
choices: List of dicts with 'label' and 'value'.
"""
return {
"type": "message",
"content": {
"type": "quick-reply",
"title": title,
"choices": choices
}
}
# Example Usage:
# payload = create_quick_reply_payload(
# "How can we help you today?",
# [
# {"label": "Billing", "value": "billing_inquiry"},
# {"label": "Technical Support", "value": "tech_support"},
# {"label": "Speak to Agent", "value": "transfer_to_agent"}
# ]
# )
Card Payload
For cards, the content type is card. This supports images, headers, and action buttons. The buttons in a card can be postback (sends value, no visible text change) or web_url (opens a browser).
def create_card_payload(title: str, subtitle: str, image_url: str, buttons: list) -> dict:
"""
Creates a payload for a Card message.
"""
return {
"type": "message",
"content": {
"type": "card",
"title": title,
"subtitle": subtitle,
"image": {
"url": image_url
},
"actions": buttons
}
}
# Example Usage:
# payload = create_card_payload(
# "New Product Launch",
# "Check out our latest features.",
# "https://example.com/images/product.jpg",
# [
# {
# "type": "postback",
# "title": "Learn More",
# "payload": "learn_more_product_123"
# },
# {
# "type": "web_url",
# "title": "Visit Website",
# "url": "https://example.com/products/123"
# }
# ]
# )
Step 3: Send the Message
The final step is to POST the payload to the messaging endpoint.
Endpoint: POST /api/v2/conversations/messaging/contacts/{contactId}/messages
Scope: messages:send
Important: The type field in the root of the JSON body must be "message". The content field holds the structured data.
def send_structured_message(auth: GenesysAuth, contact_id: str, payload: dict) -> dict:
"""
Sends a structured message to a specific contact.
Args:
auth: GenesysAuth instance
contact_id: The ID of the contact in the conversation
payload: The message payload dict
Returns:
The response JSON containing the message ID and status.
"""
url = f"{auth.base_url}/api/v2/conversations/messaging/contacts/{contact_id}/messages"
headers = auth.get_headers()
# Retry logic for 429 Too Many Requests
max_retries = 3
retry_count = 0
while retry_count < max_retries:
try:
response = requests.post(url, json=payload, headers=headers)
# Handle Rate Limiting
if response.status_code == 429:
retry_after = int(response.headers.get('Retry-After', 5))
print(f"Rate limited. Retrying in {retry_after} seconds...")
import time
time.sleep(retry_after)
retry_count += 1
continue
# Handle Authentication Errors
if response.status_code == 401:
auth.access_token = None # Force refresh
headers = auth.get_headers()
continue
# Handle Business Logic Errors
if response.status_code == 400:
print(f"Bad Request: {response.text}", file=sys.stderr)
# Common cause: Invalid contact ID or invalid content structure
return None
if response.status_code == 404:
print(f"Contact or Conversation not found.", file=sys.stderr)
return None
if response.status_code == 200 or response.status_code == 201:
print("Message sent successfully.")
return response.json()
# Handle unexpected errors
print(f"Unexpected error: {response.status_code} {response.text}", file=sys.stderr)
return None
except requests.exceptions.RequestException as e:
print(f"Network error: {e}", file=sys.stderr)
return None
print("Max retries exceeded due to rate limiting.")
return None
Complete Working Example
This script combines authentication, validation, payload construction, and sending. It sends a Quick Reply menu to a user.
import os
import sys
import time
import requests
from typing import Dict, Optional, List
# --- Configuration ---
# Replace these with your actual Genesys Cloud credentials
CLIENT_ID = os.getenv('GENESYS_CLIENT_ID')
CLIENT_SECRET = os.getenv('GENESYS_CLIENT_SECRET')
BASE_URL = os.getenv('GENESYS_BASE_URL', 'https://api.mypurecloud.com')
CONTACT_ID = os.getenv('TARGET_CONTACT_ID') # Must be a valid, active contact ID
if not all([CLIENT_ID, CLIENT_SECRET, CONTACT_ID]):
print("Error: Missing environment variables. Set GENESYS_CLIENT_ID, GENESYS_CLIENT_SECRET, and TARGET_CONTACT_ID.", file=sys.stderr)
sys.exit(1)
# --- Authentication Module ---
class GenesysAuth:
def __init__(self, client_id: str, client_secret: str, base_url: str):
self.client_id = client_id
self.client_secret = client_secret
self.base_url = base_url.rstrip('/')
self.token_url = f"{self.base_url}/oauth/token"
self.access_token: Optional[str] = None
self.token_expiry: float = 0
def get_token(self) -> str:
if self.access_token and time.time() < self.token_expiry - 60:
return self.access_token
payload = {
'grant_type': 'client_credentials',
'client_id': self.client_id,
'client_secret': self.client_secret
}
headers = {'Content-Type': 'application/x-www-form-urlencoded'}
response = requests.post(self.token_url, data=payload, headers=headers)
if response.status_code != 200:
raise Exception(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']
return self.access_token
def get_headers(self) -> Dict[str, str]:
return {
'Authorization': f"Bearer {self.get_token()}",
'Content-Type': 'application/json'
}
# --- Business Logic ---
def send_quick_reply_menu(auth: GenesysAuth, contact_id: str) -> None:
"""
Sends a quick reply menu to the specified contact.
"""
# 1. Validate Contact
url_check = f"{auth.base_url}/api/v2/conversations/messaging/contacts/{contact_id}"
headers = auth.get_headers()
resp_check = requests.get(url_check, headers=headers)
if resp_check.status_code != 200:
print(f"Failed to validate contact. Status: {resp_check.status_code}", file=sys.stderr)
return
# 2. Define Payload
# This creates a menu with 3 options
message_payload = {
"type": "message",
"content": {
"type": "quick-reply",
"title": "Select a service department:",
"choices": [
{
"label": "Sales",
"value": "dept_sales"
},
{
"label": "Support",
"value": "dept_support"
},
{
"label": "Billing",
"value": "dept_billing"
}
]
}
}
# 3. Send Message
url_send = f"{auth.base_url}/api/v2/conversations/messaging/contacts/{contact_id}/messages"
# Retry loop for resilience
attempts = 0
max_attempts = 3
while attempts < max_attempts:
try:
response = requests.post(url_send, json=message_payload, headers=headers)
if response.status_code == 429:
retry_after = int(response.headers.get('Retry-After', 2))
print(f"Rate limited. Waiting {retry_after}s...")
time.sleep(retry_after)
attempts += 1
continue
if response.status_code == 401:
auth.access_token = None # Refresh token
headers = auth.get_headers()
continue
if response.status_code in [200, 201]:
print("Success! Message sent.")
print(f"Response: {response.json()}")
return
else:
print(f"Error sending message: {response.status_code} {response.text}", file=sys.stderr)
return
except Exception as e:
print(f"Exception: {e}", file=sys.stderr)
return
print("Failed to send message after multiple retries.")
# --- Main Execution ---
if __name__ == "__main__":
try:
auth = GenesysAuth(CLIENT_ID, CLIENT_SECRET, BASE_URL)
print(f"Sending structured message to Contact ID: {CONTACT_ID}")
send_quick_reply_menu(auth, CONTACT_ID)
except Exception as e:
print(f"Fatal Error: {e}", file=sys.stderr)
sys.exit(1)
Common Errors & Debugging
Error: 400 Bad Request - “Invalid content type”
- Cause: The
content.typefield in your JSON payload is misspelled or unsupported. For example, using"quickreply"instead of"quick-reply". - Fix: Verify the
content.typematches the exact strings defined in the API spec:text,image,audio,video,file,quick-reply,card, orlocation.
Error: 403 Forbidden - “Insufficient permissions”
- Cause: The OAuth client used to generate the token lacks the
messages:sendscope. - Fix: Go to the Genesys Cloud Admin Console > Security > OAuth > Clients. Edit your client and add the
messages:sendscope. Regenerate the token.
Error: 404 Not Found
- Cause: The
contactIdprovided does not exist, or the conversation associated with that contact has been closed/deleted. - Fix: Ensure you are using the
contactIdreturned from thePOST /api/v2/conversations/messaging/contactsendpoint (when the user initiates chat) or from theGET .../contactslist. Do not use theconversationIdas thecontactId.
Error: 429 Too Many Requests
- Cause: You have exceeded the rate limit for messaging endpoints. Genesys Cloud enforces strict rate limits to protect platform stability.
- Fix: Implement exponential backoff. The response header
Retry-Afterindicates how many seconds to wait. Never poll aggressively.
Error: Message Not Appearing in Client
- Cause: The message was sent successfully (200 OK) but the client (WhatsApp, Facebook Messenger, Web Chat) does not support the specific rich media type.
- Fix: Check the channel capabilities. For example, WhatsApp has strict template requirements for outgoing messages after 24 hours. If you are sending via WhatsApp, you must use the
whatsappchannel-specific payload structures or ensure the message is within the 24-hour service window. For generic Open Messaging, this usually applies to Web Chat or custom adapters.