How to handle file uploads from customers in Web Messaging — accepted MIME types and size limits

How to handle file uploads from customers in Web Messaging — accepted MIME types and size limits

What You Will Build

  • This tutorial demonstrates how to programmatically upload files to Genesys Cloud CX Web Messaging channels, validating MIME types and enforcing size limits before transmission.
  • You will use the Genesys Cloud Platform API v2 endpoints for asset upload and the genesys-cloud SDK to manage the file transfer lifecycle.
  • The implementation covers Python (using requests and the official SDK) and JavaScript (using fetch and the official SDK).

Prerequisites

  • OAuth Client Type: A Genesys Cloud Application with webmessaging and files:read scopes.
  • SDK Version: Genesys Cloud Python SDK >= 120.0.0 or JavaScript SDK >= 120.0.0.
  • Runtime: Python 3.8+ or Node.js 16+.
  • Dependencies:
    • Python: pip install requests genesys-cloud-purecloud-platform-client
    • JavaScript: npm install @genesys/cloud-purecloud-platform-client

Authentication Setup

File uploads require a valid access token with specific scopes. The standard OAuth2 client credentials flow is sufficient for server-side integrations. For client-side Web Messaging implementations, you typically rely on the embedded widget’s internal authentication, but if you are building a custom backend proxy or a bot-driven upload handler, you must manage the token lifecycle.

The required scope for uploading files associated with a conversation is webmessaging combined with files:read or files:write depending on the specific asset operation.

Python Authentication

import os
from purecloudplatform.client import Configuration, ApiClient

def get_authenticated_api_client() -> ApiClient:
    """
    Creates an authenticated Genesys Cloud API client using environment variables.
    """
    config = Configuration(
        environment=os.getenv("GENESYS_ENVIRONMENT", "us-east-1"),
        client_id=os.getenv("GENESYS_CLIENT_ID"),
        client_secret=os.getenv("GENESYS_CLIENT_SECRET")
    )
    
    api_client = ApiClient(configuration=config)
    return api_client

api_client = get_authenticated_api_client()

JavaScript Authentication

const { Configuration, ApiClient } = require('@genesys/cloud-purecloud-platform-client');

function getAuthenticatedApiClient() {
    const config = new Configuration({
        environment: process.env.GENESYS_ENVIRONMENT || 'us-east-1',
        clientId: process.env.GENESYS_CLIENT_ID,
        clientSecret: process.env.GENESYS_CLIENT_SECRET
    });

    const apiClient = new ApiClient(config);
    return apiClient;
}

const apiClient = getAuthenticatedApiClient();

Implementation

Step 1: Validate File Constraints Before Upload

Genesys Cloud enforces strict limits on file uploads within Web Messaging. Before sending data over the network, your application must validate the file against these constraints to prevent 400 Bad Request errors.

Constraints:

  • Maximum File Size: 10 MB (10,485,760 bytes).
  • Accepted MIME Types:
    • Images: image/jpeg, image/png, image/gif, image/bmp, image/webp
    • Documents: application/pdf, text/plain
    • Archives: application/zip
    • Note: Executables, scripts, and HTML files are blocked by default for security reasons.

Python Validation Logic

import os
import mimetypes

ALLOWED_MIME_TYPES = {
    'image/jpeg', 'image/png', 'image/gif', 'image/bmp', 'image/webp',
    'application/pdf', 'text/plain', 'application/zip'
}

MAX_FILE_SIZE = 10 * 1024 * 1024  # 10 MB

def validate_file(file_path: str) -> tuple[bool, str]:
    """
    Validates a file against Genesys Cloud Web Messaging constraints.
    Returns (is_valid, error_message).
    """
    if not os.path.exists(file_path):
        return False, "File does not exist."

    file_size = os.path.getsize(file_path)
    if file_size > MAX_FILE_SIZE:
        return False, f"File size {file_size} bytes exceeds limit of {MAX_FILE_SIZE} bytes."

    # Guess MIME type from filename
    mime_type, _ = mimetypes.guess_type(file_path)
    
    if not mime_type:
        return False, "Could not determine MIME type from filename."
        
    if mime_type not in ALLOWED_MIME_TYPES:
        return False, f"MIME type '{mime_type}' is not allowed. Allowed: {', '.join(ALLOWED_MIME_TYPES)}"

    return True, "Valid"

JavaScript Validation Logic

const ALLOWED_MIME_TYPES = [
    'image/jpeg', 'image/png', 'image/gif', 'image/bmp', 'image/webp',
    'application/pdf', 'text/plain', 'application/zip'
];

const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10 MB

function validateFile(fileBuffer, fileName) {
    if (!fileBuffer || fileBuffer.length === 0) {
        return { isValid: false, error: "File buffer is empty." };
    }

    if (fileBuffer.length > MAX_FILE_SIZE) {
        return { isValid: false, error: `File size exceeds limit of ${MAX_FILE_SIZE} bytes.` };
    }

    // Simple MIME detection based on extension
    const extension = fileName.split('.').pop().toLowerCase();
    let mimeType = '';
    
    switch(extension) {
        case 'jpg': case 'jpeg': mimeType = 'image/jpeg'; break;
        case 'png': mimeType = 'image/png'; break;
        case 'gif': mimeType = 'image/gif'; break;
        case 'bmp': mimeType = 'image/bmp'; break;
        case 'webp': mimeType = 'image/webp'; break;
        case 'pdf': mimeType = 'application/pdf'; break;
        case 'txt': mimeType = 'text/plain'; break;
        case 'zip': mimeType = 'application/zip'; break;
        default: mimeType = 'application/octet-stream';
    }

    if (!ALLOWED_MIME_TYPES.includes(mimeType)) {
        return { isValid: false, error: `MIME type '${mimeType}' is not allowed.` };
    }

    return { isValid: true, error: null, mimeType };
}

Step 2: Upload the File Asset

In Genesys Cloud, you do not attach a file directly to a message payload in the same way you might with a standard HTTP multipart form post to a generic endpoint. Instead, you upload the file to the Genesys Cloud Assets service, which returns a fileId. You then reference this fileId in the Web Messaging conversation API.

The endpoint for uploading is POST /api/v2/files/assets.

Required Headers:

  • Content-Type: Must match the actual file MIME type (e.g., application/pdf).
  • Authorization: Bearer token.

Python File Upload

import requests
from purecloudplatform.client.rest import ApiException

def upload_file_asset(api_client: ApiClient, file_path: str, mime_type: str) -> str:
    """
    Uploads a file to Genesys Cloud Assets and returns the file ID.
    """
    # Get the base URL from the configuration
    base_url = api_client.configuration.host
    
    # The endpoint for asset upload
    upload_url = f"{base_url}/api/v2/files/assets"
    
    # Prepare headers
    headers = {
        'Content-Type': mime_type,
        'Authorization': f"Bearer {api_client.configuration.access_token}"
    }
    
    try:
        with open(file_path, 'rb') as f:
            file_data = f.read()
            
        # Send the file
        response = requests.post(
            upload_url,
            data=file_data,
            headers=headers
        )
        
        if response.status_code == 200:
            result = response.json()
            return result.get('id')
        else:
            raise Exception(f"Upload failed with status {response.status_code}: {response.text}")
            
    except ApiException as e:
        raise Exception(f"API Error: {e.body}")
    except Exception as e:
        raise e

JavaScript File Upload

const fs = require('fs');

async function uploadFileAsset(apiClient, filePath, mimeType) {
    const baseUrl = apiClient.configuration.host;
    const uploadUrl = `${baseUrl}/api/v2/files/assets`;

    const fileBuffer = fs.readFileSync(filePath);

    try {
        const response = await fetch(uploadUrl, {
            method: 'POST',
            headers: {
                'Content-Type': mimeType,
                'Authorization': `Bearer ${apiClient.configuration.accessToken}`
            },
            body: fileBuffer
        });

        if (!response.ok) {
            const errorText = await response.text();
            throw new Error(`Upload failed with status ${response.status}: ${errorText}`);
        }

        const result = await response.json();
        return result.id;
    } catch (error) {
        throw new Error(`Failed to upload file: ${error.message}`);
    }
}

Step 3: Send the File in a Web Messaging Conversation

Once you have the fileId, you must send a message in the Web Messaging conversation that references this asset. This is done via the POST /api/v2/conversations/messages endpoint.

You must ensure the conversationId is valid and active. The message body requires the type to be file and the attachments array to contain the fileId.

OAuth Scope: webmessaging

Python Send Message

from purecloudplatform.client import ConversationApi, MessageApi
from purecloudplatform.client.rest import ApiException
from purecloudplatform.client.model import ConversationMessagePost, ConversationMessageAttachment

def send_file_message(api_client: ApiClient, conversation_id: str, file_id: str, file_name: str) -> dict:
    """
    Sends a file message in an existing Web Messaging conversation.
    """
    message_api = MessageApi(api_client)
    
    # Create the attachment object
    attachment = ConversationMessageAttachment(
        file_id=file_id,
        file_name=file_name
    )
    
    # Create the message payload
    message_payload = ConversationMessagePost(
        type='file',
        attachments=[attachment]
    )
    
    try:
        # Send the message
        response = message_api.post_conversation_message(
            conversation_id=conversation_id,
            body=message_payload
        )
        return response.to_dict()
        
    except ApiException as e:
        if e.status == 400:
            raise Exception(f"Bad Request: Invalid conversation ID or file ID. Details: {e.body}")
        elif e.status == 403:
            raise Exception(f"Forbidden: Insufficient permissions. Details: {e.body}")
        else:
            raise Exception(f"API Error: {e.body}")

JavaScript Send Message

const { MessageApi } = require('@genesys/cloud-purecloud-platform-client');

async function sendFileMessage(apiClient, conversationId, fileId, fileName) {
    const messageApi = new MessageApi(apiClient);

    const messagePayload = {
        type: 'file',
        attachments: [
            {
                fileId: fileId,
                fileName: fileName
            }
        ]
    };

    try {
        const response = await messageApi.postConversationMessage(conversationId, messagePayload);
        return response.body;
    } catch (error) {
        if (error.status === 400) {
            throw new Error(`Bad Request: Invalid conversation ID or file ID. Details: ${error.body}`);
        } else if (error.status === 403) {
            throw new Error(`Forbidden: Insufficient permissions. Details: ${error.body}`);
        } else {
            throw new Error(`API Error: ${error.body}`);
        }
    }
}

Complete Working Example

Below is a complete Python script that validates, uploads, and sends a file in a Web Messaging conversation.

import os
import sys
import mimetypes
from purecloudplatform.client import Configuration, ApiClient, MessageApi
import requests

# Configuration
GENESYS_ENVIRONMENT = os.getenv("GENESYS_ENVIRONMENT", "us-east-1")
GENESYS_CLIENT_ID = os.getenv("GENESYS_CLIENT_ID")
GENESYS_CLIENT_SECRET = os.getenv("GENESYS_CLIENT_SECRET")
CONVERSATION_ID = os.getenv("GENESYS_CONVERSATION_ID")
FILE_PATH = os.getenv("FILE_PATH", "sample.pdf")

ALLOWED_MIME_TYPES = {
    'image/jpeg', 'image/png', 'image/gif', 'image/bmp', 'image/webp',
    'application/pdf', 'text/plain', 'application/zip'
}
MAX_FILE_SIZE = 10 * 1024 * 1024  # 10 MB

def init_api_client():
    config = Configuration(
        environment=GENESYS_ENVIRONMENT,
        client_id=GENESYS_CLIENT_ID,
        client_secret=GENESYS_CLIENT_SECRET
    )
    return ApiClient(configuration=config)

def validate_file(file_path):
    if not os.path.exists(file_path):
        raise FileNotFoundError(f"File not found: {file_path}")
    
    file_size = os.path.getsize(file_path)
    if file_size > MAX_FILE_SIZE:
        raise ValueError(f"File size {file_size} exceeds limit of {MAX_FILE_SIZE}")
    
    mime_type, _ = mimetypes.guess_type(file_path)
    if not mime_type or mime_type not in ALLOWED_MIME_TYPES:
        raise ValueError(f"MIME type '{mime_type}' is not allowed.")
        
    return mime_type

def upload_file(api_client, file_path, mime_type):
    base_url = api_client.configuration.host
    upload_url = f"{base_url}/api/v2/files/assets"
    
    headers = {
        'Content-Type': mime_type,
        'Authorization': f"Bearer {api_client.configuration.access_token}"
    }
    
    with open(file_path, 'rb') as f:
        file_data = f.read()
        
    response = requests.post(upload_url, data=file_data, headers=headers)
    
    if response.status_code != 200:
        raise RuntimeError(f"Upload failed: {response.status_code} - {response.text}")
        
    return response.json()['id']

def send_message(api_client, conversation_id, file_id, file_name):
    message_api = MessageApi(api_client)
    
    from purecloudplatform.client.model import ConversationMessagePost, ConversationMessageAttachment
    
    attachment = ConversationMessageAttachment(
        file_id=file_id,
        file_name=file_name
    )
    
    message_payload = ConversationMessagePost(
        type='file',
        attachments=[attachment]
    )
    
    response = message_api.post_conversation_message(
        conversation_id=conversation_id,
        body=message_payload
    )
    return response

def main():
    if not all([GENESYS_CLIENT_ID, GENESYS_CLIENT_SECRET, CONVERSATION_ID]):
        print("Error: Missing environment variables.")
        sys.exit(1)
        
    api_client = init_api_client()
    
    try:
        # Step 1: Validate
        print(f"Validating {FILE_PATH}...")
        mime_type = validate_file(FILE_PATH)
        print(f"MIME Type: {mime_type}")
        
        # Step 2: Upload
        print("Uploading file...")
        file_id = upload_file(api_client, FILE_PATH, mime_type)
        print(f"File ID: {file_id}")
        
        # Step 3: Send
        file_name = os.path.basename(FILE_PATH)
        print(f"Sending file to conversation {CONVERSATION_ID}...")
        send_message(api_client, CONVERSATION_ID, file_id, file_name)
        print("Message sent successfully.")
        
    except Exception as e:
        print(f"Error: {e}")
        sys.exit(1)

if __name__ == "__main__":
    main()

Common Errors & Debugging

Error: 400 Bad Request - Invalid MIME Type

  • Cause: The Content-Type header in the upload request does not match the actual file content, or the MIME type is not in the allowed list.
  • Fix: Ensure the mimetypes.guess_type result is correct. If the file has no extension, explicitly set the correct MIME type in the upload headers. Verify the MIME type is in ALLOWED_MIME_TYPES.

Error: 400 Bad Request - File Too Large

  • Cause: The file exceeds the 10 MB limit.
  • Fix: Implement client-side validation before upload. Compress the file if possible (e.g., convert PNG to JPEG, zip multiple files).

Error: 403 Forbidden - Insufficient Scopes

  • Cause: The OAuth token lacks the webmessaging or files:write scope.
  • Fix: Regenerate the access token with the correct scopes. Ensure the Application in Genesys Cloud Admin has these scopes granted.

Error: 404 Not Found - Conversation Not Found

  • Cause: The conversationId provided is invalid or the conversation has ended/expired.
  • Fix: Verify the conversation ID is from an active Web Messaging session. Check the conversation status in the Genesys Cloud Admin console.

Error: 429 Too Many Requests

  • Cause: Rate limiting due to excessive upload attempts.
  • Fix: Implement exponential backoff retry logic. Do not retry immediately; wait 1-2 seconds before the first retry, doubling the wait time for subsequent retries.

Official References

2 Likes