# Webhook

クラウドフォンやクラウドブラウザーの操作が最終結果に達すると、MoreLogin が自社の HTTPS エンドポイントへ JSON イベントを POST するため、結果をポーリングする必要はありません。コールバック URL はチームごとに 1 つだけ保存され、Local API と Open API は同じレコードに書き込みます。

## コールバック URL を設定する

どちらの入口でも同じ操作を提供しています。Open API：

```bash
curl -X POST "https://api.morelogin.com/webhook/config" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $TOKEN" \
  -d '{
    "callbackUrl": "https://example.com/morelogin/webhook",
    "enabled": true
  }'
```

Local API：

```bash
curl -X POST "http://127.0.0.1:40000/api/webhook/config" \
  -H "Content-Type: application/json" \
  -d '{
    "callbackUrl": "https://example.com/morelogin/webhook",
    "enabled": true
  }'
```

Local API のこのルートはデスクトップクライアントが提供するため、MoreLogin クライアント v2.66.0 以降が必要です。Open API の入口はクライアントに依存しません。

`teamId` はリクエストから読み取りません。各呼び出しはサーバー側コンテキストのチームを設定するため、他チームのイベントを自分のエンドポイントへ向けることはできません。

URL は HTTPS で、1〜1024 文字、ホスト名があり、ユーザー情報とフラグメントを含まず、ポートが正しい必要があります。解決先のすべてのアドレスが公開経路である必要があり、localhost、プライベート、リンクローカル、CGNAT、クラウドメタデータ、プロトコル予約、ドキュメント用、6to4 などの範囲は [`41001`](/ja/api-reference/getting-started/error-codes) で拒否されます。配信が接続を開く時点でも同じ検査を行うため、後からプライベート範囲に解決されるホスト名は、その配信が失敗するだけで、あなたのネットワークには到達しません。

保存はチーム単位の upsert なので、同じ本文を 2 回送っても影響はありません。

### 配信を停止する

`enabled: false` は保存済みの URL を残したまま、新しいイベントの作成を停止します。すでにキューにあるイベントは 1 件ずつキャンセルされますが、キャンセルされるのは各イベントが次に承認される時点で、かつ設定がまだ停止中の場合だけです。そのためすぐに再有効化すれば、キュー内のイベントは破棄されずそのまま配信されます。

## 配信の中身

各配信は次のヘッダーを持つ `POST` です：

| ヘッダー | 値 |
|  --- | --- |
| `Content-Type` | `application/json` |
| `User-Agent` | `MoreLogin-Webhook/1.0` |
| `X-Webhook-Event-Id` | 本文の `eventId` と同じ |
| `X-Webhook-Event-Type` | 本文の `eventType` と同じ |


2 つの `X-Webhook-*` ヘッダーは意図的に本文のフィールドと重複させています。これによりルーターやキューは JSON を解析せずに振り分けられます。

```json
{
  "eventId": "c7f3a2b10d8e4f6a9c1b2d3e4f5a6b7c",
  "eventType": "cloud_phone.power_off",
  "eventTime": "1787241600000",
  "data": {
    "teamId": "10001",
    "cloudPhoneId": "20001",
    "reason": "MONEY_SAVING"
  },
  "result": {
    "success": true,
    "code": "0",
    "message": null
  }
}
```

| フィールド | 説明 |
|  --- | --- |
| `eventId` | 一意のイベント ID。同じイベントの再送や内部リプレイでも再利用されるため、これが冪等キーになります |
| `eventType` | 安定したイベント名。値は下の表を参照 |
| `eventTime` | 最終結果が発生した時刻。Unix エポックのミリ秒で、**文字列として送信** |
| `data.teamId` | このリソースを所有するチーム。**文字列として送信** |
| `data.cloudPhoneId` | このイベントの対象となるクラウドフォン。**文字列として送信** |
| `data.cloudBrowserId` | このイベントの対象となるクラウドブラウザーのプロファイル。**文字列として送信** |
| `data.runId` | このイベントの対象となるクラウドブラウザーの実行インスタンス |
| `data.reason` | `cloud_phone.power_off` と `cloud_browser.stop` のみに付きます |
| `result.success` | この操作が最終的に成功したかどうか |
| `result.code` | 成功時は `0`。それ以外は言語に依存しない安定したビジネスエラーコード |
| `result.message` | 成功時は `null`。それ以外はチームの言語による説明で、無い場合は `en-US` にフォールバック |


数値の識別子と `eventTime` はすべて JSON 文字列として届きます。API の他の部分と同じ扱いです（[共通レスポンス形式](/ja/api-reference/getting-started/response-format) を参照）。数値ではなく文字列として解析してください。そうしないと 64 ビット ID の精度が失われます。

## イベント

| `eventType` | 報告される最終結果 |
|  --- | --- |
| `cloud_phone.power_on` | 起動の成功または失敗 |
| `cloud_phone.power_off` | 停止の成功。停止が失敗した場合はイベントは発生しません |
| `cloud_phone.restart` | 再起動の成功または失敗 |
| `cloud_phone.reset` | リセットの成功または失敗 |
| `cloud_phone.new_machine` | ワンクリック新規端末化の成功または失敗 |
| `cloud_browser.start` | クラウドブラウザーの起動の成功または失敗 |
| `cloud_browser.stop` | クラウドブラウザーの停止試行ごとの成功・失敗 |


クラウドフォンのイベントは `teamId` と `cloudPhoneId` を含みます。クラウドブラウザーのイベントは `teamId`、`cloudBrowserId`、`runId` を含みます。

起動、再起動、リセット、ワンクリック新規端末化が `success: true` を報告するのは、プロバイダーのコールバックが成功し、ローカル同期がコミットされた後だけです。起動時の課金失敗、または非同期のプロキシ検査が失敗して後処理まで完了した場合は起動失敗を報告し、**別途**停止イベントは発生しません。プリセットアプリのインストールや期限切れファイルの削除など後回しにされた処理は、この結果を変更しません。

停止操作が起動を中断した場合、その試行では起動イベントは発生しません。その後に停止が成功すれば、独立した `cloud_phone.power_off` が発生します。

### 停止理由

`cloud_phone.power_off` は `data.reason` を追加します：

| 値 | 意味 |
|  --- | --- |
| `NORMAL` | 通常の停止 |
| `MONEY_SAVING` | 節約モード |
| `OTHER` | 未払い、期限切れ、保守、その他 |


`cloud_browser.stop` は `data.reason` を追加します：

| 値 | 意味 |
|  --- | --- |
| `NORMAL` | 通常の終了 |
| `MONEY_SAVING` | 節約モード |
| `ENERGY_SAVING` | 省電力モード |
| `BROWSER_EXITED` | ブラウザーが予期せず終了した |
| `OTHER` | その他 |


内部のアーカイブ失敗は `result` のみに影響し、停止開始時に確定した `reason` を上書きすることはありません。

## 配信、リトライ、順序

`2xx` はすべて配信成功として扱います。それ以外のステータス、接続タイムアウト、読み取りタイムアウト、ネットワークエラーはすべて失敗です。接続タイムアウトは 5 秒、読み取りタイムアウトは 30 秒です。

配信が失敗した場合、最大 6 回リトライし、間隔は 10 秒、30 秒、60 秒、3 分、10 分、30 分です。つまり 1 つのイベントで最大 7 回のリクエストになります。6 回目のリトライが失敗すると、そのイベントは自動リトライを停止します。

同じクラウドフォン、または同じクラウドブラウザーのイベントは、作成順に 1 件ずつ配信されます。異なるリソースは並行して配信されるため、それらの間の順序は前提にしないでください。

1 つのイベントのリトライは、常に同じ `eventId`、`eventTime`、本文を再利用します。ネットワークや復旧の例外的な状況では、これらの値が変わらないまま同じイベントが複数回配信されることがあるため、**`eventId` で重複排除してください**。

クラウドブラウザーの停止では、1 回の停止試行がちょうど 1 つのイベントを生成します。その後の手動リトライ、手動の強制解放、システムによる強制解放はいずれも新しい操作であり、新しい `eventId` が割り当てられます。2 回の試行が 1 つのイベントにまとめられることはありません。

クラウドフォンの操作は、プロバイダーのタスク ID、`traceId`、または安定した業務時刻で識別します。いずれも存在しない場合、サービスは UUID を生成し、現在の最終コールバックを新しい操作として扱うため、2 つの異なる操作が 1 つにまとまるのではなく、新しい `eventId` が発行されます。

## 受信側の実装

イベントを保存できたらすぐに `2xx` を返し、実処理は非同期で行ってください。レスポンス本文は無視されます。処理が遅いと 30 秒の読み取り予算を使い切り、受信済みであってもそのイベントはリトライ待ちに回されます。

`eventId` で重複排除してください。重複は既に処理済みとして扱い、副作用を再実行しないようにします。

自分のエンドポイントに届いたからといって、本文を信頼できる入力として扱わないでください。本リリースではコールバックに署名しないため、URL を知った人は誰でも送信できます。推測しにくい URL を使い、コストの大きい処理を行う前に API で状態を確認してください。たとえばクラウドフォンなら [`POST /cloudphone/info`](/ja/api-reference/cloud-phone/open-api)、クラウドブラウザーの実行なら `POST /cloudbrowser/page` です。

`result.code` は同期 API と同じビジネスエラーコードを使うため、失敗は [エラーコード](/ja/api-reference/getting-started/error-codes) で調べられます。`result.message` は人間向けの文字列で、文言も言語も変わる可能性があります。分岐は `result.success` と `result.code` で行い、`result.message` では決して行わないでください。

各イベントの機械可読な契約（スキーマとイベントごとの例を含む）は、共有リソースの仕様とともに [Shared Resources Open API](/ja/api-reference/shared-resources/open-api) で公開されています。