HubSpot を Apps Script で Sheets に同期 — 1.3 万件を 4 分で
最終更新日: / 公開日:
最終更新日: / 公開日:
本記事のポイント
- HubSpot 公式の Sheets 連携は Marketing Hub Professional 以上が前提です。ただし Starter でも REST API は全エンドポイントが開放されているので、Apps Script を書けば同じことが月額 $0 でできます
- Private App の Token 発行から全件取得・型崩れ対策・毎朝の自動実行までを、コピペで動くコードと巻末の一括実行プロンプト付きで STEP 順に再現できます
- 実装した結果、Companies / Contacts / Deals 合計 1.3 万件が毎朝 4 分以内で Google Sheets に同期され、Zapier の月額 $20〜30 (年間 $240〜360) をゼロにできました
「HubSpot のデータを Sheets で自由に触りたいだけなのに、そのためだけに Zapier を契約している」。HubSpot Starter を使う現場で、よく聞く悩みです。原因はプランではありません。Starter でもデータは REST API で全部取れるのに、可視化の手前の「Sheets に落とす」ところだけを月額サービスに頼っているからです。本記事では、ある業務用美容機器メーカーの支援で実際に組んだ Apps Script 同期を、そのまま再現できる手順に落として公開します。Apps Script が未経験でも、コードは巻末の一括実行プロンプトで生成できるので、必要なのは「何を Sheets に出したいか」を決める力だけです。
できあがるもの — 毎朝 6:00 に 1.3 万件が Sheets に届く
完成形から見せます。下図の流れで、HubSpot の Companies / Contacts / Deals が毎朝自動で Google Sheets に書き込まれ、そのまま Looker Studio や Pivot で分析できる状態になります。人がボタンを押す作業はゼロです。

| 項目 | 内容 |
|---|---|
| 構築の所要時間 | 半日〜1 日 (Apps Script 未経験 + 一括実行プロンプト利用の場合) |
| 前提条件 | HubSpot Starter 以上 / Google アカウント / Claude Code またはブラウザ版 Claude |
| 追加ランニングコスト | $0/月 (Apps Script も Looker Studio も無料) |
| 同期規模の実測 | Companies 約 5,000 + Contacts 約 6,000 + Deals 約 2,000 = 1.3 万件を約 4 分 |
Zapier や Make.com にも監視 UI やリトライ機構という運用上の価値はあります。非エンジニアがフローを日常的に編集する体制ならそちらを残す判断もあります。マーケ担当 1 人が Claude Code でメンテする体制を組めるなら、この STEP を進めるほうがコストと自由度の両面で有利です。以下、STEP 1 から順に進めます。円換算はすべて 1 ドル = 150 円 (2026 年 5 月時点) で計算しています。
STEP 1. HubSpot Private App を作って Token を発行する
「Token ってどこで作るんですか。API キーとは違うんですか」。実装の入り口で必ず出る質問です。HubSpot の REST API を外部から叩くには、Private App を 1 つ作り、その Token を使います。所要は 5 分ほどです。
やることは 3 つです。
- HubSpot 管理画面の Settings > Integrations > Private Apps から「Create a private app」を選ぶ
- Scopes タブで、同期したいオブジェクトの読み取り権限を付ける (
crm.objects.contacts.read/crm.objects.companies.read/crm.objects.deals.read。担当者名の変換もするならcrm.objects.owners.readも) - 発行された Token をコピーし、Apps Script 側の
PropertiesServiceに格納する (コード本体には書かない)
つまずきポイントは Scopes の付け忘れです。読み取りに必要な scope が 1 つでも欠けていると、API は 403 を返します。エラーメッセージに不足している scope 名が出るので、その scope を Private App に追加して Token を再確認してください。今回は読み取りだけなので .read で足りますが、後続記事のように Kintone から書き戻す場合は .write も要ります。
Token は Apps Script エディタの「プロジェクトの設定 > スクリプト プロパティ」に HUBSPOT_TOKEN という名前で登録します。こうしておくと、コードを共有しても Token が漏れません。
STEP 2. Apps Script で全件取得する — Search API と Pagination
「サンプル通り書いたのに、1 ページ目の 100 件しか取れない」。HubSpot API の最初の関門がこれです。原因は Pagination (ページ送り) の書き方にあります。
HubSpot の Search API は POST で叩き、paging.next.after というカーソルを次のリクエストに渡すことで続きを取得します。件数を知らなくても、このカーソルが空になるまでループすれば全件取れます。Claude Code に「HubSpot の Contacts を Private App Token で 100 件ずつ Pagination で全件取得する Apps Script を書いて」と指示すると、以下の骨格がそのまま出てきます。
const HUBSPOT_TOKEN = PropertiesService.getScriptProperties().getProperty('HUBSPOT_TOKEN');
const BASE_URL = 'https://api.hubapi.com';
function fetchAllContacts() {
const properties = ['email', 'firstname', 'lastname', 'company', 'createdate', 'lifecyclestage'];
let after = undefined;
const results = [];
do {
const payload = {
limit: 100,
properties: properties,
filterGroups: [{
filters: [{ propertyName: 'createdate', operator: 'GT', value: '2024-01-01' }]
}],
after: after
};
const res = UrlFetchApp.fetch(`${BASE_URL}/crm/v3/objects/contacts/search`, {
method: 'post',
contentType: 'application/json',
headers: { Authorization: `Bearer ${HUBSPOT_TOKEN}` },
payload: JSON.stringify(payload),
muteHttpExceptions: true
});
const body = JSON.parse(res.getContentText());
results.push(...body.results);
after = body.paging?.next?.after;
} while (after);
return results;
}
押さえるべきは 3 点です。Token は PropertiesService から読み込みコード本体に書かないこと。Search API は filterGroups で「過去に作成された Contacts のみ」のような絞り込みができること。そして after = body.paging?.next?.after をループ条件にすれば全件取得になることです。
つまずきポイントは Search API の「同一 query で 10,000 件まで」という上限です。1 万件を超える対象は、createdate でレンジを切って複数 query に分割するか、List API (/crm/v3/objects/contacts) に切り替えます。今回の 1.3 万件はオブジェクトごとに分けているため、各 query は 1 万件を下回り、この上限には当たりませんでした。
STEP 3. 型崩れ 3 種を潰して Sheets に書き込む
取得したデータを Sheets に書くだけなら簡単に見えますが、ここで多くの人が半日溶かします。HubSpot API は値を文字列で返し、Google Sheets は書き込み時に自動で型を推論するためです。このズレが原因で、Looker Studio 側で「日付が動かない」「金額が合計できない」という症状に化けます。踏む前に 3 種の対策をコードへ入れておきます。
| ハマり | 症状 | 対策 |
|---|---|---|
| Date 自動変換 | 2026-04 等の文字列を勝手に Date 化 → toString() で Mon Apr 01 2026... に化ける |
range.setNumberFormat('@') を setValues の 前 に呼び、列全体を Plain Text 化 |
| Number coerce | HubSpot は金額を文字列で返す ("3500000") → Looker Studio で SUM 不可 |
指定した数値列だけ Number(value) 変換してから書き込み |
| Pagination 漏れ | カーソルの undefined チェックが甘く 1 ページで止まる | while (after) で明示的にカーソル変数を判定 (STEP 2 で対応済み) |
コードに落とすとこうなります。
function writeToSheet(sheet, headers, rows) {
sheet.clear();
const range = sheet.getRange(1, 1, rows.length + 1, headers.length);
range.setNumberFormat('@'); // ← setValues の前に Plain Text 強制
const numericColumns = ['amount', 'days_in_stage'];
const normalized = rows.map(row => row.map((cell, i) => {
return numericColumns.includes(headers[i]) ? Number(cell) || 0 : cell;
}));
sheet.getRange(1, 1, 1, headers.length).setValues([headers]);
sheet.getRange(2, 1, normalized.length, headers.length).setValues(normalized);
}

つまずきポイントは setNumberFormat('@') を呼ぶ順番です。setValues() の 後 に呼ぶと、値は既に型推論されているので効きません。必ず書き込みの前に列を Plain Text 化してください。Plain Text にすると Sheet 上の =SUM(...) は効かなくなりますが、Looker Studio 側でデータソースを「Number 型」として読ませれば集計は問題なく通ります。Sheet 内で =SUM を多用したい列だけ setNumberFormat('0') に切り替える手もあります。
STEP 4. 3 オブジェクトをまとめて毎朝自動実行する
同期関数が動いたら自動化します。ここで意識するのが Apps Script の「6 分制限」です。1 関数の処理が 6 分を超えると強制終了され、Sheet が中途半端な状態で残ります。Companies / Contacts / Deals を 1 関数に詰め込むと、データが増えた瞬間にこの上限に当たります。
対策は、オブジェクトごとに関数を分け、それらを順に呼ぶオーケストレーターを 1 本だけ Trigger に登録する構成です。この分割で、1.3 万件を約 4 分で完走させています。
| 関数 | 対象 | 1 回の処理時間 |
|---|---|---|
syncCompaniesToSheet |
Companies 全件 | 約 1 分 20 秒 |
syncContactsToSheet |
Contacts 全件 | 約 1 分 30 秒 |
syncDealsToSheet |
Deals 全件 | 約 1 分 10 秒 |
syncOrchestrator |
上記 3 関数を順次呼び出し | 約 4 分 |
function syncOrchestrator() {
const ss = SpreadsheetApp.openById('YOUR_SHEET_ID');
syncCompaniesToSheet(ss.getSheetByName('companies_raw'));
syncContactsToSheet(ss.getSheetByName('contacts_raw'));
syncDealsToSheet(ss.getSheetByName('deals_raw'));
Logger.log(`Sync completed at ${new Date().toISOString()}`);
}
Trigger は Apps Script エディタ左メニューの「トリガー」から「トリガーを追加」を選び、関数 syncOrchestrator / イベントソース「時間主導型」/ タイプ「日次」/ 時刻「6:00-7:00」を指定します。これで毎朝 1 回だけ自動実行になります。

つまずきポイントは、Trigger 実行のログが Apps Script の「実行数」画面でしか追えないことです。止まったときに気付けるよう、Logger.log の結果を別タブ sync_log (timestamp / function_name / status / count) にも追記し、Looker Studio に「最終同期日時」を表示しておくと安全です。データが 5 万件を超えて 4 分で終わらなくなったら、hs_lastmodifieddate を使った増分同期に切り替えます。その手順は Kintone と HubSpot の並行運用 で解説しています。
STEP 5. Owner ID と Pipeline ID を名前に変換する
運用に入って 1 ヶ月もすると、営業現場から「Owner ID が数字で出てくるけど、これ誰?」というフィードバックが来ます。技術的には ID のほうが扱いやすいのですが、営業に共有するダッシュボードに 12345678 と出ても誰も読めません。Owner が 5 名なら Looker Studio の計算フィールドに CASE WHEN を書き並べても耐えられますが、20 名を超えると保守できなくなります。
そこで、マッピング用の Sheet を別途同期し、Looker Studio 側で 1 度だけ Join します。これらは人事異動や Pipeline 改定でしか変わらないので、週 1 回の Trigger で十分です。
| マッピング Sheet | データ元 | 同期頻度 |
|---|---|---|
owners_master |
HubSpot Owners API (/crm/v3/owners) |
週 1 回 |
pipelines_master |
HubSpot Pipelines API (/crm/v3/pipelines/deals) |
週 1 回 |
stages_master |
Pipelines API のレスポンス内 stages を平坦化 | 週 1 回 |
Looker Studio 側では、このマッピング Sheet を別データソースとして登録し、Blend (データの統合) で Owner ID や Stage ID を名前に置き換えます。VLOOKUP を Sheet 内で組んでも同じことができます。
つまずきポイントは、マッピング Sheet を作るぶん Apps Script のメンテ対象が増えることです。「Owner が増減したら週次 Trigger を待つか、手動で syncOwnersMaster を実行する」といった運用手順を 1 ページ残しておくと、引き継ぎが楽になります。
動作確認 — 何が見えれば成功か
ここまで組んだら、syncOrchestrator を手動で 1 回実行して、次の 3 点を確認します。
- Sheet:
companies_raw/contacts_raw/deals_rawの 3 タブに、ヘッダー付きで全件が埋まっている。金額列が左寄せの数値、日付列が2026-04-01の文字列のまま化けていない - sync_log タブ: 各関数の行に
status = successと件数が記録されている - Looker Studio: データソースを更新すると最新の値が反映され、「最終同期日時」が今の時刻になっている
3 点が揃えば、翌朝 6:00 からは自動で回ります。初日は念のため実行数画面で Trigger の成功を 1 度だけ確認しておくと安心です。
まとめ — データ層が整えば、次は可視化
HubSpot Starter でも、REST API を Apps Script から叩けばデータは全部取れます。あとは Token 発行 → 全件取得 → 型崩れ対策 → 自動実行 → 名前変換の 5 STEP をなぞるだけで、1.3 万件が毎朝 4 分で Google Sheets に届く状態になります。コードは巻末の一括実行プロンプトで生成できるので、Apps Script 未経験でも半日〜1 日で組めます。
データ層が整ったら、次は可視化層です。この記事は、Starter で Professional 級ダッシュボードを再現するシリーズの「データ層」を担っています。全体像と可視化・分析の各記事は下表から辿れます。
| # | タイトル | 役割 |
|---|---|---|
| #1 (Pillar) | HubSpot Starter で Pro 級ダッシュボードを再現する 4 層スタック | 全体設計 |
| #2 (本記事) | HubSpot を Apps Script で Google Sheets に同期 | データ層 |
| #3 | Looker Studio で HubSpot ダッシュボードを 30 分構築 | 可視化層 |
| #6 | Kintone と HubSpot の並行運用 — 増分同期で破綻させない | 既存 CRM 統合 |
humbulls では、こうしたデータ層の設計から Looker Studio の構築まで一気通貫で伴走する Growth Partner サービス を提供しています。実装テンプレを含む詳細ガイドは BtoB マーケ AI 活用ガイド で配布しています。
🤖 AI 実行キット
本文の STEP 1〜5 を、そのまま Claude Code に丸ごと実行させるためのキットです。プロンプトの記入例を自社の値に置き換えて渡してください。
キット① 一括で HubSpot → Sheets 同期を構築する — 半日
種別: 実装キット 使うもの: Claude Code (ブラウザ版 Claude でもコードは生成できますが、Apps Script エディタへの貼り付けは手動になります) 事前に用意するもの: HubSpot の Private App Token (STEP 1 で発行) / 同期先の Google Sheets の ID / 出したいプロパティ名のメモ
プロンプト:
HubSpot Starter のデータを Apps Script で Google Sheets に毎朝同期する
スクリプト一式を書いてください。
【私の環境】
- 同期先 Sheets ID: 1AbC...(記入例。Sheets URL の /d/ と /edit の間)
- 同期するオブジェクトと取得プロパティ:
- companies: name, domain, createdate(記入例。自社の必要列に置き換え)
- contacts: email, firstname, lastname, company, createdate, lifecyclestage
- deals: dealname, amount, dealstage, pipeline, closedate, createdate
- 金額など数値として集計したい列: amount, days_in_stage
- Token はスクリプトプロパティ HUBSPOT_TOKEN から読む
【必ず守る実装条件(本文の対策を転写)】
1. Search API を POST で叩き、paging.next.after を while(after) でループして全件取得する
2. Sheets へ書く前に range.setNumberFormat('@') を setValues の「前」に呼ぶ
(後に呼ぶと型推論後で効かないため)
3. 上記の数値列だけ Number(value) に変換してから書き込む
4. companies / contacts / deals を別関数に分け、syncOrchestrator で順次実行する
(1 関数 6 分制限を超えないため)
5. 各関数の timestamp / function_name / status / count を sync_log タブに追記する
【出力】
- fetchAll{Object}() 3 関数 + writeToSheet() + syncOrchestrator() + logSync()
- 日次 6:00 実行の Trigger を設定する手順を最後にコメントで併記
出力の確認ポイント:
setNumberFormat('@')が必ずsetValues()より前の行にあるか (順番が逆だと日付が化けます)- Pagination が
while (after)で書かれ、ifの 1 回判定になっていないか - 数値列の変換対象が、自社で集計したい列名と一致しているか
うまくいかないとき:
- 実行すると 403 が返る → Private App の Scopes 不足です。エラーに出る scope 名を STEP 1 の Private App に追加して再実行してください
- 特定オブジェクトだけ途中で止まる → その query が 1 万件を超えています。
createdateでレンジを切って複数回に分割するプロンプトを追記してください
よくあるつまずきの早見表:
| 症状 | 原因 | 対処 |
|---|---|---|
| API が 403 を返す | Private App の scope 不足 | STEP 1 で不足 scope を追加 |
| Looker Studio で日付が動かない | Date 自動変換 | setNumberFormat('@') を setValues の前に |
| 金額が SUM できない | Number coerce | 数値列を Number() で変換 |
| 実行が 6 分で強制終了 | 1 関数に詰め込みすぎ | オブジェクトごとに関数分割 |
| 1 ページ目しか取れない | Pagination 漏れ | while (after) でカーソル判定 |
参考文献
- HubSpot Private Apps — HubSpot 開発者ドキュメント (取得日: 2026-05)
- HubSpot CRM Search API — HubSpot 開発者ドキュメント (取得日: 2026-05)
- HubSpot CRM Properties — Number 型が文字列で返る仕様 (取得日: 2026-05)
- HubSpot Owners API — HubSpot 開発者ドキュメント (取得日: 2026-05)
- Apps Script Range#setNumberFormat — Google 公式 (取得日: 2026-05)
- Apps Script Triggers — Google 公式 (取得日: 2026-05)
- Apps Script Quotas — 6 分制限の公式記述 (取得日: 2026-05)
- Zapier — Pricing — Zapier 公式 (取得日: 2026-05)
- Looker Studio Data Blending — Google Support (取得日: 2026-05)