Detecting Genesys Cloud Telephony API fax tones in media streams via Telephony API with Java
What You Will Build
A Java service that connects to a Genesys Cloud Telephony WebSocket, sends a fax tone detection directive with stream reference, tone matrix, and analyze parameters, processes binary tone events, triggers automatic redirects, tracks latency, validates signal clarity, configures synchronization webhooks, and generates structured audit logs. This tutorial uses the Genesys Cloud CX Telephony API and Webhook API with the official Java SDK. The implementation covers Java 17+.
Prerequisites
- Genesys Cloud CX OAuth 2.0 Client Credentials or JWT grant type
- Required OAuth scopes:
telephony:session:write,telephony:session:read,webhook:write,webhook:read,webhook:read:all - Genesys Cloud Java SDK version 2024.3.0 or newer (
com.mypurecloud.api.client:platform-client) - Java Development Kit 17 or newer
- External dependencies:
org.slf4j:slf4j-api:2.0.9,com.fasterxml.jackson.core:jackson-databind:2.15.2 - Active Genesys Cloud Telephony session capable of media streaming (typically initiated via a routing queue or direct call)
Authentication Setup
The Genesys Cloud Java SDK provides a TokenClient that handles OAuth 2.0 token acquisition and automatic refresh. You must configure the base path, client ID, client secret, and the required scopes. The SDK caches the access token and requests a new one before expiration.
import com.mypurecloud.api.client.ApiClient;
import com.mypurecloud.api.client.auth.TokenClient;
import java.util.Set;
public class GenesysAuth {
public static ApiClient initializeApiClient(String clientId, String clientSecret, String basePath) throws Exception {
ApiClient apiClient = new ApiClient();
apiClient.setBasePath(basePath);
TokenClient tokenClient = new TokenClient(
clientId,
clientSecret,
Set.of("telephony:session:write", "telephony:session:read", "webhook:write", "webhook:read")
);
apiClient.setAccessTokenGenerator(tokenClient::getAccessToken);
// Validate connectivity by fetching a lightweight endpoint
apiClient.getApiClientConfiguration().setHttpClient(apiClient.getHttpClient());
return apiClient;
}
}
The setAccessTokenGenerator method ensures every subsequent API call and WebSocket handshake attaches a valid bearer token. If the token expires, the SDK automatically triggers a refresh before the next request.
Implementation
Step 1: Initialize Telephony Session and Construct Detect Payload
The Telephony API streams media over a dedicated WebSocket connection. You must open a session, obtain a stream reference, and construct a DetectRequest that specifies the tone matrix and analyze directive. The server enforces maximum sample rate limits. Fax tone detection requires an 8 kHz sample rate to align with PSTN fax standards.
import com.mypurecloud.api.client.TelephonyClient;
import com.mypurecloud.api.client.model.TelephonySession;
import com.mypurecloud.api.client.model.TelephonySessionRequest;
import com.mypurecloud.api.client.model.TelephonyDetectRequest;
import com.mypurecloud.api.client.model.ToneMatrix;
import com.mypurecloud.api.client.model.Tone;
import com.mypurecloud.api.client.model.AnalyzeDirective;
import java.util.List;
public class FaxToneDetector {
private final TelephonyClient telephonyClient;
private TelephonySession session;
private String streamReference;
public FaxToneDetector(ApiClient apiClient) {
this.telephonyClient = new TelephonyClient(apiClient);
}
public void initializeSession() throws Exception {
TelephonySessionRequest request = new TelephonySessionRequest()
.applicationId("fax-tone-detector")
.mediaType("audio")
.sampleRate(8000); // Mandatory for fax tone accuracy
session = telephonyClient.openSession(request);
streamReference = session.getStreamReference();
System.out.println("Session opened. Stream reference: " + streamReference);
}
public void sendDetectDirective() throws Exception {
List<Tone> faxTones = List.of(
new Tone().type("CED").minDuration(100).maxDuration(2000),
new Tone().type("CNG").minDuration(100).maxDuration(2000),
new Tone().type("DIS").minDuration(100).maxDuration(2000)
);
ToneMatrix matrix = new ToneMatrix().tones(faxTones);
AnalyzeDirective analyze = new AnalyzeDirective()
.enabled(true)
.maxSampleRate(8000)
.cepstralAnalysis(true);
TelephonyDetectRequest detectRequest = new TelephonyDetectRequest()
.streamReference(streamReference)
.toneMatrix(matrix)
.analyze(analyze)
.redirectOnDetect(true); // Triggers automatic redirect upon match
telephonyClient.sendDetectRequest(detectRequest);
System.out.println("Detect directive sent with tone matrix and analyze parameters.");
}
}
The maxSampleRate(8000) parameter prevents the server from downsampling incorrectly, which would distort fax tone frequencies. The cepstralAnalysis(true) flag instructs the Genesys Cloud media engine to perform cepstral coefficient calculation and frequency match evaluation before returning a result. The redirectOnDetect(true) flag prepares the session to accept a redirect operation immediately after a tone match.
Step 2: Process Binary Frames and Validate Tone Events
The Telephony WebSocket transmits binary frames that the SDK deserializes into JSON objects. You must register a message listener to capture toneDetected events. The response contains frequency, duration, confidence, and signal level metrics. You must validate noise floor and signal clarity before trusting the classification.
import com.mypurecloud.api.client.TelephonyClient;
import com.mypurecloud.api.client.model.TelephonyMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.time.Instant;
public class ToneEventHandler {
private static final Logger logger = LoggerFactory.getLogger(ToneEventHandler.class);
private final TelephonyClient telephonyClient;
private Instant detectTimestamp;
public ToneEventHandler(TelephonyClient telephonyClient) {
this.telephonyClient = telephonyClient;
}
public void registerListener() {
telephonyClient.addMessageListener(message -> {
if (message.getType() != null && message.getType().equals("toneDetected")) {
processToneDetected(message);
}
});
}
public void setDetectTimestamp(Instant timestamp) {
this.detectTimestamp = timestamp;
}
private void processToneDetected(TelephonyMessage message) {
String toneType = message.getPayload().getString("toneType");
double confidence = message.getPayload().getDouble("confidence");
double signalLevel = message.getPayload().getDouble("signalLevel");
double frequency = message.getPayload().getDouble("frequency");
long duration = message.getPayload().getLong("duration");
// Signal clarity verification pipeline
if (confidence < 0.75 || signalLevel < -40.0) {
logger.warn("Tone detection rejected due to low confidence or high noise floor. Confidence: {}, Signal: {}", confidence, signalLevel);
return;
}
// Latency tracking
long latencyMs = 0;
if (detectTimestamp != null) {
latencyMs = java.time.Duration.between(detectTimestamp, Instant.now()).toMillis();
}
logger.info("Fax tone detected. Type: {}, Frequency: {} Hz, Duration: {} ms, Latency: {} ms",
toneType, frequency, duration, latencyMs);
// Trigger safe detect iteration redirect
triggerRedirect(toneType);
}
private void triggerRedirect(String toneType) {
// Implementation in Step 3
}
}
The Genesys Cloud server performs the atomic cepstral coefficient calculation and frequency match evaluation. The client receives the validated result. The noise floor check (signalLevel < -40.0) prevents misrouting during high ambient noise or line degradation. The latency calculation tracks the time between directive submission and server response, which is critical for monitoring Telephony API scaling performance.
Step 3: Handle Redirects, Latency Tracking and Audit Logging
When a fax tone matches, you must send a redirect operation to route the media stream to a fax gateway or IVR node. You must also log the event for telephony governance. The audit log captures stream reference, tone type, confidence, latency, and validation status.
import com.mypurecloud.api.client.model.TelephonyRedirectRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.time.Instant;
import java.time.format.DateTimeFormatter;
public class RedirectAndAuditHandler {
private static final Logger auditLogger = LoggerFactory.getLogger("telephony.audit");
private static final Logger appLogger = LoggerFactory.getLogger(RedirectAndAuditHandler.class);
private final TelephonyClient telephonyClient;
private final String streamReference;
public RedirectAndAuditHandler(TelephonyClient telephonyClient, String streamReference) {
this.telephonyClient = telephonyClient;
this.streamReference = streamReference;
}
public void triggerRedirect(String toneType) {
try {
TelephonyRedirectRequest redirectRequest = new TelephonyRedirectRequest()
.streamReference(streamReference)
.targetUri("wss://api.mypurecloud.com/api/v2/telephony/sessions/fax-gateway")
.reason("fax-tone-detected-" + toneType);
telephonyClient.sendRedirectRequest(redirectRequest);
appLogger.info("Redirect triggered successfully for tone: {}", toneType);
} catch (Exception e) {
appLogger.error("Redirect failed for tone {}. Error: {}", toneType, e.getMessage());
}
}
public void writeAuditLog(String toneType, double confidence, long latencyMs, boolean passedValidation) {
String timestamp = Instant.now().format(DateTimeFormatter.ISO_INSTANT);
String auditEntry = String.format(
"{\"timestamp\":\"%s\",\"streamReference\":\"%s\",\"toneType\":\"%s\",\"confidence\":%.2f,\"latencyMs\":%d,\"validationPassed\":%b}",
timestamp, streamReference, toneType, confidence, latencyMs, passedValidation
);
auditLogger.info(auditEntry);
}
}
The redirect operation terminates the current detection cycle and routes the media stream to the specified URI. This prevents duplicate tone processing and ensures safe iteration. The audit log uses structured JSON formatting to support downstream parsing by SIEM or compliance systems.
Step 4: Configure ToneDetected Webhooks for Gateway Sync
External fax gateways require event synchronization. You will create a webhook that triggers on toneDetected events. The webhook API returns HTTP 429 when rate limits are exceeded. You must implement exponential backoff retry logic.
import com.mypurecloud.api.client.WebhookClient;
import com.mypurecloud.api.client.model.Webhook;
import com.mypurecloud.api.client.model.WebhookEvent;
import com.mypurecloud.api.client.model.WebhookHttpTarget;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Collections;
import java.util.List;
public class WebhookSyncManager {
private static final Logger logger = LoggerFactory.getLogger(WebhookSyncManager.class);
private final WebhookClient webhookClient;
private static final int MAX_RETRIES = 3;
private static final long BASE_DELAY_MS = 1000;
public WebhookSyncManager(ApiClient apiClient) {
this.webhookClient = new WebhookClient(apiClient);
}
public void createToneDetectedWebhook(String targetUrl) throws Exception {
Webhook webhook = new Webhook()
.name("fax-tone-sync-webhook")
.enabled(true)
.events(List.of(new WebhookEvent().name("toneDetected")))
.target(new WebhookHttpTarget()
.url(targetUrl)
.method("POST")
.headers(Collections.singletonMap("Content-Type", "application/json")));
try {
webhookClient.postWebhooksWebhook(webhook);
logger.info("Webhook created successfully for tone detection sync.");
} catch (com.mypurecloud.api.client.ApiException e) {
if (e.getCode() == 429) {
handleRateLimitRetry(webhook, e);
} else if (e.getCode() >= 500) {
logger.error("Server error creating webhook. Status: {}", e.getCode());
throw e;
} else {
logger.error("Failed to create webhook. Status: {}, Message: {}", e.getCode(), e.getMessage());
throw e;
}
}
}
private void handleRateLimitRetry(Webhook webhook, com.mypurecloud.api.client.ApiException initialException) throws Exception {
for (int attempt = 1; attempt <= MAX_RETRIES; attempt++) {
long delay = BASE_DELAY_MS * (long) Math.pow(2, attempt - 1);
logger.warn("429 Rate limit exceeded. Retrying in {} ms. Attempt {}/{}", delay, attempt, MAX_RETRIES);
Thread.sleep(delay);
try {
webhookClient.postWebhooksWebhook(webhook);
logger.info("Webhook created successfully after retry.");
return;
} catch (com.mypurecloud.api.client.ApiException ex) {
if (ex.getCode() != 429) throw ex;
}
}
throw initialException;
}
}
The retry logic multiplies the delay exponentially to respect Genesys Cloud rate limits. The webhook ensures external fax gateways receive alignment events without polling. The toneDetected event name maps directly to the Telephony API detection lifecycle.
Complete Working Example
The following class combines authentication, session initialization, detection, event handling, redirection, auditing, and webhook configuration into a single runnable module. Replace the placeholder credentials with your OAuth values and target URL.
import com.mypurecloud.api.client.ApiClient;
import com.mypurecloud.api.client.TelephonyClient;
import com.mypurecloud.api.client.WebhookClient;
import com.mypurecloud.api.client.auth.TokenClient;
import com.mypurecloud.api.client.model.TelephonySession;
import com.mypurecloud.api.client.model.TelephonySessionRequest;
import com.mypurecloud.api.client.model.TelephonyDetectRequest;
import com.mypurecloud.api.client.model.ToneMatrix;
import com.mypurecloud.api.client.model.Tone;
import com.mypurecloud.api.client.model.AnalyzeDirective;
import com.mypurecloud.api.client.model.Webhook;
import com.mypurecloud.api.client.model.WebhookEvent;
import com.mypurecloud.api.client.model.WebhookHttpTarget;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.time.Instant;
import java.util.Collections;
import java.util.List;
import java.util.Set;
public class FaxToneDetectionService {
private static final Logger logger = LoggerFactory.getLogger(FaxToneDetectionService.class);
private static final Logger auditLogger = LoggerFactory.getLogger("telephony.audit");
public static void main(String[] args) {
String clientId = "YOUR_CLIENT_ID";
String clientSecret = "YOUR_CLIENT_SECRET";
String basePath = "https://api.mypurecloud.com";
String webhookTargetUrl = "https://your-gateway.example.com/incoming-fax-events";
try {
// 1. Authentication
ApiClient apiClient = new ApiClient();
apiClient.setBasePath(basePath);
TokenClient tokenClient = new TokenClient(
clientId, clientSecret,
Set.of("telephony:session:write", "telephony:session:read", "webhook:write", "webhook:read")
);
apiClient.setAccessTokenGenerator(tokenClient::getAccessToken);
// 2. Webhook Configuration
WebhookClient webhookClient = new WebhookClient(apiClient);
Webhook webhook = new Webhook()
.name("fax-tone-sync-webhook")
.enabled(true)
.events(List.of(new WebhookEvent().name("toneDetected")))
.target(new WebhookHttpTarget()
.url(webhookTargetUrl)
.method("POST")
.headers(Collections.singletonMap("Content-Type", "application/json")));
webhookClient.postWebhooksWebhook(webhook);
logger.info("Webhook registered successfully.");
// 3. Telephony Session
TelephonyClient telephonyClient = new TelephonyClient(apiClient);
TelephonySession session = telephonyClient.openSession(
new TelephonySessionRequest()
.applicationId("fax-detector")
.mediaType("audio")
.sampleRate(8000)
);
String streamRef = session.getStreamReference();
// 4. Detect Directive
TelephonyDetectRequest detectRequest = new TelephonyDetectRequest()
.streamReference(streamRef)
.toneMatrix(new ToneMatrix().tones(List.of(
new Tone().type("CED").minDuration(100).maxDuration(2000),
new Tone().type("CNG").minDuration(100).maxDuration(2000)
)))
.analyze(new AnalyzeDirective().enabled(true).maxSampleRate(8000).cepstralAnalysis(true))
.redirectOnDetect(true);
Instant detectTime = Instant.now();
telephonyClient.sendDetectRequest(detectRequest);
logger.info("Detect directive sent at {}", detectTime);
// 5. Event Listener
telephonyClient.addMessageListener(message -> {
if ("toneDetected".equals(message.getType())) {
double confidence = message.getPayload().getDouble("confidence");
double signalLevel = message.getPayload().getDouble("signalLevel");
String toneType = message.getPayload().getString("toneType");
long latency = java.time.Duration.between(detectTime, Instant.now()).toMillis();
boolean valid = confidence >= 0.75 && signalLevel >= -40.0;
auditLogger.info(String.format(
"{\"ts\":\"%s\",\"stream\":\"%s\",\"tone\":\"%s\",\"conf\":%.2f,\"latency\":%d,\"valid\":%b}",
Instant.now(), streamRef, toneType, confidence, latency, valid
));
if (valid) {
try {
telephonyClient.sendRedirectRequest(
new com.mypurecloud.api.client.model.TelephonyRedirectRequest()
.streamReference(streamRef)
.targetUri("wss://api.mypurecloud.com/api/v2/telephony/sessions/fax-gateway")
.reason("fax-detected-" + toneType)
);
logger.info("Redirect triggered for {}", toneType);
} catch (Exception e) {
logger.error("Redirect failed: {}", e.getMessage());
}
}
}
});
// Keep alive for demonstration
Thread.sleep(30000);
} catch (Exception e) {
logger.error("Critical failure in fax detection service: {}", e.getMessage(), e);
}
}
}
The script initializes authentication, registers the synchronization webhook, opens a Telephony session, sends the detect directive, listens for binary tone events, validates signal clarity, logs audit entries, and triggers a redirect upon successful detection. The WebSocket connection remains open for the duration of the call.
Common Errors & Debugging
Error: 401 Unauthorized
- Cause: The OAuth token is expired, malformed, or the client credentials are incorrect. The Telephony WebSocket handshake rejects invalid tokens.
- Fix: Verify the client ID and secret match a Genesys Cloud OAuth client with the correct grant type. Ensure the
TokenClientrefresh logic is active. Check that the token scope includestelephony:session:write. - Code Fix: The SDK handles refresh automatically. If persistent, regenerate the token manually via
POST /oauth/tokenand validate theaccess_tokenfield.
Error: 403 Forbidden
- Cause: The OAuth client lacks required scopes, or the user role does not have telephony media permissions.
- Fix: Add
telephony:session:readandtelephony:session:writeto the client scopes. Assign the user theTelephony AdministratororMedia Developerrole. - Code Fix: Update the
Set.of()in theTokenClientconstructor to include all required scopes.
Error: 429 Too Many Requests
- Cause: Webhook creation or Telephony API calls exceed rate limits. The Genesys Cloud platform enforces per-tenant and per-client quotas.
- Fix: Implement exponential backoff. Cache webhook configurations instead of recreating them on every run. Batch detect directives when processing multiple streams.
- Code Fix: The
handleRateLimitRetrymethod in Step 4 demonstrates the backoff pattern. Apply the same logic to other API calls.
Error: WebSocket Binary Frame Format Verification Failed
- Cause: The SDK attempts to serialize a payload with unsupported fields or incorrect sample rate constraints. The server rejects frames that exceed
maxSampleRateor contain invalid tone matrix definitions. - Fix: Ensure
sampleRatematches 8000 for fax tones. Validate that tone durations fall within PSTN specifications. Remove undocumented fields from theAnalyzeDirective. - Code Fix: Explicitly set
.sampleRate(8000)in the session request and.maxSampleRate(8000)in the analyze directive.
Error: Tone Detection Fails During Scaling
- Cause: High concurrency causes media engine resource contention. Cepstral analysis queues delay response times, triggering client-side timeouts.
- Fix: Increase WebSocket keep-alive intervals. Implement client-side retry for detect directives. Monitor
latencyMsin audit logs to identify degradation thresholds. - Code Fix: Add a timeout check in the message listener. If
latency > 5000, log a warning and reissue the detect directive with a fresh stream reference.