> 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 に戻されたとき、MaiAgent は Webhook で handoff.created / handoff.returned\_to\_ai イベントを指定のシステムエンドポイントに送信します。本ページでは、イベント形式、署名検証、リトライ動作、および有人対応への引き継ぎ後に外部システムで会話を引き継ぐ連携方法について説明します

カスタマーサポート窓口（受信トレイ）で有人担当者へのアサインが完了すると、MaiAgent は設定されたエンドポイントに HTTPS POST で `handoff.created` イベントを送信します。これにより、ディスパッチ、アラート、CRM システムはポーリングせずに自動で対応を引き継げます。会話が AI に戻ると、同じエンドポイントが `handoff.returned_to_ai` を受信し、対応中のシステムは有人対応への引き継ぎが終了したことを認識できます。

## 有効化方法

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

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

Webhook、通知センター、Email は互いに影響しない 3 つの独立した設定です。デフォルトではオフになっており、受信トレイごとに有効化します。このチャネルは、受信トレイに既存の「メッセージ Webhook」（メッセージの送受信用）とは別のものです。

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

| event                    | 送信されるタイミング                                                             | 備考                                                   |
| ------------------------ | ---------------------------------------------------------------------- | ---------------------------------------------------- |
| `handoff.created`        | 会話が有人担当者にアサインされたとき（手動アサイン、自己アサイン、転送、自動アサインを含みます）                       | `assigneeId` はアサインされた担当者を示します。別の担当者に転送すると、もう一度送信されます |
| `handoff.returned_to_ai` | 担当者が管理画面で「AI Agent に戻す」をクリックしたとき、または `return-to-ai` API で会話を AI に戻したとき | 会話の担当者がいなくなるため、`assigneeId` は `null` です              |

2 種類のイベントは、Payload のキー、署名、リトライ動作がすべて同じです。受信側では、**`event` フィールドに基づいて処理を分岐**してください。今後イベントの種類が追加される可能性があります。未知の `event` を受信した場合は、エラーとして扱わず、2xx を返して無視してください。

`handoff.returned_to_ai` は「**担当者が明示的に会話を AI に戻した**」ことを意味し、「有人対応フローが終了した」ことと同じではありません。担当者が会話を「解決済み」にした場合や、受信トレイへのアクセス権を失って会話が再びキューに入った場合、このイベントは送信されません。完全な解決シグナルが必要な場合は、会話 API で `status` を照会するか、メッセージ Webhook の `conversation.progress_status` で状態変化を監視してください。

## イベント形式

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

```json
{
  "event": "handoff.created",
  "conversationId": "6c2b7c1e-...",
  "inboxId": "0f9a3d42-...",
  "organizationId": "b1d20c77-...",
  "assigneeId": "3e8f5a90-...",
  "contactId": "a4c1f6b3-...",
  "conversationUrl": "https://admin.maiagent.ai/..."
}
```

| フィールド           | 型                     | 説明                                                                    |
| --------------- | --------------------- | --------------------------------------------------------------------- |
| event           | string                | `handoff.created` または `handoff.returned_to_ai`（上記の「イベントの種類」を参照してください） |
| conversationId  | string (uuid)         | 会話の識別子                                                                |
| inboxId         | string (uuid)         | 受信トレイの識別子                                                             |
| organizationId  | string (uuid)         | 組織の識別子                                                                |
| assigneeId      | string (uuid) \| null | アサインされた担当者のメンバー識別子。`handoff.returned_to_ai` の場合は `null` です            |
| contactId       | string (uuid) \| null | 顧客（連絡先）の識別子                                                           |
| conversationUrl | string (uri)          | 管理画面の会話ページへのリンク                                                       |

Payload に含まれるのは識別情報と管理画面へのリンクのみで、**会話のテキスト内容は含まれません**。会話内容が必要な場合は、識別子を使用して会話 API とメッセージ API を呼び出してください。

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

管理画面で署名シークレットを設定すると、各リクエストに次のヘッダーが付与されます：

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

値は、シークレットを使用して**リクエストボディの生バイト列**から計算した HMAC-SHA256 の 16 進ダイジェストです。検証には、受信した生バイト列をそのまま使用してください。先にパースして再シリアライズすると、フィールドの順序や空白の違いによりダイジェストが一致しなくなります。

```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)
```

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

## 最小構成の受信例 <a href="#receiver-example" id="receiver-example"></a>

次の実行可能な Python（Flask）の受信側では、生バイト列を読み取り、署名を検証し、`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()  # 生バイト列を使用し、request.json を再シリアライズしないでください
    if not verify_signature(raw, request.headers.get("X-MaiAgent-Signature-256")):
        abort(401)

    event = request.get_json(force=True)
    key = (event["event"], event["conversationId"])  # リトライでは同一内容が送信されるため、この組み合わせで重複排除します

    if event["event"] == "handoff.created":
        # システムでチケット／セッションを作成します。別の担当者への転送時は assigneeId が変わったイベントが再送されます
        open_ticket(event["conversationId"], event["assigneeId"], event["conversationUrl"])
    elif event["event"] == "handoff.returned_to_ai":
        close_ticket(event["conversationId"])
    # 未知のイベント：エラーとして扱わず、2xx を返して無視します

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


def open_ticket(conversation_id, assignee_id, url):
    ...


def close_ticket(conversation_id):
    ...
```

{% endcode %}

```bash
HANDOFF_WEBHOOK_SECRET=your-secret flask --app receiver run --port 8080
```

次のコマンドでリクエストをローカルに再現できます。署名は同じシークレットを使用して計算します。

```bash
BODY='{"event":"handoff.created","conversationId":"6c2b7c1e-0000-0000-0000-000000000001","inboxId":"0f9a3d42-0000-0000-0000-000000000002","organizationId":"b1d20c77-0000-0000-0000-000000000003","assigneeId":"3e8f5a90-0000-0000-0000-000000000004","contactId":"a4c1f6b3-0000-0000-0000-000000000005","conversationUrl":"https://admin.maiagent.ai/conversations/index?conversationId=6c2b7c1e-0000-0000-0000-000000000001"}'
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 回、自動的にリトライします。リトライでは最初のリクエストとまったく同じ内容が送信されるため、受信側で冪等に処理してください（例：`event` + `conversationId` で重複排除します）。
* Webhook リクエストが失敗しても、有人対応への引き継ぎフロー、通知センター、Email 通知には影響しません。

## 有人対応への引き継ぎ後に外部システムで会話を担当する <a href="#external-takeover" id="external-takeover"></a>

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

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

1. **有人対応への引き継ぎ開始を検知します：** `handoff.created` を受信したら、`conversationId` を使用してシステムでチケットまたはセッションを作成し、`assigneeId` と `contactId` を記録します。
2. **顧客メッセージと担当者の返信を受信します：** 管理画面で、対象の受信トレイに `incoming`（顧客メッセージ）方向と `outgoing`（AI または担当者の返信）方向のメッセージ Webhook を追加します。各メッセージの `conversation` オブジェクトには現在の処理状態が含まれます。AI モードのトラフィックを除外するには、`progress_status` が `human_serving` のメッセージのみを処理してください：

   | フィールド                             | 説明                                                                            |
   | --------------------------------- | ----------------------------------------------------------------------------- |
   | 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 に再度引き継ぎます。担当者が MaiAgent の管理画面で「AI Agent に戻す」をクリックした場合は `handoff.returned_to_ai` を受信するため、そのイベントに基づいてシステム内のセッションを終了してください。

### リクエスト例 <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": "Web サイトサポート", "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"
```

次の Python（`requests`）の例は、3 つの手順をまとめたものです：

```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` で重複排除してください。
* 会話が別の担当者に転送されると、新しい担当者の `assigneeId` を含む `handoff.created` イベントがもう一度送信されます。チケットを重複して作成せず、`conversationId` に基づいて既存のセッションを更新してください。
* メッセージ 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.
