Hand off a conversation
Changes who answers a conversation's incoming messages: agent (your VoiceLayer agent replies), api (your code replies) or human (a person replies from the dashboard). Use it to escalate to a person, or to take a conversation over from the agent. Each change emits one conversation.controller_changed event.
Pass expectedController to change it only if nobody else already has: otherwise the request is refused with 409 controller_conflict.
Requires an API key with the conversations:control scope.
apiKeyAuthorizationBearer <token>A secret API key from the dashboard (Developers → API keys): vl_live_…, or vl_test_… for test mode. Each operation says which scope the key needs.
id*stringThe conversation id.
idempotency-key?stringA unique value (a UUID works) that makes a retry safe: sending the same key again returns the first result instead of acting twice. 1–255 printable characters.
1 <= length <= 255application/json- body
controller*stringWho answers from now on: agent, api or human.
"agent""api""human"reason?stringWhy, in a sentence. Shown to the person who takes over and recorded in the event.
1 <= length <= 500expectedController?stringChange it only if this is the current controller. Use it to avoid two handoffs racing each other.
"agent""api""human"The conversation after the change
application/json- response
conversation*changed*booleancurl -X PATCH "https://api.vlayers.ai/v1/conversations/$CONVERSATION_ID" \ -H "Authorization: Bearer $VOICELAYER_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "controller": "human", "reason": "The customer asked for a refund." }'{ "conversation": { "id": "0192f5b0-7c3e-7a51-9b2d-4e8f1a6c3d21", "channel": "sms", "lineId": "0192f5a8-1a2b-7c3d-8e4f-5a6b7c8d9e01", "bindingId": null, "peer": { "address": "+15550123" }, "controller": "human", "status": "open", "openedAt": "2026-10-20T15:04:05.000Z", "lastActivityAt": "2026-10-20T15:04:05.000Z", "test": true }, "changed": true}