# Hand off a conversation

Source: https://docs.vlayers.ai/reference/conversations/update-conversation

`PATCH /v1/conversations/{id}` · scope `conversations:control`

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.

| Name | In | Type | Required | Description |
|---|---|---|---|---|
| `id` | path | string | yes | The conversation id. Example: `"0192f5b0-7c3e-7a51-9b2d-4e8f1a6c3d21"` |
| `idempotency-key` | header | string | no | A 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. Example: `"7f3c9a2e-5b1d-4c8e-9f0a-2d6b8e4c1a37"` |
| `controller` | body | "agent" \| "api" \| "human" | yes | Who answers from now on: `agent`, `api` or `human`. |
| `reason` | body | string | no | Why, in a sentence. Shown to the person who takes over and recorded in the event. |
| `expectedController` | body | "agent" \| "api" \| "human" | no | Change it only if this is the current controller. Use it to avoid two handoffs racing each other. |

## Request

cURL:

```bash
curl -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."
  }'
```

TypeScript:

```ts
import { createVoiceLayerClient, updateConversation } from '@voicelayer/api';

const client = createVoiceLayerClient({ apiKey: process.env.VOICELAYER_API_KEY!, throwOnError: true });

const { data } = await updateConversation({
  client,
  path: {
    id: process.env.CONVERSATION_ID!
  },
  body: {
    controller: 'human',
    reason: 'The customer asked for a refund.'
  }
});
console.log(data.conversation.controller);
```

Python:

```python
import os
import voicelayer

config = voicelayer.Configuration(access_token=os.environ["VOICELAYER_API_KEY"])
with voicelayer.ApiClient(config) as client:
    api = voicelayer.ConversationsApi(client)
    result = api.update_conversation(
        id=os.environ["CONVERSATION_ID"],
        update_conversation_request=voicelayer.UpdateConversationRequest.from_dict({
            "controller": "human",
            "reason": "The customer asked for a refund.",
        }),
    )
    print(result.conversation.controller)
```

## Response `200`

```json
{
  "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
}
```
