Meta広告APIのcode:17を実運用で潰した4手順

Meta広告APIで自動化を書くと、どこかで必ず code:17 に当たる。やっかいなのは、このエラーが HTTP 400 として返ってくる点だ。本文を読まないと「リクエストが不正」と誤診する。実際、2026年8月にクリエイティブ監査スクリプトが「HTTP Error 400」とだけ出力した。原因の切り分けに時間を要した。コードは正しかった。単に呼びすぎていただけである。

先に結論を置く。code:17 は時間で回復する一時エラーだ。対処は4つしかない。(1)HTTPステータスでなく本文の error.code で分岐する。(2)成功後も2秒待つ。(3)30秒刻みのバックオフで再試行する。(4)書込系は再試行しない。以下、実運用で踏んだ順に書く。

1. code:17 は HTTP 400 で返るので誤診する

レート制限時のレスポンス本文は type が OAuthException になる。code は 17、error_subcode は 2446079 だ。メッセージは "User request limit reached" である。日本語環境では「広告アカウントのAPI呼び出しが多すぎます」と出る。

問題は、これが 429 ではなく 400 Bad Request で返ることだ。Pythonの urllib.request.urlopen は 400 で HTTPError を投げる。素直に書くと例外で落ち、ログには「取得失敗」としか残らない。パラメータの綴りを疑って時間を溶かす典型パターンだ。

もう一つの罠が is_transient が false になっている点である。恒久エラーに見えるが、実際は数分から1時間で回復する。この値を見て「仕様変更で弾かれた」と判断してはいけない。仕様の詳細は Meta 公式の Rate Limiting ドキュメントが一次情報になる。URLは https://developers.facebook.com/docs/marketing-api/overview/rate-limiting/ だ。

対処は単純で、例外ハンドラの中でエラー本文をパースし、ステータスではなく code の値で分岐することだ。これだけで「原因不明の400」が「待てば通るやつ」に変わる。

2. クォータは広告アカウント単位で、稼働広告数に比例する

ここを誤解している実装が多い。code:17 のクォータはアプリ単位ではなく 広告アカウント単位で効く。つまり、読み取りを繰り返した口座だけが落ちる。複数口座を回すスクリプトで1口座だけエラーになるのはこのためだ。実際、運用中の口座のうち、最も参照回数が多い1口座だけが繰り返し当たった。

公式ドキュメントによると、ads_management の1時間あたりの上限は次の式で決まる。開発ティアなら 300 + 40 × 稼働中の広告数となる。フルアクセスなら 100,000 + 40 × 稼働中の広告数である。稼働広告が20本しかない口座を開発ティアで叩けば、1時間あたり 1,100 相当しか余裕がない計算になる。

この式から導ける設計上の結論は一つだ。「広告1件ごとに insights を1回叩く」実装をやめる。在庫の累計消化を数えるために広告を1件ずつループする書き方は、件数×2回以上の呼び出しになり、すぐ上限に触れる。可能な限り階層をまとめて取り、フィールド指定で1回に寄せる。レポート系の設計思想はMeta広告レポートをPythonで自動化する実装手順にも書いた。

3. そのまま使えるリトライ関数の骨格

ここが本記事の実体だ。ブックマークして、自分の環境に合わせて書き換えて使ってほしい。実運用のスクリプトで使っている構造をそのまま出す。

(1)API呼び出しを関数1つに閉じ込め、直接 requests や urlopen を呼ぶ箇所をコード中に散らさない。(2)例外を受けたら本文を読む。urllib なら except 節でエラー本文が読める。1行で書くと d = json.loads(e.read().decode()) となる。(3)d["error"]["code"] が 17、1、2 のいずれかなら一時エラーとみなす。(4)再試行の待ち時間は固定にせず、30秒 × (試行回数 + 1) で伸ばす。つまり30秒、60秒、90秒と待つ。(5)試行回数の上限は4回にする。それでも通らない場合は口座単位で詰まっているので、待ち時間を増やすより時間帯をずらすほうが速い。

加えて、成功したときにも 2秒 sleep を入れて呼び出しレートを平準化する。これが効く。当初は8秒固定のスリープだけで再試行する実装を使っていたが、一括入稿の直後には足りなかった。30秒刻みのバックオフに変えて初めて安定した。

チェックリストとして持っておく値は4つだけでよい。成功後スリープ2秒、バックオフ初期値30秒、最大試行4回、判定は error.code。この4つを全スクリプトで揃えておくと、口座を増やしても挙動が読める。

4. 書込はリトライしない。検証は別プロセスに逃がす

最も重要な例外がこれだ。読み取りは冪等なので何度でも再試行してよい。しかし 作成系の POST をリトライすると二重作成になる。Meta側でリクエストが通った後にタイムアウトした場合、こちらはエラーに見えても広告は作られている。リトライすれば同じ広告が2本並ぶ。

実際に効いた運用ルールは、入稿と検証をプロセスごと分けることだ。広告を9本まとめて作成した直後に、同じプロセスで read-back のGETを叩く。これはほぼ確実に code:17 で弾かれる。書込のクォータを使い切った直後だからだ。検証は別スクリプトにして、数分あけてから走らせる。バックグラウンド実行にして完了通知を受ける形が扱いやすい。

もう一点、status を変えた直後の effective_status は IN_PROCESS を返す。数十秒で確定する。これを不一致として扱うと、正しく適用できているのに検証が赤になる。read-back の合否は、こちらが送った status の値で判定するのが正しい。事前検証の考え方はvalidate_onlyで入稿ミスを実行前に弾く3手順で扱った。

この手順の完全版はnoteの実践ガイドにまとめている。

[PR] 取得したデータを社内やクライアント向けの資料に落とす工程まで自動化するなら、以下のツールも組み合わせられる:

[PR] AIスライド作成ツール「イルシル」を試してみる

まとめ

code:17 は実装のバグではない。呼び出し設計の問題である。HTTP 400 と is_transient:false という2つの表示に引きずられなければ、対処は4手順で終わる。本文の error.code で分岐し、成功後も2秒待ち、30秒刻みで再試行し、書込だけは再試行しない。まずは既存スクリプトの例外ハンドラを開いて、エラー本文をパースしているか確認するところから始めるとよい。

実務でそのまま使いたい人へ

本記事の手法の完全版(実際のコード・テンプレート・つまずき対処つき)は、運営者のnote(note.com/ryo_ai_hack)で公開している。実データに基づく実践ガイドをまとめて読める。

DMM 生成AI CAMP

コメント

このブログの人気の投稿

Claude Skills販売で月5万稼ぐ3ステップ実践

Claude CodeでExcel自動化副業を月5万にする手順

Yahoo広告APIをPythonで自動化して詰まった5つの罠