How to Handle File Attachments in Genesys Cloud Web Messaging with Python

How to Handle File Attachments in Genesys Cloud Web Messaging with Python

What You Will Build

  • A Python script that authenticates with Genesys Cloud, uploads a binary file to the media service, and sends a Web Messaging conversation message containing the file attachment reference.
  • This tutorial uses the Genesys Cloud Platform API v2 and the genesys-cloud-py SDK.
  • The implementation covers MIME type validation, size limit enforcement, and the two-step process required to attach files to messages.

Prerequisites

  • OAuth Client Type: Public or Confidential Client with agent:conversation:read and analytics:conversation:details:read scopes are not sufficient for sending. You need conversation:send and media:upload scopes.
  • SDK Version: genesys-cloud-py version 5.0.0 or higher.
  • Language/Runtime: Python 3.8+.
  • External Dependencies:
    • genesys-cloud-py
    • requests
    • python-magic (for robust MIME type detection, though standard mimetypes is used in examples for simplicity).

Authentication Setup

Genesys Cloud uses OAuth 2.0 for API access. For file uploads in Web Messaging, you typically act on behalf of an agent or a bot. The most common flow for server-side integrations is the Client Credentials flow.

import os
from purecloud_platform_client import PureCloudPlatformClientV2, Configuration

def get_platform_client():
    """
    Initializes the Genesys Cloud Platform Client using Client Credentials.
    """
    client_id = os.environ.get("GENESYS_CLIENT_ID")
    client_secret = os.environ.get("GENESYS_CLIENT_SECRET")
    environment = os.environ.get("GENESYS_ENVIRONMENT", "mypurecloud.com")

    if not client_id or not client_secret:
        raise ValueError("GENESYS_CLIENT_ID and GENESYS_CLIENT_SECRET environment variables are required.")

    config = Configuration(
        host=f"https://{environment}",
        client_id=client_id,
        client_secret=client_secret
    )

    platform_client = PureCloudPlatformClientV2(config)
    return platform_client

OAuth Scopes Required:

  • conversation:send: To post messages to the Web Messaging conversation.
  • media:upload: To upload binary data to the Genesys Cloud media service.

Implementation

Step 1: Validate File Constraints Before Upload

Genesys Cloud Web Messaging imposes strict limits on file attachments. Attempting to upload a file that violates these constraints will result in an HTTP 400 Bad Request or 413 Payload Too Large.

Constraints:

  • Maximum Size: 10 MB (10,485,760 bytes).
  • Allowed MIME Types:
    • Images: image/png, image/jpeg, image/gif, image/webp
    • Documents: application/pdf, text/plain, text/csv
    • Archives: application/zip, application/x-zip-compressed
    • Office Docs: application/msword, application/vnd.openxmlformats-officedocument.wordprocessingml.document (docx), etc.

Always validate on the client side before initiating the network request to save bandwidth and avoid API errors.

import os
import mimetypes
from typing import Tuple

ALLOWED_MIME_TYPES = {
    'image/png',
    'image/jpeg',
    'image/gif',
    'image/webp',
    'application/pdf',
    'text/plain',
    'text/csv',
    'application/zip',
    'application/x-zip-compressed',
    'application/msword',
    'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
    'application/vnd.ms-excel',
    'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
}

MAX_FILE_SIZE_BYTES = 10 * 1024 * 1024  # 10 MB

def validate_file(file_path: str) -> Tuple[bool, str]:
    """
    Validates the file size and MIME type against Genesys Cloud constraints.
    
    Returns:
        Tuple[bool, str]: (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_BYTES:
        return False, f"File size {file_size} bytes exceeds the 10 MB limit."

    # Guess MIME type based on extension
    mime_type, _ = mimetypes.guess_type(file_path)
    
    if mime_type is None:
        return False, "Could not determine MIME type. Ensure the file has a valid extension."
    
    if mime_type not in ALLOWED_MIME_TYPES:
        return False, f"MIME type '{mime_type}' is not allowed. Allowed types: {ALLOWED_MIME_TYPES}"

    return True, "Valid"

Step 2: Upload the File to Genesys Cloud Media Service

Genesys Cloud does not accept binary data directly in the Web Messaging message body. You must first upload the file to the Genesys Cloud Media Service using the POST /api/v2/media/upload endpoint. This returns a mediaUri which is then referenced in the message.

This step requires the media:upload scope.

import requests
from purecloud_platform_client import PureCloudPlatformClientV2

def upload_file_to_media_service(platform_client: PureCloudPlatformClientV2, file_path: str) -> str:
    """
    Uploads a file to the Genesys Cloud Media Service.
    
    Args:
        platform_client: Authenticated PureCloudPlatformClientV2 instance.
        file_path: Path to the local file.
        
    Returns:
        str: The mediaUri of the uploaded file.
    """
    # Get the access token from the platform client
    # Note: In production, handle token refresh logic if the token expires mid-operation
    token = platform_client.auth_client.get_access_token()
    if not token:
        raise Exception("Failed to retrieve access token.")

    url = f"{platform_client.configuration.host}/api/v2/media/upload"
    
    headers = {
        "Authorization": f"Bearer {token}",
        "Accept": "application/json"
        # Content-Type is set automatically by requests when using files=
    }

    # Open file in binary mode
    with open(file_path, 'rb') as f:
        files = {
            'file': (os.path.basename(file_path), f, 'application/octet-stream')
        }
        
        try:
            response = requests.post(url, headers=headers, files=files)
            response.raise_for_status()
        except requests.exceptions.HTTPError as e:
            if response.status_code == 413:
                raise Exception("File too large. Genesys Cloud limit is 10 MB.")
            elif response.status_code == 400:
                raise Exception(f"Bad Request: {response.text}")
            else:
                raise Exception(f"Upload failed with status {response.status_code}: {response.text}")

    data = response.json()
    
    # The response contains the mediaUri
    if 'mediaUri' not in data:
        raise Exception("Unexpected response format from media upload service.")
        
    return data['mediaUri']

Expected Response:

{
  "mediaUri": "https://mypurecloud.com/api/v2/media/attachments/abc-123-def-456",
  "fileName": "invoice.pdf",
  "fileSize": 102400,
  "mimeType": "application/pdf"
}

Step 3: Send the Web Messaging Message with Attachment

Once you have the mediaUri, you can send a message to the Web Messaging conversation. You must use the POST /api/v2/conversations/messages endpoint. The message body must include the attachments array with the mediaUri.

This step requires the conversation:send scope.

from purecloud_platform_client import MessageBody, Attachment

def send_web_messaging_message(platform_client: PureCloudPlatformClientV2, conversation_id: str, media_uri: str, file_name: str) -> dict:
    """
    Sends a Web Messaging message with a file attachment.
    
    Args:
        platform_client: Authenticated PureCloudPlatformClientV2 instance.
        conversation_id: The ID of the Web Messaging conversation.
        media_uri: The mediaUri returned from the upload step.
        file_name: The original name of the file for display purposes.
        
    Returns:
        dict: The response from the Genesys Cloud API.
    """
    from purecloud_platform_client import ApiClient
    
    # Initialize the ConversationApi client
    conversation_api = platform_client.ConversationApi()
    
    # Create the attachment object
    attachment = Attachment()
    attachment.media_uri = media_uri
    attachment.file_name = file_name
    
    # Create the message body
    message_body = MessageBody()
    message_body.attachments = [attachment]
    # Optional: Add text alongside the attachment
    message_body.text = "Please find the attached document."
    
    try:
        # Send the message
        response = conversation_api.post_conversations_messages(
            conversation_id=conversation_id,
            body=message_body,
            conversation_type="webmessaging" # Explicitly specify type if needed, though usually inferred from ID
        )
        return response
    except Exception as e:
        print(f"Failed to send message: {e}")
        raise

Expected Request Body:

{
  "text": "Please find the attached document.",
  "attachments": [
    {
      "mediaUri": "https://mypurecloud.com/api/v2/media/attachments/abc-123-def-456",
      "fileName": "invoice.pdf"
    }
  ]
}

Complete Working Example

This script combines validation, upload, and message sending into a single workflow. It assumes you have a valid conversation_id for an active Web Messaging session.

import os
import sys
import mimetypes
from typing import Tuple

# Import Genesys Cloud SDK
from purecloud_platform_client import PureCloudPlatformClientV2, Configuration, MessageBody, Attachment
import requests

# --- Configuration ---
ALLOWED_MIME_TYPES = {
    'image/png', 'image/jpeg', 'image/gif', 'image/webp',
    'application/pdf', 'text/plain', 'text/csv',
    'application/zip', 'application/x-zip-compressed',
    'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
}
MAX_FILE_SIZE_BYTES = 10 * 1024 * 1024

def get_platform_client():
    client_id = os.environ.get("GENESYS_CLIENT_ID")
    client_secret = os.environ.get("GENESYS_CLIENT_SECRET")
    environment = os.environ.get("GENESYS_ENVIRONMENT", "mypurecloud.com")

    if not client_id or not client_secret:
        raise ValueError("Set GENESYS_CLIENT_ID and GENESYS_CLIENT_SECRET env vars.")

    config = Configuration(
        host=f"https://{environment}",
        client_id=client_id,
        client_secret=client_secret
    )
    return PureCloudPlatformClientV2(config)

def validate_file(file_path: str) -> Tuple[bool, str]:
    if not os.path.exists(file_path):
        return False, "File not found."
    if os.path.getsize(file_path) > MAX_FILE_SIZE_BYTES:
        return False, "File exceeds 10 MB limit."
    mime_type, _ = mimetypes.guess_type(file_path)
    if not mime_type or mime_type not in ALLOWED_MIME_TYPES:
        return False, f"MIME type '{mime_type}' not allowed."
    return True, "OK"

def upload_file(platform_client: PureCloudPlatformClientV2, file_path: str) -> str:
    token = platform_client.auth_client.get_access_token()
    if not token:
        raise Exception("No access token available.")
    
    url = f"{platform_client.configuration.host}/api/v2/media/upload"
    headers = {"Authorization": f"Bearer {token}", "Accept": "application/json"}
    
    with open(file_path, 'rb') as f:
        files = {'file': (os.path.basename(file_path), f, 'application/octet-stream')}
        response = requests.post(url, headers=headers, files=files)
        response.raise_for_status()
        
    data = response.json()
    if 'mediaUri' not in data:
        raise Exception("Invalid upload response.")
    return data['mediaUri']

def send_message(platform_client: PureCloudPlatformClientV2, conversation_id: str, media_uri: str, file_name: str) -> dict:
    conversation_api = platform_client.ConversationApi()
    
    attachment = Attachment()
    attachment.media_uri = media_uri
    attachment.file_name = file_name
    
    body = MessageBody()
    body.attachments = [attachment]
    body.text = "Here is your file."
    
    try:
        response = conversation_api.post_conversations_messages(
            conversation_id=conversation_id,
            body=body,
            conversation_type="webmessaging"
        )
        return response
    except Exception as e:
        print(f"Message send error: {e}")
        raise

def main():
    if len(sys.argv) != 3:
        print("Usage: python send_attachment.py <conversation_id> <file_path>")
        sys.exit(1)

    conversation_id = sys.argv[1]
    file_path = sys.argv[2]

    # 1. Validate
    is_valid, msg = validate_file(file_path)
    if not is_valid:
        print(f"Validation failed: {msg}")
        sys.exit(1)

    # 2. Authenticate
    platform_client = get_platform_client()

    # 3. Upload
    try:
        media_uri = upload_file(platform_client, file_path)
        print(f"File uploaded successfully. Media URI: {media_uri}")
    except Exception as e:
        print(f"Upload failed: {e}")
        sys.exit(1)

    # 4. Send Message
    try:
        response = send_message(platform_client, conversation_id, media_uri, os.path.basename(file_path))
        print(f"Message sent successfully. Message ID: {response.id if hasattr(response, 'id') else 'N/A'}")
    except Exception as e:
        print(f"Send failed: {e}")
        sys.exit(1)

if __name__ == "__main__":
    main()

Common Errors & Debugging

Error: 400 Bad Request on Media Upload

  • Cause: The file MIME type is not supported, or the file is corrupted.
  • Fix: Verify the file extension matches the content. Genesys Cloud checks the header and sometimes the magic bytes. Ensure you are using one of the ALLOWED_MIME_TYPES listed in Step 1.
  • Code Fix: Add explicit Content-Type in the files tuple if mimetypes.guess_type is failing.
# Explicitly set MIME type in the files tuple
files = {
    'file': (os.path.basename(file_path), f, 'application/pdf') # Hardcoded for testing
}

Error: 413 Payload Too Large

  • Cause: The file exceeds the 10 MB limit.
  • Fix: Compress the file or split it into smaller parts. Genesys Cloud Web Messaging does not support chunked uploads for files larger than 10 MB in this context.

Error: 401 Unauthorized on Message Send

  • Cause: The OAuth token does not have the conversation:send scope.
  • Fix: Check your OAuth client configuration in the Genesys Cloud Admin Console. Go to Setup > Applications > OAuth > [Your Client] and ensure conversation:send is checked.

Error: 403 Forbidden

  • Cause: The user associated with the OAuth token does not have permission to send messages in the target conversation, or the conversation is closed.
  • Fix: Ensure the conversation is active. For Web Messaging, the conversation must be in a state that accepts messages (e.g., ACTIVE or RESOLVED if history is allowed, but typically ACTIVE).

Official References