本文へスキップ

本ページはプロモーションが含まれています

Python

Threads APIで自動投稿|60日のトークン期限で止まった話

CATEGORY
DRAWN BY
TOM
DATE
REV.
READ
約14分
SHEETS
8枚
Threads API(Pythonで自動投稿とトークン更新)

トム

SESで約8年、環境を変えられないもどかしさの中にいました。32歳で自社開発企業に転職し、今はClaude Codeと自動化で、仕事も時間も組み直している途中です。
・Javaバックエンドエンジニア(Java歴12年以上)
・資格:基本情報技術者/応用情報技術者/Java Silver/Python3エンジニア認定基礎
詳細なプロフィール

𝕏

Threads APIの自動投稿は、コードを1行も変えていないのに止まることがあります。 原因はアクセストークンの期限で、長期トークンでも60日で切れます。 私は2026年6月にPythonでThreadsの自動投稿を組み、トークンが切れて止まったあと、10月に廃止しました。 この記事では、そのとき動いていた投稿・取得のコードをもとに、Threads APIの準備から投稿、エンゲージメントの取得、トークン更新までをまとめます。 これから自動投稿を組む人が、同じ止まり方をしないための記録です。

この記事でわかること

  • Threads APIでできることと、投稿数・文字数の上限
  • 認可コードから長期トークンまでの3段階の取り方
  • Pythonで投稿する「作成→公開」の2ステップのコード
  • 自分の投稿といいね数・閲覧数を取るコード
  • 60日のトークン期限と、更新を自動化する方法

PARTS LIST /目次

Threads APIとは?Pythonから投稿・取得できる公式API

Threads APIとは、MetaがThreadsの投稿・取得・分析のために公開している公式のAPIです。 Pythonなら requests だけで、テキスト投稿から自分の投稿のいいね数の取得まで一通りできます。 ただし、何でも自由にできるわけではありません。 投稿数・文字数に上限があり、使える機能はアプリに付けた権限(スコープ)で決まります。

以下の数値は、2026年11月14日時点のMeta公式ドキュメントで確かめました。参照したのはThreads API overviewとLong-lived tokensです。

Threads APIでできること・できないこと

できることは、自分のアカウントに関わる操作が中心です。 他人の投稿の検索は、専用の権限の審査に通ってはじめて使えます。

  • テキスト・画像・動画・カルーセル(2〜20枚)の投稿
  • 返信の投稿と、自分の投稿への返信の管理
  • 自分の投稿一覧と、投稿ごとのいいね・返信・閲覧数の取得
  • キーワード・トピックタグでの投稿検索(threads_keyword_search の承認が必要)

注意したいのは検索です。 threads_keyword_search が承認されていないアプリでは、検索しても自分の投稿しか返ってきません。 私のコードにも「Threadsは他人の投稿を検索するAPIが無いため対象外」というコメントが残っています。今読むと、少し古い認識でした。

投稿は24時間250件・本文は500字まで

上限は、個人の自動投稿ならまず当たらない数です。 それでも、予約投稿のツールを作るなら自分のコード側でも数えておくよう、公式ドキュメントに書かれています。

項目上限(2026年11月時点)
APIからの投稿24時間で250件(カルーセルは1件と数える)
返信24時間で1,000件
テキストの文字数500字(絵文字はUTF-8のバイト数で数える)
本文のリンク5個を超えると投稿に失敗する
キーワード検索24時間で2,200回(アプリをまたいでユーザー単位)

24時間の数え方は「直近24時間」の移動枠です。 今の使用量は GET /{threads-user-id}/threads_publishing_limit で確かめられ、quota_usage と quota_total(250)が返ります。

絵文字の数え方は見落としやすい点です。 500字ちょうどの文に絵文字を足すと、見た目の文字数は同じでも上限を超えることがあります。

Threads APIを使う準備:アプリ作成から長期トークンまで

準備でいちばん時間がかかるのは、コードではなくトークンの取得です。 Meta Appを作り、自分をテスターに入れ、認可コード・短期トークン・長期トークンの順に取っていきます。 私は2026年6月14日の作業で、この流れにたどり着くまでに何度もエラーを踏みました。 先に全体の順番を図にしておきます。

flowchart TD
    A["Meta Appを作る<br>Threads APIのユースケースを追加"] --> B["自分をThreadsテスターに追加<br>Threadsアプリ側で招待を承認"]
    B --> C["認可ウィンドウで許可<br>認可コード(1時間・1回限り)"]
    C --> D["短期トークン<br>有効期限1時間"]
    D -->|"th_exchange_token"| E["長期トークン<br>有効期限60日"]
    E -->|"th_refresh_token<br>発行から24時間以降"| E

Meta Appを作り自分をThreadsテスターに追加する

最初に、Meta for Developersでアプリを作り、Threads APIのユースケースを追加します。 アプリが審査を通るまでは、投稿できるのはアプリの「Threadsテスター」に入っているアカウントだけです。 自分のアカウントで使うなら、次の2か所の操作が要ります。

step
1
アプリの「アプリの役割」で自分をThreadsテスターに追加する

開発者側の画面で、自分のThreadsアカウントを招待します。

step
2
Threadsの設定で招待を承認する

Threads側の設定画面(アカウント → ウェブサイトのアクセス許可)に届いた招待を承認します。 承認していないと、認可コードをトークンに交換する段階で失敗することがあります。

リダイレクトURI(認可のあとに戻ってくるURL)も、アプリの設定に登録しておきます。 私が設定した2026年6月は「ビジネス向けFacebookログイン → 設定 → クライアントOAuth設定」の中にあり、場所を探すだけで時間を使いました。 Metaの管理画面はよく変わるので、見つからなければ「Redirect Callback URLs」で探すと早いです。

認可コード→短期トークン→長期トークンの3段階

トークンは3段階で取ります。 認可ウィンドウ(https://threads.net/oauth/authorize)を開いて許可すると、リダイレクトURIに code が付いて戻ってきます。 この認可コードは1時間有効で、1回しか使えません。

https://threads.net/oauth/authorize?client_id=アプリID&redirect_uri=登録したURI&scope=threads_basic,threads_content_publish,threads_manage_insights&response_type=code

戻ってきたURLの末尾には #_ が付きますが、コードには含めません。 認可コードを短期トークン(1時間)に換え、さらに長期トークン(60日)に換えます。

import os

import requests

APP_ID = os.environ["THREADS_APP_ID"]
APP_SECRET = os.environ["THREADS_APP_SECRET"]
REDIRECT_URI = os.environ["THREADS_REDIRECT_URI"]


def exchange_code(code: str) -> str:
    """認可コードを短期トークン(1時間)に交換する."""
    resp = requests.post(
        "https://graph.threads.net/oauth/access_token",
        data={
            "client_id": APP_ID,
            "client_secret": APP_SECRET,
            "grant_type": "authorization_code",
            "redirect_uri": REDIRECT_URI,
            "code": code,
        },
    )
    resp.raise_for_status()
    return resp.json()["access_token"]


def to_long_lived(short_token: str) -> dict:
    """短期トークンを長期トークン(60日)に交換する."""
    resp = requests.get(
        "https://graph.threads.net/access_token",
        params={
            "grant_type": "th_exchange_token",
            "client_secret": APP_SECRET,
            "access_token": short_token,
        },
    )
    resp.raise_for_status()
    # access_token と expires_in(期限までの秒数)が返る
    return resp.json()

長期トークンの交換にはアプリシークレットが要ります。 公式ドキュメントでもサーバー側だけで扱うよう書かれていて、ブラウザやアプリの中に置いてはいけません。

必要なスコープ(権限)は用途で選ぶ

スコープは、認可ウィンドウの scope に並べた分だけ付きます。 あとから足したくなったら、認可からやり直しです。 私は投稿と分析に使う3つを付けていました。

スコープ使う場面
threads_basicすべてのエンドポイントで必須
threads_content_publish投稿の作成・公開
threads_manage_insightsいいね・閲覧数などの取得
threads_read_replies返信の読み取り
threads_manage_replies返信の投稿・非表示
threads_keyword_search公開投稿のキーワード検索

迷ったら、投稿だけなら上の2つ、数字も見たいなら3つ目まで付けておけば足ります。

PythonでThreads APIに投稿する基本コード

Threads APIの投稿は、1回のリクエストでは終わりません。 まず「メディアコンテナ」という下書きを作り、そのIDを渡して公開する2段構えです。 ここからは、私が自動投稿で4か月動かしていた threads_poster.py を、記事用に要点だけ残して載せます。 認証情報の読み込みやログの細部は省きました。

作成→公開の2ステップで投稿する

投稿の中心は2つのPOSTです。 /{threads-user-id}/threads で下書きを作り、返ってきたIDを /{threads-user-id}/threads_publish に渡します。

import time

import requests

API_BASE = "https://graph.threads.net/v1.0"


class ThreadsPoster:
    """Threads API でテキスト投稿する."""

    def __init__(self, access_token: str, user_id: str):
        self.access_token = access_token
        self.user_id = user_id
        self._session = requests.Session()

    def post_text(self, text: str) -> str:
        """テキスト投稿を作成→公開し、投稿IDを返す."""
        # 1. メディアコンテナ(下書き)を作る
        resp = self._session.post(
            f"{API_BASE}/{self.user_id}/threads",
            params={"media_type": "TEXT", "text": text, "access_token": self.access_token},
        )
        resp.raise_for_status()
        creation_id = resp.json()["id"]

        # 2. 少し待ってから公開する
        time.sleep(1)
        resp = self._session.post(
            f"{API_BASE}/{self.user_id}/threads_publish",
            params={"creation_id": creation_id, "access_token": self.access_token},
        )
        resp.raise_for_status()
        return resp.json()["id"]

公開の前の待ち時間は、公式ドキュメントでは「約30秒」が目安です。 私のコードは1秒しか待っておらず、テキストだけの投稿はそれで通っていました。 画像や動画はサーバー側の処理に時間がかかるので、30秒待つか、コンテナの状態を確かめてから公開します。

ユーザーIDは、トークンを作ったあとに GET /v1.0/me を1回呼べば id で返ってきます。

topic_tagでハッシュタグを1つ付ける

本文に #Python と書いただけでは、Threads上でタグとして扱われないことがあります。 タグにしたいときは topic_tag パラメーターで渡します。 私のコードは、本文の先頭にあるハッシュタグを1つ拾って topic_tag に入れていました。

import re

# 英数字・アンダースコア・日本語をタグの文字として拾う
HASHTAG_RE = re.compile(r"#([0-9A-Za-z_぀-ヿ㐀-鿿ヲ-゚]+)")


def extract_topic_tag(text: str) -> str | None:
    """本文で最初に出てくるハッシュタグを topic_tag 用に返す(数字だけのタグは除く)."""
    for m in HASHTAG_RE.finditer(text):
        if not m.group(1).isdigit():
            return m.group(1)
    return None

タグは1投稿に1つです。 2つ目以降のハッシュタグは、本文にただの文字として残ります。 公式ドキュメントでは、ピリオド(.)とアンパサンド(&)がタグに使えない文字とされています。 上の正規表現はどちらも拾わないので、そのまま渡して問題ありません。

トークンとユーザーIDは.envで渡す

アクセストークンとユーザーIDは、コードに書かず .env から読みます。 私は THREADS_ACCESS_TOKEN と THREADS_USER_ID の2つを .env に置き、起動時に読み込んでいました。

launchdで定期実行すると、ターミナルで設定した環境変数が届かないことがあります。 .env の置き場所とlaunchdでの読ませ方は、Discord Webhookの記事の「.envで渡す」節に同じ形で書いたので、そちらを見てください。

もう1つ気をつけたいのがログです。 requests の例外メッセージにはURLが入り、URLにはクエリの access_token がそのまま含まれます。 私は2026年10月5日に、ログの伏せ字の対象に access_token を足しました。 伏せ方はPython loggingの記事の「APIキーやWebhook URLをログに残さない」で扱っています。

Threads APIで自分の投稿とエンゲージメントを取得する

投稿を自動にすると、次に知りたくなるのは「どれが読まれたか」です。 Threads APIでは、自分の投稿一覧と、投稿ごとのいいね・返信・閲覧数が取れます。 私はこの数字を集めて、反応のあった投稿を次の投稿文づくりの参考にしていました。 取得は、一覧と数字の2段階に分かれます。

投稿一覧を取得するコード

自分の投稿は GET /{threads-user-id}/threads で新しい順に返ってきます。 fields で欲しい項目を指定し、limit で件数を絞ります。

def get_my_threads(self, limit: int = 25) -> list[dict]:
    """自分の投稿一覧を新しい順に取得する."""
    resp = self._session.get(
        f"{API_BASE}/{self.user_id}/threads",
        params={
            "fields": "id,text,timestamp,permalink",
            "limit": limit,
            "access_token": self.access_token,
        },
    )
    resp.raise_for_status()
    return resp.json().get("data", [])

投稿のURLは自分で組み立てず、permalink を取るのが安全です。 URLの中の投稿の識別子は、APIが返す数字のIDとは別の文字列だからです。 私の旧コードにはIDからURLを組む予備の処理もありましたが、正しいURLにならない作りで、実際に使っていたのは permalink のほうでした。

insightsでいいね・返信・閲覧数を取る

投稿ごとの数字は GET /{threads-media-id}/insights で取ります。 metric に欲しい指標を並べると、指標ごとの値が返ってきます。

def get_post_insights(self, post_id: str) -> dict[str, int]:
    """投稿1件のいいね・返信・リポスト・引用・閲覧数を取得する."""
    resp = self._session.get(
        f"{API_BASE}/{post_id}/insights",
        params={
            "metric": "likes,replies,reposts,quotes,views",
            "access_token": self.access_token,
        },
    )
    resp.raise_for_status()
    result = {}
    for entry in resp.json().get("data", []):
        # 値は values と total_value のどちらかの形で返る
        if entry.get("values"):
            result[entry["name"]] = entry["values"][0].get("value", 0)
        else:
            result[entry["name"]] = entry.get("total_value", {}).get("value", 0)
    return result

使える指標は views・likes・replies・reposts・quotes・shares です。 views は公式ドキュメントで「開発中」の扱いになっているので、数字の比較には使いすぎないほうが無難です。

1件ずつ呼ぶので、25件の一覧なら25回のリクエストになります。 数字の取り方は分かっても、取れた数字が小さすぎて分析にならない、という問題は残りました。 2026年9月27日の集計では、取得した直近25件のうち、いちばん反応があった投稿でもいいね1件でした。

全件失敗したらスコープ不足を疑う

insightsの取得は、1件だけ失敗することもあれば、全件まとめて失敗することもあります。 全件が失敗するときは、たいてい threads_manage_insights を付け忘れています。 私のコードは、全件失敗したら例外を上げて止まるようにしていました。

failures = 0
for t in threads:
    try:
        metrics = self.get_post_insights(t["id"])
    except requests.HTTPError:
        failures += 1
        metrics = {}
    # ...一覧に数字を足す処理

if threads and failures == len(threads):
    raise requests.HTTPError("全件でinsightsの取得に失敗(threads_manage_insights の不足を疑う)")

止めずに続けると、全投稿が「いいね0件」として集計されます。 数字が全部0でも、本当に反応がなかったのか権限が足りないのか、見分けがつきません。 間違った数字で分析が回り続けるよりは、止まって気づけるほうがましです。

アクセストークンの60日期限と更新方法

Threads APIの長期トークンは60日で切れます。 切れる前に更新すればまた60日延びますが、一度切れたら更新はできず、認可からやり直しです。 期限まわりの決まりは、公式ドキュメントでは次のとおりです。

短期トークン
1時間。切れたものは長期トークンに交換できない
長期トークン
60日
更新できる条件
発行から24時間以上たっていて、まだ切れていないこと
更新後
更新した日から60日。60日更新しなければ切れて、更新もできなくなる

refresh_access_tokenで60日延ばす

更新は GET /refresh_access_token を1回呼ぶだけです。 今の長期トークンを渡すと、新しいトークンと期限までの秒数が返ってきます。

def refresh_token(long_token: str) -> dict:
    """切れる前の長期トークンを更新し、新しいトークンと expires_in を返す."""
    resp = requests.get(
        "https://graph.threads.net/refresh_access_token",
        params={"grant_type": "th_refresh_token", "access_token": long_token},
    )
    resp.raise_for_status()
    return resp.json()

expires_in は秒で、公式のサンプルでは 5183944(約60日)です。 アプリシークレットは要らないので、交換より手軽です。 私は8月に、リマインダーの通知を見て、この更新を手で実行しました。

LOG2026-06-1460日後のリマインダーを仕掛けて終わりにした

テスト投稿が通ったあと、日記には「60日後にトークンリフレッシュが必要なため、8/3 7:00にリマインダーcronを設定」とだけ書いています。更新そのものを自動にするところまでは手を付けませんでした。

8月はそのリマインダーで気づいて、手で更新しました。手で更新するやり方は、1回目は助かります。2回目以降も同じように気づける前提になっているのが弱いところでした。

更新を自動化するならlaunchdで定期実行する

手で更新する運用は、いつか忘れます。 自動にするなら、更新したトークンを .env に書き戻すスクリプトを作り、定期実行に載せます。 以下は私が運用では作らなかった例として、要点だけ書いたものです。

import os
from pathlib import Path

import requests
from dotenv import load_dotenv, set_key

ENV_FILE = Path(__file__).parent / ".env"


def main() -> None:
    """長期トークンを更新し、.env の THREADS_ACCESS_TOKEN を書き換える."""
    load_dotenv(ENV_FILE)
    resp = requests.get(
        "https://graph.threads.net/refresh_access_token",
        params={"grant_type": "th_refresh_token", "access_token": os.environ["THREADS_ACCESS_TOKEN"]},
    )
    resp.raise_for_status()
    data = resp.json()
    set_key(str(ENV_FILE), "THREADS_ACCESS_TOKEN", data["access_token"])
    print(f"更新しました。残り{data['expires_in'] // 86400}日")


if __name__ == "__main__":
    main()

更新は発行から24時間たてば何度でもできるので、週1回くらい回しておけば期限は来ません。 失敗したときに気づけるよう、例外をDiscordなどに通知しておくと安心です。 Macで定期実行する手順はlaunchdの使い方にまとめています。

4か月の運用で分かった3つの落とし穴

Threads APIの自動投稿は、2026年6月14日に設定し、10月18日に廃止しました。 その間に踏んだ穴のうち、コードを読み直して「最初に知っていれば」と思った3つを並べます。 1つ目と2つ目は準備の段階、3つ目は運用の終わり方です。

Graph API Explorerのトークンは長期に変換できなかった

最初は、Graph API Explorerで出したトークンをそのまま長期トークンに換えようとしました。 手軽な方法として手順書にも書いていたやり方です。 ところが私の環境では、このトークンは長期トークンに変換できませんでした。

結局、認可ウィンドウから認可コードを取り、短期トークン、長期トークンと順に換える流れでやり直しています。 公式ドキュメントのGraph API Explorerは、テスト用にトークンを作る道具という位置づけです。 自動投稿で60日使い続けるなら、最初から認可ウィンドウの流れで取るほうが遠回りになりません。

grant_typeはth_exchange_token(sは付かない)

長期トークンへの交換では、grant_type の値で引っかかりました。 私の手順書には ths_exchange_token と書いてありました。 正しくは th_exchange_token で、sは付きません。

ハマりポイント
grant_typeの値は「th_」で始まる

交換は th_exchange_token、更新は th_refresh_token です。私の手順書は両方とも ths_ になっていて、2026年6月に直しました。エラーが出たら、まずこの1文字を疑ってください。

URLも合わせて確かめておくと安心です。 交換と更新の2つは https://graph.threads.net/access_token と https://graph.threads.net/refresh_access_token で、投稿のように /v1.0/ は付けません。

トークン切れで止まり、そのまま廃止した

8月に手で更新したあと、次の更新は自動にしないままでした。 その後トークンが切れ、10月18日のコミットで「トークン期限切れのため復活予定なし」としてThreadsの投稿処理を消しています。 一度切れた長期トークンは更新できないので、復活させるには認可ウィンドウからやり直す必要がありました。

私の運用

リマインダーで気づいて手で更新。2回目は更新されず、期限切れで止まった

今から組むなら

更新スクリプトを週1回の定期実行に載せ、失敗だけ通知する

同時投稿の作りも、止まったことを目立たなくします。 私のコードは投稿先ごとに例外を受け止めていたので、Threadsが失敗してもBlueskyとMastodonへの投稿は続きます。 ジョブ全体は毎回最後まで走り、Threadsの失敗はログに1行残るだけでした。

Threads APIのよくある質問

Threads APIを使い始めるときに引っかかりやすい点を、Q&Aの形でまとめました。 数値はどれも2026年11月14日時点のMeta公式ドキュメントで確かめたものです。 コードや手順の詳しい説明は、各見出しの本文もあわせて確認してください。

Q1. Threads APIの長期トークンの有効期限は?

A1. 60日です。発行から24時間以上たっていて、まだ切れていなければ、refresh_access_token で更新して60日延ばせます。切れたトークンは更新できないので、認可からやり直しになります。

Q2. Threads APIで1日に何件まで投稿できますか?

A2. APIからの投稿は直近24時間で250件までです。カルーセルは1件と数えます。返信は別枠で24時間に1,000件までです。

Q3. Threads APIで投稿できる文字数は?

A3. テキスト投稿は500字までです。絵文字はUTF-8のバイト数で数えるため、絵文字が多い文は見た目より早く上限に届きます。

Q4. Threads APIで他人の投稿を検索できますか?

A4. threads_keyword_search の権限が承認されていれば、公開投稿をキーワードやトピックタグで検索できます。承認前は、検索しても自分の投稿だけが返ります。

Q5. Threads APIでいいね数や閲覧数は取れますか?

A5. 自分の投稿なら threads_manage_insights の権限を付けて、/{threads-media-id}/insights から取れます。指標は views・likes・replies・reposts・quotes・shares です。

Q6. 長期トークンへの交換でエラーになるのはなぜ?

A6. まず grant_type が th_exchange_token になっているかを確かめてください。短期トークンが1時間を過ぎて切れている場合や、自分をThreadsテスターに追加して承認していない場合も失敗します。

Threads APIを使い始める3つのポイントまとめ

Threads APIの投稿そのものは、作成と公開の2回のPOSTで済みます。 難しいのはその手前のトークンと、60日ごとに来る期限のほうでした。 これから組むなら、次の3つを最初に済ませておくと止まりにくくなります。

  • トークンは認可ウィンドウ→短期→長期の流れで取る(grant_type は th_ で始まる)
  • 投稿と分析に使う threads_basic・threads_content_publish・threads_manage_insights をまとめて付ける
  • トークン更新のスクリプトを、投稿のコードと同じ日に定期実行へ載せる

私のThreadsの自動投稿は、3つ目を後回しにした結果、10月に止まりました。 投稿のコードは4か月まともに動いていたので、止めたのはコードではなく、更新を手作業に残した判断のほうです。 もう一度Threadsに戻るかは、まだ決めていません。

NEXT STEP
長期トークンを取ったら、その日のうちに更新スクリプトを定期実行に登録する。

投稿のコードより先に、期限の切れない仕組みを作っておくと、60日後に慌てずに済みます。

途中経過は、また書きます。

NEXT READ本棚から1本

AIで開発を組み直すなら、まずこの5冊から

入門・実践・設計まで、Claude Codeの本をレベル別に5冊選んで比べました。

→ おすすめ5冊を見る

トム

SESで約8年、環境を変えられないもどかしさの中にいました。32歳で自社開発企業に転職し、今はClaude Codeと自動化で、仕事も時間も組み直している途中です。
・Javaバックエンドエンジニア(Java歴12年以上)
・資格:基本情報技術者/応用情報技術者/Java Silver/Python3エンジニア認定基礎
詳細なプロフィール

𝕏

Python

TOC