Is it possible to selectively disconnect a single participant from an active conference call using the Genesys Cloud Conversations API without terminating the entire session?
I am building an async proxy service using Python FastAPI and httpx to handle interaction control for a contact center integration. The current requirement is to allow supervisors to ‘kick’ a specific agent from a conference while keeping the customer and other parties connected.
I am using the DELETE /api/v2/conversations/{conversationId}/participants/{participantId} endpoint. My implementation retrieves the conversation ID from the webhook payload, identifies the target participant ID, and issues the deletion request with a valid Bearer token and the correct x-gcc-division-id header.
HTTP 409 Conflict: Participant cannot be removed at this time.
The error response indicates that the participant removal is not permitted, but the documentation for the Conversations API is vague regarding the specific state constraints that trigger this conflict. I have verified that the conversation is in an active state and the participant is not the last remaining one.
I suspect this might be related to the call media type (e.g., telephony vs web) or the current call flow state. Has anyone successfully implemented selective participant removal in a conference scenario? If so, are there specific preconditions or alternative endpoints (such as using the Interaction API) that I should be using instead of the standard participant deletion endpoint?
What’s happening here is that you are likely hitting a state lock on the conversation resource while trying to mutate it via a standard delete or update call.
In Go, I bypass this by using the DeleteParticipant endpoint with explicit retry logic to handle the 409 Conflict.
resp, err := platformClient.ConversationsApi.DeleteConversationParticipant(conversationID, participantID, nil, nil, nil)
if err != nil {
// Implement exponential backoff here
}
Verify the participant ID matches the internal Genesys ID and not an external channel ID.
The easiest way to fix this is to ensure your proxy implements exponential backoff for the 409 response, as the platform’s internal state lock resolves within milliseconds; raw retries without jitter will just hammer the API.
Relying on exponential backoff is technically sound, yet it often fails to address the underlying state synchronization issue within the conference bridge. The platform requires the participant to be in a stable state before the disconnect action can be processed without conflict.
You must implement a pre-check using GetRESTProxy to verify the participant’s current leg status. If the leg is still initializing or changing state, the disconnect request will inevitably fail with a 409 error regardless of your retry logic.
As far as I remember, the 409 Conflict isn’t just about timing; it’s about the participant’s current leg state. You cannot delete a participant who is still in a connecting or ringing state. The API rejects the mutation because the bridge hasn’t fully established the leg yet. Instead of blind retries, you need to poll the conversation details until the specific participant’s state is connected or on_hold, then execute the delete.
Here is a robust Python httpx snippet that handles this state check before attempting the removal:
import httpx
import asyncio
async def kick_participant(client: httpx.AsyncClient, conv_id: str, part_id: str):
# First, verify the participant is in a stable state
async with client.get(f"/api/v2/conversations/conversations/{conv_id}") as resp:
data = resp.json()
participant = next((p for p in data["participants"] if p["id"] == part_id), None)
if not participant:
raise ValueError("Participant not found in conversation")
# Wait until state is stable (connected, on_hold, etc.)
while participant.get("state") in ["connecting", "ringing"]:
await asyncio.sleep(0.5)
# Re-fetch to check state change
async with client.get(f"/api/v2/conversations/conversations/{conv_id}") as resp:
data = resp.json()
participant = next((p for p in data["participants"] if p["id"] == part_id), None)
# Now safely delete
async with client.delete(f"/api/v2/conversations/conversations/{conv_id}/participants/{part_id}") as resp:
if resp.status_code != 204:
raise Exception(f"Failed to kick: {resp.status_code}")
# Note: Ensure your client has the 'conversations:write' scope
The key is the while loop checking the state field. If you try to delete while the state is connecting, Genesys returns 409. Once it hits connected, the delete call succeeds immediately. Also, make sure your FastAPI proxy is passing the correct OAuth token with conversations:write scope, otherwise you’ll get 403s before you even hit the logic. This approach is cleaner than exponential backoff because it targets the root cause.