Reading and Writing Participant Attributes via Genesys Cloud APIs During Live Calls
What You Will Build
- You will build a script that identifies a live voice interaction by Conversation ID and updates the
participantAttributesfor a specific user. - You will use the Genesys Cloud Platform API v2 (
/api/v2/conversations/voice/participants) to retrieve current state and patch attributes. - The tutorial covers Python with the
genesyscloudSDK and rawrequestsfor transparency.
Prerequisites
- OAuth Client Type: Confidential Client (Client Credentials Flow).
- Required Scopes:
conversation:view(to read participant details)conversation:update(to write participant attributes)user:read(optional, for debugging user IDs)
- SDK Version:
genesyscloud>= 1.0.0 (Python) or@genesyscloud/purecloud-platform-client-v2(Node.js). - Runtime: Python 3.9+ or Node.js 18+.
- External Dependency:
pip install genesyscloud requests python-dotenv.
Authentication Setup
Genesys Cloud uses OAuth 2.0. For server-side integrations, the Client Credentials flow is standard. You must cache the token and handle expiration. The following code initializes the client using environment variables.
import os
import time
import requests
from dotenv import load_dotenv
load_dotenv()
CLIENT_ID = os.getenv("GENESYS_CLIENT_ID")
CLIENT_SECRET = os.getenv("GENESYS_CLIENT_SECRET")
ENVIRONMENT = os.getenv("GENESYS_ENVIRONMENT", "us-east-1")
# Base URL construction
BASE_URL = f"https://{ENVIRONMENT}.mypurecloud.com"
def get_access_token() -> str:
"""
Retrieves an OAuth2 access token using Client Credentials flow.
Implements basic caching to avoid unnecessary token requests.
"""
# Check if we have a valid token in memory (simplified for tutorial)
# In production, use a thread-safe cache with TTL
token_url = f"{BASE_URL}/oauth/token"
payload = {
"grant_type": "client_credentials",
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET
}
headers = {
"Content-Type": "application/x-www-form-urlencoded"
}
response = requests.post(token_url, data=payload, headers=headers)
if response.status_code != 200:
raise Exception(f"Authentication failed: {response.status_code} - {response.text}")
return response.json()["access_token"]
# Initial fetch
access_token = get_access_token()
auth_header = {"Authorization": f"Bearer {access_token}"}
Implementation
Step 1: Identifying the Live Conversation and Participant
You cannot update attributes without knowing the specific conversationId and participantId. In a live scenario, this data usually comes from an event stream (WebSockets/AMQP) or a webhook. For this tutorial, we assume you have the conversationId. We must retrieve the participant list to find the correct participantId corresponding to the external system’s user or agent.
Endpoint: GET /api/v2/conversations/voice/participants
Scope: conversation:view
def get_participants(conversation_id: str) -> list:
"""
Retrieves all participants in a live voice conversation.
"""
url = f"{BASE_URL}/api/v2/conversations/voice/participants"
# Query parameters to filter by conversation ID
params = {
"conversationId": conversation_id
}
response = requests.get(url, headers=auth_header, params=params)
if response.status_code == 401:
# Token expired, refresh and retry
print("Token expired. Refreshing...")
global auth_header
auth_header = {"Authorization": f"Bearer {get_access_token()}"}
response = requests.get(url, headers=auth_header, params=params)
if response.status_code != 200:
raise Exception(f"Failed to get participants: {response.status_code} - {response.text}")
return response.json()["entities"]
Expected Response:
{
"entities": [
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"name": "John Doe",
"type": "user",
"state": "connected",
"participantAttributes": {
"externalSystemId": "EXT-1001",
"priority": "normal"
}
},
{
"id": "b2c3d4e5-f6a7-8901-bcde-f12345678901",
"name": "Jane Smith",
"type": "user",
"state": "connected",
"participantAttributes": {}
}
],
"pageSize": 25,
"pageNumber": 1
}
Step 2: Updating Participant Attributes
Genesys Cloud participant attributes are key-value pairs stored as a JSON object. You do not replace the entire participant object; you use a PATCH request to update specific fields. The critical field is participantAttributes.
Important Constraint: Participant attributes are limited to 1KB per participant. They are intended for lightweight data (flags, routing hints, simple IDs), not large payloads.
Endpoint: PATCH /api/v2/conversations/voice/participants/{participantId}
Scope: conversation:update
def update_participant_attributes(conversation_id: str, participant_id: str, new_attributes: dict) -> dict:
"""
Updates the participantAttributes for a specific participant.
Uses PATCH to merge new attributes with existing ones.
"""
url = f"{BASE_URL}/api/v2/conversations/voice/participants/{participant_id}"
# The body must contain the attributes to update.
# Note: We do not send the entire participant object, only the fields to change.
body = {
"participantAttributes": new_attributes
}
headers = {
**auth_header,
"Content-Type": "application/json"
}
response = requests.patch(url, json=body, headers=headers)
# Handle 409 Conflict (Resource state mismatch)
if response.status_code == 409:
print(f"Conflict: The conversation or participant state may have changed. Response: {response.text}")
# In a robust system, re-fetch the participant state and retry
# Handle 404 Not Found
if response.status_code == 404:
raise Exception(f"Participant {participant_id} not found in conversation {conversation_id}")
if response.status_code != 200:
raise Exception(f"Update failed: {response.status_code} - {response.text}")
return response.json()
Step 3: Reading Back the Updated Attributes
To confirm the write operation succeeded, you should read the participant data again. This verifies that the external system’s changes are visible to Genesys Cloud logic (such as routing rules or scripts).
def get_single_participant(conversation_id: str, participant_id: str) -> dict:
"""
Retrieves details for a single participant.
"""
url = f"{BASE_URL}/api/v2/conversations/voice/participants/{participant_id}"
params = {
"conversationId": conversation_id
}
response = requests.get(url, headers=auth_header, params=params)
if response.status_code != 200:
raise Exception(f"Failed to get participant: {response.status_code} - {response.text}")
return response.json()
Complete Working Example
The following script ties the steps together. It simulates an external system updating a customer’s priority level during a call.
import os
import sys
import time
import requests
from dotenv import load_dotenv
# Load environment variables
load_dotenv()
CLIENT_ID = os.getenv("GENESYS_CLIENT_ID")
CLIENT_SECRET = os.getenv("GENESYS_CLIENT_SECRET")
ENVIRONMENT = os.getenv("GENESYS_ENVIRONMENT", "us-east-1")
if not CLIENT_ID or not CLIENT_SECRET:
print("Error: GENESYS_CLIENT_ID and GENESYS_CLIENT_SECRET must be set in .env")
sys.exit(1)
BASE_URL = f"https://{ENVIRONMENT}.mypurecloud.com"
def get_access_token() -> str:
token_url = f"{BASE_URL}/oauth/token"
payload = {
"grant_type": "client_credentials",
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET
}
headers = {"Content-Type": "application/x-www-form-urlencoded"}
response = requests.post(token_url, data=payload, headers=headers)
if response.status_code != 200:
raise Exception(f"Auth failed: {response.text}")
return response.json()["access_token"]
def main():
# 1. Authenticate
print("Authenticating...")
access_token = get_access_token()
headers = {"Authorization": f"Bearer {access_token}", "Content-Type": "application/json"}
# 2. Define Target
# REPLACE THIS WITH A REAL LIVE CONVERSATION ID
# You can find a live conversation ID via the Admin Console or WebSockets
conversation_id = "YOUR_LIVE_CONVERSATION_ID_HERE"
if conversation_id == "YOUR_LIVE_CONVERSATION_ID_HERE":
print("Error: You must provide a real live Conversation ID.")
return
print(f"Targeting Conversation: {conversation_id}")
# 3. Get Participants
print("Fetching participants...")
participants_url = f"{BASE_URL}/api/v2/conversations/voice/participants"
params = {"conversationId": conversation_id}
resp = requests.get(participants_url, headers=headers, params=params)
if resp.status_code != 200:
print(f"Error fetching participants: {resp.status_code} {resp.text}")
return
participants = resp.json().get("entities", [])
if not participants:
print("No participants found in this conversation.")
return
# Select the first user participant (agent or customer)
target_participant = None
for p in participants:
if p.get("type") == "user" and p.get("state") == "connected":
target_participant = p
break
if not target_participant:
print("No connected user participants found.")
return
participant_id = target_participant["id"]
print(f"Targeting Participant: {target_participant.get('name')} (ID: {participant_id})")
# 4. Update Attributes
# Example: Adding a VIP flag and a custom external reference
new_attrs = {
"isVIP": "true",
"externalTicketRef": "TKT-998877",
"lastUpdatedBy": "ExternalSystemScript"
}
print(f"Updating attributes: {new_attrs}")
update_url = f"{BASE_URL}/api/v2/conversations/voice/participants/{participant_id}"
body = {"participantAttributes": new_attrs}
# Retry logic for 429 Rate Limiting
max_retries = 3
for attempt in range(max_retries):
update_resp = requests.patch(update_url, json=body, headers=headers)
if update_resp.status_code == 429:
retry_after = int(update_resp.headers.get("Retry-After", 5))
print(f"Rate limited (429). Waiting {retry_after}s...")
time.sleep(retry_after)
continue
elif update_resp.status_code == 200:
print("Attribute update successful.")
break
else:
print(f"Update failed: {update_resp.status_code} - {update_resp.text}")
return
# 5. Verify Update
print("Verifying update...")
verify_url = f"{BASE_URL}/api/v2/conversations/voice/participants/{participant_id}"
verify_params = {"conversationId": conversation_id}
verify_resp = requests.get(verify_url, headers=headers, params=verify_params)
if verify_resp.status_code == 200:
updated_participant = verify_resp.json()
current_attrs = updated_participant.get("participantAttributes", {})
print(f"Current Attributes: {current_attrs}")
# Check if our keys exist
if "isVIP" in current_attrs:
print("Verification Passed: 'isVIP' attribute is present.")
else:
print("Verification Failed: 'isVIP' attribute not found.")
else:
print(f"Verification failed: {verify_resp.status_code}")
if __name__ == "__main__":
main()
Common Errors & Debugging
Error: 403 Forbidden
Cause: The OAuth token does not have the conversation:update scope.
Fix: Update your OAuth Client configuration in the Genesys Cloud Admin Console. Navigate to Organization > Integrations > OAuth 2.0 Clients. Edit your client and ensure conversation:update is checked under the Scopes section.
Error: 404 Not Found
Cause: The conversationId or participantId is invalid, or the conversation has ended.
Fix: Participant attributes only persist while the conversation is active. If the call ends, the participant object is archived. Ensure you are targeting a live conversation. Use the state field in the participant list to verify the participant is connected or ringing.
Error: 409 Conflict
Cause: The participant entity was modified by another process (e.g., a transfer or disposition change) between your read and write operations.
Fix: Implement an optimistic locking strategy. Re-fetch the participant details, check the lastUpdated timestamp, and retry the PATCH request. Genesys Cloud does not use ETags for participants, so you must handle state drift manually.
Error: 429 Too Many Requests
Cause: You are exceeding the API rate limits (typically 100 requests per second for this endpoint, but lower for specific sub-operations).
Fix: Implement exponential backoff. The response header Retry-After indicates how many seconds to wait.
# Example Backoff Logic
def api_call_with_retry(request_func, *args, max_retries=3):
for i in range(max_retries):
response = request_func(*args)
if response.status_code == 429:
wait_time = int(response.headers.get("Retry-After", 2 ** i))
time.sleep(wait_time)
continue
return response
raise Exception("Max retries exceeded")