X API v2 で POST /2/tweets が 403「You are not permitted to perform this action」になる本当の原因 — ハッシュタグ・スパム判定の落とし穴

,

X API v2 の POST /2/tweets が 403 になる原因を切り分けるイメージ

アプリ(GeoConverterPro / GeoPrism JP)の宣伝用に、X(旧 Twitter)へ画像付き投稿を自動化しようとしたところ、POST /2/tweets が延々と 403 を返し続けました。エラー本文は次のとおりです。

{
  "detail": "You are not permitted to perform this action.",
  "type": "about:blank",
  "title": "Forbidden",
  "status": 403
}

この文言でネット検索すると「Pay-Per-Use プランのエンロール不具合」「有料化してもツイート作成権限が付与されないバグ」という情報が多数出てきます。私も最初はそれを疑いましたが、丸一日かけて切り分けた結果、真因はまったく別のところにありました。同じエラーで詰まっている方が多そうなので、切り分けの過程ごと共有します。結論だけ知りたい方は最後の「まとめ」へどうぞ。

環境

  • X API v2、有料(Pay-Per-Use)プラン、アプリは Project 配下
  • 認証:OAuth 1.0a ユーザーコンテキスト(consumer key/secret + access token/secret、権限は Read and write)
  • 投稿処理:メディアを v1.1 media/upload でアップロード → POST /2/tweetsmedia_ids を添付
  • 言語:Python(requests + requests_oauthlib

「エンロール不具合」に見えてしまう理由

厄介なのは、エンロール不具合の症状と見分けがつかない点です。

  • GET 系エンドポイントは正常に認証される
  • v1.1 のメディアアップロードは成功し、media_id_string も返ってくる
  • ダッシュボード上は Active、クレジットも残高あり
  • なのに POST /2/tweets だけが 403

この状態だと「書き込み(ツイート作成)のエンロールが有効化されていないのでは」と考えるのが自然です。ですが、ここで手を止めずに変数を1つずつ潰すのが結果的に近道でした。

切り分け:容疑者を1つずつ消す

① 認証方式(OAuth 1.0a か 2.0 か)

「書き込みは OAuth 2.0 ユーザーコンテキストでないと通らない」という報告があったため、OAuth 2.0(PKCE、tweet.write スコープ付き)でも試しました。→ 結果は同じ 403。しかも 401(認証失敗)ではなく 403(認証は通るが権限なし)。認証方式は原因ではないと分かりました。

② トークンの取り違え

メディアアップロードとツイート作成で別々のトークンを使っていないか。→ コード上、同一の認証オブジェクトを両方に渡しており一致。別スクリプトにトークンをハードコードして単独で POST /2/tweets を叩くと、短いテキストなら 201 で成功しました。つまりトークンは正常で、書き込み権限自体は生きている。

③ 重複投稿(ここが分岐点)

同じ本文を投げ直すと 403 になりますが、そのときのエラーは別物でした。

{ "detail": "You are not allowed to create a Tweet with duplicate content.", ... }

重複は “duplicate content“。私が悩んでいた 403 は “not permitted to perform this action“。文言が違う=重複が原因ではない、と切り分けられます。X は重複をちゃんと専用メッセージで返してくるので、ここは判断材料になります。

④ 文字数・メディア・リンク

  • 文字数:問題の投稿は 280 字未満。長さは無関係。
  • メディア(画像)--no-media で画像を外し、テキストのみにしても 403。画像も無罪。
  • リンク URL:自前ドメイン(新規・低評価)の初出 URL でも、単体なら 201 で通る。リンクも無罪。

ここまでで、認証・トークン・重複・文字数・画像・リンクがすべて容疑から外れました。

⑤ 残ったのはハッシュタグ

同じ本文で、ハッシュタグを付けると 403 / 外すと 201 という差がきれいに再現しました。

本文 + #ジオイド #測量士補 #測地系 #GeoPrismJP        → 403 not permitted
本文 + https://(自前ドメインのリンク)                → 201 OK

つまり犯人はハッシュタグでした。より正確には「新規(低評価)アカウントが、ハッシュタグ付きの宣伝的な投稿を API から行う」という組み合わせが、X の自動スパム検知に引っかかり、POST /2/tweets を 403「not permitted to perform this action」で弾いていた、というのが実測からの結論です。

なぜ紛らわしいのか

この 403 が厄介なのは、次の3点が重なるためです。

  1. エラー文言が汎用的(「この操作は許可されていません」)で、原因を特定するヒントがゼロ。
  2. エンロール不具合の 403 とまったく同じ文言なので、検索すると誤診に誘導される。
  3. 確率的に振れる。ハッシュタグ付きでも稀に 201 で通ることがあり、「間欠不具合」に見えてさらに混乱する。スパム判定はアカウント評価など複数シグナルの総合スコアで、決定論的ではないためです。

高信頼ドメイン(例:apps.apple.com)のリンクだとハッシュタグ付きでも通りやすい、という傾向も観測しました。X の内部ヒューリスティックは非公開なので、あくまで経験則です。

対策:X ではハッシュタグを外す

確実に通す最小の変更は「X 投稿の本文からハッシュタグを除去する」ことでした。他の SNS(Threads 等)はこの制限が無いので、ハッシュタグは維持して構いません。プラットフォームごとに本文を出し分けます。

import re

def limit_hashtags(text: str, n: int) -> str:
    """本文中のハッシュタグを先頭 n 個だけ残して除去。n=0 で全除去。URL は触らない。"""
    tag_re = re.compile(r'(?<!\S)#\S+')
    if len(tag_re.findall(text)) <= n:
        return text
    kept = 0
    def repl(m):
        nonlocal kept
        if kept < n:
            kept += 1
            return m.group(0)
        return ""
    out = tag_re.sub(repl, text)
    out = re.sub(r'[ \t]+', ' ', out)
    out = re.sub(r' *\n', '\n', out)
    out = re.sub(r'\n{3,}', '\n\n', out)
    return out.strip()

# X へ投稿する本文だけハッシュタグを外す(Threads 等はそのまま)
text_for_x = limit_hashtags(original_text, 0)

n12 にすれば「先頭のハッシュタグだけ残す」運用も可能です。ただし確実に通るのは 0(全除去)だったので、まずは 0 で安定運用し、アカウントの評価が育ってから 1〜2 に増やして様子を見るのが無難です。

まとめ

  • POST /2/tweets の 403「You are not permitted to perform this action」は、必ずしも Pay-Per-Use のエンロール不具合ではない
  • 重複投稿は “duplicate content” という別文言で返るので区別できる。
  • 認証方式(OAuth1/2)・トークン・文字数・画像・リンクを1つずつ潰した結果、真因はハッシュタグ付き投稿に対する X 側のスパム判定だった。
  • 対策は単純で、X 投稿の本文からハッシュタグを外す(他 SNS は維持)。高信頼ドメインのリンクは比較的通りやすい。
  • スパム判定は確率的に振れるため、「たまに通る=間欠不具合」と誤認しないこと。

同じ 403 で消耗している方は、まずハッシュタグを外した短文を1回投げてみてください。それで 201 が返るなら、原因はエンロールではなくスパム判定側です。切り分けが一気に進みます。

なお、この検証は座標変換アプリ GeoConverterPro のX自動投稿化の副産物です。iPhone だけで JGD2024〜東京測地系、平面直角座標、CSV 一括変換までオフラインで完結するアプリなので、測量・GIS 関係の方はよければ覗いてみてください。


開発者より: アプリ・Kindle本・オープンソースの一覧は GitHub: amru195704 にまとめています。


お願い
本記事の情報は参考目的で掲載しており、正確性・完全性を保証するものではありません。誤記・不正確な情報がございましたら、コメント欄よりご指摘いただければ、確認のうえ修正いたします。


アプリを入手(App Store)GeoConverterPro(座標変換) | GeoPrism JP(測地系の可視化・学習)


コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

Back to top