JOURNAL / 広告運用
広告運用Meta広告レポートの自動化|GASの最小実例・API接続例と検証の手順
株式会社Sunday Park / 実務ガイド
Meta広告レポートの自動化は、標準レポートで足りるか、保守込みツールを使うか、GASとAPIで自社向けに作るかを先に決めます。この記事では日別・広告別の消化額、表示回数、リンククリックを扱う最小コードと完成表を掲載します。匿名の架空データによる18のオフライン試験を実行済みです。実Meta APIとGASサービスへの接続・実行は未試験として区別しています。
CONTENTS / この記事で分かること
自動化の方法は、自由度と保守負担で選ぶ
定型の媒体内レポートなら、まず広告マネージャで必要な列と期間を揃えます。社内の独自指標やデータとの結合が必要ならGAS/API、社内に保守担当がいないなら保守込みの外部ツールが候補です。出力先がスプレッドシートというだけで自作を選ぶ必要はありません。
| 方法 | できること・向く条件 | 費用と保守 |
|---|---|---|
| Meta広告マネージャの標準レポート | 媒体内の列・内訳・レポート表示を利用。欲しい出力が標準で作れるかを先に確認。定期エクスポートの利用可否・宛先・頻度は現在の画面で確認 | 追加ツールを導入する前の選択肢。媒体横断の結合や社内受注の加工は別途必要 |
| インハウスプラス Light | 公式案内はAPI連携、Looker Studio/スプレッドシート出力、毎日更新、3アカウントまで。Meta広告レポートを提供 | 月4,980円税込。初期・BigQuery費用なし、最低契約期間なし、1か月更新。保守込み。1広告アカウントで異なる2レポートを作る場合は2アカウントとして数える |
| GAS+Meta API | 独自の検査・集計・保存処理を作れる。この記事の例は単一アカウント・単一通貨・少量の日次データ向け | コード自体に販売料はないが、開発・Google環境・保守は別。Google実行枠とMeta側の制限の両方を管理する |
標準の列・内訳・レポート表示はMeta公式の運用ガイドで確認できます。ツールの条件はインハウスプラス公式料金と同社のMetaレポート紹介に基づきます。ツールに対する実アカウント接続試験は行っていません。
取得前に、レポートの定義書をつくる
今回の最小レポートでは、成果帰属の違いをコードに隠さないため、購入件数やROASを含めず、配信量とクリックまでを対象にします。CPAを追加する場合は、先に対象イベントとアトリビューション条件を別途定義してください。
| 項目 | 採用する定義 |
|---|---|
| 粒度 | 単一アカウントの日別・広告別。配信面などの内訳なし。キーはdate_start+account_id+ad_id |
| 金額 | APIのspend、通貨はaccount_currency。請求書の税額・代理店費・制作費を加えた総費用ではない |
| 反応 | impressionsとinline_link_clicks。clicks全体とは混ぜない。リンクCTR=リンククリック合計÷表示合計×100 |
| 単価 | リンクCPC=spend合計÷リンククリック合計。分母0は空欄。行ごとの率/単価は平均しない |
| 期間 | デモは2026年10月1〜2日。API例は広告アカウントのタイムゾーンにおける昨日までの7日を毎回取得 |
| 更新と安全策 | 全ページ取得・型/通貨/重複を検査後、新しいREADYタブへ保存。既存のレポートは消さない。タブ同士を合算しない |
Meta公式SDKのAdsInsightsで、上記の項目名を確認できます。同じ名前の指標でも異なる期間・粒度・設定を混ぜないことが前提です。
自動集計する指標が判断につながるかを確認するには、CPAの定義と改善の切り分けで有効成果と費用の範囲をそろえます。
完成するレポートを先に見る
以下は付属の架空入力をNode.jsで処理して実際に生成した表です。顧客の実績を匿名化したものではなく、最初から個人情報を含めず作ったサンプルです。通貨はJPY、媒体の広告費のみです。
| 日付 / 広告 | spend | 表示回数 | リンククリック | リンクCTR | リンクCPC |
|---|---|---|---|---|---|
| 10月1日 / 商品説明A | 10,000 | 10,000 | 200 | 2.00% | 50.00 |
| 10月1日 / 使用場面B | 10,000 | 5,000 | 50 | 1.00% | 200.00 |
| 10月2日 / 商品説明A | 5,000 | 5,000 | 100 | 2.00% | 50.00 |
| 合計 | 25,000 | 20,000 | 350 | 1.75% | 71.43 |
この表から分かるのは配信量とクリックの単価までです。使用場面Bのクリック単価は高いものの、購入や利益の列がないため、広告を止める結論までは出せません。自動化で省ける作業と、次に必要な判断材料を分けます。
アプリ・対象アカウント・必要権限を確認する
デモはMetaへの接続を必要としません。API版は、管理者が認めた対象アカウントへの読み取り接続を既に持つ開発担当向けです。アプリ作成、OAuth許諾、トークン発行、権限拡張の自動実行は含めません。
実接続へ進む前に、公式認証資料で、アプリの利用形態、必要な権限と審査、対象広告アカウントへのアクセス、対応APIバージョン、App Secret Proof等の追加要件を確認します。公式SDKの認証例が存在しても、手元のアカウントで必要な準備が全て済んでいるという証拠にはなりません。
API版は既存のスクリプトプロパティから設定を読みます。META_ACCOUNT_ID、META_API_VERSION、META_TIMEZONE、META_CURRENCY、META_SPREADSHEET_IDと、承認済みの認証設定META_TOKENが必要です。Properties Serviceは設定保存機能で、プロジェクト編集者から秘密を隔離する保管庫ではありません。コード・シート・通知へトークンを書き出さず、保管方法と編集権限を管理者が判断してください。
まずMetaへ接続しないGASデモを試す
コード・架空入力・完成CSV・オフライン試験一式をダウンロードできます。外部パッケージは不要です。デモ用のReport.gsは以下の全文をそのまま使用できます。
- 検証用の空のGoogleスプレッドシートで、拡張機能からApps Scriptを開きます
- Report.gsのコードをコピーし、実行対象にdemoMetaReportを選びます
- コードとGoogleの要求権限を確認して実行すると、新規のMeta_READY_…タブに架空レポートを書き出す設計です
- 費用25,000円、表示20,000回、クリック350回、CTR1.75%、CPC71.43円という完成例と比較します
このGoogle上での実行手順は未実施です。実際に実行したのは、同じ集計関数を使う付属のNode.js試験です。Report.gs単独にはMetaへの通信処理がありません。
/* Original educational example. No live API/GAS execution has been verified.
* demoMetaReport uses only fictional data; this file does not contact Meta.
*/
function metaFixture_() {
return [
{date_start:'2026-10-01',date_stop:'2026-10-01',account_id:'demo-account',ad_id:'demo-a',ad_name:'商品説明A',account_currency:'JPY',spend:'10000',impressions:'10000',inline_link_clicks:'200'},
{date_start:'2026-10-01',date_stop:'2026-10-01',account_id:'demo-account',ad_id:'demo-b',ad_name:'使用場面B',account_currency:'JPY',spend:'10000',impressions:'5000',inline_link_clicks:'50'},
{date_start:'2026-10-02',date_stop:'2026-10-02',account_id:'demo-account',ad_id:'demo-a',ad_name:'商品説明A',account_currency:'JPY',spend:'5000',impressions:'5000',inline_link_clicks:'100'}
];
}
function validDate_(s) {
return typeof s === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(s) &&
!isNaN(Date.parse(s + 'T00:00:00Z')) &&
new Date(s + 'T00:00:00Z').toISOString().slice(0,10) === s;
}
function metric_(v, integer) {
if ((typeof v !== 'string' && typeof v !== 'number') || String(v).trim() === '')
throw new Error('Missing metric');
const n = Number(v);
if (!Number.isFinite(n) || n < 0 || (integer && !Number.isSafeInteger(n)))
throw new Error('Invalid metric');
return n;
}
function cellText_(v) {
const s = String(v);
return /^[\s]*[=+@-]/.test(s) ? "'" + s : s;
}
function buildMetaReport_(raw, cfg) {
if (!validDate_(cfg.since) || !validDate_(cfg.until) || cfg.since > cfg.until)
throw new Error('Invalid date window');
if (!Array.isArray(raw) || !raw.length) throw new Error('Empty response: review before replacing report');
const seen = new Set();
const rows = raw.map(function(r) {
if (!validDate_(r.date_start) || r.date_stop !== r.date_start ||
r.date_start < cfg.since || r.date_start > cfg.until ||
r.account_id !== cfg.account || r.account_currency !== cfg.currency ||
typeof r.ad_id !== 'string' || !r.ad_id || typeof r.ad_name !== 'string')
throw new Error('Unexpected scope or schema');
const key = [r.date_start,r.account_id,r.ad_id].join('|');
if (seen.has(key)) throw new Error('Duplicate daily-ad key');
seen.add(key);
return [r.date_start,cellText_(r.ad_id),cellText_(r.ad_name),cfg.currency,
metric_(r.spend,false),metric_(r.impressions,true),metric_(r.inline_link_clicks,true)];
}).sort(function(a,b) { return (a[0]+'|'+a[1]).localeCompare(b[0]+'|'+b[1]); });
const total = rows.reduce(function(a,r) { return [a[0]+r[4],a[1]+r[5],a[2]+r[6]]; },[0,0,0]);
function ratio(n,d,m) { return d === 0 ? '' : Math.round(n/d*m*100)/100; }
function withRates(r) { return r.concat([ratio(r[6],r[5],100),ratio(r[4],r[6],1)]); }
return {
matrix:[['日付','広告ID','広告名','通貨','spend','impressions','inline_link_clicks','リンクCTR %','リンクCPC']]
.concat(rows.map(withRates)).concat([withRates(['合計','','',cfg.currency].concat(total))]),
rowCount:rows.length, scope:cfg
};
}
function writeMetaSnapshot_(report, ss) {
// Never erase the previous report. Incomplete sheets remain DRAFT.
const suffix = new Date().getTime()+'_'+Utilities.getUuid().slice(0,6);
const sheet = ss.insertSheet('Meta_DRAFT_'+suffix);
sheet.getRange(1,1,report.matrix.length,9).setValues(report.matrix);
sheet.setFrozenRows(1);
sheet.getRange(1,1,1,9).setFontWeight('bold');
sheet.getRange(report.matrix.length+2,1,4,2).setValues([
['対象期間',report.scope.since+' / '+report.scope.until],
['定義','日別・広告別・内訳なし / '+report.scope.currency],
['生成日時 UTC',new Date().toISOString()],
['状態','全ページ取得・検査後のスナップショット。過去のタブと合算しない']
]);
SpreadsheetApp.flush();
sheet.setName('Meta_READY_'+suffix);
return sheet.getName();
}
function demoMetaReport() {
const cfg = {since:'2026-10-01',until:'2026-10-02',account:'demo-account',currency:'JPY'};
const report = buildMetaReport_(metaFixture_(),cfg);
return writeMetaSnapshot_(report,SpreadsheetApp.getActiveSpreadsheet());
}
API接続例は、承認済みの読取環境で追加する
Meta公式SDKのAdAccount.get_insightsでは、GET /insightsとlevel、time_range、time_increment、limitを確認できます。下の任意アダプターは日別・広告別を取得し、続きがあれば同じ公式エンドポイントへカーソルを渡します。返されたnext URLを無条件に開きません。
LiveAdapter.gsをReport.gsと同じプロジェクトへ追加し、必要な確認を済ませた環境でliveMetaReportを選ぶ構成です。APIバージョンは固定せず、利用時にサポートされる版を設定します。通信・認証・Googleサービス実行は未試験で、実アカウントへ接続できることを保証したコードではありません。
LiveAdapter.gsの全文を開く(任意・実接続未試験)
/* Optional live adapter: untested against Meta or Google services.
* Uses existing, separately authorized configuration; it creates no credentials,
* apps, permission grants or triggers. Never log properties, URLs or response bodies.
*/
function metaWindow_(today, days) {
if (!validDate_(today) || !Number.isInteger(days) || days < 1 || days > 31)
throw new Error('Invalid window settings');
const midnight = Date.parse(today+'T00:00:00Z');
return {since:new Date(midnight-days*86400000).toISOString().slice(0,10),
until:new Date(midnight-86400000).toISOString().slice(0,10)};
}
function fetchMetaRows_(cfg, transport, pause) {
if (!/^v\d+\.\d+$/.test(cfg.version) || !/^\d+$/.test(cfg.account) || !cfg.token)
throw new Error('Missing approved API configuration');
const base = 'https://graph.facebook.com/'+cfg.version+'/act_'+cfg.account+'/insights';
const fields = 'date_start,date_stop,account_id,ad_id,ad_name,account_currency,spend,impressions,inline_link_clicks';
let after = '', all = [];
const seenCursors = new Set();
for (let page = 0; page < 20; page++) {
const params = {fields:fields,level:'ad',time_increment:'1',limit:'100',
time_range:JSON.stringify({since:cfg.since,until:cfg.until}),access_token:cfg.token};
if (after) params.after = after;
const url = base+'?'+Object.keys(params).map(function(k) {
return encodeURIComponent(k)+'='+encodeURIComponent(params[k]);
}).join('&');
let body;
for (let attempt = 0; attempt < 3; attempt++) {
let response;
try { response = transport(url); }
catch (_) { throw new Error('Transport failed; report unchanged'); }
const status = response.status;
if ((status === 429 || status >= 500) && attempt < 2) {
pause(500*Math.pow(2,attempt)); continue;
}
if (status < 200 || status >= 300) throw new Error('Meta HTTP '+status+'; report unchanged');
try { body = JSON.parse(response.text); }
catch (_) { throw new Error('Invalid JSON; report unchanged'); }
if (body.error || !Array.isArray(body.data)) throw new Error('Meta response error; report unchanged');
break;
}
all = all.concat(body.data);
if (!body.paging || !body.paging.next) return all;
// Rebuild the fixed official endpoint; never follow a returned arbitrary URL.
after = body.paging.cursors && body.paging.cursors.after;
if (!after || seenCursors.has(after)) throw new Error('Invalid pagination; report unchanged');
seenCursors.add(after);
}
throw new Error('Page limit reached; shorten the date range');
}
function liveMetaReport() {
const p = PropertiesService.getScriptProperties().getProperties();
const required = ['META_ACCOUNT_ID','META_API_VERSION','META_TOKEN','META_TIMEZONE','META_CURRENCY','META_SPREADSHEET_ID'];
if (required.some(function(k) { return !p[k]; })) throw new Error('Approved configuration required');
const period = metaWindow_(Utilities.formatDate(new Date(),p.META_TIMEZONE,'yyyy-MM-dd'),7);
const cfg = {account:p.META_ACCOUNT_ID,version:p.META_API_VERSION,token:p.META_TOKEN,
currency:p.META_CURRENCY,since:period.since,until:period.until};
const lock = LockService.getScriptLock();
if (!lock.tryLock(1000)) throw new Error('Another report is running');
try {
const raw = fetchMetaRows_(cfg,function(url) {
const r = UrlFetchApp.fetch(url,{method:'get',muteHttpExceptions:true,followRedirects:false});
return {status:r.getResponseCode(),text:r.getContentText()};
},function(ms) { Utilities.sleep(ms); });
// Do not pass a token into the report object or into the sheet writer.
const report = buildMetaReport_(raw,{account:cfg.account,currency:cfg.currency,since:cfg.since,until:cfg.until});
return writeMetaSnapshot_(report,SpreadsheetApp.openById(p.META_SPREADSHEET_ID));
} finally { lock.releaseLock(); }
}
認証パラメータを含むURL、プロパティ全体、API応答全文をログへ出さないでください。HTTPエラーは状態コードだけを返します。SDKの公開例にあるトークンを使う認証方式を参照していますが、アカウント固有の追加認証要件には別の対応が必要です。
毎回の取得範囲を、新しいスナップショットへ保存する
この最小例は履歴台帳へ末尾追加する方式ではなく、対象期間の全データを再計算して新しいスナップショットを作ります。同じ期間を二度処理しても、一つの表の合計は二重になりません。過去のREADYタブと新しいREADYタブを足し合わせないでください。
保存時はDRAFT名で作成し、データ・定義・生成時刻の書き込み後にREADYへ変更します。途中で失敗しても過去のタブを削除しません。DRAFTが残った場合は今回の実行は未完成です。READYへの変更とデータ保存を分散トランザクションとして保証する実装ではありません。
長期の運用台帳へ発展させるなら、日付・アカウントID・広告ID・内訳・定義の版を一意キーにして更新します。対象期間を再取得したとき、以前あった行が今回なくなった場合の扱いまで決めてください。単なる追加・上書きだけでは、消えた行の古い値が残る場合があります。広告名は識別キーにしません。
02 / SAFE REFRESH考え方を図で整理
取得期間を計算し、新しいスナップショットへ
- 01
取得
対象期間を明示
- 02
検査
完了・値・型を確認
- 03
計算
件数合計から比率を算出
- 04
保存
新しいDRAFTへ書き込む
- 05
確定
書き込み後にREADYへ
CPAやCTRは、元の件数から再計算する
複数行を集計するとき、行ごとのCPAを単純平均すると、各行の成果件数の違いが無視されます。対象範囲の費用合計を成果件数合計で割って計算します。CTRも、同じ定義のクリック合計を表示回数合計で割るなど、分子と分母を揃えて再計算します。
架空例として、広告Aが費用一万円・成果一件、広告Bが費用一万円・成果九件なら、合計CPAは二万円÷十件で二千円です。各CPAの単純平均とは一致しません。成果ゼロの場合は無理にゼロ円を表示せず、計算不能や未獲得と分かる表示にします。
取得失敗、未配信、成果ゼロ、値が存在しない状態は区別します。取得に失敗した数字をゼロに変えると、成果の悪化や配信停止と誤解される可能性があります。未取得を警告し、前回更新時刻が見えるようにします。率や金額の丸めは、元データの精度を残したうえで表示時に行いましょう。
トリガーの時刻と所有者を管理する
GASのインストール型トリガーは、作成したユーザーの権限で実行されます。誰が作成し、誰が保守し、不在時に誰が対応するかを記録します。時間主導の実行は指定した時間帯の中でずれる場合があるため、「毎朝必ず何時何分に届く」と一律に約束しないようにします。
対象アカウントのタイムゾーン、スクリプトの時刻、シートの日付表示を確認します。実行した場所の「昨日」をそのまま取得対象にすると、広告アカウントの日付境界と合わない場合があります。取得対象の開始・終了と取得日時を保存し、後から照合できるようにしてください。
手動の再実行と定期実行が重なる場合は、同時に同じ場所へ書き込まない制御が必要です。GASにはLockServiceがあります。利用するロックの範囲、取得できない場合の処理、処理終了時の解放を設計し、ロックを導入しただけで重複の問題がすべて消えるとは考えないようにします。
定期化する場合は実接続の照合後、Apps Scriptのトリガー画面でliveMetaReportと時間主導型を選びます。スクリプトのタイムゾーンとMETA_TIMEZONEを揃え、まず1回の実行結果を確認してください。サンプルコードはトリガーを自動作成しません。新しいタブが増えるため、継続運用の前に保持期間を決めます。
エラーは種類を分け、再実行と通知を設計する
このAPI例はHTTP 429または5xxに対し初回を含め最大3回試行(再試行は最大2回)し、認証・権限・不正な応答は停止します。20ページを超えた場合も停止し、期間を短くして確認する設計です。これらの上限はサンプルの安全策で、Metaの公式上限ではありません。通信例外、200応答内のAPIエラー、非同期ジョブ、大量取得の再開処理は別途設計が必要です。
必須指標が欠けた場合はゼロへ置換せず停止します。対象期間にデータが一件もない場合も自動的に「0円・0件」のレポートを作らず、無配信か取得条件の誤りかを担当者が確認します。全ページ取得が済むまで表を保存しません。
Googleの公開クォータでは通常のスクリプト実行は1回6分などの制限があります。制限は変更されるため、規模を増やす前に実時間を測り、分割・非同期・別基盤を検討します。トークンや元のAPI応答を含めず、対象期間、成功した段階、失敗分類、前回成功時刻を担当者へ知らせる運用にします。このサンプルには通知送信は含めていません。
本番運用前に、異常系と再実行をテストする
実際に実行したオフライン試験
付属フォルダーで「node test-offline.cjs」を実行します。2026年10月4日にNode.js v24.19.0で18ケースが通過し、完成CSVを生成しました。通信は模擬関数へ置換し、MetaにもGoogleにも接続していません。
| 対象 | 実行結果または残る確認 |
|---|---|
| 集計・再実行 | 加重合計、同じ入力の再処理、広告名変更、0分母をオフラインで確認 |
| 入力検査 | 重複キー、異通貨、欠損、負数、不正日付、空データを拒否。数式に見える広告名を文字列化する試験も通過 |
| 模擬API通信 | 2ページ結合、429の上限つき再試行、401停止、不正JSON、カーソル循環、途中ページ403停止を確認 |
| 日付 | 月をまたぐ7日間を純粋関数で確認。アカウント固有のタイムゾーンは実接続時に確認が必要 |
| 未試験 | Meta実API、実認証、Google Sheets書き込み、UrlFetchApp、LockService、トリガー、広告マネージャとの数値照合 |
本番へ進む前の合格条件
- 読み取り対象・認証・APIバージョンを管理者が確認する
- 管理画面と同じ期間・列・通貨で少量の実データを照合する
- 失敗時は前回READYが残り、DRAFTを完成表と誤認しないことを確認する
- 日付境界・複数ページ・実行時間・同時実行を実環境で試す
- 定期実行の所有者、失敗の連絡先、スナップショットの保持期間を決める
毎日動くことと、毎日正しいことは別の確認です。インハウス化支援へ相談する場合は、欲しい表に加え、対象アカウント数、更新頻度、保守担当を共有してください。
取得結果と成果イベントの件数が合わない場合は、コンバージョンAPIの計測テストも確認し、集計処理と送信処理を分けて調べます。
参考情報・出典
- Meta公式SDK AdsInsights費用・通貨・広告ID・アクション等の項目定義/確認:2026-10-04
- Apps Script Quotas利用枠と超過時の停止・変更可能性/確認:2026-10-04
- Apps Script Installable Triggers作成者権限と時間主導実行の時刻の揺れ/確認:2026-10-04
- Apps Script Properties Service設定値のスコープと保存機能/確認:2026-10-04
- Apps Script LockService同時アクセスの制御/確認:2026-10-04
- Apps Script Loggingログ機能/確認:2026-10-04
- Meta Insights API最新パラメータと取得仕様の公式参照先。429で本文未取得/本文の直接確認未了
- Meta認証の案内利用形態別の認証・権限の公式参照先。本文未取得/本文の直接確認未了
- Meta公式SDK AdAccountget_insights: GET /insightsとlevel/time_range/time_increment/limit等/確認:2026-10-04
- Meta公式SDK README認証トークンを使う初期化、アカウント形式、公開リクエスト例。アプリ別必要権限の完全検証ではない/確認:2026-10-04
- Google UrlFetchAppGET、muteHttpExceptions、followRedirectsと外部通信の権限/確認:2026-10-04
- Google Range2次元配列のsetValues/確認:2026-10-04
- インハウスプラス サブスク公式料金Light月額4980円税込、3アカウント、毎日更新、初期/BigQuery費用なし、1か月更新/確認:2026-10-04
- Meta Horizon 運用ガイド広告マネージャのColumns/Breakdown/Reportsの公式説明/確認:2026-10-04