「Google広告のコンバージョン数が、実際の問い合わせ数よりやけに多い」——そう感じたら、多くの解説記事は「タグの二重設置を疑え」と言います。それも一因ですが、2024年以降に増えた本当の原因はもう一つあります。GA4 から取り込んだ新しいコンバージョンと、もとからある古いコンバージョンが、電話タップ1回という"同じ行動"を別々に数えてしまうことです。放置すると、Google の自動入札(AIが予算配分を最適化する仕組み)が水増しされた数字を学習し、広告費が静かに無駄になります。
この記事は、実際に住宅会社の広告アカウントでこれが起き、管理画面と API の両方で検証して直した記録です。「AIエージェント 失敗」のような一般論ではなく、明日あなたのアカウントで同じことが起きた時に使える判断基準を、先に渡します。
結論 — どのコンバージョンを「集計に入れる/外す」か(先に答え)
二度と同じことを調べなくて済むよう、判断基準を冒頭に置きます。
- 同じ1回の行動を指す複数のコンバージョンは、1つだけ「集計対象」にする。 残りは消すのではなく「集計から外す(記録は残す)」。
- 集計から外すのに「コンバージョンを削除」してはいけない。 削除は元に戻せません(後述の罠5)。正しくは「コンバージョンに含める=オフ」にするだけ。記録は残るので後から検証できます。
- どれを残すか — 最も「お金に近い1回」を残します(例:電話の発生=1件。
電話クリック_LPを残し、GA4 由来のphone_click/click_to_callは集計オフ)。
| 状況 | やること |
|---|---|
| GA4 インポートCV と既存CV が同じ行動を二重に数えている | お金に近い 1 つだけ集計オン、他は集計オフ(削除しない) |
| 自動入札の成果が急に「良く見える」のに売上が増えない | 二重計上を疑い、集計対象を 1 行動 1 つに整理 |
| 一度オフにしたが履歴を見たい | オフでも記録は残る。レポートで参照可 |
これが答えです。以下は「なぜ削除では駄目で、設定でしか外せないのか」を、実際にAPIで操作して確かめた証拠です(広告計測をここまで触れる会社か、の判断材料にもどうぞ)。
証拠 — 「削除して戻す」が API から拒否された記録
ある日の午後、私は 1 行の コードを書きました。広告クライアントの Conversion Action を HIDDEN に戻すだけの単純な mutation です。
--apply を付けて実行した瞬間、API は 1 秒もかからずに拒絶しました。
````
google.ads.googleads.errors.GoogleAdsException:
status = StatusCode.INVALID_ARGUMENT
details = "Request contains an invalid argument."
message: "The error code is not in this version."
trigger { int64_value: 4 }
field_path_elements { field_name: "operations" index: 0 }
{ field_name: "update" }
{ field_name: "status" }
「The error code is not in this version」── このエラーメッセージは、 API v24 の仕様の隙間を突いた瞬間にだけ顔を出します。本記事では、google-ads Python SDK で Conversion Action を mutate しようとして踏んだ 5 つの罠 と、それぞれの突破コードを記録します。
背景 — なぜ HIDDEN に戻したかったか
クライアントの Google Ads アカウントで、 からインポートした Conversion Action 3 件を一度 ENABLED に戻しました。GA4 側で発火しているリードシグナルを Google Ads でも見えるようにするためです。
しかし管理画面で確認したところ、既存の WEBPAGE 系 Conversion Action(モデルハウス見学予約・電話クリック等)と 同じユーザー行動で重複計上される可能性 に気づきました。
| Conversion Action | type | category | 既存 incl_metric | 私が ENABLED 化 |
|---|---|---|---|---|
| モデルハウス見学予約_TESLA | WEBPAGE | SUBMIT_LEAD_FORM | True | — |
| 電話クリック_LP | WEBPAGE | PHONE_CALL_LEAD | True | — |
| ランディ申し込み完了 (GA4 import) | GA4_CUSTOM | DEFAULT | False | ENABLED 化 |
| phone_click (GA4 import) | GA4_CUSTOM | PHONE_CALL_LEAD | False | ENABLED 化 |
| click_to_call (GA4 import) | GA4_CUSTOM | PHONE_CALL_LEAD | False | ENABLED 化 |
電話タップ 1 回で 電話クリック_LP + phone_click + click_to_call の 3 倍計上 になる懸念があり、慌てて HIDDEN に戻すスクリプトを書きました。それが冒頭のエラーに繋がります。
罠 1: ENABLED → HIDDEN は API から mutate できない
最初に書いた mutation コードはこうでした。
``python
op = client.get_type("ConversionActionOperation")
op.update.resource_name = ca_service.conversion_action_path(cid, conversion_action_id)
op.update.status = client.enums.ConversionActionStatusEnum.HIDDEN
op.update_mask.paths.append("status")
resp = ca_service.mutate_conversion_actions(
customer_id=cid, operations=[op]
)
`
シンプルです。何が問題か分かりません。
エラーログをよく読むと、ヒントが隠れていました。
``
trigger { int64_value: 4 }
field_path_elements { field_name: "status" }
int64_value: 4 は status enum の 4 番目の値 を意味します。ConversionActionStatusEnum のソースを引くと、4 番目は HIDDEN です。
`python`
ConversionActionStatusEnum:
0: UNSPECIFIED
1: UNKNOWN
2: ENABLED
3: REMOVED
4: HIDDEN
つまり API は 「status フィールドに HIDDEN という値を入れる mutation は、このバージョンでは許可されていない」 と返してきていたわけです。
💡 KEY TAKEAWAYS
Google Ads API の ConversionActionStatusEnum.HIDDENは UI からのみ設定可能な内部状態 として設計されており、API 経由の mutation では拒否されます。同様の制約は他の enum でも存在するので、The error code is not in this versionを見たら、まずtrigger.int64_valueから enum 値を逆引きします。
突破方法: include_in_conversions_metric=False で集計から外す
集計から外すという目的だけなら、include_in_conversions_metric フィールドを False にすれば達成できます。こちらは API で mutate 可能です。
`python
op = client.get_type("ConversionActionOperation")
op.update.resource_name = ca_service.conversion_action_path(cid, conversion_action_id)
op.update.include_in_conversions_metric = False
op.update_mask.paths.append("include_in_conversions_metric")
resp = ca_service.mutate_conversion_actions(
customer_id=cid, operations=[op]
)
`
この方法であれば:
- ENABLED は維持(管理画面に残る)
- 「Conversions」レポートには出ない(自動入札も使わない)
- 「All conversions」レポートには出る(参考値として残る)
補足: include_in_conversions_metric と primary_for_goal の違い
両者は独立フィールドで、組み合わせは 4 通りあります。
| incl_metric | primary_for_goal | 自動入札への影響 | 集計列 |
|---|---|---|---|
| True | True | 影響大、★主要 CV | Conversions / All Conv |
| True | False | 影響あり(集計に入る) | Conversions / All Conv |
| False | True | 無し(マイクロ CV 扱い) | All Conv のみ |
| False | False | 無し(参考値) | All Conv のみ |
CV 重複の懸念があるときは、status を mutate せず include_in_conversions_metric を切り替えるのが定石です。
罠 2: SDK v24 で client.get_type("FieldMask") が消えた
実は罠 1 を書く前に、もっと初歩的なミスをしていました。
`python`
client.copy_from(
op.update_mask,
client.get_type("FieldMask")(paths=["status"])
)
これは v22 まで動いていたコードです。v24 では:
``
ValueError: Specified type 'FieldMask' does not exist in Google Ads API v24
FieldMask という型自体が SDK の type レジストリから削除されました。
突破方法: paths を直接 append する
正しい書き方:
`python`
op.update_mask.paths.append("status")複数フィールドの場合
op.update_mask.paths.extend(["status", "name", "value_settings.default_value"])
update_mask は protobuf の FieldMask メッセージで、paths は repeated string です。新しい SDK では type を経由せず、operation の field に直接書きます。
💡 KEY TAKEAWAYS
google-ads SDK のメジャーバージョン更新では、こうした 型レジストリの整理 が必ず混じります。エラー文言で does not exist in Google Ads API vXXを見たら、リリースノートで該当型を検索します。
罠 3: USER_PERMISSION_DENIED は「別 MCC 配下」のサイン
別の場面で MCC 配下のクライアント一覧を取得しようとしました。
`python
customer_service = client.get_service("CustomerService")
ga_service = client.get_service("GoogleAdsService")
accessible = customer_service.list_accessible_customers()
for resource in accessible.resource_names:
cid = resource.split("/")[-1]
query = "SELECT customer_client.id, customer_client.descriptive_name FROM customer_client"
try:
for batch in ga_service.search_stream(customer_id=cid, query=query):
for row in batch.results:
print(row.customer_client.descriptive_name, row.customer_client.id)
except GoogleAdsException as e:
print(f" {cid}: {extract_error_code(e)}")
`
list_accessible_customers() で 18 件の top-level customer が返ってきましたが、そのうち 5 件が USER_PERMISSION_DENIED を返しました。
``
9066450572: authorization_error: USER_PERMISSION_DENIED
8538982978: authorization_error: USER_PERMISSION_DENIED
9482254684: authorization_error: USER_PERMISSION_DENIED
4815331144: authorization_error: USER_PERMISSION_DENIED
9585924613: authorization_error: USER_PERMISSION_DENIED
最初は 「権限がない、招待を受けていない」 と解釈しました。実際は違いました。
突破方法: login_customer_id を変えて再試行
USER_PERMISSION_DENIED は その customer が別 MCC の配下にある サインです。私が現在使っている login_customer_id(自分の MCC)からは見えないだけで、別の MCC を login_customer_id に指定すれば取れます。
`python`別 MCC 配下のアカウントを取りに行く
client_for_other_mcc = GoogleAdsClient.load_from_dict({
"developer_token": cfg["developer_token"],
"refresh_token": token_data["refresh_token"],
"client_id": installed["client_id"],
"client_secret": installed["client_secret"],
"login_customer_id": "OTHER_MCC_ID_HERE", # 別 MCC の 10 桁 ID
"use_proto_plus": True,
})
複数 MCC を運用している場合は、それぞれを login_customer_id にして並列で叩きます。
| エラーコード | 意味 | 対処 |
|---|---|---|
USER_PERMISSION_DENIED | 別 MCC 配下にある | login_customer_id を変えて再試行 |
CUSTOMER_NOT_ENABLED | アカウントが停止・解約済み | スキップ可、運用停止アカウント |
DEVELOPER_TOKEN_NOT_APPROVED | Test access のまま | Basic access を申請 |
AUTHENTICATION_ERROR | token 失効 | refresh または再認証 |
罠 4: search vs search_stream の使い分け
メソッドが 2 つあります。
`pythonA. search
resp = ga.search(customer_id=cid, query=q)
for row in resp: # ListPager
print(row.campaign.name)
B. search_stream
stream = ga.search_stream(customer_id=cid, query=q) for batch in stream: # generator of batches for row in batch.results: print(row.campaign.name) `
使い分け
観点 search search_stream 戻り値 ListPager(自動ページング) generator(batch 単位) total_results 取れる 取れない メモリ効率 △(全件保持) ○(batch 単位で開放) 大量データ向き × ○ 並列処理 × ○
大量データ取得は search_stream、件数や ListPager 機能が欲しいときは search と覚えるとシンプルです。
私が今回踏んだ罠は、
search_stream の戻り値を「list だ」と誤解して len(stream) した瞬間です。search_stream の戻り値は generator なので len() は使えません。for batch in stream で逐次処理してください。
罠 5: REMOVED は不可逆、UI でも復元不可
罠 1 で「HIDDEN がダメなら REMOVED にすればよい」と一瞬考えました。
`python
op.update.status = client.enums.ConversionActionStatusEnum.REMOVED
`
REMOVED は API で許可されています。ところが REMOVED は不可逆 です。一度 REMOVED にした Conversion Action は、UI でも API でも ENABLED に戻せません。
REMOVED したい本当の理由が「もう使わない」なら問題ありません。「一時的に集計除外」が目的なら、
include_in_conversions_metric=False で運用するのが正解です。
完成形 — 共有の一括追加
罠を全部回避した実用コードを置きます。Tesla キャンペーンに EXACT match の共有除外キーワードを 33 件追加した実例です。
`python
from google.ads.googleads.client import GoogleAdsClient
from google.ads.googleads.errors import GoogleAdsException
CAMPAIGN_ID = 23806896702
NEGATIVE_KEYWORDS_EXACT = [
"太陽 光",
"太陽 光 発電",
"蓄電池 奈良",
# ... 33 件
]
def add_exact_negatives(client, cid: str, campaign_id: int, keywords: list[str]):
cs = client.get_service("CampaignService")
cc_service = client.get_service("CampaignCriterionService")
operations = []
for kw in keywords:
op = client.get_type("CampaignCriterionOperation")
cc = op.create
cc.campaign = cs.campaign_path(cid, campaign_id)
cc.negative = True
cc.keyword.text = kw
cc.keyword.match_type = client.enums.KeywordMatchTypeEnum.EXACT
operations.append(op)
try:
resp = cc_service.mutate_campaign_criteria(
customer_id=cid, operations=operations
)
return [r.resource_name for r in resp.results]
except GoogleAdsException as e:
for err in e.failure.errors:
print(f" ❌ {err.message}")
raise
`
このコードは v24 で動作確認済みです。
教訓
実装の途中で詰まったときに、私が頼った 4 つの習慣を共有します。
- エラーログの
trigger.int64_value を必ず読む — enum 値の特定にこれが効くfield_path_elements` で問題のフィールド位置を特定する — どの operation のどの field が問題か即座に分かるGoogle Ads API は仕様が広く、ドキュメントだけでは到達しきれない領域がいくつもあります。本記事の 5 つの罠は、私たちが今週踏んだリアルな記録です。同じ場所で詰まらない方が一人でも増えれば本望です。
次のアクション
Google Ads API レベルで広告計測を掘る案件は、得てして「タグ・SDK・MCC・OAuth」の複合問題になります。EXBANK では Python google-ads SDK を使った計測診断・mutation 自動化を伴走支援しています。
計測基盤構築サービス では、API 経由の Conversion Action 監査・除外キーワード一括追加・キャンペーン状態の自動同期 までセットで提供しています。サンプル実装と運用テンプレで、まずは 1 週間の試行運用から始められます。
御社の Google Ads 運用を「API レベルで掘れる」状態にしませんか。
