---
title: "Slack APIでメッセージを取得する方法：conversations.historyとrepliesのページング・スコープ・差分取得【2026年11月】"
url: "https://www.issoh.co.jp/tech/details/19485/"
published: 2026-11-08
updated: 2026-11-08
categories: ["業務システム"]
publisher: "株式会社一創"
---

# Slack APIでメッセージを取得する方法：conversations.historyとrepliesのページング・スコープ・差分取得【2026年11月】

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のリファレンス](https://docs.slack.dev/reference/methods/conversations.history/)によると、このメソッドは指定した会話のメッセージを、期間内の新しいものから順に返します。`channel`には「#general」のような名前ではなく、`C`で始まる会話IDを渡します。

1回で返る件数は`limit`で指定し、既定は100件、上限は999件です。ただし公式は1回200件以下でページをたどるよう推奨しています。期間は`oldest`と`latest`にUnix時刻（秒、小数部つき）で指定し、既定は0から現在までです。

スレッドの中の返信を読むには、リファレンスが案内するとおり`conversations.replies`を別に呼びます。チャンネルの投稿一覧だけを集めても、議論の大半がスレッドで進むチャンネルでは中身の多くが欠けます。

### スレッド返信はconversations.repliesに親のtsを渡して別に取る仕組み

[conversations.repliesのリファレンス](https://docs.slack.dev/reference/methods/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の取得と確認](https://www.issoh.co.jp/tech/details/19472/)で解説しています。

| 会話の種類       | 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](https://docs.slack.dev/reference/methods/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](https://docs.slack.dev/reference/methods/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のページ](https://docs.slack.dev/apis/web-api/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](https://docs.slack.dev/tools/python-slack-sdk/web/)に載っている組み込みの仕組みで、利用できるバージョンはv3.9.0以降です。待ち時間と再送回数の決め方は[Slack APIの料金とレート制限の解説](https://www.issoh.co.jp/tech/details/19479/)で扱っています。カーソルには有効期限があり、数時間や数日にわたって保存しないよう求めているのが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日の変更履歴](https://docs.slack.dev/changelog/2025/05/29/rate-limit-changes-for-non-marketplace-apps/)により、Slack Marketplaceの審査を通っていない商用配布アプリでは、2つのメソッドがTier 1の毎分1回に下がり、`limit`の上限と既定値も15件になりました。適用されるのは、同日以降に作られたアプリと、既存アプリの新規インストールです。

[日本語のFAQ](https://docs.slack.dev/ja-jp/2025-05-terms-rate-limit-update-and-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レート制限（レートリミット）とは？](https://www.issoh.co.jp/tech/details/16179/)で整理しています。

### users.infoで投稿者名を引くキャッシュとJSON Linesで保存する形式

取得したメッセージの`user`は`U`で始まるIDなので、そのままでは誰の投稿か読めません。[users.info](https://docs.slack.dev/reference/methods/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で回答させる作り方](https://www.issoh.co.jp/tech/details/19483/)で解説しています。

## Slack APIで取得処理を自作する場面とエクスポートや外注で済ませる場面の判断基準

### 社内用アプリでメッセージの差分取得を自作すべき要件と導入を見送る条件

自作を選ぶのは、取得したメッセージを毎日あるいは毎時、別のシステムへ流し続ける場合です。問い合わせチャンネルの内容をチケット管理へ転記する、議事録チャンネルを全文検索の索引へ入れる、といった継続処理は、エクスポートの手作業では回りません。

見送るのは、生成AIのアシスタントに過去の議論を読ませたいだけの場合です。この用途なら、取得処理を書かずに[Slack MCPサーバーの接続手順](https://www.issoh.co.jp/tech/details/17875/)でAIから直接チャンネルを読ませるほうが、保存先の管理も要らず早く済みます。Slack APIの受信方式やトークンの全体像は[Slack APIとは？Web API・Events API・Boltの違いと実装手順](https://www.issoh.co.jp/tech/details/18717/)にまとめています。

### 管理画面のエクスポートで足りる場面とプランごとの取得範囲の差

過去ログを1回だけ取り出したい、移行のために丸ごと保管したい、という目的ならAPIは要りません。[ワークスペースのデータをエクスポートする](https://slack.com/intl/ja-jp/help/articles/201658943)のヘルプによると、メッセージとファイルのリンクがJSONでまとめられ、zipでダウンロードできます。

範囲はプランで変わります。フリーとプロはパブリックチャンネルだけで、ビジネスプラスとEnterpriseでは申請と承認を経ればプライベートチャンネルとDMも含められます。フリープランは、ファイルへのリンクを過去90日分しか出せません。実行できるのはワークスペースのオーナーや「エクスポート管理者」で、開発者の手元では始められない点も計画に入れておきます。

### 配布アプリや基幹システム連携で取得処理の開発を任せる要件の線引き

外部の力を借りたほうがよいのは2つの場面です。1つは複数の顧客企業へ配るアプリで、Marketplaceの審査を通すかどうかで1分あたりに取れる件数が数百倍変わり、設計の前提から決め直すことになります。もう1つは、取得したメッセージを顧客管理や会計などの基幹システムへ書き込む連携で、差分取得の取り逃しや二重登録がそのまま業務データの誤りになります。

要件の整理から取得・保存・連携先への登録までをまとめて任せたい場合は、[API開発・システム連携](https://www.issoh.co.jp/service/system/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の違いと実装手順](https://www.issoh.co.jp/tech/details/18717/)：メッセージ取得の前提になるSlack APIの全体像と最小実装
- [Slack APIの料金：無料で使える範囲とプラン別のAPI・レート制限・429対処の実装](https://www.issoh.co.jp/tech/details/19479/)：取得処理に効くTierの一覧と429の再送設定
- [Slack Botの作り方：Bolt for Pythonでメンション返信からLambda運用まで](https://www.issoh.co.jp/tech/details/18861/)：取得したログに応答するBotを組む手順
- [API連携の実装方法：REST・認証・エラー処理の手順をcurlとPythonのコード例で解説](https://www.issoh.co.jp/tech/details/15351/)：Slack以外のAPIにも通じるエラー処理と再送の書き方

---

出典: [Slack APIでメッセージを取得する方法：conversations.historyとrepliesのページング・スコープ・差分取得【2026年11月】](<https://www.issoh.co.jp/tech/details/19485/>)（株式会社一創）
