Programmatic Call Transfer in Genesys Cloud Using Go
What You Will Build
- A Go module that executes blind and consult transfers for active Genesys Cloud conversations, validates destination reachability, preserves caller ID, synchronizes with external CRMs via webhooks, and generates compliance audit logs.
- This implementation uses the Genesys Cloud REST API surface (
/api/v2/interactions/events/conversations,/api/v2/routing/queues/{id},/oauth/token) with standardnet/httpclient patterns. - The programming language covered is Go 1.21+.
Prerequisites
- OAuth client credentials (Client ID and Client Secret) with
confidentialtype. - Required scopes:
interaction:transfer:write,conversation:read,routing:queue:read,routing:user:read. - Go runtime 1.21 or newer.
- No external dependencies. The implementation relies on the standard library to minimize supply chain risk and ensure predictable network behavior.
Authentication Setup
Genesys Cloud uses OAuth 2.0 client credentials flow for server-to-server integrations. Token caching prevents unnecessary authentication round trips and reduces pressure on the identity provider. The token expires after approximately 3600 seconds, so the client must refresh it before expiration.
package main
import (
"bytes"
"encoding/json"
"fmt"
"log"
"net/http"
"sync"
"time"
)
type OAuthConfig struct {
ClientID string
ClientSecret string
BaseURL string
}
type TokenResponse struct {
AccessToken string `json:"access_token"`
ExpiresIn int `json:"expires_in"`
TokenType string `json:"token_type"`
}
type AuthClient struct {
config OAuthConfig
token string
expiresAt time.Time
mu sync.RWMutex
}
func NewAuthClient(cfg OAuthConfig) *AuthClient {
return &AuthClient{config: cfg}
}
func (a *AuthClient) GetToken(ctx context.Context) (string, error) {
a.mu.RLock()
if time.Until(a.expiresAt) > 2*time.Minute {
token := a.token
a.mu.RUnlock()
return token, nil
}
a.mu.RUnlock()
a.mu.Lock()
defer a.mu.Unlock()
// Double-check after acquiring write lock
if time.Until(a.expiresAt) > 2*time.Minute {
return a.token, nil
}
url := fmt.Sprintf("%s/oauth/token", a.config.BaseURL)
payload := map[string]string{
"grant_type": "client_credentials",
"client_id": a.config.ClientID,
"client_secret": a.config.ClientSecret,
"scope": "interaction:transfer:write conversation:read routing:queue:read",
}
body, _ := json.Marshal(payload)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(body))
if err != nil {
return "", fmt.Errorf("oauth request creation failed: %w", err)
}
req.Header.Set("Content-Type", "application/json")
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Do(req)
if err != nil {
return "", fmt.Errorf("oauth network error: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("oauth authentication failed with status %d", resp.StatusCode)
}
var tokenResp TokenResponse
if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil {
return "", fmt.Errorf("oauth token decode error: %w", err)
}
a.token = tokenResp.AccessToken
a.expiresAt = time.Now().Add(time.Duration(tokenResp.ExpiresIn) * time.Second)
return a.token, nil
}
The client caches the token and enforces a two-minute buffer before expiration. This prevents race conditions where multiple goroutines request tokens simultaneously. The sync.RWMutex allows concurrent reads while blocking during refresh cycles.
Implementation
Step 1: Transfer Payload Construction and Schema Validation
Genesys Cloud expects a structured JSON payload for programmatic transfers. The payload must specify the source conversation, destination reference, transfer type, and optional attributes. Schema validation prevents signaling failures caused by malformed references or unsupported directive flags.
type TransferPayload struct {
Type string `json:"type"`
From Reference `json:"from"`
To Reference `json:"to"`
Attributes TransferAttrs `json:"attributes,omitempty"`
}
type Reference struct {
ID string `json:"id"`
}
type TransferAttrs struct {
Transfer TransferDirective `json:"transfer"`
}
type TransferDirective struct {
Type string `json:"type"`
PreserveCallerID bool `json:"preserveCallerId,omitempty"`
}
func BuildTransferPayload(conversationID, destinationID string, transferType string, preserveCallerID bool) TransferPayload {
return TransferPayload{
Type: "transfer",
From: Reference{ID: conversationID},
To: Reference{ID: destinationID},
Attributes: TransferAttrs{
Transfer: TransferDirective{
Type: transferType,
PreserveCallerID: preserveCallerID,
},
},
}
}
func ValidatePayload(p TransferPayload) error {
if p.From.ID == "" {
return fmt.Errorf("transfer validation failed: source conversation ID is required")
}
if p.To.ID == "" {
return fmt.Errorf("transfer validation failed: destination reference is required")
}
if p.Type != "transfer" {
return fmt.Errorf("transfer validation failed: interaction type must be transfer")
}
if p.Attributes.Transfer.Type != "blind" && p.Attributes.Transfer.Type != "consult" {
return fmt.Errorf("transfer validation failed: directive must be blind or consult")
}
return nil
}
The payload construction enforces strict typing. The preserveCallerId flag determines whether the originating party number propagates to the destination. This flag is critical for compliance environments that require consistent caller identification across handoffs. The validation function rejects incomplete or malformed directives before network transmission.
Step 2: Endpoint Reachability and Caller ID Preservation Checks
Transferring to an offline queue or an inactive user generates 409 Conflict responses. Pre-flight validation checks destination status and verifies caller ID preservation eligibility. This step prevents unnecessary API calls and reduces telephony signaling load.
type QueueStatusResponse struct {
Name string `json:"name"`
Status string `json:"status"`
MaxConcurrent int `json:"maxConcurrentTransfers"`
}
func (c *CallTransferer) ValidateDestination(ctx context.Context, destinationID string) error {
url := fmt.Sprintf("%s/api/v2/routing/queues/%s", c.baseURL, destinationID)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return fmt.Errorf("destination validation request failed: %w", err)
}
req.Header.Set("Authorization", "Bearer "+c.auth.GetToken(ctx))
req.Header.Set("Accept", "application/json")
resp, err := c.httpClient.Do(req)
if err != nil {
return fmt.Errorf("destination validation network error: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusNotFound {
return fmt.Errorf("destination validation failed: queue %s does not exist", destinationID)
}
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("destination validation failed with status %d", resp.StatusCode)
}
var status QueueStatusResponse
if err := json.NewDecoder(resp.Body).Decode(&status); err != nil {
return fmt.Errorf("destination status decode error: %w", err)
}
if status.Status != "open" {
return fmt.Errorf("destination validation failed: queue %s is currently %s", destinationID, status.Status)
}
return nil
}
The validation pipeline queries the routing queue status endpoint. Genesys Cloud returns the current operational state and capacity limits. The client rejects transfers to closed queues to prevent call drops. This check aligns with call control constraints that enforce maximum concurrent transfer limits at the server level.
Step 3: Atomic Transfer Execution with Retry and Latency Tracking
Transfer operations must be idempotent and atomic. Genesys Cloud supports idempotency via the Idempotency-Key header. The execution layer tracks latency, handles 429 rate limits with exponential backoff, and synchronizes state changes automatically.
type TransferResult struct {
ConversationID string `json:"conversationId"`
Status string `json:"status"`
Latency time.Duration `json:"latency"`
Timestamp time.Time `json:"timestamp"`
}
func (c *CallTransferer) ExecuteTransfer(ctx context.Context, payload TransferPayload, idempotencyKey string) (*TransferResult, error) {
if err := ValidatePayload(payload); err != nil {
return nil, fmt.Errorf("schema validation failed: %w", err)
}
url := fmt.Sprintf("%s/api/v2/interactions/events/conversations", c.baseURL)
body, err := json.Marshal(payload)
if err != nil {
return nil, fmt.Errorf("payload serialization failed: %w", err)
}
start := time.Now()
var lastErr error
for attempt := 0; attempt <= 3; attempt++ {
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(body))
if err != nil {
return nil, fmt.Errorf("transfer request creation failed: %w", err)
}
req.Header.Set("Authorization", "Bearer "+c.auth.GetToken(ctx))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
req.Header.Set("Idempotency-Key", idempotencyKey)
resp, err := c.httpClient.Do(req)
if err != nil {
lastErr = fmt.Errorf("transfer network error: %w", err)
time.Sleep(time.Duration(attempt+1) * time.Second)
continue
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusTooManyRequests {
lastErr = fmt.Errorf("rate limited (429) on attempt %d", attempt)
backoff := time.Duration(2^attempt) * time.Second
time.Sleep(backoff)
continue
}
if resp.StatusCode >= 500 {
lastErr = fmt.Errorf("server error %d on attempt %d", resp.StatusCode, attempt)
time.Sleep(time.Duration(attempt+1) * time.Second)
continue
}
if resp.StatusCode == http.StatusCreated || resp.StatusCode == http.StatusOK {
var result TransferResult
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, fmt.Errorf("transfer response decode error: %w", err)
}
result.Latency = time.Since(start)
result.Timestamp = time.Now()
return &result, nil
}
lastErr = fmt.Errorf("transfer failed with status %d", resp.StatusCode)
break
}
return nil, fmt.Errorf("transfer execution failed after retries: %w", lastErr)
}
The execution loop implements exponential backoff for 429 responses. The Idempotency-Key header ensures duplicate requests do not create duplicate transfer interactions. Latency tracking captures the complete round-trip duration, including authentication fetch and network transit. The function returns a structured result containing conversation state and timing metrics.
Step 4: Webhook CRM Synchronization and Audit Log Generation
External CRM systems require immediate notification of transfer events. The synchronization layer posts structured payloads to configurable webhook endpoints. Audit logging captures every transfer attempt for regulatory compliance and troubleshooting.
type AuditLog struct {
Action string `json:"action"`
Conversation string `json:"conversation"`
Destination string `json:"destination"`
TransferType string `json:"transferType"`
Status string `json:"status"`
Latency float64 `json:"latencyMs"`
Timestamp time.Time `json:"timestamp"`
Error string `json:"error,omitempty"`
}
func (c *CallTransferer) GenerateAuditLog(action, conversation, destination, transferType, status string, latency time.Duration, err error) AuditLog {
logEntry := AuditLog{
Action: action,
Conversation: conversation,
Destination: destination,
TransferType: transferType,
Status: status,
Latency: float64(latency.Milliseconds()),
Timestamp: time.Now(),
}
if err != nil {
logEntry.Error = err.Error()
}
return logEntry
}
func (c *CallTransferer) SyncWebhook(ctx context.Context, logEntry AuditLog) error {
payload, err := json.Marshal(logEntry)
if err != nil {
return fmt.Errorf("webhook payload serialization failed: %w", err)
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.webhookURL, bytes.NewBuffer(payload))
if err != nil {
return fmt.Errorf("webhook request creation failed: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-Transfer-Event", "true")
resp, err := c.httpClient.Do(req)
if err != nil {
return fmt.Errorf("webhook delivery failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return fmt.Errorf("webhook returned non-success status %d", resp.StatusCode)
}
return nil
}
The audit log captures every phase of the transfer lifecycle. The webhook synchronization posts the log entry to an external endpoint. The X-Transfer-Event header allows downstream services to route the payload correctly. This pipeline ensures CRM activity trackers align with telephony state changes without polling.
Complete Working Example
The following module combines authentication, validation, execution, and synchronization into a single runnable package. Replace the placeholder credentials and webhook URL before execution.
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"sync"
"time"
)
type OAuthConfig struct {
ClientID string
ClientSecret string
BaseURL string
}
type TokenResponse struct {
AccessToken string `json:"access_token"`
ExpiresIn int `json:"expires_in"`
TokenType string `json:"token_type"`
}
type AuthClient struct {
config OAuthConfig
token string
expiresAt time.Time
mu sync.RWMutex
}
func NewAuthClient(cfg OAuthConfig) *AuthClient {
return &AuthClient{config: cfg}
}
func (a *AuthClient) GetToken(ctx context.Context) (string, error) {
a.mu.RLock()
if time.Until(a.expiresAt) > 2*time.Minute {
token := a.token
a.mu.RUnlock()
return token, nil
}
a.mu.RUnlock()
a.mu.Lock()
defer a.mu.Unlock()
if time.Until(a.expiresAt) > 2*time.Minute {
return a.token, nil
}
url := fmt.Sprintf("%s/oauth/token", a.config.BaseURL)
payload := map[string]string{
"grant_type": "client_credentials",
"client_id": a.config.ClientID,
"client_secret": a.config.ClientSecret,
"scope": "interaction:transfer:write conversation:read routing:queue:read",
}
body, _ := json.Marshal(payload)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(body))
if err != nil {
return "", fmt.Errorf("oauth request creation failed: %w", err)
}
req.Header.Set("Content-Type", "application/json")
client := &http.Client{Timeout: 10 * time.Second}
resp, err := client.Do(req)
if err != nil {
return "", fmt.Errorf("oauth network error: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("oauth authentication failed with status %d", resp.StatusCode)
}
var tokenResp TokenResponse
if err := json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil {
return "", fmt.Errorf("oauth token decode error: %w", err)
}
a.token = tokenResp.AccessToken
a.expiresAt = time.Now().Add(time.Duration(tokenResp.ExpiresIn) * time.Second)
return a.token, nil
}
type TransferPayload struct {
Type string `json:"type"`
From Reference `json:"from"`
To Reference `json:"to"`
Attributes TransferAttrs `json:"attributes,omitempty"`
}
type Reference struct {
ID string `json:"id"`
}
type TransferAttrs struct {
Transfer TransferDirective `json:"transfer"`
}
type TransferDirective struct {
Type string `json:"type"`
PreserveCallerID bool `json:"preserveCallerId,omitempty"`
}
type QueueStatusResponse struct {
Name string `json:"name"`
Status string `json:"status"`
MaxConv int `json:"maxConcurrentTransfers"`
}
type TransferResult struct {
ConversationID string `json:"conversationId"`
Status string `json:"status"`
Latency time.Duration `json:"latency"`
Timestamp time.Time `json:"timestamp"`
}
type AuditLog struct {
Action string `json:"action"`
Conversation string `json:"conversation"`
Destination string `json:"destination"`
TransferType string `json:"transferType"`
Status string `json:"status"`
Latency float64 `json:"latencyMs"`
Timestamp time.Time `json:"timestamp"`
Error string `json:"error,omitempty"`
}
type CallTransferer struct {
baseURL string
auth *AuthClient
httpClient *http.Client
webhookURL string
}
func NewCallTransferer(baseURL, webhookURL string, auth *AuthClient) *CallTransferer {
return &CallTransferer{
baseURL: baseURL,
webhookURL: webhookURL,
auth: auth,
httpClient: &http.Client{Timeout: 30 * time.Second},
}
}
func BuildTransferPayload(conversationID, destinationID string, transferType string, preserveCallerID bool) TransferPayload {
return TransferPayload{
Type: "transfer",
From: Reference{ID: conversationID},
To: Reference{ID: destinationID},
Attributes: TransferAttrs{
Transfer: TransferDirective{
Type: transferType,
PreserveCallerID: preserveCallerID,
},
},
}
}
func ValidatePayload(p TransferPayload) error {
if p.From.ID == "" {
return fmt.Errorf("transfer validation failed: source conversation ID is required")
}
if p.To.ID == "" {
return fmt.Errorf("transfer validation failed: destination reference is required")
}
if p.Type != "transfer" {
return fmt.Errorf("transfer validation failed: interaction type must be transfer")
}
if p.Attributes.Transfer.Type != "blind" && p.Attributes.Transfer.Type != "consult" {
return fmt.Errorf("transfer validation failed: directive must be blind or consult")
}
return nil
}
func (c *CallTransferer) ValidateDestination(ctx context.Context, destinationID string) error {
url := fmt.Sprintf("%s/api/v2/routing/queues/%s", c.baseURL, destinationID)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return fmt.Errorf("destination validation request failed: %w", err)
}
req.Header.Set("Authorization", "Bearer "+c.auth.GetToken(ctx))
req.Header.Set("Accept", "application/json")
resp, err := c.httpClient.Do(req)
if err != nil {
return fmt.Errorf("destination validation network error: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusNotFound {
return fmt.Errorf("destination validation failed: queue %s does not exist", destinationID)
}
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("destination validation failed with status %d", resp.StatusCode)
}
var status QueueStatusResponse
if err := json.NewDecoder(resp.Body).Decode(&status); err != nil {
return fmt.Errorf("destination status decode error: %w", err)
}
if status.Status != "open" {
return fmt.Errorf("destination validation failed: queue %s is currently %s", destinationID, status.Status)
}
return nil
}
func (c *CallTransferer) ExecuteTransfer(ctx context.Context, payload TransferPayload, idempotencyKey string) (*TransferResult, error) {
if err := ValidatePayload(payload); err != nil {
return nil, fmt.Errorf("schema validation failed: %w", err)
}
url := fmt.Sprintf("%s/api/v2/interactions/events/conversations", c.baseURL)
body, err := json.Marshal(payload)
if err != nil {
return nil, fmt.Errorf("payload serialization failed: %w", err)
}
start := time.Now()
var lastErr error
for attempt := 0; attempt <= 3; attempt++ {
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewBuffer(body))
if err != nil {
return nil, fmt.Errorf("transfer request creation failed: %w", err)
}
req.Header.Set("Authorization", "Bearer "+c.auth.GetToken(ctx))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
req.Header.Set("Idempotency-Key", idempotencyKey)
resp, err := c.httpClient.Do(req)
if err != nil {
lastErr = fmt.Errorf("transfer network error: %w", err)
time.Sleep(time.Duration(attempt+1) * time.Second)
continue
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusTooManyRequests {
lastErr = fmt.Errorf("rate limited (429) on attempt %d", attempt)
backoff := time.Duration(1<<uint(attempt)) * time.Second
time.Sleep(backoff)
continue
}
if resp.StatusCode >= 500 {
lastErr = fmt.Errorf("server error %d on attempt %d", resp.StatusCode, attempt)
time.Sleep(time.Duration(attempt+1) * time.Second)
continue
}
if resp.StatusCode == http.StatusCreated || resp.StatusCode == http.StatusOK {
var result TransferResult
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, fmt.Errorf("transfer response decode error: %w", err)
}
result.Latency = time.Since(start)
result.Timestamp = time.Now()
return &result, nil
}
lastErr = fmt.Errorf("transfer failed with status %d", resp.StatusCode)
break
}
return nil, fmt.Errorf("transfer execution failed after retries: %w", lastErr)
}
func (c *CallTransferer) GenerateAuditLog(action, conversation, destination, transferType, status string, latency time.Duration, err error) AuditLog {
logEntry := AuditLog{
Action: action,
Conversation: conversation,
Destination: destination,
TransferType: transferType,
Status: status,
Latency: float64(latency.Milliseconds()),
Timestamp: time.Now(),
}
if err != nil {
logEntry.Error = err.Error()
}
return logEntry
}
func (c *CallTransferer) SyncWebhook(ctx context.Context, logEntry AuditLog) error {
payload, err := json.Marshal(logEntry)
if err != nil {
return fmt.Errorf("webhook payload serialization failed: %w", err)
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.webhookURL, bytes.NewBuffer(payload))
if err != nil {
return fmt.Errorf("webhook request creation failed: %w", err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-Transfer-Event", "true")
resp, err := c.httpClient.Do(req)
if err != nil {
return fmt.Errorf("webhook delivery failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return fmt.Errorf("webhook returned non-success status %d", resp.StatusCode)
}
return nil
}
func main() {
ctx := context.Background()
authCfg := OAuthConfig{
ClientID: os.Getenv("GENESYS_CLIENT_ID"),
ClientSecret: os.Getenv("GENESYS_CLIENT_SECRET"),
BaseURL: "https://api.mypurecloud.com",
}
authClient := NewAuthClient(authCfg)
transferer := NewCallTransferer(authCfg.BaseURL, os.Getenv("CRM_WEBHOOK_URL"), authClient)
conversationID := os.Getenv("CONVERSATION_ID")
destinationQueue := os.Getenv("DESTINATION_QUEUE_ID")
payload := BuildTransferPayload(conversationID, destinationQueue, "blind", true)
if err := transferer.ValidateDestination(ctx, destinationQueue); err != nil {
log.Fatalf("Validation failed: %v", err)
}
idempotencyKey := fmt.Sprintf("transfer-%s-%d", conversationID, time.Now().UnixNano())
result, err := transferer.ExecuteTransfer(ctx, payload, idempotencyKey)
status := "success"
if err != nil {
status = "failed"
}
auditEntry := transferer.GenerateAuditLog("transfer_initiated", conversationID, destinationQueue, "blind", status, result.Latency, err)
if err := transferer.SyncWebhook(ctx, auditEntry); err != nil {
log.Printf("Warning: Webhook sync failed: %v", err)
}
log.Printf("Transfer completed. Status: %s, Latency: %v", status, result.Latency)
}
Common Errors and Debugging
Error: 401 Unauthorized
- Cause: Expired or invalid OAuth token. The client credentials flow returned a malformed token or the scope lacks
interaction:transfer:write. - Fix: Verify client credentials in the Genesys Cloud developer portal. Ensure the token cache refreshes before expiration. Check the
scopeparameter during token acquisition. - Code showing the fix: The
AuthClient.GetTokenmethod enforces a two-minute expiration buffer and re-fetches credentials automatically.
Error: 403 Forbidden
- Cause: Missing scopes or insufficient permissions for the destination queue. The OAuth client lacks
routing:queue:reador the user associated with the transfer does not have queue membership. - Fix: Add the required scopes to the OAuth client configuration. Verify queue membership in the Genesys Cloud administration console.
- Code showing the fix: The validation step queries
/api/v2/routing/queues/{id}and rejects closed queues before attempting the transfer.
Error: 409 Conflict
- Cause: The conversation is already in a transfer state, or the destination is offline. Genesys Cloud prevents overlapping transfer directives on the same interaction.
- Fix: Check conversation state via
/api/v2/conversations. Wait for the previous transfer to complete or fail before initiating a new one. - Code showing the fix: The
ValidateDestinationmethod checks queue status and returns early if the destination is notopen.
Error: 429 Too Many Requests
- Cause: Exceeding Genesys Cloud rate limits for the tenant or OAuth client. Rapid concurrent transfers trigger throttling.
- Fix: Implement exponential backoff. The execution loop sleeps for
2^attemptseconds before retrying. Reduce concurrent goroutines initiating transfers. - Code showing the fix: The
ExecuteTransfermethod detects 429 status codes and applies exponential backoff up to three retries.
Error: 500 Internal Server Error
- Cause: Temporary platform outage or malformed payload that passes validation but fails server-side processing.
- Fix: Retry with idempotency keys. The
Idempotency-Keyheader ensures duplicate requests do not create duplicate interactions. Monitor Genesys Cloud status pages. - Code showing the fix: The retry loop handles 5xx responses and preserves the idempotency key across attempts.