How to handle file uploads from customers in Web Messaging — accepted MIME types and size limits
What You Will Build
A production-ready Python module that fetches Web Chat configuration limits, validates customer files against those limits, uploads attachments via the Genesys Cloud CX Attachments API, and handles validation failures with explicit retry logic. This tutorial uses the Genesys Cloud CX REST API with requests. The implementation covers Python 3.9+.
Prerequisites
- OAuth 2.0 Client Credentials flow configured in Genesys Cloud
- Required scopes:
webchat:configuration:read,webchat:attachment:upload,webchat:send - Python 3.9 or newer
- Dependencies:
requests,urllib3,mimetypes(stdlib),typing(stdlib) - A valid Web Chat Configuration ID (obtainable via
/api/v2/webchat/configurations)
Authentication Setup
Genesys Cloud uses OAuth 2.0 for all API access. The client credentials flow is appropriate for server-side integrations that process customer uploads. The token endpoint is https://api.mypurecloud.com/oauth/token. You must cache the token until expiration and handle 401 Unauthorized responses by refreshing or re-authenticating.
import requests
import time
from typing import Optional
class GenesysAuth:
def __init__(self, client_id: str, client_secret: str, environment: str = "mypurecloud.com"):
self.client_id = client_id
self.client_secret = client_secret
self.base_url = f"https://api.{environment}"
self.token_url = f"{self.base_url}/oauth/token"
self.access_token: Optional[str] = None
self.expires_at: float = 0.0
def get_token(self) -> str:
if self.access_token and time.time() < self.expires_at:
return self.access_token
payload = {
"grant_type": "client_credentials",
"scope": "webchat:configuration:read webchat:attachment:upload webchat:send"
}
response = requests.post(
self.token_url,
auth=(self.client_id, self.client_secret),
data=payload,
timeout=10
)
response.raise_for_status()
data = response.json()
self.access_token = data["access_token"]
self.expires_at = time.time() + data["expires_in"] - 30 # 30s buffer
return self.access_token
The expires_in field returns seconds until token invalidation. Subtracting thirty seconds prevents edge-case 401 responses during high-throughput upload windows. The scope string must contain all required permissions separated by spaces.
Implementation
Step 1: Retrieve Web Chat Configuration Limits
Genesys Cloud enforces file attachment rules at the Web Chat configuration level. The server rejects uploads that violate these rules, but client-side validation reduces unnecessary network traffic and provides immediate feedback. The configuration endpoint returns the maximum allowed file size in bytes and an array of permitted MIME types.
HTTP Request Cycle
GET /api/v2/webchat/configurations/{webchatConfigurationId}
Host: api.mypurecloud.com
Authorization: Bearer <access_token>
Accept: application/json
Realistic Response
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"name": "Customer Support Web Chat",
"fileAttachments": {
"maxSize": 10485760,
"allowedMimeTypes": [
"image/jpeg",
"image/png",
"application/pdf",
"text/plain",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"
]
}
}
from typing import Dict, List
class WebChatConfigFetcher:
def __init__(self, auth: GenesysAuth):
self.auth = auth
self.base_url = f"https://api.{auth.environment}"
def get_limits(self, config_id: str) -> Dict[str, any]:
url = f"{self.base_url}/api/v2/webchat/configurations/{config_id}"
headers = {
"Authorization": f"Bearer {self.auth.get_token()}",
"Accept": "application/json"
}
response = requests.get(url, headers=headers, timeout=15)
response.raise_for_status()
data = response.json()
file_config = data.get("fileAttachments", {})
return {
"max_size_bytes": file_config.get("maxSize", 10485760),
"allowed_mimes": file_config.get("allowedMimeTypes", [])
}
The fileAttachments object is optional in older deployments. The code falls back to a ten-megabyte default and an empty MIME list if the configuration omits these fields. An empty MIME list means the server enforces a strict default whitelist, so client-side validation should reject unknown types.
Step 2: Validate File Size and MIME Type Client-Side
Relying solely on server-side validation increases latency and consumes API quota. You must verify the file before transmission. Python’s os.path.getsize provides byte-level accuracy. MIME detection requires careful handling because file extensions are easily spoofed. The mimetypes module reads magic numbers when available, but production systems should pair it with server-side validation.
import os
import mimetypes
def validate_file(file_path: str, max_size: int, allowed_mimes: List[str]) -> str:
if not os.path.isfile(file_path):
raise FileNotFoundError(f"Path does not exist or is not a file: {file_path}")
file_size = os.path.getsize(file_path)
if file_size > max_size:
raise ValueError(
f"File size {file_size} bytes exceeds limit of {max_size} bytes. "
f"Reduce file size or compress before uploading."
)
detected_mime, _ = mimetypes.guess_type(file_path)
if detected_mime is None:
raise ValueError("Unable to detect MIME type. Provide a valid file extension.")
if detected_mime not in allowed_mimes:
raise ValueError(
f"MIME type '{detected_mime}' is not permitted. "
f"Allowed types: {', '.join(allowed_mimes)}"
)
return detected_mime
This function fails fast. It raises explicit exceptions that your application can catch and translate into user-facing messages. The server will still validate the upload, but this prevents sending invalid payloads to the API gateway.
Step 3: Upload the File with Retry Logic
The attachment upload endpoint accepts multipart/form-data. Genesys Cloud returns a 429 Too Many Requests response when the account exceeds the upload rate limit. The API design enforces per-tenant throttling to protect the message bus from flood conditions. You must implement exponential backoff with jitter to comply with rate limits without blocking other services.
HTTP Request Cycle
POST /api/v2/attachments/upload
Host: api.mypurecloud.com
Authorization: Bearer <access_token>
Content-Type: multipart/form-data; boundary=----FormBoundary7MA4YWxkTrZu0gW
Realistic Response
{
"id": "9f8e7d6c-5b4a-3210-fedc-ba9876543210",
"name": "invoice_q3.pdf",
"type": "application/pdf",
"size": 245760
}
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
import time
class AttachmentUploader:
def __init__(self, auth: GenesysAuth):
self.auth = auth
self.base_url = f"https://api.{auth.environment}"
self.session = requests.Session()
self._setup_retry_policy()
def _setup_retry_policy(self):
retry_strategy = Retry(
total=3,
backoff_factor=1.5,
status_forcelist=[429, 500, 502, 503, 504],
allowed_methods=["POST"]
)
adapter = HTTPAdapter(max_retries=retry_strategy)
self.session.mount("https://", adapter)
def upload(self, file_path: str, mime_type: str) -> Dict[str, any]:
url = f"{self.base_url}/api/v2/attachments/upload"
headers = {
"Authorization": f"Bearer {self.auth.get_token()}",
"Accept": "application/json"
}
with open(file_path, "rb") as f:
files = {"file": (os.path.basename(file_path), f, mime_type)}
response = self.session.post(
url,
headers=headers,
files=files,
timeout=30
)
if response.status_code == 400:
raise ValueError(f"Server rejected file: {response.json().get('message', 'Invalid payload')}")
if response.status_code == 413:
raise ValueError("File size exceeds server-side limit. Check Web Chat configuration.")
if response.status_code == 401:
self.auth.access_token = None # Force token refresh on next call
raise ConnectionError("Authentication token expired. Refresh required.")
response.raise_for_status()
return response.json()
The HTTPAdapter with urllib3.util.Retry handles transient failures automatically. The backoff_factor controls the delay between retries using the formula sleep = backoff_factor * (2 ** (retry - 1)). This prevents cascading failures when the API gateway throttles upload requests.
Step 4: Process the Attachment Response
The upload response contains the attachmentId. You reference this ID when sending a message to the conversation. The API does not attach the file automatically. You must include the attachment reference in the message payload.
HTTP Request Cycle for Message Submission
POST /api/v2/conversations/messages/{conversationId}/messages
Host: api.mypurecloud.com
Authorization: Bearer <access_token>
Content-Type: application/json
Realistic Request Body
{
"to": {
"id": "agent-user-id",
"type": "user"
},
"from": {
"id": "customer-contact-id",
"type": "contact"
},
"text": "Please review the attached document.",
"attachments": [
{
"id": "9f8e7d6c-5b4a-3210-fedc-ba9876543210",
"name": "invoice_q3.pdf",
"type": "application/pdf"
}
]
}
The attachments array expects the exact ID returned from /api/v2/attachments/upload. The server validates that the attachment belongs to the conversation context. If you upload a file outside an active conversation, you must associate it during message submission or use the conversation-scoped upload flow.
Complete Working Example
import os
import requests
import time
from typing import Optional, Dict, List
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
import mimetypes
class GenesysAuth:
def __init__(self, client_id: str, client_secret: str, environment: str = "mypurecloud.com"):
self.client_id = client_id
self.client_secret = client_secret
self.environment = environment
self.base_url = f"https://api.{environment}"
self.token_url = f"{self.base_url}/oauth/token"
self.access_token: Optional[str] = None
self.expires_at: float = 0.0
def get_token(self) -> str:
if self.access_token and time.time() < self.expires_at:
return self.access_token
payload = {
"grant_type": "client_credentials",
"scope": "webchat:configuration:read webchat:attachment:upload webchat:send"
}
response = requests.post(self.token_url, auth=(self.client_id, self.client_secret), data=payload, timeout=10)
response.raise_for_status()
data = response.json()
self.access_token = data["access_token"]
self.expires_at = time.time() + data["expires_in"] - 30
return self.access_token
class WebChatConfigFetcher:
def __init__(self, auth: GenesysAuth):
self.auth = auth
self.base_url = f"https://api.{auth.environment}"
def get_limits(self, config_id: str) -> Dict[str, any]:
url = f"{self.base_url}/api/v2/webchat/configurations/{config_id}"
headers = {"Authorization": f"Bearer {self.auth.get_token()}", "Accept": "application/json"}
response = requests.get(url, headers=headers, timeout=15)
response.raise_for_status()
data = response.json()
file_config = data.get("fileAttachments", {})
return {
"max_size_bytes": file_config.get("maxSize", 10485760),
"allowed_mimes": file_config.get("allowedMimeTypes", [])
}
def validate_file(file_path: str, max_size: int, allowed_mimes: List[str]) -> str:
if not os.path.isfile(file_path):
raise FileNotFoundError(f"Path does not exist or is not a file: {file_path}")
file_size = os.path.getsize(file_path)
if file_size > max_size:
raise ValueError(f"File size {file_size} bytes exceeds limit of {max_size} bytes.")
detected_mime, _ = mimetypes.guess_type(file_path)
if detected_mime is None:
raise ValueError("Unable to detect MIME type. Provide a valid file extension.")
if detected_mime not in allowed_mimes:
raise ValueError(f"MIME type '{detected_mime}' is not permitted. Allowed: {', '.join(allowed_mimes)}")
return detected_mime
class AttachmentUploader:
def __init__(self, auth: GenesysAuth):
self.auth = auth
self.base_url = f"https://api.{auth.environment}"
self.session = requests.Session()
retry_strategy = Retry(total=3, backoff_factor=1.5, status_forcelist=[429, 500, 502, 503, 504], allowed_methods=["POST"])
adapter = HTTPAdapter(max_retries=retry_strategy)
self.session.mount("https://", adapter)
def upload(self, file_path: str, mime_type: str) -> Dict[str, any]:
url = f"{self.base_url}/api/v2/attachments/upload"
headers = {"Authorization": f"Bearer {self.auth.get_token()}", "Accept": "application/json"}
with open(file_path, "rb") as f:
files = {"file": (os.path.basename(file_path), f, mime_type)}
response = self.session.post(url, headers=headers, files=files, timeout=30)
if response.status_code == 400:
raise ValueError(f"Server rejected file: {response.json().get('message', 'Invalid payload')}")
if response.status_code == 413:
raise ValueError("File size exceeds server-side limit.")
if response.status_code == 401:
self.auth.access_token = None
raise ConnectionError("Authentication token expired.")
response.raise_for_status()
return response.json()
def main():
auth = GenesysAuth(client_id="YOUR_CLIENT_ID", client_secret="YOUR_CLIENT_SECRET")
config_fetcher = WebChatConfigFetcher(auth)
uploader = AttachmentUploader(auth)
config_id = "YOUR_WEBCHAT_CONFIG_ID"
file_path = "/path/to/customer/document.pdf"
limits = config_fetcher.get_limits(config_id)
mime_type = validate_file(file_path, limits["max_size_bytes"], limits["allowed_mimes"])
attachment = uploader.upload(file_path, mime_type)
print(f"Upload successful. Attachment ID: {attachment['id']}")
if __name__ == "__main__":
main()
Replace YOUR_CLIENT_ID, YOUR_CLIENT_SECRET, YOUR_WEBCHAT_CONFIG_ID, and file_path with your environment values. The script validates, uploads, and prints the attachment identifier for subsequent message submission.
Common Errors & Debugging
Error: 400 Bad Request
- Cause: The MIME type is not in the allowed list, or the multipart payload is malformed.
- Fix: Verify the
Content-Typeheader matches the file extension. Ensure thefilefield name in the multipart form matches the API specification exactly. Check the Web Chat configuration for custom MIME restrictions. - Code Fix: Use
mimetypes.guess_typeto confirm detection matches the configuration. Log the raw response body to inspect themessagefield returned by the API.
Error: 413 Payload Too Large
- Cause: The file exceeds the
maxSizedefined in the Web Chat configuration or the platform-wide limit. - Fix: Compress the file before upload. Update the Web Chat configuration to increase
maxSizeif business requirements allow. Never bypass server-side limits client-side. - Code Fix: The
validate_filefunction catches this before transmission. If the error occurs during upload, the server enforced a stricter limit than the configuration returned. Re-fetch the configuration and compare values.
Error: 429 Too Many Requests
- Cause: The account has exceeded the upload rate limit. Genesys Cloud throttles attachment uploads to protect the message bus.
- Fix: The retry policy in
AttachmentUploaderhandles this automatically. Monitor theRetry-Afterheader if you need precise backoff timing. Space out concurrent uploads using a queue with concurrency limits. - Code Fix: The
urllib3.util.Retryconfiguration includes429instatus_forcelist. Increasetotalretries orbackoff_factorif your workload requires higher resilience.
Error: 500 / 503 Internal Server Error
- Cause: Temporary platform outage or storage backend unavailability.
- Fix: Wait and retry. The retry policy covers these status codes. If the error persists beyond ten minutes, check the Genesys Cloud status dashboard.
- Code Fix: The
HTTPAdaptermounts retry logic for500and503. Ensure your application does not block indefinitely by setting a maximum timeout on the session.