Reading and writing participant attributes from an external system during a live voice call
What You Will Build
- You will build a service that retrieves real-time participant attributes from a live Genesys Cloud CX voice conversation and updates them via an external API call.
- This uses the Genesys Cloud CX Conversations API (
/api/v2/conversations/voice) and the PureCloudPlatformClientV2 Python SDK. - The tutorial covers Python 3.9+ with the
purecloudplatformclientv2library.
Prerequisites
- OAuth Client Type: Confidential Client (Client Credentials Flow).
- Required Scopes:
conversation:viewto read participant data, andconversation:writeif you intend to update call metadata or transfer the call based on attribute changes. - SDK Version:
purecloudplatformclientv2>= 149.0.0. - Runtime: Python 3.9 or higher.
- External Dependency:
requestsfor the simulated external system call.
Install the dependencies:
pip install purecloudplatformclientv2 requests
Authentication Setup
Genesys Cloud uses OAuth 2.0. For server-to-server integrations that update participant attributes during a call, you must use the Client Credentials flow. This requires your API user to have the necessary permissions and be associated with an organization.
You must cache the access token. Generating a new token for every API call will trigger rate limits. The token is valid for approximately 35 minutes.
import os
import time
from purecloudplatformclientv2 import ApiClient, Configuration
from purecloudplatformclientv2.rest import ApiException
# Configuration
CLIENT_ID = os.environ.get("GENESYS_CLIENT_ID")
CLIENT_SECRET = os.environ.get("GENESYS_CLIENT_SECRET")
ENVIRONMENT = "mypurecloud.com" # Change to your region, e.g., usw2.pure.cloud
def get_authenticated_api_client() -> ApiClient:
"""
Creates and returns an authenticated ApiClient instance.
In production, implement token caching with TTL checks.
"""
if not CLIENT_ID or not CLIENT_SECRET:
raise ValueError("GENESYS_CLIENT_ID and GENESYS_CLIENT_SECRET must be set")
configuration = Configuration(
host=f"https://{ENVIRONMENT}",
oauth_client_id=CLIENT_ID,
oauth_client_secret=CLIENT_SECRET
)
# The SDK handles the OAuth2 Client Credentials flow automatically
# when configured with client_id and client_secret.
api_client = ApiClient(configuration)
# Verify authentication by fetching the current user profile
# This forces the SDK to fetch a token if not present
try:
api_client.call_api(
"/api/v2/user/me",
"GET",
header_params={},
body=None,
post_params={},
files={},
response_type="User",
auth_settings=["OAuth2"]
)
except ApiException as e:
if e.status == 401:
raise RuntimeError("Authentication failed. Check Client ID and Secret.")
raise
return api_client
Implementation
Step 1: Identifying the Active Voice Conversation
To read participant attributes, you must first identify the specific conversation ID. In a live call scenario, this ID is often passed via a webhook, a callback from the Genesys Cloud Agent Desktop SDK, or derived from a recent conversation search.
For this tutorial, we assume you have the conversationId. If you do not, you must query the analytics or conversation history. Here is how to retrieve the live conversation details using the SDK.
Required Scope: conversation:view
from purecloudplatformclientv2 import ConversationApi
from purecloudplatformclientv2.rest import ApiException
def get_live_voice_conversation(api_client: ApiClient, conversation_id: str) -> dict:
"""
Retrieves the current state of a voice conversation.
Returns the conversation object as a dictionary.
"""
conversation_api = ConversationApi(api_client)
try:
# Fetch the conversation by ID
# The response is a VoiceConversation object
conversation = conversation_api.get_conversation_voice_conversation(
conversation_id=conversation_id
)
# Convert SDK object to dictionary for easier handling
return conversation.to_dict()
except ApiException as e:
if e.status == 404:
print(f"Conversation {conversation_id} not found.")
return None
elif e.status == 429:
print("Rate limited. Implement exponential backoff.")
return None
else:
print(f"Error fetching conversation: {e.body}")
return None
Step 2: Extracting Participant Attributes
A VoiceConversation object contains a participants array. Each participant has an attributes field, which is a JSON object (Map[String, String]). This is where custom data lives.
Genesys Cloud allows you to set attributes on a participant via the API. These attributes can be read by other systems. Common use cases include:
- Passing CRM case IDs.
- Storing sentiment analysis scores.
- Tracking step progress in a scripted flow.
You must iterate through the participants to find the one you care about (e.g., the Agent or the Customer).
def get_participant_attributes(conversation_data: dict, participant_id: str) -> dict:
"""
Extracts attributes for a specific participant from the conversation data.
Args:
conversation_data: The dictionary representation of the VoiceConversation.
participant_id: The ID of the participant (e.g., the agent's user ID).
Returns:
A dictionary of attributes, or an empty dictionary if not found.
"""
participants = conversation_data.get("participants", [])
for participant in participants:
if participant.get("id") == participant_id:
# Attributes is a map of string to string
return participant.get("attributes", {})
return {}
Step 3: Updating Participant Attributes via External System Simulation
In a real-world scenario, you might read an attribute (e.g., crm_case_id), send it to an external CRM, receive an updated status (e.g., case_resolved: true), and then write that new status back to the Genesys Cloud participant.
To write attributes back to Genesys Cloud, you use the PATCH method on the participant endpoint.
Required Scope: conversation:write
from purecloudplatformclientv2 import ConversationApi
from purecloudplatformclientv2.model import ParticipantPatchRequest
from purecloudplatformclientv2.rest import ApiException
import requests
def update_participant_attribute(
api_client: ApiClient,
conversation_id: str,
participant_id: str,
new_attribute_key: str,
new_attribute_value: str
) -> bool:
"""
Updates a single attribute on a participant in a live voice conversation.
Args:
api_client: The authenticated API client.
conversation_id: The ID of the voice conversation.
participant_id: The ID of the participant to update.
new_attribute_key: The key for the attribute (e.g., "sentiment_score").
new_attribute_value: The value for the attribute (e.g., "0.85").
Returns:
True if successful, False otherwise.
"""
conversation_api = ConversationApi(api_client)
# Construct the patch request
# Note: The attributes field in the patch request is a map[string]string
attributes_patch = {new_attribute_key: new_attribute_value}
patch_request = ParticipantPatchRequest(
attributes=attributes_patch
)
try:
# PATCH /api/v2/conversations/voice/{conversationId}/participants/{participantId}
conversation_api.patch_conversation_voice_conversation_participant(
conversation_id=conversation_id,
participant_id=participant_id,
body=patch_request
)
print(f"Successfully updated attribute '{new_attribute_key}' for participant {participant_id}")
return True
except ApiException as e:
if e.status == 404:
print(f"Participant {participant_id} not found in conversation {conversation_id}")
elif e.status == 400:
print(f"Bad Request: {e.body}")
elif e.status == 429:
print("Rate limited. Retry with backoff.")
else:
print(f"Error updating participant: {e.body}")
return False
Step 4: Integrating with an External System
Here is how you tie it together. You read the current attributes, send them to an external HTTP endpoint (simulated here), and write the response back.
import json
import time
def sync_participant_data_with_external_system(
api_client: ApiClient,
conversation_id: str,
participant_id: str,
external_api_url: str
) -> None:
"""
Full workflow: Read Genesys attributes -> Call External API -> Write Back.
"""
# 1. Read Current State
conversation_data = get_live_voice_conversation(api_client, conversation_id)
if not conversation_data:
return
current_attributes = get_participant_attributes(conversation_data, participant_id)
# Simulate an external API payload
external_payload = {
"genesys_conversation_id": conversation_id,
"genesys_participant_id": participant_id,
"current_attributes": current_attributes,
"timestamp": time.time()
}
# 2. Call External System
try:
headers = {"Content-Type": "application/json"}
response = requests.post(external_api_url, json=external_payload, headers=headers, timeout=10)
response.raise_for_status()
external_response = response.json()
# Assume the external system returns a new attribute to store
new_key = "external_case_status"
new_value = external_response.get("status", "unknown")
# 3. Write Back to Genesys Cloud
if new_key and new_value:
update_participant_attribute(
api_client,
conversation_id,
participant_id,
new_key,
str(new_value)
)
except requests.exceptions.RequestException as e:
print(f"External API call failed: {e}")
except json.JSONDecodeError:
print("Failed to parse external API response")
Complete Working Example
This script demonstrates the end-to-end flow. It requires environment variables for credentials and a valid CONVERSATION_ID.
import os
import sys
import time
import requests
from purecloudplatformclientv2 import ApiClient, Configuration, ConversationApi
from purecloudplatformclientv2.rest import ApiException
from purecloudplatformclientv2.model import ParticipantPatchRequest
# --- Configuration ---
CLIENT_ID = os.environ.get("GENESYS_CLIENT_ID")
CLIENT_SECRET = os.environ.get("GENESYS_CLIENT_SECRET")
ENVIRONMENT = "mypurecloud.com"
CONVERSATION_ID = os.environ.get("TEST_CONVERSATION_ID", "your-conversation-id-here")
PARTICIPANT_ID = os.environ.get("TEST_PARTICIPANT_ID", "your-participant-id-here")
EXTERNAL_API_URL = "https://httpbin.org/post" # Mock endpoint for demo
def get_authenticated_api_client() -> ApiClient:
configuration = Configuration(
host=f"https://{ENVIRONMENT}",
oauth_client_id=CLIENT_ID,
oauth_client_secret=CLIENT_SECRET
)
api_client = ApiClient(configuration)
return api_client
def get_live_voice_conversation(api_client: ApiClient, conversation_id: str) -> dict:
conversation_api = ConversationApi(api_client)
try:
conversation = conversation_api.get_conversation_voice_conversation(conversation_id=conversation_id)
return conversation.to_dict()
except ApiException as e:
print(f"Error fetching conversation: {e.status} - {e.body}")
return None
def get_participant_attributes(conversation_data: dict, participant_id: str) -> dict:
participants = conversation_data.get("participants", [])
for participant in participants:
if participant.get("id") == participant_id:
return participant.get("attributes", {})
return {}
def update_participant_attribute(
api_client: ApiClient,
conversation_id: str,
participant_id: str,
new_attribute_key: str,
new_attribute_value: str
) -> bool:
conversation_api = ConversationApi(api_client)
attributes_patch = {new_attribute_key: new_attribute_value}
patch_request = ParticipantPatchRequest(attributes=attributes_patch)
try:
conversation_api.patch_conversation_voice_conversation_participant(
conversation_id=conversation_id,
participant_id=participant_id,
body=patch_request
)
print(f"Updated attribute: {new_attribute_key}={new_attribute_value}")
return True
except ApiException as e:
print(f"Update failed: {e.status} - {e.body}")
return False
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)
api_client = get_authenticated_api_client()
# Step 1: Read
print(f"Fetching conversation {CONVERSATION_ID}...")
conv_data = get_live_voice_conversation(api_client, CONVERSATION_ID)
if not conv_data:
print("Failed to fetch conversation. Ensure ID is valid and you have permissions.")
return
current_attrs = get_participant_attributes(conv_data, PARTICIPANT_ID)
print(f"Current attributes: {current_attrs}")
# Step 2: External Call
print(f"Calling external system: {EXTERNAL_API_URL}")
try:
payload = {
"conversation_id": CONVERSATION_ID,
"participant_id": PARTICIPANT_ID,
"existing_attributes": current_attrs
}
response = requests.post(EXTERNAL_API_URL, json=payload, timeout=5)
response.raise_for_status()
ext_data = response.json()
# Simulate external system returning a new value
new_attr_key = "external_sync_status"
new_attr_value = "synced"
# Step 3: Write Back
print(f"Writing back to Genesys Cloud...")
success = update_participant_attribute(
api_client,
CONVERSATION_ID,
PARTICIPANT_ID,
new_attr_key,
new_attr_value
)
if success:
# Verify update
time.sleep(1) # Allow propagation
updated_conv = get_live_voice_conversation(api_client, CONVERSATION_ID)
if updated_conv:
updated_attrs = get_participant_attributes(updated_conv, PARTICIPANT_ID)
print(f"Verified attributes: {updated_attrs}")
except Exception as e:
print(f"Workflow error: {e}")
if __name__ == "__main__":
main()
Common Errors & Debugging
Error: 403 Forbidden on PATCH Participant
- Cause: The OAuth token lacks the
conversation:writescope, or the API user does not have the required security profile permissions in Genesys Cloud. - Fix: Ensure your OAuth client is granted
conversation:write. Verify the user associated with the client has the “Modify Conversation” capability in their security profile.
Error: 409 Conflict on Participant Update
- Cause: You are trying to update a participant attribute while another process is simultaneously modifying the same participant. Genesys Cloud uses optimistic locking.
- Fix: Implement a retry mechanism with a short delay (e.g., 100ms) if a 409 is received. Check the
ETagheader if you are doing conditional updates, though for simple attribute patches, a retry usually suffices.
Error: 404 Not Found on Participant
- Cause: The
participantIddoes not exist in the conversation, or the conversation has ended. - Fix: Verify the
conversationIdis active. Check theparticipantsarray from the GET request to ensure the ID matches exactly. Note that participant IDs are UUIDs, not user IDs, though they are linked.
Error: Rate Limiting (429 Too Many Requests)
- Cause: Your application is making too many requests per second. Genesys Cloud enforces strict rate limits on the Conversations API.
- Fix: Implement exponential backoff. Do not poll for updates. Use webhooks to listen for
conversation:participant:updatedevents instead of polling the GET endpoint.