> For the complete documentation index, see [llms.txt](https://docs.maiagent.ai/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.maiagent.ai/api/ja/preparation/handoff-webhook.md).

# 有人対応への引き継ぎ Webhook イベント

会話の有人対応への切り替えライフサイクル（AI による切り替え判断、担当者への割り当て、AI への復帰、待機タイムアウト）を Webhook で指定したシステムのエンドポイントに送信します。イベントの種類と形式、署名検証、再試行の動作、および切り替え後に外部システムで会話を引き継ぐ連携方法を説明します。

有人対応への切り替えの各段階（AI による有人対応が必要との判断、担当者への割り当て、AI への復帰、待機タイムアウト）で、MaiAgent は設定したエンドポイントに HTTPS POST でイベントを送信します。これにより、ポーリングなしで、担当者の手配、アラート、CRM、または独自のカスタマーサポートシステムが自動的に対応を引き継げます。イベント名は有人対応への切り替えの状態遷移を表し、固定の公開契約です。各イベントには、会話イベント履歴 API（`GET /api/v1/conversations/{id}/events/`）の対応する `event_type` があります（下表参照）。照合や、同じ情報をポーリングで取得する際に使用できます。

## 有効化する方法

お客様の管理者または連携担当者が管理画面で設定できます。エンジニアによる支援は不要です。

1. 管理画面 → 設定 → 受信トレイ → 対象の受信トレイを選択 → 「有人対応への切り替え」タブ → 「通知」サブタブ
2. 「Webhook 通知（システム連携）」をオンにし、受信側の URL を入力します（`https://` で始まる URL のみ使用できます）
3. （任意ですが推奨します）署名用シークレットを入力して送信元の検証を有効にします（下記の「署名検証」を参照してください）

Webhook、通知センター、Email はそれぞれ独立したスイッチで、互いに影響しません。Webhook はデフォルトで無効で、受信トレイごとに有効化します。このチャネルは、受信トレイに既存の「メッセージ Webhook」（メッセージの送受信）とは別のものです。

## イベントの種類 <a href="#event-types" id="event-types"></a>

| event                    | イベント履歴 API の `event_type` | 送信タイミング                                                             | 備考                                                                                                                            |
| ------------------------ | ------------------------- | ------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `handoff.requested`      | `transfer_triggered`      | AI が有人対応を必要と判断し、会話が待機キューに入り、AI の自動返信が停止したとき                         | 各切り替えフローの最初のイベントです。`assignee` は `null` です。担当者が独自システムで対応する場合、これが引き継ぎの合図になります                                                   |
| `handoff.assigned`       | `human_assigned`          | MaiAgent 管理画面のサポートメンバーに会話が割り当てられたとき（手動割り当て、自分での担当取得、転送、自動割り当てを含みます） | `assignee` は割り当てられた担当者です。別の担当者への転送時には、異なるイベント `id` で再度送信されます                                                                  |
| `handoff.returned_to_ai` | `returned_to_ai`          | 担当者が管理画面で「AI Agent に戻す」を押すか、`return-to-ai` API で会話を AI に戻したとき       | `assignee` は `null` です（会話には有人担当者がいなくなっています）                                                                                   |
| `handoff.queue_timeout`  | `queue_timeout`           | 受信トレイで設定した「待機タイムアウト」を超えても担当者が引き継がず、MaiAgent が会話を自動的に AI に戻したとき      | `assignee` は `null` です。担当者が独自システムで対応する場合、このイベントは MaiAgent が気付かないうちに AI 返信を再開したことを意味します。アラートが必要な異常として扱うか、待機タイムアウトを 0 に設定してください |

4 種類のイベントは、ペイロードのキー、署名、再試行の動作がすべて同じです。受信側では **`event` フィールドに応じて処理を振り分けてください**。今後イベントの種類が追加される場合があります。不明な `event` を受信した場合は、エラーとして扱わず、2xx を返して無視してください。

`handoff.returned_to_ai` と `handoff.queue_timeout` は、どちらも「AI の自動返信が再開した」ことを意味します。前者は人（担当者またはお客様のシステム）が明示的に行い、後者はタイムアウトにより自動で行われる点が異なります。どちらも「会話が終了した」ことを意味しません。担当者が会話を直接「解決済み」にした場合や、受信トレイのアクセス権限を失って会話が待機キューに戻った場合、イベントは送信されません。終了状態を確実に把握するには、会話 API で `status` を取得するか、メッセージ Webhook の `conversation.progress_status` の変化を確認してください。

## イベントの形式

```
POST <設定した URL>
Content-Type: application/json
```

```json
{
  "event": "handoff.assigned",
  "id": "9f0e1d2c-...",
  "occurred_at": "2026-09-09T02:30:00.123456+00:00",
  "conversation": {
    "id": "6c2b7c1e-...",
    "url": "https://admin.maiagent.ai/..."
  },
  "inbox": { "id": "0f9a3d42-..." },
  "organization": { "id": "b1d20c77-..." },
  "contact": { "id": "a4c1f6b3-..." },
  "assignee": { "id": "3e8f5a90-..." }
}
```

| フィールド            | 型                  | 説明                                                                                                                 |
| ---------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------ |
| event            | string             | イベント名（上記の「イベントの種類」を参照してください）                                                                                       |
| id               | string (uuid)      | イベント識別子です。イベント履歴 API が返す `id` と同じです。再試行時も内容は完全に同じなので、**この値で重複を排除してください**                                           |
| occurred\_at     | string (date-time) | イベント発生時刻（ISO 8601）です。到着順序は保証されないため、**この値で並べ替えてください**                                                               |
| conversation.id  | string (uuid)      | 会話識別子です                                                                                                            |
| conversation.url | string (uri)       | 管理画面の会話ページへのリンクです                                                                                                  |
| inbox.id         | string (uuid)      | 受信トレイ識別子です                                                                                                         |
| organization.id  | string (uuid)      | 組織識別子です                                                                                                            |
| contact          | object \| null     | 顧客（連絡先）の `{ "id" }` です。会話に連絡先がない場合は `null` です                                                                      |
| assignee         | object \| null     | **イベント発生時点**の担当者 `{ "id" }` です。イベントのスナップショットなので、送信の遅延や後の転送によって変わりません。`handoff.assigned` のみ値があり、その他のイベントは `null` です |

フィールド名とネスト構造は、受信トレイの「メッセージ Webhook」の MaiAgent 標準形式と同じです（snake\_case、関連オブジェクトは `{ "id" }` としてネストします）。同じ受信側で両方の Webhook を処理できます。ペイロードに含まれるのは識別情報、イベント時刻、管理画面へのリンクだけで、**会話のテキストや有人対応への切り替え理由は含まれません**。会話内容が必要な場合は、識別子を使って会話 API とメッセージ API を呼び出してください。

## 署名検証（有効化を推奨します）

管理画面で署名用シークレットを設定すると、各送信に次のヘッダーが付加されます。

```
X-MaiAgent-Signature-256: sha256=<hex digest>
```

値は、シークレットを使い、**元のリクエストボディの bytes** に対して計算した HMAC-SHA256 の16進数ダイジェストです。検証には必ず受信した元の bytes を使用してください。解析後に再シリアライズすると、フィールドの順序や空白の違いでダイジェストが一致しなくなります。

```python
import hashlib
import hmac


def verify_signature(raw_body: bytes, header_value: str, secret: str) -> bool:
    expected = 'sha256=' + hmac.new(secret.encode(), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, header_value)
```

シークレットは書き込み専用です。保存後は再表示されず、管理画面には「設定済み」とだけ表示されます。フィールドを空欄のまま保存すると、既存のシークレットが維持されます。変更するには新しい値を入力し、削除するには「シークレットをクリア」を使用してください。シークレットが未設定の場合、署名ヘッダーは付加されません。

## 最小構成の受信側サンプル <a href="#receiver-example" id="receiver-example"></a>

次の Python（Flask）の受信側サンプルは、そのまま起動できます。元の bytes を読み取って署名を検証し、`event` で処理を振り分け、時間のかかる処理の前に 2xx を返します。`HANDOFF_WEBHOOK_SECRET` には、管理画面で設定した署名用シークレットを指定してください。

{% code title="receiver.py" %}

```python
import hashlib
import hmac
import os

from flask import Flask, abort, request

app = Flask(__name__)
SECRET = os.environ["HANDOFF_WEBHOOK_SECRET"]


def verify_signature(raw_body: bytes, header_value: str | None) -> bool:
    if not header_value:
        return False
    expected = "sha256=" + hmac.new(SECRET.encode(), raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, header_value)


@app.post("/webhooks/handoff")
def handoff_webhook():
    raw = request.get_data()  # 元の bytes を取得します。request.json を再シリアライズしないでください
    if not verify_signature(raw, request.headers.get("X-MaiAgent-Signature-256")):
        abort(401)

    event = request.get_json(force=True)
    if already_processed(event["id"]):  # 再試行時も内容は同じです。イベント id で重複を排除します
        return "", 204

    conversation_id = event["conversation"]["id"]
    if event["event"] == "handoff.requested":
        # AI は返信を停止しています。独自システムで有人対応する場合は、ここでチケットを作成し担当者を割り当てます
        open_ticket(conversation_id, event["conversation"]["url"])
    elif event["event"] == "handoff.assigned":
        # MaiAgent 管理画面の担当者に割り当てられました。転送時には再度イベントが届き、assignee が変わります
        assign_ticket(conversation_id, event["assignee"]["id"])
    elif event["event"] == "handoff.returned_to_ai":
        close_ticket(conversation_id)
    elif event["event"] == "handoff.queue_timeout":
        # MaiAgent が AI 返信を自動再開しました。担当者がまだ対応中ならアラートが必要な異常です
        close_ticket(conversation_id, reason="queue_timeout")
    # 不明なイベントはエラーにせず、2xx を返して無視します

    return "", 204  # 10 秒以内に 2xx を返します。時間のかかる処理はバックグラウンドで行います


def already_processed(event_id):
    ...


def open_ticket(conversation_id, url):
    ...


def assign_ticket(conversation_id, assignee_id):
    ...


def close_ticket(conversation_id, reason=None):
    ...
```

{% endcode %}

```bash
HANDOFF_WEBHOOK_SECRET=your_secret flask --app receiver run --port 8080
```

ローカルテストでは、次のコマンドで送信を再現できます（同じシークレットで署名を計算します）。

```bash
BODY='{"assignee":{"id":"3e8f5a90-0000-0000-0000-000000000004"},"contact":{"id":"a4c1f6b3-0000-0000-0000-000000000005"},"conversation":{"id":"6c2b7c1e-0000-0000-0000-000000000001","url":"https://admin.maiagent.ai/conversations/index?conversationId=6c2b7c1e-0000-0000-0000-000000000001"},"event":"handoff.assigned","id":"9f0e1d2c-0000-0000-0000-000000000006","inbox":{"id":"0f9a3d42-0000-0000-0000-000000000002"},"occurred_at":"2026-09-09T02:30:00.123456+00:00","organization":{"id":"b1d20c77-0000-0000-0000-000000000003"}}'
SIG=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "your_secret" | awk '{print $NF}')
curl -X POST http://localhost:8080/webhooks/handoff \
  -H "Content-Type: application/json" \
  -H "X-MaiAgent-Signature-256: sha256=$SIG" \
  -d "$BODY"
```

## 応答と再試行

* 受信側は 10 秒以内に 2xx ステータスコードで応答してください。応答内容は任意です。
* タイムアウト、接続失敗、または 2xx 以外の応答の場合、システムは 60 秒間隔で最大 3 回、自動で再試行します。再試行時の内容は元の送信と完全に同じです（同じ `id`）。受信側ではイベント `id` で重複を排除してください。各イベントは独立したバックグラウンドタスクで送信されるため、`handoff.requested` とその直後の `handoff.assigned` が順不同で届く場合があります。`occurred_at` で並べ替えてください。
* Webhook の送信失敗は、有人対応への切り替えの主処理、通知センターの通知、Email 通知には影響しません。

## 有人対応への切り替え後に外部システムで会話を引き継ぐ方法 <a href="#external-takeover" id="external-takeover"></a>

担当者が MaiAgent 管理画面ではなく独自のカスタマーサポートシステムで返信する場合、このページのイベントと、受信トレイの既存の「メッセージ Webhook」、メッセージ API を組み合わせることで、有人対応中の会話を双方向に同期できます。すべて既存の機能を使用するため、追加の有効化は不要です。

訪問者のチャネル（Web、LINE など）がすでにお客様のシステムに接続され、MaiAgent を AI エンジンとしてのみ使用する場合は、技術ドキュメントの[サードパーティのカスタマーサポートシステムとの連携（有人対応への切り替えを含む）](https://docs.maiagent.ai/tech/api-integration/customer-service-system-integration)を参照してください。この節では、訪問者は MaiAgent のチャネルを利用し、有人担当者だけがお客様のシステムを利用する場合を説明します。

### 連携フロー <a href="#takeover-flow" id="takeover-flow"></a>

1. **有人対応への切り替え開始を検知します**：`handoff.requested` を受信したら、`conversation.id` を使ってシステム内でチケットまたはセッションを作成し、`contact.id` を記録します。後続の `handoff.assigned` には `assignee.id` が含まれ、担当者の更新に使用できます。受信トレイの「待機タイムアウト」は 0 に設定することを推奨します。そうしない場合、タイムアウト時に `handoff.queue_timeout` を受信し、AI が自動で返信を再開します。
2. **顧客メッセージと担当者の返信を受信します**：管理画面の対象受信トレイの「メッセージ Webhook」で、`incoming`（顧客メッセージ）と `outgoing`（AI または担当者の返信）の Webhook を追加します。各メッセージの `conversation` オブジェクトには現在の処理状態が含まれます。`progress_status` が `human_serving` のメッセージだけを処理すれば、AI モードの通信を除外できます。

   | フィールド                             | 説明                                                                            |
   | --------------------------------- | ----------------------------------------------------------------------------- |
   | conversation.status               | `open` / `queued` / `resolved`                                                |
   | conversation.progress\_status     | `ai_processing` / `waiting_for_human` / `human_serving` / `resolved` / `open` |
   | conversation.auto\_reply\_enabled | AI が自動返信するかどうかです。有人対応への切り替え後は `false` です                                      |
   | conversation.assignee             | 担当者の `{ "id", "name" }` です。担当者がいない場合は `null` です                               |
   | sender.type                       | `contact`（顧客）/ `user`（有人担当者）/ `chatbot`（AI）です                                 |
3. **担当者の返信を会話に書き戻します**：API キーで `POST /api/v1/messages/outgoing/` を呼び出し、`conversation` と `content` を指定します。MaiAgent は顧客のチャネルにメッセージを転送します。送信者は API キーを所有するメンバーです。このメンバーには対象受信トレイのアクセス権限が必要です（「会話とメッセージ」のエンドポイント説明を参照してください）。
4. **有人対応を終了します**：担当者の対応が完了したら、`PATCH /api/v1/conversations/{id}/return-to-ai/` を呼び出して AI に戻します。API を呼び出した場合も、担当者が MaiAgent 管理画面で「AI Agent に戻す」を押した場合も、`handoff.returned_to_ai` を受信します。これに基づいてシステム内のセッションを閉じてください。

### API 呼び出し例 <a href="#takeover-examples" id="takeover-examples"></a>

**（1）有人対応中に受信するメッセージ Webhook**（`maiagent_standard` 形式の抜粋）です。`conversation.progress_status` で有人対応モードかどうかを判定し、`sender.type` で発言者を確認します。

```json
{
  "id": "9d2f7a10-...",
  "type": "incoming",
  "content": "注文の配送先住所を変更したいです",
  "created_at": "2026-09-09T02:30:00+00:00",
  "conversation": {
    "id": "6c2b7c1e-...",
    "title": "...",
    "type": "individual",
    "status": "open",
    "progress_status": "human_serving",
    "auto_reply_enabled": false,
    "assignee": { "id": "3e8f5a90-...", "name": "担当者の美咲" }
  },
  "inbox": { "id": "0f9a3d42-...", "name": "公式サイトサポート", "channel_type": "web" },
  "contact": { "id": "a4c1f6b3-...", "name": "山田太郎", "email": null, "phone_number": null },
  "sender": { "id": "a4c1f6b3-...", "name": "山田太郎", "type": "contact" },
  "attachments": [],
  "metadata": {}
}
```

**（2）担当者の返信を会話に書き戻します**。必要なのは `conversation` と `content` だけで、その他のフィールドは任意です。API キーを所有するメンバーの名義で顧客のチャネルに送信されます。

```bash
curl -X POST "https://api.maiagent.ai/api/v1/messages/outgoing/" \
  -H "Authorization: Api-Key YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "conversation": "6c2b7c1e-...",
    "content": "こんにちは。配送先住所を東京都新宿区…に更新しました。"
  }'
```

**（3）対応完了後、会話を AI に戻します**。リクエストボディは不要です。成功すると 200 と会話オブジェクトが返され、その後 AI が自動返信を再開し、`assignee` がクリアされます。

```bash
curl -X PATCH "https://api.maiagent.ai/api/v1/conversations/6c2b7c1e-.../return-to-ai/" \
  -H "Authorization: Api-Key YOUR_API_KEY"
```

3 つの手順をまとめた例です（Python、`requests`）。

```python
import requests

API = "https://api.maiagent.ai/api/v1"
HEADERS = {"Authorization": "Api-Key YOUR_API_KEY"}


def on_message_webhook(payload: dict) -> None:
    conv = payload["conversation"]
    if conv["progress_status"] != "human_serving":
        return  # AI モードの通信はカスタマーサポートシステムに送信しません
    if payload["sender"]["type"] == "contact":
        push_to_agent_console(conv["id"], payload["content"])  # 顧客メッセージ → 担当者の画面


def reply(conversation_id: str, text: str) -> None:
    requests.post(f"{API}/messages/outgoing/", headers=HEADERS,
                  json={"conversation": conversation_id, "content": text}, timeout=10).raise_for_status()


def finish(conversation_id: str) -> None:
    requests.patch(f"{API}/conversations/{conversation_id}/return-to-ai/", headers=HEADERS, timeout=10).raise_for_status()
```

### 注意事項 <a href="#takeover-notes" id="takeover-notes"></a>

* API で書き戻した返信は、`outgoing` メッセージ Webhook でも送信されます（`sender.type` は `user` です）。システム内で重複表示しないよう、メッセージ `id` で重複を排除してください。
* 別の担当者への転送時には `handoff.assigned` が再度送信され、`assignee.id` は新しい担当者を示します。重複したチケットを作成せず、`conversation.id` を使って既存のセッションを更新してください。
* メッセージ Webhook と有人対応への切り替え Webhook は、設定と署名方式が別々です。前者は設定したシークレットを `X-Webhook-Secret` ヘッダーでそのまま送信し、後者はこのページの HMAC-SHA256 署名を使用します。


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.maiagent.ai/api/ja/preparation/handoff-webhook.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
