> ## Documentation Index
> Fetch the complete documentation index at: https://docs-agents.fpt.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# API 連携

> 顧客とのチャット画面がすでにある場合に。Webhook でメッセージを受け取り、HMAC で署名し、コールバックで回答を返し、会話履歴を読み直します

API チャネルは、顧客との会話経路をすでに持っているチーム向けです。自社アプリ、自社で運用する Zalo OA、コンタクトセンター、CRM など。顧客のメッセージは Webhook でプラットフォームに転送し、Agent の回答はコールバックで自社の Webhook に返ります。

<Info icon="key">
  双方向で 1 つの秘密鍵と 1 つの署名方式を共有するので、署名の関数は一度書けば送信にも受信にも使えます。
</Info>

## 連携方式を選ぶ

| 観点 | Live Chat | API チャネル |
| - | - | - |
| チャット画面 | FPT.AI が提供 | 自社で用意 |
| 実装の手間 | script タグを 1 つ貼るだけ | 受信 Webhook と送信クライアントを書く |
| 回答 | トークン単位でリアルタイムに流れる (SSE) | 回答 1 件につきコールバック 1 回 |
| 認証 | 顧客ごとに自動発行される `visitorToken` | パケットごとの HMAC-SHA256 署名 |
| 顧客の識別 | プラットフォームが生成 (匿名) | `user.id` で自社が指定 |
| 受信の添付 | 可 | 管理者が有効にしていれば可 |
| ボタン、カルーセル | 可 | 可 |

<Note>
  2 つは排他ではありません。1 つの Agent が、ウェブサイトのウィジェットと自社システムの API チャネルに同時に応答できます。それぞれ別の接続で、別のキーを持ちます。
</Note>

## 環境とドメイン

| 環境 | API チャネルの Webhook | 履歴 API |
| - | - | - |
| 本番 | `https://console-agents.fpt.ai` | `https://console-agents.fpt.ai` |
| ステージング | `https://console-agents-staging.fpt.ai` | `https://console-agents-staging.fpt.ai` |

<Note>
  すべて 1 つのアプリケーションドメインの下にあり、パスで区別します。API チャネルの受信 Webhook は `https://console-agents.fpt.ai/webhooks/api`、履歴 API は `https://console-agents.fpt.ai/direct-bff/…` です。Webhook のアドレスは組み立てずに、必ず Console からそのままコピーしてください。
</Note>

## API チャネルをつなぎ、キーを受け取る

<Steps>
  <Step title="FPT AI Agent Platform にサインインする">
    Console を開き、アカウントでサインインします。
  </Step>

  <Step title="設定する Agent を選ぶ">
    自社システムが転送するメッセージに応答する Agent です。
  </Step>

  <Step title="チャネルタブを開く">
    **チャネル** は Agent のナビゲーションバーにあります。
  </Step>

  <Step title="API のタイルをクリックする">
    API チャネルの設定パネルが開きます。
  </Step>

  <Step title="チャネルの設定を入力する">
    項目は 2 つです。下の表を参照してください。
  </Step>

  <Step title="設定を保存をクリックしてチャネルを作る">
    チャネルタブの API タイルが **設定済み** に変わります。
  </Step>

  <Step title="新しいキーを生成をクリックする">
    ダイアログを閉じる前に、**ルーティングキー** と **署名キー** をコピーしてください。
  </Step>
</Steps>

<img src="https://mintcdn.com/fpt-62e894b4/OEIa1zuMs6GjYr6X/images/en_api_config.jpg?fit=max&auto=format&n=OEIa1zuMs6GjYr6X&q=85&s=76dfea6991fa8ede2ebc2e27f1292616" alt="API チャネルの設定パネル" width="1562" height="784" data-path="images/en_api_config.jpg" />

| 項目 | 説明 | 必須 |
| - | - | - |
| コールバック URL | プラットフォームが Agent の回答を返す先。`https` で、2048 文字以内、インターネットから到達できること。エンドポイントが未完成なら空のままでも構いません。その場合もチャネルはメッセージを受け取りますが、回答は返されません | 任意 |
| 無操作時間 (時間) | この時間より長く会話が止まると、顧客の次のメッセージは新しい会話になります。1 から 720 時間、既定は 24 | 任意 |

<Warning>
  署名キーは、作られた瞬間に一度だけ表示されます。システムはハッシュしか保存しないので、あとから見る方法はありません。ダイアログを閉じる前に、秘密情報の保管先にコピーしてください。
</Warning>

### 受け取るもの

| 値 | 例 | 内容 |
| - | - | - |
| ルーティングキー | `api_7Kd2xQ9mPz4vR8nLcJt3Aw` | あなたの接続を識別します。URL ではなく、本文の `integrationKey` フィールドで送ります |
| Webhook URL | `https://console-agents.fpt.ai/webhooks/api` | 顧客のメッセージを送る先。すべての組織で共通です |
| 署名キー | (一度だけ表示) | 双方向の署名と検証に使う秘密鍵 |
| コールバック URL | `https://your-domain.com/agent-replies` | 手順 5 で入力した、自社の Webhook |

### コールバック URL をテストする

入力欄の横の **コールバックをテスト** ボタンは、署名付きのリクエストをあなたのアドレスに送り、正しい応答を期待します。実装の仕方は下の「Webhook エンドポイントを検証する」を参照してください。このボタンは組織あたり毎分 10 回までです。

### 秘密鍵を入れ替える

<Steps>
  <Step title="新しいキーを生成をクリックする">
    この時点から、古いキーと新しいキーの両方が受け付けられます。
  </Step>

  <Step title="新しいキーをすべてのサーバーに配る">
    送信側と受信側の両方を確認してください。
  </Step>

  <Step title="入れ替えを完了をクリックする">
    古いキーが無効になります。以後は新しいキーだけが通ります。
  </Step>
</Steps>

<Warning>
  最後の手順を飛ばさないでください。入れ替えを完了するまで古いキーは有効なままなので、忘れると実質的に何も入れ替わっていません。
</Warning>

## イベントの署名を検証する

双方向のすべてのパケットは、チャネルの秘密鍵を使った HMAC-SHA256 で署名されます。署名は `X-Hub-Signature-256` ヘッダーに `sha256=` の接頭辞付きで入り、送信時刻を載せた `X-Hub-Timestamp` ヘッダーが並びます。

署名の対象は、`timestamp`、ドット、生の本文をつないだ文字列です。

```text theme={null}
mac = HMAC-SHA256(secret, timestamp + "." + rawRequestBody)
```

* 送るバイト列そのものに署名してください。JSON をオブジェクトに解析してから再度文字列化すると、キーの順番が変わるだけでも署名が変わります。
* タイムスタンプは署名の中に入っているので、パケットを捕まえた攻撃者が書き換えることはできません。現在の Unix 時刻を秒で送ってください。
* サーバー時刻から前後 5 分を超えてずれたタイムスタンプは拒否されます。NTP で同期しておいてください。
* 例外はありません。署名を省くモードはなく、キーを生成していない接続にも免除はありません。

### Go での署名

```go theme={null}
package main

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"fmt"
	"strconv"
	"time"
)

func sign(secret string, body []byte, timestamp string) string {
	h := hmac.New(sha256.New, []byte(secret))
	h.Write([]byte(timestamp))
	h.Write([]byte("."))
	h.Write(body)
	return "sha256=" + hex.EncodeToString(h.Sum(nil))
}

func main() {
	// 生の本文。実際に送るバイト列そのもの。
	body := []byte(`{"integrationKey":"api_7Kd2xQ9mPz4vR8nLcJt3Aw",` +
		`"messageId":"msg-1001","user":{"id":"cust-42"},` +
		`"text":"Hello"}`)
	secretKey := "my_secret_key"
	timestamp := strconv.FormatInt(time.Now().Unix(), 10)
	fmt.Println("X-Hub-Timestamp:", timestamp)
	fmt.Println("X-Hub-Signature-256:", sign(secretKey, body, timestamp))
}
```

### Node.js での署名

```js theme={null}
const crypto = require('crypto');

function sign(secret, body, timestamp) {
  return 'sha256=' + crypto
    .createHmac('sha256', secret)
    .update(timestamp + '.' + body)
    .digest('hex');
}

// 生の本文。実際に送る文字列そのもの。
const body = JSON.stringify({
  integrationKey: 'api_7Kd2xQ9mPz4vR8nLcJt3Aw',
  messageId: 'msg-1001',
  user: { id: 'cust-42' },
  text: 'Hello'
});

const secretKey = 'my_secret_key';
const timestamp = Math.floor(Date.now() / 1000).toString();
console.log('X-Hub-Timestamp:', timestamp);
console.log('X-Hub-Signature-256:', sign(secretKey, body, timestamp));
```

### プラットフォームから届く署名を検証する

同じ関数を再利用し、タイミングから情報が漏れないよう定数時間の比較を使います。

```js theme={null}
const express = require('express');
const crypto = require('crypto');
const app = express();

// 必須: 署名を計算できるよう、生の本文を保持する。
app.use(express.raw({ type: 'application/json' }));

app.post('/agent-replies', (req, res) => {
  const ts  = req.get('X-Hub-Timestamp') || '';
  const got = req.get('X-Hub-Signature-256') || '';
  const raw = req.body;                       // Buffer。JSON.parse 済みではない

  // 1. まず時刻のずれを確認する (最大 5 分)。
  const skew = Math.abs(Math.floor(Date.now() / 1000) - Number(ts));
  if (!Number.isFinite(skew) || skew > 300) return res.sendStatus(401);

  // 2. 署名を比較する。
  const want = sign(process.env.CHANNEL_SECRET, raw, ts);
  const a = Buffer.from(got), b = Buffer.from(want);
  if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
    return res.sendStatus(401);
  }

  const event = JSON.parse(raw.toString('utf8'));

  // 3. すぐに 200 を返し、処理は非同期で。
  res.sendStatus(200);
  handleAsync(event);
});
```

<Warning>
  署名が合わない原因のほとんどは、Web フレームワークが本文をオブジェクトに解析してしまい、生のバイト列を読めなくなっていることです。生のまま保持する設定(`express.raw`、`bodyParser.raw`、または入力ストリームを直接読む)にして、検証が済んでから JSON を解析してください。
</Warning>

## 顧客のメッセージを受け取る

顧客が自社システムに送ったメッセージは、Webhook でプラットフォームに転送します。イベントは 2 つの部分、すなわちペイロードと、それを認証する署名からなります。

```mermaid theme={null}
sequenceDiagram
  participant C as 自社の基幹アプリ
  participant S as FPT AI Agents Webhook Server
  C->>S: POST /webhooks/api - 利用者メッセージのイベント
  Note over C,S: ヘッダー X-Hub-Timestamp と X-Hub-Signature-256
  S->>S: HMAC-SHA256 を検証
  S->>S: messageId による重複排除
  S->>S: イベントを処理し AI Agents へ渡す
  S-->>C: 200 OK
```

| HTTP メソッド | URI |
| - | - |
| POST | `/webhooks/api` |

| HTTP ヘッダー | 値 |
| - | - |
| `Content-Type` | `application/json` |
| `X-Hub-Timestamp` | Unix 時刻 (秒) |
| `X-Hub-Signature-256` | `sha256=…` |

### テキストメッセージ

```json theme={null}
{
  "integrationKey": "api_7Kd2xQ9mPz4vR8nLcJt3Aw",
  "messageId": "msg-1001",
  "user": {
    "id": "cust-42",
    "name": "Nguyen Van A",
    "avatar": "https://example.com/avatar.png",
    "locale": "ja"
  },
  "text": "注文はどこまで来ていますか",
  "metadata": { "orderId": "SO-7781" }
}
```

### ボタンやクイックリプライからのメッセージ

```json theme={null}
{
  "integrationKey": "api_7Kd2xQ9mPz4vR8nLcJt3Aw",
  "messageId": "msg-1002",
  "user": { "id": "cust-42" },
  "text": "配送状況を追跡",
  "postback": "track:SO-7781"
}
```

### 添付付きのメッセージ

```json theme={null}
{
  "integrationKey": "api_7Kd2xQ9mPz4vR8nLcJt3Aw",
  "messageId": "msg-1003",
  "user": { "id": "cust-42" },
  "text": "不良品の写真です",
  "attachments": [
    {
      "url": "https://example.com/files/faulty-product.jpg",
      "name": "faulty-product.jpg",
      "mimeType": "image/jpeg"
    }
  ]
}
```

### パラメータ

| パラメータ | 例 | 説明 | 必須 |
| - | - | - | - |
| `integrationKey` | `api_7Kd2xQ9…` | Console のチャネルのルーティングキー。256 文字まで | 必須 |
| `messageId` | `msg-1001` | 自分で生成するメッセージ ID。端から端までの重複排除キーです。256 文字まで | 必須 |
| `user` | | 送信者のオブジェクト | 必須 |
| `user.id` | `cust-42` | 自社システムでの顧客 ID。256 文字まで | 必須 |
| `user.name` | `Nguyen Van A` | 顧客名。256 文字まで | 任意 |
| `user.avatar` | `https://…/a.png` | 顧客の画像。2048 文字まで | 任意 |
| `user.locale` | `ja` | 顧客の言語。256 文字まで | 任意 |
| `text` | `注文はどこまで来ていますか` | メッセージ本文。16,384 文字まで。`postback` がある場合も必須です | 必須 |
| `postback` | `track:SO-7781` | 押されたボタンやクイックリプライの隠しペイロード。1024 文字まで | 任意 |
| `metadata` | `{"orderId":"SO-7781"}` | 自社の追加データ。32 キーまで、JSON 化して 8192 バイトまで | 任意 |
| `attachments` | | 添付の一覧。5 件まで | 任意 |
| `attachments[].url` | `https://…/a.jpg` | ファイルの URL。プラットフォームが取得します。2048 文字まで | 必須 |
| `attachments[].name` | `photo.jpg` | ファイル名。256 文字まで | 任意 |
| `attachments[].mimeType` | `image/jpeg` | ファイルの種類 | 任意 |

<Warning>
  `text` は常に必須です。`postback` だけのメッセージでも必要です。顧客のターンは保存され、担当者が会話を引き継ぐときに表示されるので、文字のない `postback` は受信箱に空の吹き出しを残します。ボタンのラベルを `text` として送ってください。
</Warning>

<Note>
  `postback` は別の文脈として Agent に届き、顧客の発言として扱われることはありません。人が打った言葉と、画面が運んできた値を区別できるようにするためです。ボタンもクイックリプライも、このフィールド 1 つで扱います。
</Note>

### cURL の例

```bash theme={null}
curl --location 'https://console-agents.fpt.ai/webhooks/api' \
  --header 'Content-Type: application/json' \
  --header 'X-Hub-Timestamp: 1755763200' \
  --header 'X-Hub-Signature-256: sha256=c22b7f7c1957fb301d6181c5fbf4dc841d735af48ddc14e9a4dade1658fdc3e5' \
  --data '{
    "integrationKey": "api_7Kd2xQ9mPz4vR8nLcJt3Aw",
    "messageId": "msg-1001",
    "user": { "id": "cust-42", "name": "Nguyen Duc Minh" },
    "metadata": { "customer_id": "2449956338403583" },
    "text": "hello"
  }'
```

### レスポンスコード

| ステータス | 意味 | 対応 |
| - | - | - |
| 200 | 受理 | 何もしません。Agent は非同期でコールバックを返します |
| 400 | 本文が不正。JSON の誤り、必須項目の欠落、上限超過 | リクエストを直してください。再送しても通りません |
| 401 | 署名またはタイムスタンプが不正 | 秘密鍵とサーバー時刻を確認してください。本文が空なのは意図的です |
| 405 | メソッドが違う | POST のみ受け付けます |
| 503 | プラットフォームが受け取れなかった | バックオフを入れて再送してください。拒否されたわけではありません |

<Note>
  200 は **受理** であって、回答済みではありません。Agent は非同期で動き、回答はそのあと Webhook に届きます。
</Note>

### 重複排除

`messageId` は端から端までの重複排除キーです。同じ (`integrationKey`, `messageId`) の組み合わせを 5 分以内に再送しても認識して読み飛ばすので、タイムアウト後の再送で顧客に 2 回答えてしまうことはありません。

<Warning>
  自社の安定したメッセージ ID を使ってください。再送のたびに新しい ID を作ると、重複排除がまったく効かなくなります。
</Warning>

### 受信の添付

`attachments` の各要素には、プラットフォームが取得できる `url` が必要です。ファイルの中身は組織のストレージにコピーされて Agent に渡されるので、渡した URL をそのあと長く生かしておく必要はありません。

<Note>
  添付のアップロードは、管理者が有効にするまでオフです。オフのとき、また上限を超えたファイルや取得に失敗したファイルは読み飛ばされますが、`text` は Agent に届きます。つまり添付付きのメッセージがエラーになることはなく、ファイルが取り込まれたかどうかはレスポンスコードからは分かりません。
</Note>

### 追加データ

`metadata` のオブジェクトは、`client_metadata` の下に入れ子になって Agent に届きます。トップレベルに統合されることはありません。そこにあるキー、とくに宛先の識別子が、回答の届け先を決めるからです。

本文のトップレベルにある未知のフィールドは無視されるので、独自の項目を足しても拒否されません。

## Agent からのコールバック

Agent の回答は、署名付きの POST で自社の Webhook に送られます。受信方向と同じく、イベントはペイロードとその署名からなります。

```mermaid theme={null}
sequenceDiagram
  participant M as Message Dispatching
  participant W as 自社の API Webhook
  M->>W: POST callback_url - Agent の回答
  Note over M,W: ヘッダー X-Hub-Signature-256
  W->>W: HMAC-SHA256 を検証
  W->>W: eventId で重複排除
  W->>W: 回答を利用者に届ける
  W-->>M: 200 OK
  Note over M,W: 2xx 以外は 3 回まで再送 (1 秒、3 秒、9 秒)、その後 DLQ へ
```

### Webhook エンドポイントを検証する

最初の本番メッセージの前に、あなたのエンドポイントが想定どおりのものか、プラットフォームに確かめさせられます。Console の **コールバックをテスト** ボタンが、署名付きのリクエストを送ります。

```mermaid theme={null}
sequenceDiagram
  participant P as FPT AI Agents Platform
  participant W as 自社の Webhook サーバー
  P->>W: POST callback_url - type verification, challenge 6326e43c...
  W->>W: X-Hub-Signature-256 の署名を検証
  W-->>P: 200 OK と challenge 6326e43c...
  P->>P: challenge を照合
  Note over P,W: 一致すれば ok、不一致なら challenge_mismatch
```

```json theme={null}
{
  "type": "verification",
  "challenge": "6326e43c9f0b4a1d8e77c0b511a2d3f4",
  "occurredAt": "2026-09-07T11:08:40Z"
}
```

通すには、受け取った `challenge` をそのまま返す JSON 本文とともに 2xx を返します。

```js theme={null}
app.post('/agent-replies', (req, res) => {
  // ... 上と同じ要領で署名を検証 ...
  const event = JSON.parse(raw.toString('utf8'));

  if (event.type === 'verification') {
    return res.json({ challenge: event.challenge });
  }

  res.sendStatus(200);
  handleAsync(event);
});
```

* 検証のリクエストもほかのパケットとまったく同じように署名されるので、すでに書いた関数がそのまま使えます。
* `type` は検証のリクエストにだけ現れ、実際のメッセージには付きません。
* `challenge` を返すことは必須です。素の 200 では足りません。放置されたドメイン、CDN のエラーページ、ロードバランサーのどれもが 200 を返しうるからです。
* 検証ごとに `challenge` は変わります。
* プラットフォームはリダイレクトをたどりません。301、302、303 は本文を失うので、正しく返しようがありません。
* 何も保存されません。合格は、その時点でエンドポイントが正しく答えたという意味です。

| 失敗の理由 | 意味と対応 |
| - | - |
| `no_secret` | 秘密鍵がまだないので署名できません。先にキーを生成してください |
| `url_forbidden` | 絶対 `https` の URL でない、または公開されていないアドレスに解決される |
| `unreachable` | 接続できませんでした。サービスが動いていて、インターネットから届くか確認してください |
| `http_status` | エンドポイントが 2xx 以外を返しました。実際のコードは `httpStatus` にあります |
| `challenge_mismatch` | 2xx は返したものの、`challenge` をそのまま返しませんでした |

### コールバックイベントの構造

```http theme={null}
POST <callback_url>
Content-Type: application/json
User-Agent: fpt-agent-platform-channel-gateway/1
X-Hub-Timestamp: 1755763260
X-Hub-Signature-256: sha256=…
```

| フィールド | 型 | 説明 |
| - | - | - |
| `eventId` | string | イベント ID。再送しても変わりません。重複排除キーに使ってください |
| `integrationKey` | string | チャネルのルーティングキー |
| `conversationId` | string | 会話 ID。履歴 API に渡す値です。保存してください |
| `runId` | string | 回答 1 ターンの ID。ターンごとに変わります |
| `from` | `bot` または `operator` | このターンが Agent から来たか、引き継いだ人から来たか |
| `user.id` | string | 送ったとおりの顧客 ID |
| `text` | string | テキスト本文。ボタンやカルーセルがある場合も必ず入ります |
| `buttons` | Array\<Button> | 任意: ボタンの一覧 |
| `quick_replies` | Array\<QuickReply> | 任意: クイックリプライの一覧 |
| `carousels` | Array\<Carousel> | 任意: カルーセルカードの一覧 |
| `references` | Array\<Reference> | 任意: 回答が引用した出典 |
| `attachments` | Array\<Attachment> | 任意。この節の末尾の注記を参照 |
| `occurredAt` | string | 発生時刻。RFC 3339 の UTC |

<Note>
  **命名について**: 外側のフィールドは lowerCamelCase (`eventId`、`integrationKey`、`conversationId`、`occurredAt`)、内容ブロックとその中のフィールドは snake\_case (`quick_replies`、`sub_title`、`image_url`、`file_name`) です。これは手落ちではなく意図的で、内容ブロックはチャットウィジェットや会話履歴と語彙を共有しているためです。
</Note>

#### Button

| フィールド | 型 | 説明 |
| - | - | - |
| `type` | `postback`, `url`, `phone_call`, `webview` | ボタンの種類 |
| `title` | string | ボタンのラベル |
| `data` | string | 任意: `type` が `postback` のときの隠しペイロード。押されたらこの値を `postback` で返してください |
| `url` | string | 任意: `type` が `url` または `webview` のときのリンク |
| `phone` | string | 任意: `type` が `phone_call` のときの電話番号 |

#### QuickReply

| フィールド | 型 | 説明 |
| - | - | - |
| `type` | `text` または `phone_call` | クイックリプライの種類 |
| `title` | string | クイックリプライのラベル |
| `data` | string | 任意: `type` が `text` のときの隠しペイロード |
| `phone` | string | 任意: `type` が `phone_call` のときの電話番号 |

#### Carousel

| フィールド | 型 | 説明 |
| - | - | - |
| `title` | string | カルーセルカードのタイトル |
| `sub_title` | string | 任意: サブタイトル |
| `image_url` | string | 任意: 画像の URL |
| `buttons` | Array\<Button> | 任意: カードのボタン |

<Note>
  ボタン付きの画像は、要素 1 つのカルーセルとして送られます。
</Note>

#### Reference

| フィールド | 型 | 説明 |
| - | - | - |
| `id` | string | この回答ターンの中での出典の識別子 |
| `type` | `knowledge` または `web_search` | ナレッジベースか、ウェブ検索か。未知の値でも通常のカードとして表示されます |
| `title` | string | 任意: 出典のタイトル |
| `url` | string | 任意: ウェブ出典のリンク |
| `file_name` | string | 任意: ナレッジベース出典のファイル名 |
| `uri` | string | 任意: ナレッジベースでの文書の識別子 |
| `page` | number | 任意: 文書内のページ番号 |
| `metadata` | object | 任意: 出典の種類ごとの固有データ |

#### Attachment

| フィールド | 型 | 説明 |
| - | - | - |
| `name` | string | ファイル名 |
| `mime` | string | ファイルの種類 |
| `size` | number | ファイルサイズ (バイト) |
| `objectKey` | string | ストレージのオブジェクトキー。ダウンロードリンクではありません。presign API で短命のリンクと交換します |

<Note>
  いまのところ API チャネルの回答ターンにファイルを添付する機能はないので、`attachments` がコールバックに現れたことはありません。将来の互換性のために予約されています。受信方向の添付は通常どおり動きます。
</Note>

### ペイロードの例

**ボタンのないテキストメッセージ**

```json theme={null}
{
  "eventId": "out:run-8891:0",
  "integrationKey": "api_7Kd2xQ9mPz4vR8nLcJt3Aw",
  "conversationId": "7412",
  "runId": "run-8891",
  "from": "bot",
  "user": { "id": "cust-42" },
  "text": "ご注文は明日お届けの予定です。",
  "occurredAt": "2026-09-14T09:14:20Z"
}
```

**ボタン付きのメッセージ**

```json theme={null}
{
  "eventId": "out:run-8892:0",
  "integrationKey": "api_7Kd2xQ9mPz4vR8nLcJt3Aw",
  "conversationId": "7412",
  "runId": "run-8892",
  "from": "bot",
  "user": { "id": "cust-42" },
  "text": "次はどうしますか",
  "buttons": [
    { "type": "postback",   "title": "配送状況を追跡", "data": "track:SO-7781" },
    { "type": "phone_call", "title": "電話する",       "phone": "+84982123456" },
    { "type": "url",        "title": "ウェブサイト",   "url": "https://example.com" }
  ],
  "occurredAt": "2026-09-14T09:15:02Z"
}
```

**クイックリプライ**

```json theme={null}
{
  "eventId": "out:run-8893:0",
  "integrationKey": "api_7Kd2xQ9mPz4vR8nLcJt3Aw",
  "conversationId": "7412",
  "runId": "run-8893",
  "from": "bot",
  "user": { "id": "cust-42" },
  "text": "ご用件をお聞かせください",
  "quick_replies": [
    { "type": "text",       "title": "返品",     "data": "intent:return" },
    { "type": "phone_call", "title": "電話する", "phone": "+84982123456" }
  ],
  "occurredAt": "2026-09-14T09:16:30Z"
}
```

**カルーセル**

```json theme={null}
{
  "eventId": "out:run-8894:0",
  "integrationKey": "api_7Kd2xQ9mPz4vR8nLcJt3Aw",
  "conversationId": "7412",
  "runId": "run-8894",
  "from": "bot",
  "user": { "id": "cust-42" },
  "text": "お客様に合う商品はこちらです。",
  "carousels": [
    {
      "title": "ヘルメット A",
      "sub_title": "QCVN 2:2008 適合",
      "image_url": "https://example.com/img/helmet-a.jpg",
      "buttons": [
        { "type": "postback", "title": "購入", "data": "buy:HELMET-A" },
        { "type": "url", "title": "詳細", "url": "https://example.com/helmet-a" }
      ]
    },
    {
      "title": "ヘルメット B",
      "image_url": "https://example.com/img/helmet-b.jpg",
      "buttons": [
        { "type": "postback", "title": "購入", "data": "buy:HELMET-B" }
      ]
    }
  ],
  "occurredAt": "2026-09-14T09:18:11Z"
}
```

**出典付きの回答**

```json theme={null}
{
  "eventId": "out:run-8895:0",
  "integrationKey": "api_7Kd2xQ9mPz4vR8nLcJt3Aw",
  "conversationId": "7412",
  "runId": "run-8895",
  "from": "bot",
  "user": { "id": "cust-42" },
  "text": "返品は到着から 30 日以内が対象です。",
  "references": [
    { "id": "r1", "type": "knowledge", "title": "返品ポリシー",
      "file_name": "policy-2026.pdf", "page": 4 },
    { "id": "r2", "type": "web_search", "title": "保証条件",
      "url": "https://example.com/warranty" }
  ],
  "occurredAt": "2026-09-14T09:20:45Z"
}
```

### コールバックの扱い方

* `text` は `buttons` や `carousels` がある場合も必ず入っています。表示できないときの代わりになるので、テキストしか扱えないシステムでも会話は成立します。
* 空のリストは `[]` ではなく、項目ごと省かれます。`buttons`、`quick_replies`、`carousels`、`references` は、そのターンに無ければ現れません。
* `conversationId` と `from` は、古いバージョンのプラットフォームからのコールバックでは無いことがあります。無い場合は未確定として扱ってください。
* 重複排除は `runId` ではなく `eventId` で行います。
* 自社データとの突き合わせも `runId` ではなく `conversationId` で行ってください。`runId` は回答 1 ターンにすぎません。
* すぐ 200 を返し、処理は非同期にしてください。1 回あたり 10 秒しかなく、処理が遅いと失敗とみなされて再送されます。

### エンドポイントが失敗したとき

| あなたの応答 | プラットフォームの動き |
| - | - |
| 2xx | 配信済みとして扱います |
| 5xx、429、408、タイムアウト、接続エラー | バックオフ(およそ 1 秒、3 秒、9 秒)で 3 回まで再送します。返した `Retry-After` ヘッダーは 30 秒まで尊重します |
| そのほかの 4xx、またはリダイレクト | 再送しません。恒久的に拒否されたものとして扱います |
| 3 回試しても失敗 | プラットフォーム側のエラーキューに移り、ERROR のログが残ります |

<Warning>
  エンドポイントが長く落ちている間、配信は **最大 1 回** です。これは意図的な割り切りで、無制限に再送すると共有の配信基盤が詰まるためです。復旧の手段は下の履歴 API です。エラーキューはプラットフォーム側にあり、自動では再配信されません。
</Warning>

### コールバック URL の要件

* `https` であること(http は社内開発でのみ使えます)。
* URL に認証情報を埋め込まないこと。プラットフォームの認証は署名で行い、パスに隠した秘密では行いません。
* 公開インターネットのアドレスに解決されること。ループバック、プライベート、リンクローカル、ユニークローカル、マルチキャスト、キャリア NAT の各範囲は拒否されます。
* 判定は接続時に行われるので、内部アドレスに解決されるドメインも拒否されます。
* 2048 文字まで。

## 会話履歴を読み直す

これは、受け取れなかった回答ターンを回収する手段です。コールバックは最大 1 回の配信なので、3 回失敗したターンはエラーキューに残ります。この API がなければ、そのメッセージはあなたにとって失われたままです。

```http theme={null}
GET https://console-agents.fpt.ai/direct-bff/v1/conversations/7412/messages?after=9001&limit=30
Authorization: Bearer <API キー>
```

| パラメータ | 説明 | 必須 |
| - | - | - |
| `conversationId` | コールバックの同名フィールドから。正の整数 | 必須 |
| `after` | ページングのカーソル。手元にある最後のメッセージの ID。省くと会話の先頭から読みます | 任意 |
| `limit` | 1 ページあたりの件数。既定 30、最大 100。範囲外の値は拒否ではなく丸められます | 任意 |

200 のレスポンス:

```json theme={null}
{
  "conversationId": "7412",
  "messages": [
    { "id": "9001", "role": "user", "from": "customer",
      "text": "注文はどこまで来ていますか", "createdAt": "2026-09-14T10:30:00Z",
      "attachments": [
        { "name": "invoice.pdf", "type": "application/pdf", "size": 1024 }
      ] },
    { "id": "9002", "role": "assistant", "from": "bot",
      "text": "お調べします。", "createdAt": "2026-09-14T10:30:04Z",
      "buttons": [
        { "type": "postback", "title": "配送状況を追跡", "data": "track:SO-7781" }
      ] },
    { "id": "9003", "role": "assistant", "from": "operator",
      "text": "ご注文の処理が完了しました。", "createdAt": "2026-09-14T10:41:00Z" }
  ],
  "limit": 30,
  "hasMore": false
}
```

* チャネルの秘密鍵ではなく、組織の API キーを使います。秘密鍵は「自社サーバーがメッセージを転送している」ことの証明で、こちらは「自分のデータを自分で読む」操作だからです。`visitorToken` は自分のセッションしか読めず、ほかを読むと 403 `SESSION_FORBIDDEN` になります。
* カーソル方式のページングで、古い順に並びます。`after` に手元の最後のメッセージ ID を渡し、`hasMore` で次のページの有無を見ます。
* メッセージ ID は JavaScript の安全な整数の範囲を超えるため文字列ですが、数値として並び替えられます。
* `role` ではなく `from` を読んでください。人の担当者のターンは Agent と同じ `role` で保存されます。`from` の値は `customer`、`bot`、`operator` の 3 つです。
* `operator` はまだ現れません。このチャネルで担当者が Agent の代わりに応答する機能はないためです。将来の互換性のために予約されています。
* 返る行はすべて実際のターンです。`text`、ボタン、クイックリプライ、カルーセル、出典、添付のいずれかを持つ行だけが現れるので、空の吹き出しを除く処理は要りません。
* 不正な `after` は黙って無視されず、400 が返ります。
* 1 つの 404 が 3 つの場合をまとめています。会話が存在しない、別の組織のもの、社内の会話。意図的に区別できないようにしています。
* 添付には名前、種類、サイズだけが入り、オブジェクトキーは入りません。
* 履歴の読み出しは実行回数の枠を消費しません。既定で API キーあたり毎分 120 回という独自の枠を持ちます。
* 履歴の枠を超えると、429 に `Retry-After` ヘッダーだけが付いて返ります。

<Warning>
  これは復旧の手段であって、アーカイブではありません。データ保持ポリシーに従って顧客が削除されると、そのメッセージもすべて消えます。残しておきたいものは自社のシステムに同期してください。
</Warning>

## API キーと利用枠

### API キーを作る

組織の API キーは、履歴 API とファイルのダウンロードリンク API で使います。Console の **API キー** のページで作成します。

* キーは `sk-` に続く 32 文字のランダムな文字列です。たとえば `sk-9Kd2xQ…`。
* キーは作成時に一度だけ表示されます。システムはハッシュしか保存しないので、あとから見る方法はありません。
* 一覧には先頭 12 文字が出るので、見分けられます。
* キーは作った人の権限で動くため、新しい権限が増えることはありません。

### キーの失効と入れ替え

失効は削除ではなくフラグです。失効したキーの ID もあとから解決できるので、監査証跡が読める状態を保てます。一覧には失効したキーも並びます。

<Warning>
  キーはキャッシュされるため、失効が効くまで最大 30 秒かかります。レスポンスの `revocationDelaySeconds` フィールドがその秒数を伝えます。キーが漏れたときは直ちに失効させ、この遅延を見込んでください。
</Warning>

API キーの入れ替えは、この順番の 2 手順です。先に新しいキーを作り、次に古いキーを失効させる。その間は両方が有効です。

### 利用状況を確認する

```http theme={null}
GET https://console-agents.fpt.ai/direct-bff/v1/usage
Authorization: Bearer <API キー>
```

```json theme={null}
{
  "day": "2026-09-14",
  "runs": { "used": 42, "limit": 2000, "remaining": 1958,
            "resetsAt": "2026-09-15T00:00:00Z" },
  "rate": { "limit": 20, "window": "1m" }
}
```

* `runs` は組織の 1 日あたりの枠(UTC)で、すべての会話とチャネルで共有します。`resetsAt`、つまり UTC の 00:00 に戻ります。
* `rate.limit` は呼び出し元ごとの毎分の上限です。
* 利用状況の確認は枠を消費しないので、何度でも呼べます。
* API キー専用です。`visitorToken` は 403 `USAGE_FORBIDDEN` で拒否されます。

枠を消費する API は、成功時も 429 時も次の 3 つのヘッダーを返します。

```text theme={null}
X-RateLimit-Limit:     2000
X-RateLimit-Remaining: 1957
X-RateLimit-Reset:     1757894400
```

## 次に読むもの

<CardGroup cols={3}>
  <Card title="Live Chat チャネル" icon="comments" href="/ja/live-chat-channel">
    自前で作らず、用意されたチャット画面を使う場合に。
  </Card>

  <Card title="モバイル SDK" icon="mobile-screen" href="/ja/mobile-sdk">
    Android と iOS のアプリにチャット画面を組み込みます。
  </Card>

  <Card title="技術付録" icon="table-list" href="/ja/technical-appendix">
    エラーコード、システムの上限、本番前チェックリスト。
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.