Updating Participant Attributes Mid-Call with Genesys Cloud CX
What You Will Build
- This tutorial demonstrates how to programmatically update participant-specific metadata (such as
user-definedattributes) during an active voice or digital conversation. - The implementation uses the Genesys Cloud CX Conversations API to send a
PATCHrequest to the specific participant resource. - The code is provided in Python using the
requestslibrary and JavaScript usingfetch.
Prerequisites
- OAuth Client: A Genesys Cloud CX OAuth Client ID and Secret with the
bot:bot:readoranalytics:conversation:readscope is not sufficient. You require theconversation:participant:writescope. - API Version: Genesys Cloud CX API v2.
- Language/Runtime:
- Python 3.9+ with
requestsandpyjwt(if using JWT, though this guide uses Client Credentials). - Node.js 18+ with native
fetchsupport.
- Python 3.9+ with
- Dependencies:
pip install requests- No additional npm packages required for Node.js 18+.
Authentication Setup
To modify participant attributes, you must authenticate using the OAuth 2.0 Client Credentials Grant. This flow exchanges your Client ID and Secret for an access token. The token is valid for 3600 seconds (1 hour). In production, you must implement token caching to avoid hitting rate limits on the /oauth/token endpoint.
Python Authentication
import requests
import time
from typing import Optional
# Configuration
ORGANIZATION_DOMAIN = "your-organization.genesiscloud.com"
CLIENT_ID = "your-client-id"
CLIENT_SECRET = "your-client-secret"
BASE_URL = f"https://{ORGANIZATION_DOMAIN}/api/v2"
TOKEN_URL = f"https://{ORGANIZATION_DOMAIN}/oauth/token"
class GenesysAuth:
def __init__(self, client_id: str, client_secret: str, org_domain: str):
self.client_id = client_id
self.client_secret = client_secret
self.org_domain = org_domain
self.token_url = f"https://{org_domain}/oauth/token"
self.access_token: Optional[str] = None
self.token_expiry: float = 0
def get_access_token(self) -> str:
"""
Retrieves an access token. Caches the token until it expires.
"""
# If we have a token and it has not expired, return it
if self.access_token and time.time() < self.token_expiry:
return self.access_token
# Prepare the payload for Client Credentials Grant
payload = {
"grant_type": "client_credentials",
"client_id": self.client_id,
"client_secret": self.client_secret
}
try:
response = requests.post(self.token_url, data=payload)
response.raise_for_status()
token_data = response.json()
self.access_token = token_data["access_token"]
# Set expiry time, subtracting 60 seconds to ensure we refresh before hard expiry
self.token_expiry = time.time() + (token_data["expires_in"] - 60)
return self.access_token
except requests.exceptions.HTTPError as e:
if response.status_code == 401:
raise RuntimeError("Invalid Client ID or Secret.") from e
elif response.status_code == 429:
raise RuntimeError("Rate limit exceeded on OAuth endpoint. Implement exponential backoff.") from e
else:
raise RuntimeError(f"Authentication failed with status {response.status_code}: {response.text}") from e
except requests.exceptions.RequestException as e:
raise RuntimeError(f"Network error during authentication: {e}") from e
# Initialize the auth helper
auth_helper = GenesysAuth(CLIENT_ID, CLIENT_SECRET, ORGANIZATION_DOMAIN)
JavaScript Authentication
const ORGANIZATION_DOMAIN = "your-organization.genesiscloud.com";
const CLIENT_ID = "your-client-id";
const CLIENT_SECRET = "your-client-secret";
const TOKEN_URL = `https://${ORGANIZATION_DOMAIN}/oauth/token`;
let cachedToken = null;
let tokenExpiry = 0;
async function getAccessToken() {
// If we have a cached token and it hasn't expired, return it
if (cachedToken && Date.now() < tokenExpiry) {
return cachedToken;
}
const payload = new URLSearchParams({
grant_type: "client_credentials",
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET
});
try {
const response = await fetch(TOKEN_URL, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
body: payload
});
if (!response.ok) {
if (response.status === 401) {
throw new Error("Invalid Client ID or Secret.");
}
if (response.status === 429) {
throw new Error("Rate limit exceeded on OAuth endpoint.");
}
throw new Error(`Authentication failed with status ${response.status}`);
}
const data = await response.json();
cachedToken = data.access_token;
// Subtract 60 seconds to ensure refresh before hard expiry
tokenExpiry = Date.now() + (data.expires_in * 1000 - 60000);
return cachedToken;
} catch (error) {
if (error instanceof TypeError) {
throw new Error(`Network error during authentication: ${error.message}`);
}
throw error;
}
}
Implementation
Step 1: Identifying the Conversation and Participant
Before you can update attributes, you must know the conversationId and the specific participantId. These are typically available in your application context if you are building a bot, a dashboard, or a webhook handler.
If you do not have these IDs, you must query the active conversations. The following example shows how to retrieve active voice conversations for a specific user (agent) to find the target participant.
Required Scope: conversation:participant:read
Python: Fetching Active Conversations
def get_active_participants(user_id: str) -> list:
"""
Retrieves active conversations for a specific user to find participant IDs.
"""
token = auth_helper.get_access_token()
# Endpoint to get conversations by user
url = f"{BASE_URL}/api/v2/conversations/users/{user_id}/conversations"
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json"
}
# Query parameters to filter for active conversations only
params = {
"state": "active",
"type": "voice" # Change to 'message' or 'webchat' as needed
}
try:
response = requests.get(url, headers=headers, params=params)
response.raise_for_status()
data = response.json()
entities = data.get("entities", [])
participants = []
for conv in entities:
conv_id = conv["id"]
for part in conv.get("participants", []):
participants.append({
"conversationId": conv_id,
"participantId": part["id"],
"userId": part.get("userId"),
"externalContactId": part.get("externalContactId")
})
return participants
except requests.exceptions.HTTPError as e:
print(f"Error fetching conversations: {e.response.text}")
return []
except requests.exceptions.RequestException as e:
print(f"Network error: {e}")
return []
Step 2: Updating Participant Attributes
The core operation is a PATCH request to /api/v2/conversations/{conversationId}/participants/{participantId}.
Critical Note on Data Structure:
Genesys Cloud CX treats participant attributes as a hierarchy. You are not sending a full replacement of the participant object. You are sending a partial update. The most common use case is updating User-Defined Attributes (UDA).
The JSON body must target the attributes object. If you send {"attributes": {"myKey": "myValue"}}, it will merge/update the myKey within the existing attributes map.
Required Scope: conversation:participant:write
Python: The Update Function
def update_participant_attributes(conversation_id: str, participant_id: str, new_attributes: dict) -> bool:
"""
Updates user-defined attributes for a specific participant in a conversation.
Args:
conversation_id: The UUID of the conversation.
participant_id: The UUID of the participant.
new_attributes: A dictionary of key-value pairs to merge into the participant's attributes.
Returns:
True if successful, False otherwise.
"""
token = auth_helper.get_access_token()
url = f"{BASE_URL}/api/v2/conversations/{conversation_id}/participants/{participant_id}"
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json"
}
# The body must wrap the attributes in an 'attributes' key
payload = {
"attributes": new_attributes
}
try:
# Use PATCH for partial updates
response = requests.patch(url, headers=headers, json=payload)
# 204 No Content is the standard success response for PATCH in Genesys API
if response.status_code == 204:
return True
# Handle specific error codes
if response.status_code == 404:
raise RuntimeError(f"Conversation or Participant not found. Check IDs.")
elif response.status_code == 409:
raise RuntimeError("Conflict: The participant may have been removed or the conversation ended.")
elif response.status_code == 400:
raise RuntimeError(f"Bad Request: Invalid JSON structure. {response.text}")
elif response.status_code == 401:
raise RuntimeError("Unauthorized: Token may be expired or invalid.")
elif response.status_code == 403:
raise RuntimeError("Forbidden: Client lacks 'conversation:participant:write' scope.")
elif response.status_code == 429:
raise RuntimeError("Rate Limit Exceeded: Back off and retry.")
else:
raise RuntimeError(f"Unexpected error: {response.status_code} - {response.text}")
except requests.exceptions.RequestException as e:
raise RuntimeError(f"Network error during update: {e}") from e
JavaScript: The Update Function
async function updateParticipantAttributes(conversationId, participantId, newAttributes) {
const token = await getAccessToken();
const url = `https://${ORGANIZATION_DOMAIN}/api/v2/conversations/${conversationId}/participants/${participantId}`;
const payload = {
attributes: newAttributes
};
try {
const response = await fetch(url, {
method: "PATCH",
headers: {
"Authorization": `Bearer ${token}`,
"Content-Type": "application/json"
},
body: JSON.stringify(payload)
});
if (response.status === 204) {
console.log("Participant attributes updated successfully.");
return true;
}
let errorMessage = "Unknown error";
try {
const errorData = await response.json();
errorMessage = errorData.message || response.statusText;
} catch (e) {
// Response body might not be JSON
errorMessage = response.statusText;
}
switch (response.status) {
case 404:
throw new Error(`Conversation or Participant not found: ${errorMessage}`);
case 409:
throw new Error(`Conflict: Participant state changed: ${errorMessage}`);
case 401:
throw new Error(`Unauthorized: ${errorMessage}`);
case 403:
throw new Error(`Forbidden: Check OAuth scopes: ${errorMessage}`);
case 429:
throw new Error(`Rate Limit Exceeded: ${errorMessage}`);
default:
throw new Error(`HTTP Error ${response.status}: ${errorMessage}`);
}
} catch (error) {
if (error instanceof TypeError) {
throw new Error(`Network error: ${error.message}`);
}
throw error;
}
}
Step 3: Handling Edge Cases and Validation
When updating attributes mid-conversation, you must account for the asynchronous nature of the Genesys Cloud platform.
- Conversation End: If the conversation ends between the time you fetch the participant ID and the time you send the PATCH, you will receive a
409 Conflictor404 Not Found. - Attribute Size Limits: Genesys Cloud has limits on the size of participant attributes. While the exact limit can vary by contract, it is generally safe to keep the entire attributes payload under 2KB. If you attempt to store large blobs, the API will return a
400 Bad Request. - Key Naming: Keys in the attributes map are case-sensitive.
MyKeyandmykeyare distinct. It is best practice to use lowercase keys with underscores (e.g.,user_email_verified) to avoid collisions with system-reserved keys.
Verifying the Update
Since PATCH returns 204 No Content, you cannot verify the update from the response body. You must perform a subsequent GET request to confirm the attributes were persisted.
Python: Verification Helper
def verify_participant_attributes(conversation_id: str, participant_id: str) -> dict:
"""
Fetches the current attributes of a participant to verify updates.
"""
token = auth_helper.get_access_token()
url = f"{BASE_URL}/api/v2/conversations/{conversation_id}/participants/{participant_id}"
headers = {
"Authorization": f"Bearer {token}"
}
try:
response = requests.get(url, headers=headers)
response.raise_for_status()
data = response.json()
return data.get("attributes", {})
except requests.exceptions.HTTPError as e:
print(f"Verification failed: {e.response.text}")
return {}
except requests.exceptions.RequestException as e:
print(f"Network error during verification: {e}")
return {}
Complete Working Example
This Python script demonstrates the full lifecycle: authenticate, find an active conversation, update an attribute, and verify the change.
import requests
import time
import sys
# --- Configuration ---
ORGANIZATION_DOMAIN = "your-organization.genesiscloud.com"
CLIENT_ID = "your-client-id"
CLIENT_SECRET = "your-client-secret"
TARGET_USER_ID = "agent-user-id-here" # The Agent's User ID
BASE_URL = f"https://{ORGANIZATION_DOMAIN}/api/v2"
TOKEN_URL = f"https://{ORGANIZATION_DOMAIN}/oauth/token"
# --- Authentication Module ---
class GenesysAuth:
def __init__(self, client_id: str, client_secret: str, org_domain: str):
self.client_id = client_id
self.client_secret = client_secret
self.org_domain = org_domain
self.token_url = f"https://{org_domain}/oauth/token"
self.access_token = None
self.token_expiry = 0
def get_access_token(self) -> str:
if self.access_token and time.time() < self.token_expiry:
return self.access_token
payload = {
"grant_type": "client_credentials",
"client_id": self.client_id,
"client_secret": self.client_secret
}
try:
response = requests.post(self.token_url, data=payload)
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
except Exception as e:
raise RuntimeError(f"Auth failed: {e}") from e
# --- Core Logic ---
def get_active_participants(user_id: str, auth: GenesysAuth) -> list:
token = auth.get_access_token()
url = f"{BASE_URL}/api/v2/conversations/users/{user_id}/conversations"
headers = {"Authorization": f"Bearer {token}"}
params = {"state": "active", "type": "voice"}
try:
response = requests.get(url, headers=headers, params=params)
response.raise_for_status()
participants = []
for conv in response.json().get("entities", []):
for part in conv.get("participants", []):
participants.append({
"conversationId": conv["id"],
"participantId": part["id"],
"userId": part.get("userId")
})
return participants
except Exception as e:
print(f"Error fetching participants: {e}")
return []
def update_attributes(auth: GenesysAuth, conv_id: str, part_id: str, attributes: dict) -> bool:
token = auth.get_access_token()
url = f"{BASE_URL}/api/v2/conversations/{conv_id}/participants/{part_id}"
headers = {
"Authorization": f"Bearer {token}",
"Content-Type": "application/json"
}
payload = {"attributes": attributes}
try:
response = requests.patch(url, headers=headers, json=payload)
if response.status_code == 204:
return True
if response.status_code == 409:
raise RuntimeError("Conflict: Conversation state changed.")
if response.status_code == 404:
raise RuntimeError("Not Found: Invalid IDs.")
raise RuntimeError(f"Update failed: {response.status_code} - {response.text}")
except requests.exceptions.RequestException as e:
raise RuntimeError(f"Network error: {e}") from e
def verify_attributes(auth: GenesysAuth, conv_id: str, part_id: str) -> dict:
token = auth.get_access_token()
url = f"{BASE_URL}/api/v2/conversations/{conv_id}/participants/{part_id}"
headers = {"Authorization": f"Bearer {token}"}
try:
response = requests.get(url, headers=headers)
response.raise_for_status()
return response.json().get("attributes", {})
except Exception as e:
print(f"Verification failed: {e}")
return {}
# --- Execution ---
def main():
print("Initializing Genesys Cloud Auth...")
auth = GenesysAuth(CLIENT_ID, CLIENT_SECRET, ORGANIZATION_DOMAIN)
try:
# 1. Get Access Token
token = auth.get_access_token()
print("Authentication successful.")
# 2. Find Active Conversation
print(f"Searching for active conversations for user: {TARGET_USER_ID}")
participants = get_active_participants(TARGET_USER_ID, auth)
if not participants:
print("No active conversations found. Please ensure an agent is on a call.")
return
# Pick the first participant for demonstration
target = participants[0]
print(f"Found conversation: {target['conversationId']}")
print(f"Target participant: {target['participantId']}")
# 3. Define Attributes to Update
new_attrs = {
"custom_priority": "high",
"call_reason": "billing_inquiry",
"timestamp_updated": str(time.time())
}
print("Updating participant attributes...")
success = update_attributes(auth, target["conversationId"], target["participantId"], new_attrs)
if success:
print("Update sent successfully.")
# 4. Verify
print("Verifying attributes...")
current_attrs = verify_attributes(auth, target["conversationId"], target["participantId"])
print("Current Attributes:")
for k, v in current_attrs.items():
print(f" {k}: {v}")
# Check if our key exists
if current_attrs.get("custom_priority") == "high":
print("SUCCESS: Attributes verified.")
else:
print("WARNING: Attributes did not match expected values.")
except Exception as e:
print(f"Critical Error: {e}")
sys.exit(1)
if __name__ == "__main__":
main()
Common Errors & Debugging
Error: 403 Forbidden
- Cause: The OAuth client used to generate the token does not have the
conversation:participant:writescope. - Fix: Go to the Genesys Cloud Admin portal, navigate to Admin > Security > OAuth Clients, select your client, and ensure the scope
conversation:participant:writeis checked. Regenerate the token.
Error: 409 Conflict
- Cause: The conversation or participant state has changed since you last queried it. This often happens if the call dropped, the agent hung up, or the participant was transferred out of the conversation.
- Fix: Implement retry logic with exponential backoff, but also check the conversation state before retrying. If the conversation is no longer
active, do not retry.
Error: 400 Bad Request
- Cause: The JSON payload is malformed. Common mistakes include sending the attributes directly (
{"myKey": "val"}) instead of wrapping them ({"attributes": {"myKey": "val"}}), or using reserved keys. - Fix: Ensure the root object contains an
attributeskey. Check the response body for specific validation errors from Genesys Cloud.
Error: 429 Too Many Requests
- Cause: You are exceeding the rate limit for the Conversations API.
- Fix: Implement exponential backoff. Start with a 1-second delay, doubling it with each subsequent failure up to a maximum (e.g., 30 seconds).