Slack APIでチャンネルのメッセージを取得するときに呼ぶのは、親メッセージを返すconversations.historyと、スレッドの返信を返すconversations.repliesの2つです。この記事では、会話の種類ごとに要るスコープ、チャンネルIDの引き方、ボットが参加していないときのエラー、next_cursorで最後まで取り切るページング、前回の続きだけを取る差分取得までを、curlとPythonのコードで順に示します。2025年5月に配布アプリの取得上限が毎分1回・15件へ下がった点と、管理画面のエクスポートで済む場面との選び分けも扱います(2026年11月時点の公式ドキュメントに基づく)。
まとめ:Slack APIのメッセージ取得で先に決めるスコープと取得経路と制限
社内のワークスペースで使うアプリなら、ボットトークンにchannels:historyなどのスコープを付け、対象チャンネルへボットを参加させれば取得は始められます。親メッセージはconversations.history、スレッドの返信はconversations.repliesで取り、どちらもnext_cursorが空になるまでページをたどります。
定期的にログを集めるなら、毎回すべてを取り直さず、前回の最新tsをoldestに渡す差分取得にします。ただし差分取得は古いスレッドへの後からの返信を拾えないため、スレッドの再取得を別に組みます。
社外の複数企業へ配るアプリは事情が違います。2025年5月29日以降の新規インストールでは取得が毎分1回・1回15件に絞られ、全件の同期は現実的ではありません。過去ログを一度まとめて取り出したいだけなら、APIを書く前に管理画面のエクスポートで足りないかを確かめます。
conversations.historyとrepliesの取得範囲とスレッド返信の違い
conversations.historyはチャンネルの投稿を新しい順に返すメソッド
conversations.historyのリファレンスによると、このメソッドは指定した会話のメッセージを、期間内の新しいものから順に返します。channelには「#general」のような名前ではなく、Cで始まる会話IDを渡します。
1回で返る件数はlimitで指定し、既定は100件、上限は999件です。ただし公式は1回200件以下でページをたどるよう推奨しています。期間はoldestとlatestにUnix時刻(秒、小数部つき)で指定し、既定は0から現在までです。
スレッドの中の返信を読むには、リファレンスが案内するとおりconversations.repliesを別に呼びます。チャンネルの投稿一覧だけを集めても、議論の大半がスレッドで進むチャンネルでは中身の多くが欠けます。
スレッド返信はconversations.repliesに親のtsを渡して別に取る仕組み
conversations.repliesのリファレンスでは、channelとtsの2つが必須です。tsには親メッセージのタイムスタンプを渡し、応答の例では先頭に親、続いて返信が古い順に並びます。
返信の無いメッセージのtsを渡すと、そのメッセージ1件だけが返ります。親かどうかの判定基準は、conversations.historyの応答でreply_countが1以上であること、またはthread_tsとtsが一致することです。返信の有無を見ずに全メッセージでrepliesを呼ぶと、呼び出し回数が投稿数と同じだけ増えます。
応答項目のts・thread_ts・reply_count・subtypeの用途比較表
保存や後続処理で使う項目は、次の5つに絞れます。
| 項目 | 意味 | 使いどころ |
|---|---|---|
ts |
メッセージの一意な時刻ID | 重複排除の主キーと差分取得の起点 |
thread_ts |
属するスレッドの親のts |
返信を親にひも付ける |
reply_count |
スレッドの返信数 | repliesを呼ぶか判定する |
user/bot_id |
投稿者のIDまたはボットのID | 投稿者名を引く元にする |
subtype |
参加通知などの種別 | 本文以外の投稿を除外する |
tsは「1730000000.123456」のような文字列で返ります。数値に変換して保存すると末尾の桁が落ち、repliesのts指定や重複排除で一致しなくなるため、文字列のまま持ちます。
取得前に揃えるhistoryスコープとチャンネルIDとボットの参加状態
公開・非公開・DM・グループDMで必要な4つのhistoryスコープの対応表
historyとrepliesは同じスコープで動き、会話の種類ごとに必要なものが分かれています。トークンの発行とスコープの追加手順はSlack Bot Tokenの取得と確認で解説しています。
| 会話の種類 | historyスコープ | 一覧取得のスコープ | listのtypes値 |
|---|---|---|---|
| パブリックチャンネル | channels:history |
channels:read |
public_channel |
| プライベートチャンネル | groups:history |
groups:read |
private_channel |
| ダイレクトメッセージ | im:history |
im:read |
im |
| グループDM | mpim:history |
mpim:read |
mpim |
社内のログ収集なら、まずパブリックチャンネルの2つだけで始めます。非公開やDMのスコープは、取得する理由と保管先の管理者を決めてから足します。スコープを足したあとは、アプリの再インストールが必要です。
conversations.listでチャンネル名からC始まりのIDを引く手順
1つのチャンネルだけなら画面のチャンネル詳細からIDを控えれば済みますが、多数のチャンネルを扱うならconversations.listで一覧から引きます。
このメソッドはTier 2(毎分20回以上)で、typesの既定はpublic_channelだけです。非公開も含めるならtypes=public_channel,private_channelと指定します。exclude_archivedの既定はfalseなので、アーカイブ済みを除きたいときに明示する値はtrueです。一覧もlimitは200以下が推奨で、名前とIDの対応表は起動時に1回作って使い回します。
conversations.joinでの公開チャンネル参加と非公開への招待の使い分け
ボットトークンが読めるのは、ボットが参加している会話だけです。参加していないパブリックチャンネルを指定すると、not_in_channelが返ります。参加していない公開チャンネルを読めるのはユーザートークンだけ、とリファレンスに書かれています。
パブリックチャンネルなら、conversations.joinでボット自身を参加させられます。必要なスコープはchannels:joinで、すでに参加済みならエラーではなくalready_in_channelの警告が付くだけです。アーカイブ済みのチャンネルではis_archivedが返ります。
プライベートチャンネルは、メンバーが/invite @ボット名でボットを招待する運用にします。取得のためにユーザートークンで全チャンネルを読む構成は、退職などでそのユーザーが無効になると止まるうえ、権限が広すぎるので社内用でも採りません。
curlとPythonで履歴とスレッドを取り切るカーソルページングの実装
curlのconversations.history疎通でのokとhas_moreの確認
コードを書く前に、トークン・スコープ・参加状態がそろっているかをcurlで確かめます。jqで必要な項目だけを表示します。
curl -s -G "https://slack.com/api/conversations.history" \
-H "Authorization: Bearer $SLACK_BOT_TOKEN" \
--data-urlencode "channel=C0123456789" \
--data-urlencode "limit=200" \
| jq '{ok, error, has_more, count: (.messages | length), next: .response_metadata.next_cursor}'
"ok": trueで件数が出れば準備は完了です。"error": "missing_scope"ならスコープ、"not_in_channel"なら参加状態を見直します。has_moreがtrueのときは、nextの値を次の呼び出しのcursorに渡すと続きが返ります。
next_cursorが空になるまで回すPythonの履歴取得とスレッド展開のコード
Paginationのページには、next_cursorが空・null・欠落のいずれかなら終わり、とあります。limitより少ない件数でも続きがある場合があるため、件数で終わりを判定してはいけません。次のコードはPython Slack SDK(slack_sdk)で親メッセージとスレッドの返信をまとめて取り出します。
import os
from slack_sdk import WebClient
from slack_sdk.http_retry.builtin_handlers import RateLimitErrorRetryHandler
client = WebClient(token=os.environ["SLACK_BOT_TOKEN"])
# 429が返ったらRetry-Afterの秒数だけ待って再送する
client.retry_handlers.append(RateLimitErrorRetryHandler(max_retry_count=3))
def paginate(method, **kwargs):
"""next_cursorが空になるまでページをたどり、messagesを1件ずつ返す"""
cursor = None
while True:
res = method(limit=200, cursor=cursor, **kwargs)
yield from res["messages"]
cursor = res.get("response_metadata", {}).get("next_cursor")
if not cursor:
break
def fetch_channel(channel_id, oldest="0"):
"""親メッセージを取り、返信のある親だけrepliesでスレッドを展開する"""
for msg in paginate(client.conversations_history, channel=channel_id, oldest=oldest):
yield msg
if msg.get("reply_count"):
for reply in paginate(client.conversations_replies, channel=channel_id, ts=msg["ts"]):
if reply["ts"] != msg["ts"]: # 親と同じtsの行は二重になるので除く
yield reply
429の再送ハンドラはPython Slack SDKのWeb clientに載っている組み込みの仕組みで、利用できるバージョンはv3.9.0以降です。待ち時間と再送回数の決め方はSlack APIの料金とレート制限の解説で扱っています。カーソルには有効期限があり、数時間や数日にわたって保存しないよう求めているのがPaginationのページの説明です。途中で止まったときは、カーソルではなく取得済みのtsから再開します。
oldestとlatestで前回の続きだけを取る差分取得とinclusiveの境界
毎日ログを集めるなら、前回取得した中で最も新しいtsを記録し、次回はoldestに渡します。oldestやlatestと同じtsのメッセージはinclusive=trueを付けない限り除外されるので、前回の最後の1件が二重に入ることはありません。
落とし穴は、古いスレッドへの後からの返信です。3日前の親メッセージに今日返信が付いても、親のtsはoldestより古いため、差分取得のhistoryには出てきません。対策は2つあります。
- 直近7日など一定期間の親メッセージを毎回取り直し、repliesで返信を再取得する
- 保存先で
tsを主キーにし、再取得した返信を上書きして重複させない
見直す期間の長さは、そのチャンネルでスレッドがどのくらいの期間動き続けるかで決めます。問い合わせ窓口のように数日で閉じるチャンネルなら7日、長期の案件チャンネルなら30日が目安です。
ログ収集を運用するときのレート制限とエラー対処と保存形式の設計
Tier 3の毎分50回と2025年5月以降の配布アプリの毎分1回・15件制限
historyとrepliesは、どちらもTier 3(毎分50回以上)です。社内用アプリなら1回200件で毎分50回呼べるため、1万件のチャンネルでも1分ほどで取り切れます。
一方、2025年5月29日の変更履歴により、Slack Marketplaceの審査を通っていない商用配布アプリでは、2つのメソッドがTier 1の毎分1回に下がり、limitの上限と既定値も15件になりました。適用されるのは、同日以降に作られたアプリと、既存アプリの新規インストールです。
日本語のFAQでは、社内用のカスタムアプリと配布アプリの既存インストールは影響を受けないと説明されています。自社のワークスペースだけで動かすログ収集は、この制限を気にせず設計して構いません。
missing_scopeなどメッセージ取得を止めるエラーの原因と対処表
応答のHTTPステータスが200でも、okがfalseならエラーです。取得処理でよく当たるものと対処を並べます。
| error | 原因 | 対処 |
|---|---|---|
not_in_channel |
ボットが会話に未参加 | joinか招待で参加させる |
missing_scope |
historyスコープが無い | スコープを足して再インストール |
channel_not_found |
IDの誤りか見えない会話 | listでIDを引き直す |
invalid_cursor |
古いか壊れたカーソル | 取得済みのtsから再開する |
thread_not_found |
repliesのtsが不正 | 親のtsを文字列のまま渡す |
ratelimited |
呼び出しが上限を超えた | Retry-Afterの秒数だけ待つ |
このうち、処理を続けてよいのはratelimitedだけです。他のエラーは設定の誤りなので、再送せずに記録して止めます。channel_joinのような参加通知にthread_not_foundが返ることがあるとrepliesのリファレンスにあるため、subtype付きの投稿はrepliesの呼び出し対象外にする設計です。429の設計全般はAPIレート制限(レートリミット)とは?で整理しています。
users.infoで投稿者名を引くキャッシュとJSON Linesで保存する形式
取得したメッセージのuserはUで始まるIDなので、そのままでは誰の投稿か読めません。users.info(Tier 4・スコープはusers:read)で名前を引き、同じIDは2回目以降キャッシュから返します。メールアドレスまで要るときはusers:read.emailが別に必要です。
import json
from functools import lru_cache
@lru_cache(maxsize=None)
def user_name(user_id):
"""表示名が空のユーザーもいるので、実名、IDの順に代わりを使う"""
profile = client.users_info(user=user_id)["user"]["profile"]
return profile.get("display_name") or profile.get("real_name") or user_id
def dump(channel_id, path, oldest="0"):
"""1行1メッセージのJSON Linesで追記し、次回のoldestに渡すtsを返す"""
newest = oldest
with open(path, "a", encoding="utf-8") as f:
for msg in fetch_channel(channel_id, oldest=oldest):
if msg.get("subtype") in ("channel_join", "channel_leave"):
continue
row = {
"channel": channel_id,
"ts": msg["ts"],
"thread_ts": msg.get("thread_ts"),
"user": user_name(msg["user"]) if "user" in msg else msg.get("bot_id"),
"text": msg.get("text", ""),
}
f.write(json.dumps(row, ensure_ascii=False) + "\n")
newest = max(newest, msg["ts"], key=float)
return newest
JSON Linesにしておくと、途中で止まっても書けた行までは残り、BigQueryなどへの読み込みもそのまま通ります。保存したログを社内の質問応答に使う構成はSlack BotにAIで回答させる作り方で解説しています。
Slack APIで取得処理を自作する場面とエクスポートや外注で済ませる場面の判断基準
社内用アプリでメッセージの差分取得を自作すべき要件と導入を見送る条件
自作を選ぶのは、取得したメッセージを毎日あるいは毎時、別のシステムへ流し続ける場合です。問い合わせチャンネルの内容をチケット管理へ転記する、議事録チャンネルを全文検索の索引へ入れる、といった継続処理は、エクスポートの手作業では回りません。
見送るのは、生成AIのアシスタントに過去の議論を読ませたいだけの場合です。この用途なら、取得処理を書かずにSlack MCPサーバーの接続手順でAIから直接チャンネルを読ませるほうが、保存先の管理も要らず早く済みます。Slack APIの受信方式やトークンの全体像はSlack APIとは?Web API・Events API・Boltの違いと実装手順にまとめています。
管理画面のエクスポートで足りる場面とプランごとの取得範囲の差
過去ログを1回だけ取り出したい、移行のために丸ごと保管したい、という目的ならAPIは要りません。ワークスペースのデータをエクスポートするのヘルプによると、メッセージとファイルのリンクがJSONでまとめられ、zipでダウンロードできます。
範囲はプランで変わります。フリーとプロはパブリックチャンネルだけで、ビジネスプラスとEnterpriseでは申請と承認を経ればプライベートチャンネルとDMも含められます。フリープランは、ファイルへのリンクを過去90日分しか出せません。実行できるのはワークスペースのオーナーや「エクスポート管理者」で、開発者の手元では始められない点も計画に入れておきます。
配布アプリや基幹システム連携で取得処理の開発を任せる要件の線引き
外部の力を借りたほうがよいのは2つの場面です。1つは複数の顧客企業へ配るアプリで、Marketplaceの審査を通すかどうかで1分あたりに取れる件数が数百倍変わり、設計の前提から決め直すことになります。もう1つは、取得したメッセージを顧客管理や会計などの基幹システムへ書き込む連携で、差分取得の取り逃しや二重登録がそのまま業務データの誤りになります。
要件の整理から取得・保存・連携先への登録までをまとめて任せたい場合は、API開発・システム連携で、対象チャンネルと連携先の条件から構成と費用の見通しを相談できます。
よくある質問
Slack APIでのメッセージ取得について、実装の前によく確認される点をまとめました。
Slack APIで取得できるメッセージは何件までですか?
1回の呼び出しで返る件数はlimitで決まり、conversations.historyの上限は999件です。公式は1回200件以下を推奨しており、next_cursorをたどれば件数の合計に上限はありません。フリープランでは古いメッセージが上限を超えると取得できず、応答にis_limitedが付きます。2025年5月29日以降に新規インストールされた配布アプリは、1回15件・毎分1回に制限されます。
スレッドの返信がconversations.historyで取れないのはなぜですか?
conversations.historyが返すのはチャンネルに並ぶ投稿で、スレッドの中の返信はリファレンスの案内どおりconversations.repliesで取ります。reply_countが1以上の親メッセージについて、そのtsをts引数に渡して呼び出します。応答には親も含まれるため、親と同じtsの行を除いてから保存すると二重になりません。
not_in_channelエラーが出たときはどうすればよいですか?
ボットが対象のチャンネルに参加していないことが原因です。パブリックチャンネルなら、channels:joinスコープを付けてconversations.joinを呼ぶと、ボット自身が参加できます。channels:joinはパブリックチャンネル向けのスコープなので、プライベートチャンネルはメンバーにボットを招待してもらいます。ユーザートークンに切り替えると未参加の公開チャンネルも読めますが、権限が広がるため社内用のログ収集では勧めません。
特定の期間のメッセージだけを取得できますか?
期間を指定した取得は可能です。oldestとlatestにUnix時刻を秒で渡すと、その間のメッセージだけが返ります。たとえば2026年11月1日0時(日本時間)以降なら、oldest=1793458800です。境界と同じ時刻の投稿を含めたいときはinclusive=trueを付けます。期間を指定しても1回の件数はlimitまでなので、next_cursorによるページングは同じく必要です。
GASからSlackのメッセージを取得することはできますか?
できます。Google Apps ScriptのUrlFetchApp.fetchで、Authorizationヘッダにボットトークンを入れてconversations.historyのURLを呼べば、curlと同じJSONが返ります。トークンの保存先はスクリプトプロパティとし、コードには直接書かない運用です。GASは1回の実行時間に上限があるため、件数の多いチャンネルはoldestで期間を区切り、時間主導のトリガーで分けて取得します。
関連記事
- Slack APIとは?Web API・Events API・Boltの違いと実装手順:メッセージ取得の前提になるSlack APIの全体像と最小実装
- Slack APIの料金:無料で使える範囲とプラン別のAPI・レート制限・429対処の実装:取得処理に効くTierの一覧と429の再送設定
- Slack Botの作り方:Bolt for Pythonでメンション返信からLambda運用まで:取得したログに応答するBotを組む手順
- API連携の実装方法:REST・認証・エラー処理の手順をcurlとPythonのコード例で解説:Slack以外のAPIにも通じるエラー処理と再送の書き方