Slack Events APIを受けるendpointでは、署名検証、3秒以内の応答、重複排除を別々の責務として設計します。署名が正しくても処理を同期実行して応答が遅れれば再送されますし、すぐ200を返してもキュー投入前にデータを失えばイベントを処理できません。まず受信内容の真正性を確かめ、永続化できたことを確認してから短く応答し、その後の業務処理はevent_idを鍵に冪等に進めるのが基本です。
この記事では、HTTP Request URLで受信する構成を中心に、何を検証し、いつACKし、再送をどう扱うかを説明します。Socket Modeを選んだ場合は配送の入口や認証方式が異なるため、同じHTTP handlerをそのまま当てはめず、SlackのSocket Mode資料に沿って設計してください。
目次
【先に決めること】
受信方法をHTTP Request URLとSocket Modeのどちらにするか決めます。HTTPはSlackから到達できる公開URLを用意する構成です。Socket ModeはWebSocket接続を使い、公開HTTP endpointを受け口にしない構成です。ネットワーク制約、運用環境、接続の監視方法を比較し、どちらを採用するかを決めてからsubscriptionを設定します。Events APIで受信できるイベントはアプリに許可されたOAuth scopeやインストール状況に結びつきます。イベント名だけを選んでも、必要なscopeがなければ期待するデータは届きません。
HTTP方式では、Slack App設定に登録したRequest URLが、challenge応答を含む検証を通過して有効になる必要があります。イベントを購読する前に、外部から到達できるHTTPS URL、証明書、タイムアウト、応答コードを確認します。開発用の一時URLと本番URLを混同しないよう、環境ごとに接続先と署名Secretの保管先を分けます。署名Secretは環境変数やSecret Manager等のアクセス制御された保管場所に置き、ソースコード、例外ログ、APMのrequest dumpへ出さないでください。
【受信から処理までの流れ】
-
HTTP POSTのraw bodyと署名関連headerを受け取る。
-
timestampの形式と許容時間を検証し、署名をraw bodyに対して照合する。
-
署名に成功した後だけJSONを解析する。
-
url_verificationならchallengeに応答し、event_callbackなら必要なenvelope項目を検査する。
-
event_idを使い、イベントの受付記録とpayloadを耐久性のあるキューまたは受信テーブルへ保存する。
-
保存が成功したら3秒以内に成功応答を返す。
-
workerがevent_id単位で業務処理を実行し、結果と再試行状態を保存する。
この順序には意味があります。署名確認前にJSONを解析したり、payload中の値を認証情報として信用したりしないでください。認証できていないrequestは処理対象としてキューへ入れません。一方、署名検証後にキュー保存が失敗したのに成功応答を返すと、Slack側は配信済みと判断し、アプリ側ではイベントを失う可能性があります。成功応答は「業務処理が完了した」ではなく、「受信内容を検証し、後続処理できる形で受け付けた」ことを示す境界として設計します。
【署名検証で守る範囲】
Slackの署名は、署名Secret、timestamp、raw bodyからHMAC-SHA256を計算し、X-Slack-Signatureと比較します。実装では受信bodyを文字列化・JSON整形・改行正規化する前のバイト列として保持します。解析後のJSONを再シリアライズすると、空白やキー順などが変わり署名が一致しなくなるためです。比較は通常の文字列比較ではなく、時間差を観測されにくいconstant-time比較を使います。
timestampが古すぎるrequestは拒否します。Slack公式例では5分を許容時間の例として示していますが、これはシステム時計が正しいことを前提にした設定例です。実装前にサーバー時計の同期と、プロキシやAPI Gatewayがbody/headerをどう転送するか確認してください。timestampが許容範囲外なら、署名値が一致するように見えても過去のrequestの再利用を防ぐため受理しません。許容時間をむやみに延ばすと、再利用可能な時間窓も広がります。
ヘッダーが欠落、timestampが数値でない、署名の形式が不正、Secretが未設定、raw bodyが取得できないときは、成功扱いにしません。認証失敗の応答コードは実装・運用の契約で決め、詳細な内部理由は安全な監視ログに分類して記録します。ただし、受信payloadや署名Secretをそのままログへ出してはいけません。request bodyにはメッセージ、ユーザー識別子、チャンネル情報などが含まれ得ます。ログに必要なのは相関ID、結果分類、処理時間、event_idを適切に保護した形で記録することです。
【3秒以内に何を終えるか】
Slack Events APIは、イベントrequestに3秒以内で2xx応答するよう求めています。時間のかかる外部API呼び出し、AI推論、重いDB更新、通知送信をHTTP requestの中で完了させようとすると、処理時間が伸び、timeoutや再送の原因になります。受信endpointは署名を検証し、最小限のenvelope確認と耐久キューへの保存を行い、受理できたらすぐ応答してください。worker側が後続の業務処理を担当します。
「キューへ投げたつもり」で応答する前に、保存確認の意味を決めます。メモリ上の非同期タスクに載せただけなら、プロセス停止時に消える場合があります。キュー製品の永続化確認、DB transaction commit、outbox記録など、障害後も回復できる仕組みを使います。受付記録とキュー投入を二つの独立した書き込みにすると、片方だけ成功する中間状態が生じます。transactional outbox、同じDB transaction、または再照合できる状態機械を使って、受付記録とworkerへの引き渡しを整合させてください。
【再送を重複処理にしない】
SlackのEvents APIは、配信失敗後に再送します。公式仕様では、応答が3秒を超えた場合などを失敗として扱い、最大3回のretryを段階的な間隔で試みます。各retryにはx-slack-retry-numとx-slack-retry-reasonが付くため、運用上の切り分けに利用できます。具体的な間隔や失敗条件は変化する可能性があるため、再送を前提にする設計を先に行い、運用開始時には公式ページの最新記述を再確認します。retry headerだけで重複判定するのではなく、イベントそのものの識別子を使います。
Events APIのenvelopeにはevent_idがあり、Slackはこれをイベント固有の一意な識別子として説明しています。DBにevent_idを一意制約付きで保存し、同じIDを再受信した場合の動作を明示します。例えば、未処理なら既存受付を再キューする、処理中なら二重workerを防ぐ、完了済みなら業務処理を再実行せず成功応答する、という状態設計です。event_idを単にログに書くだけでは冪等性になりません。データベース制約や原子的なclaim処理によって、同時到着した二つのworkerも一方だけが処理を開始できるようにします。
【状態管理の例】
受付状態をreceived、queued、processing、succeeded、retryable_failed、permanent_failedのように分けると、どこで止まったかを調べやすくなります。状態名や遷移はシステムに合わせて定義し、Slackの公式仕様で定められている値のように扱わないでください。
表は横にスクロールできます
| 状況 | 受付側の動作 | Worker側の動作 | 再送時 |
|---|---|---|---|
| 署名不正 | 保存しない | 実行しない | 監査ログだけ記録 |
| 署名正常・保存成功 | event_idを一意に保存 | queueから取得 | 既存状態を読み、二重受付を防ぐ |
| worker処理中 | 受付成功を返す | leaseや状態で単一実行を維持 | 同じevent_idを再実行しない |
| 一時的な業務エラー | 受付済み状態を維持 | retry上限と遅延を適用 | Slack retryと自前retryを混同しない |
| 永続エラー | 受付を消さない | dead-letter等へ隔離 | 原因を直した後の再実行を管理操作にする |
| 完了済みevent_id | 既存記録を確認 | 業務副作用を繰り返さない | 保存済み結果で応答する |
Slackからのretryと、アプリ内workerのretryは別レイヤーです。HTTP応答前に失敗したとSlackが判断するケースと、2xx応答後にworkerが失敗するケースを分けて監視します。2xxを返した後のworker失敗はSlackが再送してくれるとは限らないため、アプリ側でretry、dead-letter、手動再実行の運用を設計します。逆に、署名不正や構造不正を無条件に再試行すると、無意味な負荷やログ量を増やします。どのエラーが一時的で、どのエラーが恒久的かを分類してください。
【イベントの順序と副作用】
event_idは同じイベントの重複を識別するためのキーとして使いますが、イベントの到着順や業務上の重複をすべて解決するものではありません。イベントの種類によっては、古い状態への更新が後から届いたり、別event_idで同じ業務対象を更新したりします。必要に応じてイベント時刻、対象リソースの現在状態、更新バージョンを照合し、順序が逆転しても状態が戻らないようにします。これはSlackが保証する処理モデルではなく、受け手側で検討する設計です。
外部への副作用(メール送信、課金、別APIへの書き込みなど)は、DBのevent_id記録と同じ原子操作で完了できない場合があります。副作用直前に処理済みフラグを立てると、呼び出し前の停止で実行漏れになることがあります。呼び出し後にだけフラグを立てると、応答喪失後に二重実行することがあります。対象APIがidempotency keyを提供するなら利用し、なければ業務キーで重複検出する、照合可能なoutboxを使う、再実行前に結果を照会するなど、失敗窓を明示した設計にします。exactly-onceを前提にせず、少なくとも一度届く可能性と、重複を安全に扱う責務を分けてください。
【失敗パターンから原因を切り分ける】
イベントが届かない場合は、まずSlack AppのRequest URL検証、TLS証明書、ネットワーク到達性、subscription、OAuth scope、Appのインストール・再認可を確認します。Slackが送るイベントは、アプリが許可されたscopeと利用者・botがアクセスできる範囲に依存します。チャンネルに参加していない、必要なscopeがない、アプリを再インストールしていないといった条件を、署名問題と混同しないでください。
retryが多い場合は、x-slack-retry-reasonを集計し、timeout、接続失敗、TLS、HTTPエラー、redirectを区別します。endpointの処理時間p95/p99、キュー保存の遅延、DB接続枯渇、worker滞留、応答コードを同じ時間軸で見ます。Slackのイベント配信が一時停止または無効化された場合は、原因を修復した後にApp管理画面で配送再開が必要となる状況があります。自動retryが続くことを前提に無期限で放置しないでください。
イベント数が予想より少ないときは、購読対象、App権限、workspaceごとのインストール状態を確認します。公式Docsにはworkspace/team/app単位の配信上限が記載されています。上限超過時に届く制限イベントもあるため、単に受信endpointの欠落と決めつけず、対象workspaceと時間帯を確認してください。具体的な上限値は運用前にSlack公式Docsで再確認し、固定値として長期運用資料へ埋め込まないのが安全です。
【導入前のチェックリスト】
□ HTTP Request URLとSocket Modeのどちらを採用するか決めた
□ Request URLが外部から到達可能で、HTTPS証明書が有効
□ Challenge応答の設定が完了し、Slack Appで検証済み
□ 必要なイベントsubscriptionとOAuth scopeを対応づけた
□ signing secretを安全に保存し、ログやソースに含めない
□ raw bodyを保ったまま署名検証できる
□ timestampの期限チェックとサーバー時計同期を確認した
□ 検証失敗を成功応答や業務処理へ流さない
□ 署名検証後にだけJSONを解析する
□ queueへの耐久保存が確認できてからACKする
□ 受付処理が3秒以内に収まることを負荷試験で確認した
□ event_idに一意制約または同等の排他制御がある
□ 処理中・完了・再実行可能な失敗を区別できる
□ Slack retryとworker retryを別々に監視する
□ 外部副作用の二重実行防止策がある
□ retry理由、応答時間、queue滞留を監視できる
□ payload、Secret、個人情報をログへ過剰に残さない
□ App再認可・配送再開の担当者と手順が明確
【実装前に行うテスト】
本番workspaceへ試験イベントを大量送信せず、テスト用Appまたは隔離環境で検証します。raw bodyを1バイト変えたときに署名検証が失敗すること、timestampを期限外にして拒否されること、署名なしrequestが業務処理へ進まないことを確認します。署名が正しいrequestでは、キュー保存成功後にのみ成功応答になること、処理が遅いworkerでもHTTP応答は規定時間内で返ることを測ります。
同じevent_idを同時に二度送信し、受付行が重複しないこと、業務副作用が二回起きないことを確認します。完了済みevent_idの再送では保存状態から二重実行を抑止し、retry headerの有無にかかわらずイベントIDで判定することをテストします。キュー保存失敗、DBタイムアウト、worker停止、外部API timeout、dead-letter後の再実行も試します。テストには「想定応答コード」だけでなく、データベース状態、処理回数、ログにSecretが含まれないことを含めます。ここで挙げたテストは設計チェックであり、この記事執筆時に実環境で実行した結果ではありません。
【運用時の監視】
毎日または定めた運用頻度で、イベント受信数、署名検証失敗数、3秒超過数、HTTP 2xx率、Slack retry件数、retry reason、queueの最古滞留時間、worker失敗、dead-letter件数、重複排除数を確認します。イベント数の急変は、プロダクト利用の変化だけでなくsubscriptionや権限の変更、endpoint障害、配信制限も原因になります。ダッシュボードには総数だけでなく、workspace/app単位の粒度と直近の変更履歴を添えます。
アラートは単一の重複イベント数だけで発報するより、遅延や失敗率と組み合わせます。例えば、受信数は通常でもqueue滞留が増えているならworker障害を疑い、受信数とSlack retryが同時に増えているならendpointの応答を調べます。これらの閾値はアプリの平常時データ、SLO、イベント量に基づいて決めるもので、共通の固定値が公式に定められているわけではありません。
【復旧の優先順位】
-
新規requestの署名検証とHTTP応答を回復する。
-
受付済みだが未処理のevent_idを一覧化する。
-
workerが安全に再実行できる状態か確認する。
-
完了済みイベントの外部副作用を再発させない条件を確認する。
-
retryableなイベントを段階的に再処理し、dead-letterは原因を確認してから扱う。
-
処理後にSlackのretry reason、queue滞留、エラー率を見て安定を確認する。
復旧時にDB行を一括削除したり、処理済み記録を消したりするのは避けます。処理済み記録を失うと、後から届いた重複が新規イベントとして扱われる可能性があります。再処理はevent_id、業務キー、外部副作用の状態を照合し、対象範囲を限定した手順で行います。
【結論:実装順序を固定する】
受信handlerは、raw bodyを保持した署名検証、timestamp期限確認、必要なenvelope検査、event_idの耐久記録・queue連携、成功応答の順にします。workerはevent_idの状態を原子的にclaimしてから処理し、成功・再試行可能・恒久失敗を記録します。外部副作用は同じイベントが再実行されたときに安全かを別途検証します。
3秒以内のACKは「受け取ったから処理済み」という意味ではなく、検証したイベントを失わず後続処理へ渡せたことを示す契約です。Slackの再送は障害回復の助けになりますが、アプリ側の二重実行防止やworker再試行の代わりではありません。まず受信の信頼性と冪等性を実装し、遅延・障害・同時再送のテストを通してから、対象イベントの業務ロジックを広げてください。
Sources
-
Slack, The Events API (確認日 2026-10-15): https://docs.slack.dev/apis/events-api/(新しいタブで開く)
-
Slack, Verifying requests from Slack (確認日 2026-10-15): https://docs.slack.dev/authentication/verifying-requests-from-slack/(新しいタブで開く)
Editorial Note
Slack公式仕様と受け手側の設計推奨を分けて記載しています。コード例・本番環境を実行していないため、実測やハンズオン結果は主張していません.
