Webhook
Outgoing Webhooks
CRISISから外部システムへリアルタイムにイベント通知を配信する仕組みです。
概要
Outgoing Webhookを設定すると、CRISISの標準トピックで提供された情報(地震情報、津波情報、ミサイル発射情報など)、ユーザー定義のトピックで受信した情報、CRISIS内で発生したイベント(インシデント作成、資産状態変更など)をHTTP POSTで即座に通知します。
ユースケース
- 地震情報受信時に独自の社内システムを起動
- インシデント発生時にSlack/Teamsへ通知
- 資産状態変更を外部の台帳システムに同期
クイックスタート
Step 1: CRISISコンソールでWebhookを作成
- 組織 > 設定 > Webhook を開く
- 「+ 新規作成」をクリック
- 以下を入力:
- Name:
My First Webhook
- URL:
https://your-server.example.com/webhook(HTTPS必須)
- 「作成」をクリック
作成完了後、signing_secret が表示されます。この値はこの画面でのみ確認できます。安全に保管してください。
Step 2: 受信サーバーを構築
最小限の受信サーバー例(Node.js):
const crypto = require('crypto');
const express = require('express');
const app = express();
app.use(express.raw({ type: 'application/json' }));
const SIGNING_SECRET = process.env.CRISIS_SIGNING_SECRET;
app.post('/webhook', (req, res) => {
// 署名検証
const timestamp = req.headers['x-crisis-timestamp'];
const signature = req.headers['x-crisis-signature'];
const signedPayload = `${timestamp}.${req.body}`;
const expected = 'v1=' + crypto
.createHmac('sha256', SIGNING_SECRET)
.update(signedPayload)
.digest('hex');
if (signature !== expected) {
return res.status(401).send('Invalid signature');
}
// ペイロード処理
const event = JSON.parse(req.body);
console.log(`Received: ${event.event_type}`, event.data);
res.status(200).send('OK');
});
app.listen(8080);
Step 3: テスト送信で確認
- Webhook一覧画面で作成したWebhookの「テスト送信」をクリック
- 結果を確認:
- ✅
success: true + status_code: 200 → 正常に動作しています
- ❌ 失敗した場合はURLの疎通やHTTPS証明書を確認してください
Step 4: ワークフローでWebhookアクションを追加
- ワークフロー で新規作成 or 既存を編集
- トリガーソースを選択(例:
jma.earthquake、crisis.events)
- アクション追加で「Webhook送信」を選択
- 作成したWebhookをドロップダウンから選択
- 保存
以降、条件にマッチするイベントが発生するたびにWebhookが配信されます。
トリガーソース
Webhookはワークフローのアクションとして配信されます。ワークフローのトリガーソース(トピック)によって、どのイベントでWebhookが発火するかが決まります。
CRISIS提供トピック
| トピックキー | 説明 | フィルター例 |
jma.earthquake | 地震情報 | 震度5弱以上で通知 |
jma.nankai_trough_temporary_information | 南海トラフ地震臨時情報 | 調査中・巨大地震注意・巨大地震警戒で通知 |
jma.weather.warning | 気象警報・注意報 | 大雨警報で通知 |
mlit.river.waterlevel | 河川水位情報 | 水防団待機水位以上で通知 |
mlit.kaiho.missile | ミサイル発射情報 | 発射情報で通知 |
これらはCRISISが気象庁XMLや河川情報、Lアラート情報を自動的にパースし、ワークフローエンジンに配信します。ワークフロー条件(フィルター)で震度や対象地域をフィルタリングできます。
ユーザー定義トピック
独自のトピックを作成し、外部システムからPublish APIでイベントを送信できます。詳しくは Incoming Webhooks (ワークフロートピック) を参照してください。
| ユースケース | トピック例 |
| IoT水位センサー | custom.river_level |
| 社内監視システム | custom.monitoring_alert |
| 外部SaaS連携 | custom.saas_case |
crisis.events(システム内部イベント)
CRISIS内部で発生するリソース変更イベントです。インシデント作成、資産更新、フォーム回答など、CRISISの操作に対してWebhookを配信できます。
Note
crisis.eventsトピックでは通知系アクション(Webhook送信、メール、プッシュ通知)のみ利用可能です。オペレーションブック起動はループ防止のため無効化されます。
ペイロード形式
HTTPヘッダー
| ヘッダー | 値 | 条件 |
Content-Type | application/json | 常に付与 |
X-Crisis-Timestamp | Unixタイムスタンプ(秒) | signing_secret設定時 |
X-Crisis-Signature | v1=<hex HMAC-SHA256> | signing_secret設定時 |
Authorization | 認証設定による | auth_type設定時 |
| カスタムヘッダー | Webhook設定 + ワークフローアクション設定 | 設定時 |
JSONボディ
{
"event_type": "incident.created",
"organization_id": "01HJYZ...",
"topic_id": "01HDYZ",
"topic_key": "crisis.events",
"message_id": "01HXYZ...",
"published_at": "2026-05-01T09:15:30Z",
"status": "ACTUAL",
"data": {
"event_type": "incident.created",
"organization_id": "org_abc123",
"actor_id": "user_def456",
"actor_name": "田中太郎",
"resource_id": "incident_ghi789",
"resource_crn": "crn:crisis:organizations:org_abc123:incidents/incident_ghi789",
"timestamp": "2026-05-01T09:15:30Z",
"incident_id": "incident_ghi789",
"name": "最大震度5弱",
"severity": "high"
},
"title": "インシデント作成通知",
"body_text": "最大震度5弱 が作成されました"
}
| フィールド | 説明 |
event_type | イベントタイプ(トップレベル) |
organization_id | 対象組織ID |
topic_id | ワークフロートピックID |
topic_key | トピックキー(crisis.events, jma.earthquake 等) |
message_id | メッセージ一意ID(冪等性チェックに使用可能) |
published_at | イベント発行時刻(ISO 8601) |
status | ACTUAL(実災害)/ DRILL(訓練)/ TEST(テスト) |
data | イベント固有のデータ |
title | 通知タイトル(ワークフローアクション設定時) |
body_text | 通知本文テキスト(ワークフローアクション設定時) |
署名検証
signing_secret を設定すると、各配送に HMAC-SHA256 署名が付与されます。受信サーバーで署名を検証することで、リクエストがCRISISから送信されたことを保証できます。
検証手順
X-Crisis-Timestamp ヘッダーからタイムスタンプを取得
- 署名対象文字列を構築:
"{timestamp}.{raw_request_body}"
- HMAC-SHA256を計算:
HMAC-SHA256(signing_secret, signed_payload)
- 期待値を構築:
"v1=" + hex(result)
X-Crisis-Signature ヘッダーと比較
Node.js
const crypto = require('crypto');
function verifySignature(signingSecret, timestamp, body, signature) {
const signedPayload = `${timestamp}.${body}`;
const expected = 'v1=' + crypto
.createHmac('sha256', signingSecret)
.update(signedPayload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature)
);
}
Go
package webhook
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
)
func VerifySignature(signingSecret, timestamp, body, signature string) bool {
signedPayload := fmt.Sprintf("%s.%s", timestamp, body)
mac := hmac.New(sha256.New, []byte(signingSecret))
mac.Write([]byte(signedPayload))
expected := "v1=" + hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(expected), []byte(signature))
}
cURL (テスト用)
# 署名の再計算
TIMESTAMP="1714546530"
BODY='{"event_type":"incident.created",...}'
SECRET="whsec_your_signing_secret"
SIGNATURE=$(echo -n "${TIMESTAMP}.${BODY}" | \
openssl dgst -sha256 -hmac "${SECRET}" | \
awk '{print "v1="$2}')
echo $SIGNATURE
タイムスタンプ検証(推奨)
リプレイ攻撃を防ぐため、タイムスタンプが5分以内であることを確認してください:
const MAX_AGE_SECONDS = 300; // 5分
function isTimestampValid(timestamp) {
const now = Math.floor(Date.now() / 1000);
return Math.abs(now - parseInt(timestamp)) < MAX_AGE_SECONDS;
}
イベント一覧
Incident(インシデント)
| event_type | 説明 | data に含まれる主なフィールド |
incident.created | インシデント作成 | incident_id, name, severity |
incident.updated | インシデント更新 | incident_id, name |
incident.closed | インシデント終了 | incident_id |
incident.archived | インシデントアーカイブ | incident_id |
incident.reopened | インシデント再開 | incident_id |
incident.deleted | インシデント削除 | incident_id |
incident.iam.updated | インシデントIAM変更 | |
Incident Headquarters(本部)
| event_type | 説明 |
incident.headquarters.established | 本部設置 |
incident.headquarters.updated | 本部更新 |
incident.headquarters.disbanded | 本部解散 |
Incident Operation Book(オペレーションブック)
| event_type | 説明 |
incident.operation_book.created | オペブック起動 |
incident.operation_book.updated | オペブック更新 |
incident.operation_book.severity_changed | 重大度変更 |
incident.operation_book.action.created | アクション作成 |
incident.operation_book.action.updated | アクション更新 |
incident.operation_book.action.status_changed | アクションステータス変更 |
incident.operation_book.action.acknowledged | アクション確認 |
incident.operation_book.task.created | タスク作成 |
incident.operation_book.task.updated | タスク更新 |
incident.operation_book.task.status_changed | タスクステータス変更 |
incident.operation_book.task.acknowledged | タスク確認 |
incident.operation_book.task.deadline_changed | タスク期限変更 |
Incident Form(安否確認フォーム)
| event_type | 説明 |
incident.form.created | フォーム作成 |
incident.form.updated | フォーム更新 |
incident.form.deleted | フォーム削除 |
incident.form.reminded | フォームリマインド |
incident.form.revision.created | リビジョン作成 |
incident.form.revision.updated | リビジョン更新 |
incident.form.answer.submitted | 回答送信 |
incident.form.answer.updated | 回答更新 |
Incident Sub-resources
| event_type | 説明 |
incident.chronicle.created | タイムライン追加 |
incident.chronicle.updated | タイムライン更新 |
incident.note.created | ノート作成 |
incident.note.updated | ノート更新 |
incident.note.deleted | ノート削除 |
incident.report.created | レポート作成 |
incident.report.deleted | レポート削除 |
Incident Group
| event_type | 説明 |
incident_group.created | インシデントグループ作成 |
incident_group.updated | インシデントグループ更新 |
incident_group.deleted | インシデントグループ削除 |
Asset(資産管理)
| event_type | 説明 | data |
asset.group.created | 資産グループ作成 | asset_group_id, name |
asset.group.updated | 資産グループ更新 | asset_group_id, name |
asset.group.deleted | 資産グループ削除 | asset_group_id |
asset.item.created | 資産作成 | asset_item_id, name, kind |
asset.item.updated | 資産更新 | asset_item_id, name, kind |
asset.item.deleted | 資産削除 | asset_item_id, name, kind |
asset.item.state.updated | 資産状態変更 | asset_item_id |
asset.vehicle_model.created | 車両モデル作成 | vehicle_model_id, name |
asset.vehicle_model.updated | 車両モデル更新 | vehicle_model_id, name |
asset.vehicle_model.deleted | 車両モデル削除 | vehicle_model_id |
Emergency(緊急通報)
| event_type | 説明 |
emergency.created | 緊急通報作成 |
その他
| event_type | 説明 |
webhook.delivery_failed | Webhook配送失敗 |
リトライと配送保証
リトライポリシー
Webhookの配送が失敗(非2xxレスポンスまたは接続エラー)した場合、指数バックオフでリトライします。
| 設定 | デフォルト | 説明 |
max_retries | 5 | 最大リトライ回数 |
retry_interval_seconds | 60 | 初回リトライ間隔(秒) |
retry_backoff_multiplier | 2.0 | バックオフ倍率 |
retry_max_interval_seconds | 3600 | リトライ間隔上限(秒) |
リトライスケジュール例(デフォルト設定)
| 試行 | 待機時間 | 累計 |
| 1回目 | 即時 | 0s |
| 2回目 | 60s | 1分後 |
| 3回目 | 120s | 3分後 |
| 4回目 | 240s | 7分後 |
| 5回目 | 480s | 15分後 |
| 6回目 (最後) | 960s | 31分後 |
配送保証
- At-least-once delivery: 同一イベントが複数回配送される可能性があります
- 冪等性チェック:
message_id をキーとして重複処理を排除してください
- 順序保証なし: イベントの配送順序は保証されません。
published_at で時系列を判定してください
- タイムアウト: CRISISは10秒でレスポンスを待ちます。10秒以内に2xxを返してください
受信サーバーの実装例
Google Cloud Run Functions (Node.js)
const crypto = require('crypto');
const functions = require('@google-cloud/functions-framework');
const SIGNING_SECRET = process.env.CRISIS_SIGNING_SECRET;
functions.http('crisisWebhook', (req, res) => {
// 署名検証
const timestamp = req.headers['x-crisis-timestamp'];
const signature = req.headers['x-crisis-signature'];
if (!timestamp || !signature) {
return res.status(401).json({ error: 'Missing signature headers' });
}
// タイムスタンプ検証(5分以内)
const age = Math.abs(Date.now() / 1000 - parseInt(timestamp));
if (age > 300) {
return res.status(401).json({ error: 'Timestamp too old' });
}
// HMAC検証
const body = JSON.stringify(req.body);
const signedPayload = `${timestamp}.${body}`;
const expected = 'v1=' + crypto
.createHmac('sha256', SIGNING_SECRET)
.update(signedPayload)
.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature))) {
return res.status(401).json({ error: 'Invalid signature' });
}
// イベント処理
const { event_type, data, message_id } = req.body;
switch (event_type) {
case 'incident.created':
console.log(`New incident: ${data.name} (${data.severity})`);
// TODO: 外部システムに通知
break;
case 'asset.item.state.updated':
console.log(`Asset state changed: ${data.asset_item_id}`);
break;
default:
console.log(`Unhandled event: ${event_type}`);
}
res.status(200).json({ received: true, message_id });
});
Go (net/http)
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"log"
"math"
"net/http"
"os"
"strconv"
"time"
)
var signingSecret = os.Getenv("CRISIS_SIGNING_SECRET")
type WebhookPayload struct {
EventType string `json:"event_type"`
OrganizationID string `json:"organization_id"`
MessageID string `json:"message_id"`
PublishedAt time.Time `json:"published_at"`
Status string `json:"status"`
Data map[string]any `json:"data"`
}
func webhookHandler(w http.ResponseWriter, r *http.Request) {
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "Failed to read body", http.StatusBadRequest)
return
}
// 署名検証
timestamp := r.Header.Get("X-Crisis-Timestamp")
signature := r.Header.Get("X-Crisis-Signature")
if !verifySignature(timestamp, body, signature) {
http.Error(w, "Invalid signature", http.StatusUnauthorized)
return
}
// タイムスタンプ検証
ts, _ := strconv.ParseInt(timestamp, 10, 64)
if math.Abs(float64(time.Now().Unix()-ts)) > 300 {
http.Error(w, "Timestamp too old", http.StatusUnauthorized)
return
}
// ペイロード処理
var payload WebhookPayload
if err := json.Unmarshal(body, &payload); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
log.Printf("Event: %s | Resource: %v", payload.EventType, payload.Data["resource_id"])
w.WriteHeader(http.StatusOK)
}
func verifySignature(timestamp string, body []byte, signature string) bool {
signedPayload := fmt.Sprintf("%s.%s", timestamp, string(body))
mac := hmac.New(sha256.New, []byte(signingSecret))
mac.Write([]byte(signedPayload))
expected := "v1=" + hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(expected), []byte(signature))
}
func main() {
http.HandleFunc("/webhook", webhookHandler)
log.Fatal(http.ListenAndServe(":8080", nil))
}
ベストプラクティス
| 項目 | 推奨事項 |
| レスポンス速度 | 10秒以内に200を返す。重い処理は非同期キューに入れる |
| 冪等性 | message_id で処理済みイベントを記録し、重複実行を防ぐ |
| 署名検証 | 必ず実装する。タイミング安全な比較関数を使う |
| タイムスタンプ検証 | 5分以上古いリクエストは拒否する |
| HTTPS | 受信URLは必ずHTTPS。自己署名証明書は使用不可 |
| エラーハンドリング | 一時的エラーは5xxを返す(リトライされる)。永続的エラーは4xxを返す |
| ログ | message_id と event_type をログに記録し、トラブルシューティングに備える |
| 秘密情報の管理 | signing_secret はSecret Manager等に格納し、ソースコードにハードコードしない。可能であれば Workload Identity Federation を使用する。 |