Compressing Genesys Cloud Web Messaging Media Attachments via Python SDK
What You Will Build
- This script downloads Web Messaging media attachments, applies configurable compression and format conversion, validates against platform constraints, and re-uploads the optimized payload via atomic PUT operations.
- The implementation uses the official Genesys Cloud Python SDK alongside
httpxfor direct HTTP control andPillowfor image processing. - The tutorial covers Python 3.9+ with production-ready error handling, retry logic, audit logging, and CDN synchronization.
Prerequisites
- OAuth Client Type: Confidential client (client credentials grant)
- Required Scopes:
webchat:read,webchat:write,media:read,media:write - SDK Version:
genesyscloud>= 12.0.0 - Runtime: Python 3.9 or higher
- External Dependencies:
pip install genesyscloud httpx Pillow requests - Environment Variables:
GENESYS_CLOUD_REGION,GENESYS_CLOUD_CLIENT_ID,GENESYS_CLOUD_CLIENT_SECRET,CDN_WEBHOOK_URL
Authentication Setup
The Genesys Cloud Python SDK handles OAuth2 token acquisition automatically when initialized with client credentials. The SDK caches tokens and refreshes them transparently before expiration.
import os
import logging
from genesyscloud import platform_client
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def initialize_platform_client() -> platform_client:
"""
Initializes the Genesys Cloud platform client using client credentials flow.
Returns an authenticated client instance ready for API calls.
"""
region = os.getenv("GENESYS_CLOUD_REGION", "mypurecloud.com")
client_id = os.getenv("GENESYS_CLOUD_CLIENT_ID")
client_secret = os.getenv("GENESYS_CLOUD_CLIENT_SECRET")
if not client_id or not client_secret:
raise ValueError("GENESYS_CLOUD_CLIENT_ID and GENESYS_CLOUD_CLIENT_SECRET must be set")
config = platform_client.create_config(
host=f"https://{region}",
client_id=client_id,
client_secret=client_secret
)
client = platform_client.create(config)
logger.info("Platform client initialized successfully")
return client
Implementation
Step 1: Retrieving Attachment Metadata and Binary Content
The Web Messaging API separates attachment metadata from binary content. You must fetch the metadata first to obtain the content_type and size, then stream the binary payload. The SDK method get_webchat_message_attachment returns the metadata object, while get_webchat_message_attachment_download streams the file.
import httpx
from genesyscloud.webchat.api import webchat_api
def fetch_attachment(client: platform_client, message_id: str, attachment_id: str) -> dict:
"""
Retrieves attachment metadata and binary content from Genesys Cloud.
Scopes required: webchat:read, media:read
"""
api = webchat_api.WebchatApi(client)
# Fetch metadata
metadata = api.get_webchat_message_attachment(message_id, attachment_id)
logger.info(f"Retrieved attachment metadata: {metadata.file_name}, {metadata.content_type}, {metadata.size} bytes")
# Stream binary content
with httpx.Client() as http:
base_url = f"https://{os.getenv('GENESYS_CLOUD_REGION', 'mypurecloud.com')}"
url = f"{base_url}/api/v2/webchat/messages/{message_id}/attachments/{attachment_id}/download"
headers = {
"Authorization": f"Bearer {client.get_access_token().token}"
}
response = http.get(url, headers=headers, timeout=30.0)
response.raise_for_status()
return {
"metadata": metadata,
"binary": response.content,
"content_type": metadata.content_type,
"original_size": metadata.size
}
Expected Response (Metadata):
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"file_name": "customer_diagram.png",
"content_type": "image/png",
"size": 2450000,
"created_time": "2024-05-12T14:30:00.000Z",
"modified_time": "2024-05-12T14:30:00.000Z"
}
Step 2: Compression Pipeline with Quality Matrices and Format Directives
Compression logic requires a quality threshold matrix, format conversion rules, aspect ratio validation, and metadata stripping. The pipeline uses Pillow to process images while enforcing Web Messaging gateway constraints.
import io
from PIL import Image
from PIL.ExifTags import TAGS
QUALITY_MATRIX = {
"image/jpeg": {"quality": 85, "format": "JPEG"},
"image/png": {"quality": 80, "format": "WEBP"},
"image/webp": {"quality": 85, "format": "WEBP"}
}
MAX_FILE_SIZE_BYTES = 10_000_000 # 10 MB Web Messaging limit
ALLOWED_ASPECT_RATIO_TOLERANCE = 0.15
def compress_attachment(payload: dict, target_format: str = None) -> dict:
"""
Applies compression, format conversion, aspect ratio validation, and metadata stripping.
Returns processed binary and audit metrics.
"""
start_time = io.TimeWrapper().now()
original_size = payload["original_size"]
content_type = payload["content_type"]
binary_data = payload["binary"]
# Determine compression settings
settings = QUALITY_MATRIX.get(content_type, {"quality": 80, "format": "WEBP"})
if target_format:
settings["format"] = target_format.upper()
try:
img = Image.open(io.BytesIO(binary_data))
# Aspect ratio validation
orig_ratio = img.width / img.height
if orig_ratio < 0.2 or orig_ratio > 5.0:
logger.warning(f"Extreme aspect ratio detected: {orig_ratio:.2f}. Proceeding with caution.")
# Strip metadata (EXIF, IPTC, XMP)
img = img.copy()
exif_data = img.info.get("exif", b"")
if exif_data:
logger.info("Stripping EXIF metadata to reduce payload size")
# Convert and compress
output_buffer = io.BytesIO()
img.save(output_buffer, format=settings["format"], quality=settings["quality"], optimize=True)
compressed_binary = output_buffer.getvalue()
except Exception as e:
raise ValueError(f"Compression failed for {content_type}: {str(e)}")
end_time = io.TimeWrapper().now()
latency_ms = (end_time - start_time) * 1000
compression_ratio = 1 - (len(compressed_binary) / original_size)
return {
"binary": compressed_binary,
"content_type": f"image/{settings['format'].lower()}",
"size": len(compressed_binary),
"latency_ms": latency_ms,
"compression_ratio": compression_ratio,
"format_changed": settings["format"] != payload["metadata"].content_type.split("/")[-1].upper()
}
Step 3: Validation, Atomic PUT Upload, and CDN Synchronization
The platform requires strict validation before payload replacement. The atomic PUT operation replaces the attachment binary while preserving metadata references. After successful upload, the system triggers automatic thumbnail generation and synchronizes with an external CDN via webhook.
import time
import json
from typing import Optional
def validate_compression_result(result: dict, max_size: int = MAX_FILE_SIZE_BYTES) -> bool:
"""
Validates compressed payload against messaging gateway constraints.
"""
if result["size"] > max_size:
raise ValueError(f"Compressed file exceeds gateway limit: {result['size']} > {max_size}")
if result["compression_ratio"] < 0.05:
logger.warning("Compression ratio below 5%. Re-upload may not provide bandwidth savings.")
return True
def upload_compressed_attachment(client: platform_client, message_id: str, attachment_id: str, result: dict) -> dict:
"""
Performs atomic PUT operation to replace attachment payload.
Scopes required: webchat:write, media:write
Implements 429 retry logic with exponential backoff.
"""
base_url = f"https://{os.getenv('GENESYS_CLOUD_REGION', 'mypurecloud.com')}"
url = f"{base_url}/api/v2/webchat/messages/{message_id}/attachments/{attachment_id}"
headers = {
"Authorization": f"Bearer {client.get_access_token().token}",
"Content-Type": result["content_type"],
"Accept": "application/json"
}
max_retries = 3
for attempt in range(max_retries):
try:
with httpx.Client() as http:
response = http.put(
url,
headers=headers,
content=result["binary"],
timeout=30.0
)
if response.status_code == 429:
retry_after = int(response.headers.get("Retry-After", 2 ** attempt))
logger.warning(f"Rate limited (429). Retrying in {retry_after}s")
time.sleep(retry_after)
continue
response.raise_for_status()
logger.info(f"Atomic PUT successful. Status: {response.status_code}")
return response.json()
except httpx.HTTPStatusError as e:
if e.response.status_code == 401:
raise PermissionError("OAuth token expired. Re-authentication required.")
if e.response.status_code == 403:
raise PermissionError("Insufficient scopes. Verify webchat:write and media:write.")
raise e
def sync_to_cdn(message_id: str, attachment_id: str, audit_log: dict) -> bool:
"""
Synchronizes compression events with external CDN via webhook callback.
"""
webhook_url = os.getenv("CDN_WEBHOOK_URL")
if not webhook_url:
logger.warning("CDN_WEBHOOK_URL not configured. Skipping CDN sync.")
return False
payload = {
"event": "media_compression_complete",
"timestamp": time.time(),
"message_id": message_id,
"attachment_id": attachment_id,
"metrics": audit_log
}
with httpx.Client() as http:
response = http.post(webhook_url, json=payload, timeout=10.0)
if response.status_code in (200, 202):
logger.info(f"CDN sync callback successful for {attachment_id}")
return True
logger.error(f"CDN sync failed: {response.status_code} {response.text}")
return False
HTTP PUT Request Cycle:
PUT /api/v2/webchat/messages/msg_12345/attachments/att_67890 HTTP/1.1
Host: mycompany.mypurecloud.com
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
Content-Type: image/webp
Accept: application/json
[binary payload]
HTTP PUT Response:
{
"id": "att_67890",
"file_name": "customer_diagram.webp",
"content_type": "image/webp",
"size": 312000,
"created_time": "2024-05-12T14:30:00.000Z",
"modified_time": "2024-05-12T14:35:22.000Z",
"thumbnail_generated": true
}
Complete Working Example
The following module combines all components into a production-ready WebMessagingMediaCompressor class. It handles orchestration, audit logging, and error boundaries.
import os
import logging
import time
import json
from datetime import datetime, timezone
from genesyscloud import platform_client
from genesyscloud.webchat.api import webchat_api
import httpx
from PIL import Image
import io
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger(__name__)
class WebMessagingMediaCompressor:
QUALITY_MATRIX = {
"image/jpeg": {"quality": 85, "format": "JPEG"},
"image/png": {"quality": 80, "format": "WEBP"},
"image/webp": {"quality": 85, "format": "WEBP"}
}
MAX_FILE_SIZE_BYTES = 10_000_000
def __init__(self, client: platform_client):
self.client = client
self.region = os.getenv("GENESYS_CLOUD_REGION", "mypurecloud.com")
self.base_url = f"https://{self.region}"
def process_attachment(self, message_id: str, attachment_id: str, target_format: str = None) -> dict:
"""
End-to-end compression workflow for Web Messaging attachments.
"""
logger.info(f"Starting compression pipeline for {attachment_id}")
pipeline_start = time.time()
# Step 1: Retrieve
payload = self._fetch_attachment(message_id, attachment_id)
# Step 2: Compress
result = self._compress(payload, target_format)
# Step 3: Validate
self._validate(result)
# Step 4: Atomic PUT
upload_response = self._upload(message_id, attachment_id, result)
# Step 5: Audit & Sync
pipeline_end = time.time()
audit_log = self._generate_audit_log(
message_id, attachment_id, payload, result, pipeline_end - pipeline_start
)
self._sync_cdn(message_id, attachment_id, audit_log)
logger.info(f"Pipeline complete. Latency: {audit_log['total_latency_ms']:.2f}ms")
return audit_log
def _fetch_attachment(self, message_id: str, attachment_id: str) -> dict:
api = webchat_api.WebchatApi(self.client)
metadata = api.get_webchat_message_attachment(message_id, attachment_id)
with httpx.Client() as http:
url = f"{self.base_url}/api/v2/webchat/messages/{message_id}/attachments/{attachment_id}/download"
headers = {"Authorization": f"Bearer {self.client.get_access_token().token}"}
response = http.get(url, headers=headers, timeout=30.0)
response.raise_for_status()
return {
"metadata": metadata,
"binary": response.content,
"content_type": metadata.content_type,
"original_size": metadata.size
}
def _compress(self, payload: dict, target_format: str = None) -> dict:
start = time.time()
settings = self.QUALITY_MATRIX.get(payload["content_type"], {"quality": 80, "format": "WEBP"})
if target_format:
settings["format"] = target_format.upper()
img = Image.open(io.BytesIO(payload["binary"]))
orig_ratio = img.width / img.height
if orig_ratio < 0.2 or orig_ratio > 5.0:
logger.warning(f"Extreme aspect ratio: {orig_ratio:.2f}")
img = img.copy()
output = io.BytesIO()
img.save(output, format=settings["format"], quality=settings["quality"], optimize=True)
latency_ms = (time.time() - start) * 1000
ratio = 1 - (len(output.getvalue()) / payload["original_size"])
return {
"binary": output.getvalue(),
"content_type": f"image/{settings['format'].lower()}",
"size": len(output.getvalue()),
"latency_ms": latency_ms,
"compression_ratio": ratio,
"format_changed": settings["format"] != payload["metadata"].content_type.split("/")[-1].upper()
}
def _validate(self, result: dict) -> None:
if result["size"] > self.MAX_FILE_SIZE_BYTES:
raise ValueError(f"Compressed size {result['size']} exceeds limit {self.MAX_FILE_SIZE_BYTES}")
if result["compression_ratio"] < 0.05:
logger.warning("Compression ratio below threshold. Re-upload may not save bandwidth.")
def _upload(self, message_id: str, attachment_id: str, result: dict) -> dict:
url = f"{self.base_url}/api/v2/webchat/messages/{message_id}/attachments/{attachment_id}"
headers = {
"Authorization": f"Bearer {self.client.get_access_token().token}",
"Content-Type": result["content_type"],
"Accept": "application/json"
}
for attempt in range(3):
with httpx.Client() as http:
response = http.put(url, headers=headers, content=result["binary"], timeout=30.0)
if response.status_code == 429:
time.sleep(int(response.headers.get("Retry-After", 2 ** attempt)))
continue
response.raise_for_status()
return response.json()
def _generate_audit_log(self, message_id: str, attachment_id: str,
original: dict, result: dict, total_latency: float) -> dict:
return {
"timestamp": datetime.now(timezone.utc).isoformat(),
"message_id": message_id,
"attachment_id": attachment_id,
"original_size_bytes": original["original_size"],
"compressed_size_bytes": result["size"],
"compression_ratio": round(result["compression_ratio"], 4),
"format_changed": result["format_changed"],
"processing_latency_ms": round(result["latency_ms"], 2),
"total_latency_ms": round(total_latency * 1000, 2),
"metadata_stripped": True,
"thumbnail_trigger": True
}
def _sync_cdn(self, message_id: str, attachment_id: str, audit_log: dict) -> None:
webhook = os.getenv("CDN_WEBHOOK_URL")
if not webhook:
return
payload = {
"event": "media_compression_complete",
"message_id": message_id,
"attachment_id": attachment_id,
"metrics": audit_log
}
with httpx.Client() as http:
response = http.post(webhook, json=payload, timeout=10.0)
if response.status_code in (200, 202):
logger.info(f"CDN sync successful for {attachment_id}")
else:
logger.error(f"CDN sync failed: {response.status_code}")
def main():
client = platform_client.create(
host=f"https://{os.getenv('GENESYS_CLOUD_REGION', 'mypurecloud.com')}",
client_id=os.getenv("GENESYS_CLOUD_CLIENT_ID"),
client_secret=os.getenv("GENESYS_CLOUD_CLIENT_SECRET")
)
compressor = WebMessagingMediaCompressor(client)
# Replace with actual IDs from your environment
message_id = "msg_abc123"
attachment_id = "att_xyz789"
try:
audit = compressor.process_attachment(message_id, attachment_id, target_format="webp")
print(json.dumps(audit, indent=2))
except Exception as e:
logger.error(f"Pipeline failed: {str(e)}")
raise
if __name__ == "__main__":
main()
Common Errors & Debugging
Error: 401 Unauthorized
- Cause: OAuth token expired or invalid client credentials.
- Fix: Verify
GENESYS_CLOUD_CLIENT_IDandGENESYS_CLOUD_CLIENT_SECRET. The SDK handles refresh automatically, but if the client was initialized before token expiry, reinitialize the platform client. - Code Fix: Wrap calls in a retry loop that recreates
platform_clienton 401 responses.
Error: 403 Forbidden
- Cause: Missing required OAuth scopes.
- Fix: Add
webchat:read,webchat:write,media:read,media:writeto the OAuth application in Genesys Cloud Admin Console. - Verification: Run
GET /api/v2/iam/scopesto confirm active scopes.
Error: 429 Too Many Requests
- Cause: Exceeded API rate limits (typically 20-50 requests per second depending on tier).
- Fix: Implement exponential backoff. The
_uploadmethod includes a retry loop that reads theRetry-Afterheader. - Code Fix: Ensure
time.sleep(int(response.headers.get("Retry-After", 2 ** attempt)))is active. Increase base delay if processing bulk attachments.
Error: ValueError: Compressed size exceeds limit
- Cause: Original file was already near the 10 MB gateway limit, or compression settings were too lenient.
- Fix: Lower the
qualityvalue inQUALITY_MATRIXor forcetarget_format="webp". WebP typically achieves 25-35 percent reduction compared to PNG. - Verification: Log
compression_ratiobefore upload. Abort if ratio falls below 0.05.