---
title: "Slack Incoming Webhookの実装手順：URL発行・Block Kit整形・SDK送信と429対策"
url: "https://www.issoh.co.jp/tech/details/17652/"
published: 2026-09-16
updated: 2026-10-25
categories: ["API"]
publisher: "株式会社一創"
---

# Slack Incoming Webhookの実装手順：URL発行・Block Kit整形・SDK送信と429対策

Slackへの通知を作るとき、コードより先に決まってしまうものがあります。送信先チャンネルです。Slack Incoming Webhookで払い出されるURLはインストール時に選んだ1つのチャンネルに結び付いており、公式ドキュメントは payload で送信先や表示名を実行時に上書きすることを明確に否定しています。この記事では、URLを発行する操作からcurlでの最小送信、Block Kitとmrkdwnの記法、PythonとNode.jsの公式SDKでの送り方、1秒1メッセージという投稿レートの制限、chat.postMessage へ移るべき条件までを、公式ドキュメントの記述とそのまま動くコードで整理しました。古い記事のサンプルが通らない理由も、仕様の変更点として扱います。

## まとめ：Slack Webhook実装で最初に決める送信方式と上限

Incoming Webhookは `https://hooks.slack.com/services/T00000000/B00000000/XXXX` の形で1本ずつ払い出され、公式ドキュメントでは「specific to a single user and a single channel」と説明されています。1本のURLは1チャンネル専用だと考えてください。

実装前に決めるべきことは2つ。1つは送信先を動的に変える要件の有無です。公式ドキュメントは「You cannot override the default channel (chosen by the user who installed your app), username, or icon when you’re using incoming webhooks」と明記しており、payload に `channel` を入れても無視されます。送り分けたいチャンネルの数だけURLを発行するのが現行の解です。

もう1つは投稿の頻度。Slackは投稿系に「apps may post no more than one message per second per channel」という上限を置いており、超えると HTTP 429 と `Retry-After` ヘッダが返ります。毎秒数十件のイベントを個別通知する設計は、この時点で破綻します。投稿後にメッセージを書き換えたい、スレッドにぶら下げたいという要件があるならWebhookは選びません。応答は本文 `ok` の文字列だけで、メッセージを特定する `ts` が返らないためです。

送信コードを書く段では、HTTPクライアントで直接POSTしても公式SDKを使っても構いません。ただしSDKの既定値は429を再送しない点で共通しており、Node.js版はリトライを有効にしても429だけは即失敗させます。再送の扱いはSDKに任せきりにせず、呼び出し側で決めてください。

## Slack Webhookの送信方向と受信URLへ送るJSONの基本構造

Slackで「Webhook」と呼ばれるものは向きが2種類あり、混同すると設定画面を探す場所から間違えます。仕組みそのものの説明は[Incoming Webhookとは？仕組みとSlack・Discordでの使い方](https://www.issoh.co.jp/tech/details/6596/)で整理しているので、ここではSlackアプリを前提にした実装側の構造だけを扱います。

### 通信の向きで分かれるIncoming型とOutgoing型の現行の扱い

Incoming Webhookは、外部のシステムがSlackへHTTP POSTを投げてメッセージを投稿させる仕組みです。監視サーバーやCIからの通知はすべてこちらに当たります。逆にOutgoing Webhookは、Slack側のトリガーを外部のURLへ飛ばすもので、レガシーのカスタムインテグレーションに属します。公式ドキュメントは「Though we recommend that all legacy custom integrations should migrate to Slack apps」と書いており、Slack側のイベントを新規に受けたいならイベント購読の仕組みを選ぶのが筋です。

### Workflow Builderのwebhookトリガーと見分けるURLの接頭辞

もう1つ紛らわしいのが、ワークフロービルダーの「Webhookから」始まるワークフローです。[Slackヘルプセンターの解説](https://slack.com/intl/ja-jp/help/articles/360041352714)によると、ワークフローを公開するとリクエストURLが生成され、そのURLは「すべて https://hooks.slack.com/triggers で始まり」ます。Incoming Webhookの `/services/` とはパスが違うので、手元のURLがどちらかは接頭辞で判別できます。

中身の作法も異なります。ワークフロー側は最大20個のカスタム変数を定義し、HTTPリクエスト本文のキーをその変数名に合わせて送る方式で、`text` や `blocks` を解釈するわけではありません。投稿文面はワークフローのステップ側で組みます。利用できるのは有料プランに限られ、オーナーと管理者が作成できるメンバーを制限できる、とも書かれています。無料プランで開発者がコードから通知を送るならIncoming Webhook、非エンジニアが文面を画面で管理したいならワークフロー、という分け方が実際的です。

### Content-typeヘッダをJSONで明示する理由と応答に返るok

払い出されるURLは `https://hooks.slack.com/services/` に続いてチーム識別子（T始まり）、Webhook識別子（B始まり）、トークン相当の文字列という3階層です。3つ目が実質のシークレットで、URL全体が認証情報を兼ねています。

送信時は[公式のIncoming Webhooksガイド](https://docs.slack.dev/messaging/sending-messages-using-incoming-webhooks/)が示すとおり、ボディにJSONを置き `Content-type: application/json` を明示します。ヘッダを省くとフォーム形式として送られ、パースに失敗する場合がありました。成功時は HTTP 200 とプレーンテキストの `ok`、失敗時はエラー文字列が返ります。応答本文にJSONが返ってこない点が、Web APIメソッドとの大きな違いです。

## Incoming Webhook URLを発行してcurlで最初の通知を送る手順

発行はSlackアプリの管理画面で完結します。インストール時に管理者の承認依頼が飛ぶ設定のワークスペースも多いので、先に権限を確認しておくと手戻りがありません。

### Slackアプリ作成からWebhook URL発行までの画面操作の順序

公式ガイドの手順は4段階で、順番が固定されています。手順3で選んだチャンネルがそのURLの固定の送信先になり、別のチャンネルへ送りたくなったら同じ画面で2本目を追加します。アプリ自体を作り直す必要はありません。

1. Slack APIのアプリ管理画面で「Create an app」からアプリ名とインストール先ワークスペースを指定する
2. 左メニューの「Incoming Webhooks」を開き、「Activate Incoming Webhooks」をオンにする
3. 画面下部の「Add New Webhook to Workspace」を押し、投稿先チャンネルを選んで「Authorize」する
4. 一覧に追加されたWebhook URLをコピーし、環境変数などのシークレット領域へ保存する

### curlでtextだけのJSONを送る最小実装と200・okの確認

疎通確認は1コマンドで終わります。URLを環境変数に入れ、テキストだけのJSONを投げてください。`-i` を付けてステータス行まで見ると、後述のエラー切り分けが使えます。

```
export SLACK_WEBHOOK_URL="https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX"

curl -i -X POST "$SLACK_WEBHOOK_URL" \
  -H "Content-type: application/json" \
  --data '{"text": "デプロイが完了しました"}'
```

期待する応答は `200 OK` のステータス行と本文 `ok` の2つ。ここで200が返れば、URLもチャンネルも生きています。この段階で失敗するなら、アプリケーション側のコードを疑う前にURLそのものを疑ってください。シェルの履歴にURLが残る点には注意が要るので、共用サーバーで試すときは `--data @payload.json` の形でファイル経由にします。

### 失敗時に返るinvalid\_payloadなどエラー文字列の切り分け表

公式ドキュメントは失敗時のエラー文字列を列挙しています。応答本文がそのまま原因を指すので、ログにはステータスコードと本文の両方を残してください。

| エラー文字列                | 主な原因          | 最初に見る場所      |
| --------------------- | ------------- | ------------ |
| invalid\_payload      | JSONが壊れている    | 送信ボディとヘッダ    |
| channel\_not\_found   | 投稿先が削除・改名された  | チャンネル一覧      |
| channel\_is\_archived | 投稿先がアーカイブ済み   | チャンネルの状態     |
| no\_service           | URLが失効している    | Webhookの一覧画面 |
| no\_active\_hooks     | Webhookが削除された | アプリの設定画面     |
| action\_prohibited    | 投稿が禁止されている    | 管理者のポリシー     |
| no\_text              | 本文の中身が空       | ペイロード生成処理    |

公式の一覧にはこのほか `posting_to_general_channel_denied`（全体チャンネルへの投稿制限）や `too_many_attachments` も含まれます。頻度が高いのは `invalid_payload` と `channel_not_found` の2つ。後者はSlack側の運用変更が原因で、コードは正しいのに通知だけ止まります。チャンネルの改名でURLが死ぬ挙動は見落としやすいので、通知が届かないと言われたら、まずcurlで1通打って応答本文を読んでください。

## Block Kitとmrkdwnで通知を整形する記法とエスケープ規則

テキスト1行の通知で足りるのは最初だけです。障害通知にステータスや担当者を載せ始めた時点で、整形の仕様を避けて通れなくなります。

### textとblocksを併記して通知プレビューを壊さない書き方

[Block Kit](https://docs.slack.dev/reference/block-kit/blocks/)を使うと、見出し・本文・区切り線・ボタンといった部品を配列で組み立てられます。注意したいのは `blocks` だけを送った場合の挙動で、デスクトップ通知やチャンネル一覧のプレビューには本文が出ません。`text` を要約として併記しておけば、通知領域ではその1行が使われます。

```
{
  "text": "[本番] APIの5xxエラー率が2.4%を超えました",
  "blocks": [
    {
      "type": "header",
      "text": { "type": "plain_text", "text": "本番アラート: 5xxエラー率" }
    },
    {
      "type": "section",
      "fields": [
        { "type": "mrkdwn", "text": "*環境*\nproduction" },
        { "type": "mrkdwn", "text": "*エラー率*\n2.4% (直近5分)" }
      ]
    },
    {
      "type": "section",
      "text": {
        "type": "mrkdwn",
        "text": "詳細は <https://example.com/dashboards/api|ダッシュボード> を参照してください"
      }
    }
  ]
}
```

`header` ブロックは `plain_text` しか受け付けず、装飾記法は文字列のまま表示されます。太字やリンクを入れる行は `section` の `mrkdwn` 側に置いてください。

### リンクとメンションでMarkdownと異なるSlack独自の記法

Slackの整形記法はmrkdwnと呼ばれ、一般的なMarkdownと似て非なるものです。[公式のフォーマット仕様](https://docs.slack.dev/messaging/formatting-message-text/)では、太字が `*bold*`、斜体が `_italic_`、打ち消しが `~strike~` と定義されています。アスタリスク2個の太字は効きません。

差が大きいのはリンクです。角かっこと丸かっこではなく、山かっこの中にURLとパイプ記号、表示文字を並べます。メンションも同じ構文で、ユーザーは `<@U012AB3CD>`、ユーザーグループは `<!subteam^SAZ94GDB8>`、全体への呼びかけは `<!channel>` や `<!here>` を使います。ユーザー名を文字列で書いても通知は飛ばず、IDでの指定が要る点が落とし穴です。

### アンパサンドと不等号をHTMLエンティティへ置換しないと壊れる条件

山かっことアンパサンドは制御文字として解釈されるため、本文にそのまま含めると表示が崩れます。公式仕様は `&` を `&amp;`、`<` を `&lt;`、`>` を `&gt;` へ置換するよう求めています。実務で刺さるのは、エラーメッセージやSQLをそのまま通知へ流したとき。`WHERE price < 1000 AND stock > 0` のような文字列は、置換せずに送るとリンク記法の一部と誤認され、条件式が消えて届きます。任意の文字列を載せる箇所では、送信直前にこの3文字だけを置換してください。対象を増やすと、今度はコードブロック内の記号が二重エスケープされます。

## Python slack\_sdkとNode.jsの@slack/webhookで送る実装

curlで疎通が取れたら、アプリケーションから送るコードに移ります。標準のHTTPクライアントで書いても動きますが、Slackは言語別に公式SDKを配布しており、Incoming Webhook専用のクライアントが含まれています。2026年9月時点では、PyPIの[slack-sdk](https://pypi.org/project/slack-sdk/)は3.44系（Python 3.7以上）、npmの @slack/webhook は8.0系（Node.js 20以上）が最新でした。両者は429の扱いが違うため、同じ感覚で書くと片方で通知が落ちます。

### WebhookClientに429用の再試行を足すPythonの送信コード

[Python SDKのWebhookドキュメント](https://docs.slack.dev/tools/python-slack-sdk/webhook/)では、`WebhookClient` にURLを渡して `send()` を呼び、`response.status_code` が200かつ `response.body` が `ok` なら成功と判定する形が示されています。既定で有効なのは接続エラー向けの `ConnectionErrorRetryHandler` だけで、指数バックオフとジッターを挟んだ再試行は1回だけ。429に備えるには、レート制限用のハンドラを自分で追加します。

```
pip install slack-sdk
```

```
import os

from slack_sdk.http_retry.builtin_handlers import RateLimitErrorRetryHandler
from slack_sdk.webhook import WebhookClient

webhook = WebhookClient(os.environ["SLACK_WEBHOOK_ALERT"])
# 既定は接続エラーの再試行だけなので、429向けのハンドラを追加する
webhook.retry_handlers.append(RateLimitErrorRetryHandler(max_retry_count=2))

response = webhook.send(
    text="[本番] 夜間バッチが失敗しました",
    blocks=[
        {
            "type": "section",
            "text": {"type": "mrkdwn", "text": "*job*: nightly-export\n*status*: failed"},
        }
    ],
)
if response.status_code != 200 or response.body != "ok":
    raise RuntimeError(f"Slack送信失敗: {response.status_code} {response.body}")
```

`text` と `blocks` を併記する作法は、SDK経由でも変わりません。同じ `WebhookClient` は、ボタン操作などで払い出される `response_url` への返信にも使えます。asyncioで動かす場合は `AsyncWebhookClient` があり、こちらは aiohttp の追加インストールが前提です。

### @slack/webhookは429を再送しないのでfetchで待つ実装

[Node SDKのWebhookドキュメント](https://docs.slack.dev/tools/node-slack-sdk/webhook/)では、`npm install @slack/webhook` のあと `new IncomingWebhook(url)` で初期化し、`await webhook.send({ text })` で送ります。リトライは既定でオフで、`retryConfig` を渡したときだけ有効です。ただし再試行の対象は5xxと応答のないネットワークエラーだけで、ドキュメントは「Client errors (4xx), including rate limits (429), fail immediately without a retry」と明記しています。リトライ設定を入れても429では待ってくれません。

アラートが連続しうる経路では、SDKを使わずNode.js 20以上に標準で入っている `fetch` で書き、`Retry-After` を読んで待つほうが挙動を把握しやすくなります。

```
const WEBHOOK_URL = process.env.SLACK_WEBHOOK_ALERT;

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

async function postToSlack(payload, maxRetry = 3) {
  for (let attempt = 0; attempt < maxRetry; attempt++) {
    const res = await fetch(WEBHOOK_URL, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(payload),
    });
    if (res.status === 429) {
      // Retry-After は再試行できるまでの秒数
      const wait = Number(res.headers.get("retry-after") ?? "1");
      await sleep(wait * 1000);
      continue;
    }
    const body = await res.text();
    if (!res.ok) {
      throw new Error(`Slack送信失敗: ${res.status} ${body}`);
    }
    return body; // 成功時は "ok"
  }
  throw new Error("Slackへの送信がレート制限で完了しませんでした");
}

await postToSlack({ text: "[本番] 夜間バッチが失敗しました" });
```

最終行のトップレベル `await` はESモジュール（拡張子 `.mjs` など）で動かす前提です。CommonJSのまま使うなら、呼び出しを非同期関数の中へ移してください。SDKを選ぶか直接書くかの判断は単純で、送信が散発的で429を気にしなくてよい経路は @slack/webhook、監視アラートのように連続する経路は自前の再送、と分けておけば迷いません。

## channelとusernameの実行時上書き廃止で旧サンプルが動かない理由

検索して出てくるコード例の多くは、payload に `"channel": "#general"` や `"username": "デプロイbot"` を含んでいます。これをコピーしても、いま作ったWebhookでは送信先も表示名も変わりません。エラーにもならず、指定が黙って無視されます。Node SDKのドキュメントにも `icon_emoji` を既定値に渡す例が残っていますが、アプリ版のWebhookでは表示名やアイコンはアプリの設定画面側で決まる点を前提に読んでください。

### レガシー統合だけが持っていた実行時のchannel上書きの経緯

かつてのIncoming Webhookは、Slackアプリではなく「カスタムインテグレーション」として追加するものでした。[レガシー統合のドキュメント](https://docs.slack.dev/legacy/legacy-custom-integrations/legacy-custom-integrations-incoming-webhooks/)には、当時のパラメータとして「channel – override the legacy integration’s default channel」が残っています。

そして同じページが、アプリ版との差分をこう書いています。「the only thing you can no longer do is customize the destination channel and author identity at runtime」。実行時に送信先と送信者を変える機能だけが、意図的に外されました。アイコンや名前はアプリの設定画面で固定値として持たせる形になり、送信側のコードからは触れません。サンプルが動かないのは書き手のミスではなく、前提にしている統合方式が違うためです。

### 送信先チャンネルを分けるならURLを複数発行して環境変数で持つ設計

現行仕様での解は単純で、チャンネルの数だけ「Add New Webhook to Workspace」を実行し、URLを複数持ちます。設定は `SLACK_WEBHOOK_ALERT`、`SLACK_WEBHOOK_DEPLOY` のように用途別の環境変数へ分け、宛先名からURLを引く辞書を1箇所に置いてください。避けたいのは、URLを1本だけ持って本文の先頭にチャンネル名を書く折衷案。通知の検索性が落ち、ミュート設定も効かなくなります。

送信先を利用者の入力やデータベースの値から動的に決めたい場合、Incoming Webhookは採用しません。仕様上どう工夫しても満たせない要件なので、ボットトークンと chat.postMessage に切り替えて `channel` 引数で指定する構成にします。

## 1秒1メッセージの制限と429・Retry-Afterを前提にした通知設計

個人のテスト送信では見えず、本番で必ず顔を出すのが投稿レートの制限です。監視ツールとつないだ初日にアラートが連続し、途中から届かなくなる、という形で表面化します。

### 429とRetry-Afterを読んで待つPythonの再送実装

[Slackのレート制限ドキュメント](https://docs.slack.dev/apis/web-api/rate-limits/)は、chat.postMessage でもIncoming Webhookでも同じ制限がかかると説明しています。短いバーストは許容されるものの正確な値は公開されておらず、「design your apps with a limit of 1 request per second」が公式の推奨です。超過時は HTTP 429 と `Retry-After` ヘッダが返り、ヘッダの値は再試行できるまでの秒数を示します。SDKを使わず標準ライブラリだけで書くなら、次の形になります。

```
import json
import os
import time
import urllib.error
import urllib.request

WEBHOOK_URL = os.environ["SLACK_WEBHOOK_ALERT"]

def post_to_slack(payload, max_retry=3):
    data = json.dumps(payload).encode("utf-8")
    for attempt in range(max_retry):
        req = urllib.request.Request(
            WEBHOOK_URL, data=data,
            headers={"Content-Type": "application/json"},
        )
        try:
            with urllib.request.urlopen(req, timeout=10) as res:
                return res.read().decode("utf-8")  # 成功時は "ok"
        except urllib.error.HTTPError as e:
            if e.code == 429:
                wait = int(e.headers.get("Retry-After", "1"))
                time.sleep(wait)
                continue
            raise
    raise RuntimeError("Slackへの送信がレート制限で完了しませんでした")
```

`Retry-After` を無視して固定秒数のスリープを入れると、待ち時間が足りずに429を繰り返します。ヘッダの値をそのまま使うのが最短です。再試行に上限を置くのは、Slack側の障害時に送信処理が無限に詰まるのを避けるため。上限に達した分はログへ落とし、通知経路とは別に追えるようにします。5xxや接続断まで含めた再試行の回数・間隔・打ち切りの決め方は、[指数バックオフとジッター、リトライ予算の実装判断](https://www.issoh.co.jp/tech/details/16185/)で一般化して整理しています。

### 監視アラートを1通へ集約してバースト送信を避ける実装の判断基準

再送は対症療法で、根本は送りすぎです。効くのは、一定時間のイベントをまとめて1通にする集約。たとえば10秒のウィンドウを置き、その間に発生した失敗をBlock Kitの `section` に箇条書きで並べ、件数を `text` の要約に入れます。通知の遅延は最大10秒ですが、レート制限には当たりません。

しきい値は、通知チャンネルを見ている人が1分に何通まで読めるかで決めるのが実際的です。毎分10通を超えるようなら、集約ウィンドウを延ばすか重大度で通知先を分けます。制限はチャンネル単位なので3チャンネルへ配れば3倍の枠を使えますが、同じ内容が並ぶと誰も読まなくなります。配信先を増やす前に、重大度の切り分けを先に決めてください。

## Webhookで足りなくなる境界とchat.postMessageへ切り替える判断

Incoming Webhookは「固定のチャンネルへ、一方向に、投げっぱなしで送る」用途に絞られた仕組みです。そこから外れる要件が入ると、実装が急に苦しくなります。

### Webhookの応答にtsが返らないため更新も返信もできない制約

Webhookの応答は文字列 `ok` だけ。一方で [chat.postMessage](https://docs.slack.dev/reference/methods/chat.postMessage/) は、投稿したメッセージのタイムスタンプID（`ts`）とチャンネルを含むJSONを返します。この `ts` が、後続操作のすべての鍵になります。Web APIとEvents APIの関係やトークンの種類は[Slack APIとは？Web API・Events API・Boltの違いと実装手順](https://www.issoh.co.jp/tech/details/18717/)で整理しています。

デプロイの進捗を1つのメッセージで書き換えていく、障害の続報を同じスレッドにぶら下げる、完了した通知を削除する。いずれも `ts` を保持していなければ実行できません。公式リファレンスは `thread_ts` を渡せば親メッセージへの返信として投稿できると説明していますが、その親の `ts` はWebhookから取得できないわけです。「あとでスレッドにまとめたい」という要望が出た時点で、作り替えが確定します。通知を受けて返信までする双方向のBotにしたい場合は、[Slack Botの作り方：Bolt for Pythonでメンション返信からLambda運用まで](https://www.issoh.co.jp/tech/details/18861/)で実装手順を整理しています。

### Incoming Webhookとchat.postMessageの機能差の比較表

選定を早く終わらせるために、実装の可否で並べます。認証方式と返り値の差が、そのまま機能差になっています。

| 観点     | Incoming Webhook | chat.postMessage |
| ------ | ---------------- | ---------------- |
| 認証     | URL自体がシークレット     | ボットトークン          |
| 送信先の指定 | URLごとに固定         | channel引数で指定     |
| 応答     | 文字列 ok のみ        | ts を含むJSON       |
| 更新・削除  | 不可               | ts を使って可能        |
| スレッド返信 | 不可               | thread\_ts で可能   |
| 投稿レート  | 1秒1件／チャンネル       | 1秒1件／チャンネル       |
| 導入の手数  | URLを1本発行するだけ     | スコープ設定が必要        |

判断は要件の一点で決まります。投げっぱなしでよいならWebhook、投げた後に何かするならトークン方式。検索や投稿そのものをAIエージェントに任せたい場合は、[Slack MCPサーバーの接続手順と権限設計](https://www.issoh.co.jp/tech/details/17875/)で扱う第3の経路になります。両方を併用すると同じ通知が2系統に散るので、1つの通知系統ではどちらかに寄せてください。なお受信側を自作する設計は論点が別にあり、署名検証や再送への対処は[Webhookの実装方法と作り方](https://www.issoh.co.jp/tech/details/1378/)で扱っています。

同じIncoming Webhookという名前でも、上限の置き方はツールごとに違います。LINE WORKSは発行できるURLの本数と分単位のリクエスト数で縛る方式で、詳細は[LINE WORKS Incoming Webhookの設定手順とJSON送信仕様](https://www.issoh.co.jp/tech/details/3752/)にまとめました。社内でSlackとLINE WORKSを併用して同じ監視系から両方へ流す場合、秒単位のSlackと分単位のLINE WORKSで集約ウィンドウを別々に決める必要があります。

### Webhook URLの秘匿と漏洩時の作り直しを含む運用の引き継ぎ設計

URLがそのまま認証情報である以上、扱いはAPIキーと同じです。公式ガイドは「Your webhook URL contains a secret. Don’t share it online, including via public version control repositories.」と警告し、「Slack actively searches out and revokes leaked secrets.」と続けています。公開リポジトリへ誤ってコミットすると、Slack側が検知してURLを失効させることがあります。CI/CDから送る場合は、リポジトリのシークレット機能から環境変数に渡し、ワークフロー定義にURLを書きません。

```
name: notify-deploy
on:
  workflow_run:
    workflows: ["deploy"]
    types: [completed]

jobs:
  notify:
    runs-on: ubuntu-latest
    steps:
      - name: Post result to Slack
        env:
          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_DEPLOY }}
        run: |
          curl -sS -X POST "$SLACK_WEBHOOK_URL" \
            -H "Content-type: application/json" \
            --data "{\"text\": \"deploy ${{ github.event.workflow_run.conclusion }}\"}"
```

この通知ジョブは、ビルドとテストを回す既存のワークフローが完了したときに起動する想定です。通知元になるワークフロー側の組み立ては[GitHub Actionsでビルド・自動テストを設定する方法](https://www.issoh.co.jp/tech/details/2497/)で解説しています。

引き継ぎで効くのは、どのURLがどのチャンネルに紐づくかの一覧です。アプリ名・Webhookの識別子・送信先チャンネル・利用システムの4つを台帳に持っておくと、担当者の交代時に「何に使われているか分からないので消せない」という状態を避けられました。委託先の開発が終わった時点で、該当のWebhookを削除して発行し直すのが安全側の運用です。通知系統が複数のシステムへ分散して管理しきれない場合は、連携基盤ごと整理する段階で、[API開発・システム連携](https://www.issoh.co.jp/service/system/api/)のような外部の設計支援も選択肢に入ります。

## よくある質問

Slack Incoming Webhookの実装で問い合わせが多い論点を、公式ドキュメントの記述に沿って整理します。

### Slack Incoming Webhookは無料プランでも発行できますか？

発行できます。Incoming Webhookの利用自体に有料プランの条件は課されておらず、無料プランのワークスペースでもSlackアプリを作成してURLを払い出せます。制約が出るのはワークスペース側の設定で、インストールに管理者の承認が必要なら申請してください。有料プラン限定なのはワークフロービルダーのwebhookトリガーのほうで、URLが `/triggers/` で始まるものは別の機能です。過去の通知を長期間さかのぼりたい用途では、無料プランの保存期間の条件も先に確認が要ります。

### payloadでchannelを指定しても投稿先が変わらないのはなぜですか？

Slackアプリ版のIncoming Webhookでは、公式ドキュメントが送信先チャンネル・ユーザー名・アイコンを実行時に上書きできないと明記しているためです。指定はエラーにならず無視されます。上書きが可能だったのはレガシーのカスタムインテグレーションで、古いサンプルはその前提で書かれたもの。別のチャンネルへ送るなら、設定画面から2本目のWebhookを追加してURLを分けてください。

### Webhook URLが外部に漏れた場合はどう対処しますか？

該当のWebhookをアプリ管理画面から削除し、新しいURLを発行し直します。URLは認証情報そのもので、知っている相手は誰でもそのチャンネルへ投稿できるため、パスワード変更と同じ扱いで即座に無効化するのが原則です。公開リポジトリへコミットした場合は、履歴からの削除より先にURLの再発行を済ませてください。

### 1つのWebhookで複数チャンネルへ同時に通知できますか？

できません。1本のURLは1チャンネルに固定されるため、3つのチャンネルへ配るなら3本のURLへそれぞれPOSTします。レート制限はチャンネル単位で判定されるので、同時送信そのものは問題になりません。ただし同じ内容を複数の場所へ流すと読まれなくなるため、配信先を増やす前に重大度による出し分けを設計してください。

### Webhookで送ったメッセージをあとから編集・削除できますか？

Incoming Webhookの応答には、メッセージを特定するタイムスタンプID（`ts`）が含まれないため、プログラムからの編集も削除もできません。手作業でSlack上から消すことは可能ですが、自動処理には組み込めない仕様です。進捗に応じて書き換えたい要件がある場合は、`chat:write` スコープのボットトークンを取得し、chat.postMessage が返す `ts` を保存して chat.update や chat.delete を呼ぶ構成にしてください。

## 関連記事

- [Incoming Webhookとは？仕組みとSlack・Discordでの使い方](https://www.issoh.co.jp/tech/details/6596/)：Webhookという仕組み自体の入門と、Slack以外のツールでの発行手順
- [Webhookの実装方法と作り方](https://www.issoh.co.jp/tech/details/1378/)：受信側を自作する場合の署名検証・リトライ・冪等性の設計
- [Webhookとは？読み方・仕組み・APIとの違い](https://www.issoh.co.jp/tech/details/3289/)：ポーリングとの違いをHTTPリクエストで確認する基礎編
- [LINE WORKS Incoming Webhookの設定手順とJSON送信仕様](https://www.issoh.co.jp/tech/details/3752/)：Slack以外のチャットツールで同じ通知を組む場合の上限と送信形式
- [リトライパターンとは？指数バックオフ・ジッター・リトライ予算の実装判断](https://www.issoh.co.jp/tech/details/16185/)：429や5xxに対する再試行の設計を一般化した解説
- [SendGrid API Event Webhookの実装手順](https://www.issoh.co.jp/tech/details/17345/)：外部SaaSのWebhookを受けて保存する側の実装例
- [Slack Codeとは？コードチャンネルの仕組みと組み込み手順](https://www.issoh.co.jp/tech/details/16890/)：Slack上でコードを扱う機能と権限設計

---

出典: [Slack Incoming Webhookの実装手順：URL発行・Block Kit整形・SDK送信と429対策](<https://www.issoh.co.jp/tech/details/17652/>)（株式会社一創）
