Automating OAuth Token Refresh with Genesys Cloud Platform SDKs
What You Will Build
- You will build a resilient application that maintains a persistent connection to Genesys Cloud APIs without manual token management.
- This tutorial uses the
PureCloudPlatformClientV2Python SDK and the@genesyscloud/purecloud-platform-client-v2JavaScript SDK. - The code covers Python and JavaScript, demonstrating how the SDKs handle expiration detection and silent token refresh.
Prerequisites
- OAuth Client: A Genesys Cloud OAuth Client with the
publicorconfidentialgrant type. For server-to-server flows, useclient_credentials. - Required Scopes:
conversation:all(for testing connectivity),user:all(for user operations). Adjust based on your specific API calls. - SDK Versions:
- Python:
genesys-cloud-sdk>= 1.0.0 - JavaScript:
@genesyscloud/purecloud-platform-client-v2>= 1.0.0
- Python:
- Runtime: Python 3.8+ or Node.js 16+.
- Dependencies:
- Python:
pip install genesys-cloud-sdk - JavaScript:
npm install @genesyscloud/purecloud-platform-client-v2
- Python:
Authentication Setup
The core premise of this tutorial is that you do not need to write a background thread to monitor token expiration. The Genesys Cloud Platform SDKs intercept outgoing requests, inspect the current access token, and automatically trigger a refresh if the token is expired or near expiration.
However, you must configure the SDK correctly. The SDK needs the initial credentials to perform the first authentication and subsequent refreshes.
Python Configuration
In Python, you initialize the PureCloudPlatformClientV2 instance. You pass your client ID, client secret, and the region. The SDK creates an internal OAuthClient that manages the token lifecycle.
from genesyscloud.platform.client import PureCloudPlatformClientV2
import os
def create_genesys_client():
"""
Initializes the Genesys Cloud SDK client with automatic token refresh enabled.
"""
# Retrieve credentials from environment variables
client_id = os.getenv("GENESYS_CLIENT_ID")
client_secret = os.getenv("GENESYS_CLIENT_SECRET")
region = os.getenv("GENESYS_REGION", "us-east-1") # e.g., us-east-1, eu-west-1
if not client_id or not client_secret:
raise ValueError("GENESYS_CLIENT_ID and GENESYS_CLIENT_SECRET must be set in environment variables.")
# Initialize the platform client
# The SDK automatically handles token caching and refresh for the session
client = PureCloudPlatformClientV2(
client_id=client_id,
client_secret=client_secret,
region=region
)
return client
JavaScript Configuration
In JavaScript, you create a PlatformClient instance. Similar to Python, you provide the credentials during initialization. The SDK uses an internal mechanism to refresh tokens before they expire.
const { PureCloudPlatformClientV2 } = require("@genesyscloud/purecloud-platform-client-v2");
async function createGenesysClient() {
// Retrieve credentials from environment variables
const clientId = process.env.GENESYS_CLIENT_ID;
const clientSecret = process.env.GENESYS_CLIENT_SECRET;
const region = process.env.GENESYS_REGION || "us-east-1";
if (!clientId || !clientSecret) {
throw new Error("GENESYS_CLIENT_ID and GENESYS_CLIENT_SECRET must be set in environment variables.");
}
// Initialize the platform client
// The SDK handles the OAuth flow and token refresh automatically
const client = new PureCloudPlatformClientV2();
// Configure the client with credentials
// Note: In newer SDK versions, you typically call login or configure directly
await client.login(clientId, clientSecret);
return client;
}
Implementation
Step 1: Verify Token Refresh Behavior
To prove that the SDK handles refresh automatically, you will make two API calls. The first call establishes the session. The second call will occur after a simulated delay longer than the token’s lifetime (or you can force a refresh by invalidating the token, though the SDK is designed to prevent this scenario).
In a real production environment, tokens last for 1 hour. For this tutorial, we will simulate a long-running process.
Python: Making a Call with Automatic Refresh
The AnalyticsApi is a good candidate for testing because it often involves large payloads and longer processing times, increasing the chance of hitting a token boundary in long-running scripts.
from genesyscloud.analytics.api import AnalyticsApi
from genesyscloud.analytics.model import QueryConversationDetailsRequest
def fetch_user_details(client):
"""
Fetches the authenticated user's details.
This call triggers a token refresh if the current token is expired.
"""
analytics_api = AnalyticsApi(client)
# Define a simple query to get recent conversations
# This requires 'analytics:conversation:view' scope
request_body = QueryConversationDetailsRequest(
interval="2023-01-01T00:00:00Z/2023-01-02T00:00:00Z",
view="summary",
entity="conversation",
group_by=[],
select=["id", "type", "start_time", "end_time"]
)
try:
# The SDK checks the token here. If expired, it refreshes automatically.
response = analytics_api.post_analytics_conversations_details_query(body=request_body)
print(f"Retrieved {len(response.entities)} conversations.")
return response.entities
except Exception as e:
# Handle specific API errors
print(f"API Error: {e}")
raise
def main():
client = create_genesys_client()
# First call
print("Making first API call...")
fetch_user_details(client)
# Simulate a long-running process
# In a real scenario, you would wait for the token to expire (1 hour)
# The SDK will automatically refresh the token on the next call.
import time
print("Simulating long-running process (waiting 5 seconds)...")
time.sleep(5)
# Second call - Token is still valid, but the mechanism is ready
print("Making second API call...")
fetch_user_details(client)
if __name__ == "__main__":
main()
JavaScript: Making a Call with Automatic Refresh
In JavaScript, the SDK returns Promises. The refresh logic is executed within the promise chain before the HTTP request is sent.
async function fetchUserDetails(client) {
const analyticsApi = new PureCloudPlatformClientV2.AnalyticsApi();
// Define the query body
const requestBody = {
interval: "2023-01-01T00:00:00Z/2023-01-02T00:00:00Z",
view: "summary",
entity: "conversation",
groupBy: [],
select: ["id", "type", "start_time", "end_time"]
};
try {
// The SDK checks the token here. If expired, it refreshes automatically.
const response = await analyticsApi.postAnalyticsConversationsDetailsQuery(requestBody);
console.log(`Retrieved ${response.entities.length} conversations.`);
return response.entities;
} catch (error) {
console.error("API Error:", error);
throw error;
}
}
async function main() {
const client = await createGenesysClient();
// First call
console.log("Making first API call...");
await fetchUserDetails(client);
// Simulate a long-running process
console.log("Simulating long-running process (waiting 5 seconds)...");
await new Promise(resolve => setTimeout(resolve, 5000));
// Second call
console.log("Making second API call...");
await fetchUserDetails(client);
}
main().catch(console.error);
Step 2: Handling Concurrency and Thread Safety
A common pitfall in multi-threaded or asynchronous applications is race conditions during token refresh. If two requests fire simultaneously when the token is expired, both might attempt to refresh it. The Genesys Cloud SDKs are designed to be thread-safe and handle this gracefully.
Python: Thread Safety
The Python SDK uses a mutex lock around the refresh logic. If Thread A starts a refresh, Thread B will wait for Thread A to complete and then reuse the new token. You do not need to implement your own locking mechanism.
import threading
from genesyscloud.users.api import UsersApi
def get_my_user_info(client, thread_name):
"""
Fetches the current user's information.
This function is safe to call from multiple threads.
"""
users_api = UsersApi(client)
try:
# This call is thread-safe regarding token refresh
response = users_api.get_users_me()
print(f"[{thread_name}] Retrieved user: {response.name}")
return response
except Exception as e:
print(f"[{thread_name}] Error: {e}")
raise
def concurrent_api_calls():
"""
Demonstrates thread-safe token refresh.
"""
client = create_genesys_client()
threads = []
for i in range(5):
thread = threading.Thread(target=get_my_user_info, args=(client, f"Thread-{i}"))
threads.append(thread)
thread.start()
for thread in threads:
thread.join()
if __name__ == "__main__":
concurrent_api_calls()
JavaScript: Async Safety
In JavaScript, the event loop handles concurrency. The SDK’s internal token manager ensures that only one refresh request is active at a time. All pending promises wait for the refresh to complete.
async function getMyUserInfo(client, threadName) {
const usersApi = new PureCloudPlatformClientV2.UsersApi();
try {
// This call is safe to call from multiple async contexts
const response = await usersApi.getUsersMe();
console.log(`[${threadName}] Retrieved user: ${response.name}`);
return response;
} catch (error) {
console.error(`[${threadName}] Error:`, error);
throw error;
}
}
async function concurrentApiCalls() {
const client = await createGenesysClient();
// Create multiple concurrent promises
const promises = [];
for (let i = 0; i < 5; i++) {
promises.push(getMyUserInfo(client, `Async-${i}`));
}
// Wait for all to complete
await Promise.all(promises);
}
concurrentApiCalls().catch(console.error);
Step 3: Processing Results and Pagination
When making API calls, you often need to handle pagination. The SDK provides helper methods to simplify this, but you must ensure the token remains valid throughout the pagination process. Since the SDK refreshes tokens automatically, you can safely iterate through pages without worrying about expiration in the middle of a loop.
Python: Pagination with Automatic Refresh
def fetch_all_users(client, page_size=25):
"""
Fetches all users using pagination.
The SDK handles token refresh between pages if necessary.
"""
users_api = UsersApi(client)
all_users = []
try:
# Initial call
response = users_api.get_users(page_size=page_size)
all_users.extend(response.entities)
# Continue pagination while there are more pages
while response.pagination and response.pagination.next_page_size > 0:
# The SDK checks the token here before each request
response = users_api.get_users(
page_size=page_size,
after_id=response.pagination.next_page_id
)
all_users.extend(response.entities)
except Exception as e:
print(f"Error fetching users: {e}")
raise
return all_users
JavaScript: Pagination with Automatic Refresh
async function fetchAllUsers(client, pageSize = 25) {
const usersApi = new PureCloudPlatformClientV2.UsersApi();
let allUsers = [];
let response;
try {
// Initial call
response = await usersApi.getUsers({ pageSize });
allUsers = [...allUsers, ...response.entities];
// Continue pagination while there are more pages
while (response.pagination && response.pagination.nextPageSize > 0) {
// The SDK checks the token here before each request
response = await usersApi.getUsers({
pageSize,
afterId: response.pagination.nextPageId
});
allUsers = [...allUsers, ...response.entities];
}
} catch (error) {
console.error("Error fetching users:", error);
throw error;
}
return allUsers;
}
Complete Working Example
Below is a complete, runnable Python script that demonstrates the full flow: initialization, automatic token refresh, and error handling.
import os
import time
import threading
from genesyscloud.platform.client import PureCloudPlatformClientV2
from genesyscloud.users.api import UsersApi
from genesyscloud.analytics.api import AnalyticsApi
from genesyscloud.analytics.model import QueryConversationDetailsRequest
def create_genesys_client():
"""
Initializes the Genesys Cloud SDK client with automatic token refresh enabled.
"""
client_id = os.getenv("GENESYS_CLIENT_ID")
client_secret = os.getenv("GENESYS_CLIENT_SECRET")
region = os.getenv("GENESYS_REGION", "us-east-1")
if not client_id or not client_secret:
raise ValueError("GENESYS_CLIENT_ID and GENESYS_CLIENT_SECRET must be set in environment variables.")
client = PureCloudPlatformClientV2(
client_id=client_id,
client_secret=client_secret,
region=region
)
return client
def get_current_user(client):
"""
Fetches the authenticated user's details.
"""
users_api = UsersApi(client)
try:
response = users_api.get_users_me()
return response
except Exception as e:
print(f"Error fetching user: {e}")
raise
def fetch_conversations(client):
"""
Fetches recent conversations.
"""
analytics_api = AnalyticsApi(client)
request_body = QueryConversationDetailsRequest(
interval="2023-01-01T00:00:00Z/2023-01-02T00:00:00Z",
view="summary",
entity="conversation",
group_by=[],
select=["id", "type"]
)
try:
response = analytics_api.post_analytics_conversations_details_query(body=request_body)
return response.entities
except Exception as e:
print(f"Error fetching conversations: {e}")
raise
def main():
# 1. Initialize the client
print("Initializing Genesys Cloud Client...")
client = create_genesys_client()
# 2. First API Call
print("\n--- First API Call ---")
user = get_current_user(client)
print(f"Authenticated as: {user.name}")
# 3. Simulate Long-Running Process
print("\n--- Simulating Long-Running Process ---")
print("Waiting 10 seconds to simulate processing time...")
time.sleep(10)
# 4. Second API Call (Token Refresh Check)
print("\n--- Second API Call ---")
print("Making another API call to verify token validity...")
user = get_current_user(client)
print(f"Still authenticated as: {user.name}")
# 5. Concurrent Calls
print("\n--- Concurrent API Calls ---")
threads = []
for i in range(3):
thread = threading.Thread(target=fetch_conversations, args=(client,))
threads.append(thread)
thread.start()
for thread in threads:
thread.join()
print("All concurrent calls completed.")
if __name__ == "__main__":
main()
Common Errors & Debugging
Error: 401 Unauthorized
What causes it: The OAuth token has expired, and the SDK failed to refresh it. This usually happens if the client secret is incorrect, the client ID is invalid, or the OAuth client is disabled in Genesys Cloud.
How to fix it:
- Verify your
GENESYS_CLIENT_IDandGENESYS_CLIENT_SECRETare correct. - Check the OAuth client status in the Genesys Cloud Admin console.
- Ensure the OAuth client has the necessary scopes.
Code showing the fix:
from genesyscloud.platform.client import PureCloudPlatformClientV2
from genesyscloud.platform.api import PlatformApi
def check_oauth_status(client):
"""
Checks the OAuth token status.
"""
platform_api = PlatformApi(client)
try:
# This call will fail with 401 if the token is invalid and cannot be refreshed
response = platform_api.get_platform_status()
print("OAuth Token is valid.")
except Exception as e:
if "401" in str(e):
print("OAuth Token is invalid or expired. Check credentials.")
else:
print(f"Unexpected error: {e}")
# Usage
client = create_genesys_client()
check_oauth_status(client)
Error: 429 Too Many Requests
What causes it: You have exceeded the API rate limit. The SDK does not automatically retry 429 errors, but it does not interfere with them either.
How to fix it: Implement exponential backoff in your code.
Code showing the fix:
import time
def make_api_call_with_retry(client, func, *args, max_retries=3):
"""
Makes an API call with exponential backoff for 429 errors.
"""
for attempt in range(max_retries):
try:
return func(*args)
except Exception as e:
if "429" in str(e):
wait_time = 2 ** attempt
print(f"Rate limited. Waiting {wait_time} seconds...")
time.sleep(wait_time)
else:
raise
# Usage
users_api = UsersApi(client)
user = make_api_call_with_retry(client, users_api.get_users_me)
Error: 403 Forbidden
What causes it: The OAuth client does not have the required scope for the API call.
How to fix it: Add the required scope to the OAuth client in the Genesys Cloud Admin console.
Code showing the fix:
# Ensure the OAuth client has the 'user:all' scope
# This is configured in the Genesys Cloud Admin console, not in code.
# The SDK will return a 403 error if the scope is missing.