Programmatically Start and Stop Call Recordings via the Genesys Cloud Recording API

Programmatically Start and Stop Call Recordings via the Genesys Cloud Recording API

What You Will Build

  • A script that initiates a recording for an active conversation and stops it on command.
  • This tutorial uses the Genesys Cloud CX Recording API (/api/v2/recordings).
  • The implementation covers Python using the genesyscloud SDK and raw httpx for HTTP-level control.

Prerequisites

Before writing code, ensure your environment meets these requirements.

  • OAuth Client: You need a Genesys Cloud OAuth client with the client_credentials grant type.
  • Required Scopes:
    • recording:read (to list or check status)
    • recording:write (to start or stop recordings)
    • conversation:read (optional, if you need to correlate conversation metadata)
  • SDK Version: genesyscloud >= 2.0.0 (Python).
  • Runtime: Python 3.8+.
  • Dependencies:
    pip install genesyscloud httpx
    

Authentication Setup

Genesys Cloud uses OAuth 2.0. For server-to-server integrations, the client_credentials flow is standard. You must cache the access token and handle expiration. The following example uses httpx to manage the token lifecycle, ensuring you do not hit rate limits by requesting a new token on every API call.

import httpx
import time
import os

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: str | None = None
        self.token_expiry: float = 0
        self.http_client = httpx.Client()

    def get_access_token(self) -> str:
        """
        Retrieves an OAuth access token. Caches it until 60 seconds before expiry
        to prevent race conditions during API calls.
        """
        if self.access_token and time.time() < self.token_expiry - 60:
            return self.access_token

        try:
            response = self.http_client.post(
                self.token_url,
                data={"grant_type": "client_credentials"},
                auth=(self.client_id, self.client_secret),
                headers={"Content-Type": "application/x-www-form-urlencoded"}
            )
            response.raise_for_status()
            data = response.json()
            
            self.access_token = data["access_token"]
            # Genesys tokens expire in 3600s by default. Subtract buffer.
            self.token_expiry = time.time() + data["expires_in"] - 60
            return self.access_token

        except httpx.HTTPStatusError as e:
            raise Exception(f"OAuth authentication failed: {e.response.status_code} - {e.response.text}") from e

# Usage
# auth = GenesysAuth(os.getenv("GENESYS_CLIENT_ID"), os.getenv("GENESYS_CLIENT_SECRET"))
# token = auth.get_access_token()

Implementation

Step 1: Identify the Active Conversation

You cannot record a conversation unless you know its ID. In a real-world scenario, you would hook into the Conversations API or use Webhooks to detect when a call connects. For this tutorial, we assume you have an active conversation ID. If you do not, you can list active conversations to find one.

Endpoint: GET /api/v2/conversations/voice/active
Scope: conversation:read

def get_active_voice_conversation(auth: GenesysAuth) -> str | None:
    """
    Retrieves the first active voice conversation ID.
    Returns None if no active conversations are found.
    """
    token = auth.get_access_token()
    headers = {
        "Authorization": f"Bearer {token}",
        "Accept": "application/json"
    }
    
    url = f"{auth.base_url}/api/v2/conversations/voice/active"
    
    try:
        response = auth.http_client.get(url, headers=headers)
        response.raise_for_status()
        
        data = response.json()
        entities = data.get("entities", [])
        
        if not entities:
            print("No active voice conversations found.")
            return None
            
        # Return the ID of the first active conversation
        conv_id = entities[0]["id"]
        print(f"Found active conversation: {conv_id}")
        return conv_id

    except httpx.HTTPStatusError as e:
        if e.response.status_code == 401:
            print("Token expired or invalid. Refreshing token and retrying...")
            # In a robust implementation, you would call auth.get_access_token() again here
            # and retry the request. For brevity, we raise.
            raise
        raise Exception(f"Failed to fetch conversations: {e}") from e

Step 2: Start the Recording

Once you have the conversation ID, you initiate the recording. The Genesys Cloud Recording API requires you to specify the conversationId and the mediaType (usually voice for phone calls, screen for desktop sharing).

Endpoint: POST /api/v2/recordings
Scope: recording:write
Request Body: JSON payload containing conversationId and mediaType.

def start_recording(auth: GenesysAuth, conversation_id: str, media_type: str = "voice") -> str | None:
    """
    Starts a recording for the specified conversation.
    Returns the recording ID if successful, None otherwise.
    """
    token = auth.get_access_token()
    headers = {
        "Authorization": f"Bearer {token}",
        "Content-Type": "application/json",
        "Accept": "application/json"
    }
    
    url = f"{auth.base_url}/api/v2/recordings"
    
    payload = {
        "conversationId": conversation_id,
        "mediaType": media_type
    }
    
    try:
        response = auth.http_client.post(url, json=payload, headers=headers)
        
        # 201 Created is the standard success response for resource creation
        if response.status_code == 201:
            data = response.json()
            recording_id = data.get("id")
            print(f"Recording started successfully. ID: {recording_id}")
            return recording_id
        else:
            print(f"Failed to start recording. Status: {response.status_code}")
            print(f"Response: {response.text}")
            return None

    except httpx.HTTPStatusError as e:
        # Handle specific errors
        if e.response.status_code == 409:
            print("Recording already exists for this conversation or media type.")
        elif e.response.status_code == 404:
            print("Conversation not found.")
        else:
            print(f"HTTP Error: {e.response.status_code} - {e.response.text}")
        return None
    except Exception as e:
        print(f"Unexpected error starting recording: {e}")
        return None

Step 3: Stop the Recording

To stop a recording, you must know the recordingId (returned when you started it) and the conversationId. You also need to provide the stopReason. Common reasons include user-requested, timeout, or error.

Endpoint: POST /api/v2/recordings/{recordingId}/stop
Scope: recording:write

def stop_recording(auth: GenesysAuth, conversation_id: str, recording_id: str, stop_reason: str = "user-requested") -> bool:
    """
    Stops an existing recording.
    Returns True if successful, False otherwise.
    """
    token = auth.get_access_token()
    headers = {
        "Authorization": f"Bearer {token}",
        "Content-Type": "application/json",
        "Accept": "application/json"
    }
    
    # The endpoint requires both the recording ID in the path and the conversation ID in the body
    url = f"{auth.base_url}/api/v2/recordings/{recording_id}/stop"
    
    payload = {
        "conversationId": conversation_id,
        "stopReason": stop_reason
    }
    
    try:
        response = auth.http_client.post(url, json=payload, headers=headers)
        
        # 204 No Content is expected for successful stop operations
        if response.status_code == 204:
            print(f"Recording {recording_id} stopped successfully.")
            return True
        else:
            print(f"Failed to stop recording. Status: {response.status_code}")
            print(f"Response: {response.text}")
            return False

    except httpx.HTTPStatusError as e:
        if e.response.status_code == 404:
            print("Recording or Conversation not found. It may have already ended.")
        elif e.response.status_code == 400:
            print("Bad Request. Check if the recording is already stopped.")
        else:
            print(f"HTTP Error: {e.response.status_code} - {e.response.text}")
        return False
    except Exception as e:
        print(f"Unexpected error stopping recording: {e}")
        return False

Complete Working Example

This script combines the authentication, discovery, start, and stop logic. It simulates a workflow where it finds a call, records it for 10 seconds, and then stops it.

Note: In a production environment, you would not hardcode the wait time. You would trigger the stop via an event (e.g., button click in a GUI, webhook from agent, or conversation end).

import os
import time
import sys
from typing import Optional

# Import the classes defined in the previous sections
# In a real file, ensure GenesysAuth, get_active_voice_conversation, 
# start_recording, and stop_recording are defined or imported.

def main():
    # 1. Setup Credentials
    client_id = os.getenv("GENESYS_CLIENT_ID")
    client_secret = os.getenv("GENESYS_CLIENT_SECRET")
    
    if not client_id or not client_secret:
        print("Error: GENESYS_CLIENT_ID and GENESYS_CLIENT_SECRET environment variables are required.")
        sys.exit(1)

    # Initialize Authentication
    auth = GenesysAuth(client_id, client_secret)

    try:
        # 2. Find an active conversation
        print("Looking for an active voice conversation...")
        conversation_id = get_active_voice_conversation(auth)
        
        if not conversation_id:
            print("No active conversations found. Please place a test call and run again.")
            return

        # 3. Start Recording
        print("Starting recording...")
        recording_id = start_recording(auth, conversation_id)
        
        if not recording_id:
            print("Could not start recording. Aborting.")
            return

        # 4. Wait for a duration (Simulating the call duration)
        print(f"Recording is active for 10 seconds...")
        time.sleep(10)

        # 5. Stop Recording
        print("Stopping recording...")
        success = stop_recording(auth, conversation_id, recording_id)
        
        if success:
            print("Workflow completed successfully.")
        else:
            print("Failed to stop recording cleanly.")

    except Exception as e:
        print(f"Critical failure in workflow: {e}")
        sys.exit(1)

if __name__ == "__main__":
    main()

Common Errors & Debugging

Error: 409 Conflict

  • What causes it: You attempted to start a recording for a conversation that already has an active recording of the same mediaType. Genesys Cloud does not allow multiple concurrent recordings of the same type for a single conversation.
  • How to fix it: Check the recording status before starting. If a recording exists, either stop it first or skip starting a new one.
  • Code Fix:
    # Before starting, check existing recordings
    def check_existing_recording(auth: GenesysAuth, conversation_id: str) -> bool:
        # GET /api/v2/recordings/conversations/{conversationId}/recordings
        # If the list is not empty, a recording exists.
        pass
    

Error: 403 Forbidden

  • What causes it: The OAuth token does not have the recording:write scope.
  • How to fix it: Go to the Genesys Cloud Admin Console > Platform > Applications > OAuth. Edit your client and ensure recording:write is checked. Re-generate the token.

Error: 404 Not Found

  • What causes it: The conversationId or recordingId is invalid. This often happens if the call ended while your script was still running, or if you are using a stale ID.
  • How to fix it: Verify the conversation is still active. If you are stopping a recording, ensure the recording has not already been automatically stopped due to conversation termination.

Error: 429 Too Many Requests

  • What causes it: You are exceeding the API rate limits. The Recording API has specific limits per tenant.
  • How to fix it: Implement exponential backoff in your HTTP client.
  • Code Fix:
    import time
    
    def make_request_with_retry(auth: GenesysAuth, method: str, url: str, **kwargs) -> httpx.Response:
        max_retries = 3
        for attempt in range(max_retries):
            try:
                response = getattr(auth.http_client, method)(url, **kwargs)
                if response.status_code == 429:
                    wait_time = 2 ** attempt
                    print(f"Rate limited. Waiting {wait_time} seconds...")
                    time.sleep(wait_time)
                    continue
                return response
            except httpx.HTTPStatusError as e:
                if e.response.status_code == 429:
                    wait_time = 2 ** attempt
                    print(f"Rate limited. Waiting {wait_time} seconds...")
                    time.sleep(wait_time)
                    continue
                raise
        raise Exception("Max retries exceeded due to rate limiting.")
    

Official References