はじめに
kintoneで問い合わせや商談を管理していて、Google広告も回している。
そんなとき「商談になったリードだけを広告側に成果として返したい」と思ったことはないでしょうか?
Google広告にはそのための仕組みとしてオフラインコンバージョンのインポートがありますが、2026年6月15日から、Google Ads APIで新しくオフラインコンバージョンを送る場合はデータマネージャー(Data Manager API)を使う前提になりました。
変更の背景や「そもそもなぜ商談データを返すのか」は note のほうにまとめたので、気になる方はこちらをどうぞ。
この記事では、もう少し手を動かす側の話として、
- kintoneから商談化・受注したレコードをAPIで取ってくる
- データマネージャーに取り込める形のCSVにする
- データマネージャーで取り込む
までの「とりあえず動く最小構成」を書いていきます。
いきなりAPI連携を作るのではなく、まずはCSV(Googleスプレッドシート)経由で回してみる、という方針です。
前提・環境
- Node.js 18 以上(ESM で書いています)
@kintone/rest-api-clientdotenv- Google広告アカウントの管理者権限
- Google広告の自動タグ設定が有効になっていること
自動タグ設定はGCLIDを使ったオフラインコンバージョンの前提条件なので、まだの場合は先に有効にしておきます。
kintone側は、こんな感じのリード管理アプリを想定しています(検証用に作ったデモアプリで、ダミーデータを500件ほど入れています)。
| フィールド名 | フィールドコード | タイプ |
|---|---|---|
| 会社名 | company_name | 文字列(1行) |
| メール | リンク(メールアドレス) | |
| 電話 | phone | リンク(電話番号) |
| 流入元 | utm_source | ドロップダウン |
| gclid | gclid | 文字列(1行) |
| ステータス | status | ドロップダウン(新規/接触済み/商談化/受注/失注) |
| リード作成日時 | lead_created_at | 日時 |
| 商談化日 | opportunity_date | 日付 |
| 受注日 | won_date | 日付 |
| 受注金額 | won_amount | 数値 |
ポイントは gclid を保存するフィールドがあることです。
広告をクリックして来た人のGCLIDをフォーム経由でkintoneに保存しておかないと、そもそもどのクリックから来た商談なのかをGoogleに伝えられません。フォーム側でGCLIDを拾う方法は別記事で書く予定です。
全体像
kintone(リード・商談)
↓ REST API
Node.js スクリプト(商談化/受注のレコードを抽出・整形)
↓ CSV出力
Googleスプレッドシート
↓
データマネージャー
↓
Google広告(コンバージョンとして計測)
データマネージャーはGoogleスプレッドシートをデータソースとして直接つなげます。
一方で、kintoneはデータマネージャーの対応データソースには入っていないので、間にスクリプトとスプレッドシートを挟む形にしています。
Google広告側の準備
まず、取り込み先のコンバージョンアクションを作ります。
- Google広告の管理画面で「目標」→「コンバージョン」→「概要」
- 「+ 新しいコンバージョン アクション」
- 「オフライン ソースからのコンバージョン データ」を選ぶ
- 接続方法は「直接接続」を選び、データソースとしてGoogleスプレッドシートを指定
今回は「商談化」と「受注」でコンバージョンアクションを分けました。
データマネージャーはスプレッドシートの最初のシートしか読まないので、アクションごとにスプレッドシート(またはファイル)を分けておくと後で混乱しません。
スプレッドシートには編集者権限、Google広告アカウントには管理者権限が必要です。
kintoneから商談化・受注のレコードを取得する
今回使うのは、こんな感じのリード管理アプリです。
デモ用のリード管理アプリ(データはすべてダミーです)
kintoneのAPIトークンは、アプリの設定 →「APIトークン」から発行します。今回は読み取りだけなので「レコード閲覧」だけにチェックを入れておけば十分です。
トークンを発行・変更したあとは「アプリを更新」を押さないと反映されないので注意してください(これで一回ハマりました)。
.env はこんな感じ。
KINTONE_BASE_URL=https://xxxxx.cybozu.com
KINTONE_LEAD_APP_ID=1
KINTONE_LEAD_API_TOKEN=xxxxxxxxxxxxxxxx
APIトークンはブラウザ側のJavaScriptやGitのリポジトリには置かず、ローカルやサーバー側だけで使います。.env は .gitignore に入れておきましょう。
パッケージを入れます。
npm init -y
npm i @kintone/rest-api-client dotenv
package.json に "type": "module" を追加しておきます。
取得部分のコードです。
import "dotenv/config";
import { KintoneRestAPIClient } from "@kintone/rest-api-client";
const client = new KintoneRestAPIClient({
baseUrl: process.env.KINTONE_BASE_URL,
auth: { apiToken: process.env.KINTONE_LEAD_API_TOKEN },
});
const APP_ID = process.env.KINTONE_LEAD_APP_ID;
// 指定した日付フィールドが since 以降のレコードを全件取得する
export async function fetchByDate(dateField, since) {
return client.record.getAllRecords({
app: APP_ID,
condition: `${dateField} >= "${since}"`,
fields: [
"$id",
"gclid",
"status",
"lead_created_at",
"opportunity_date",
"won_date",
"won_amount",
],
});
}
getAllRecords は内部でページングしてくれるので、500件の上限を気にせず全件取れます。
REST APIを直接叩く場合は1回500件までで、offset も上限が10,000件なので、件数が多いアプリならカーソルAPIを使うほうが安全です。
ちなみに condition には order by や limit、offset は書けません。並び順を指定したいときは orderBy を別で渡します。
商談化日・受注日で絞り込んでいるのは、ステータスだけで絞ると取りこぼすからです。
たとえば「商談化 → 受注」と進んだレコードは、今のステータスが「受注」なので status in ("商談化") には引っかかりません。商談化のコンバージョンとしては送りたいので、日付フィールドのほうで判定しています。
データマネージャー用のCSVに変換する
取ってきたレコードを、データマネージャーで取り込める形にします。
今回使う列はこのあたりです。
| 列名 | 中身 |
|---|---|
| Google Click ID | gclid |
| Conversion Time | コンバージョンの日時(タイムゾーン付き) |
| Conversion Value | 受注金額(受注のときだけ) |
| Conversion Currency | JPY |
| Order ID | 重複防止用のID |
データマネージャーでは取り込み時に列とフィールドを対応づける(マッピングする)ので、列名自体は分かりやすければOKです。1行目は見出しにしておく必要があります。
コードはこんな感じです。
import { writeFileSync } from "node:fs";
import { fetchByDate } from "./fetchLeads.js";
// 日付フィールドには時刻がないので、ここで時刻を決める
// (後述のハマりポイント参照)
function toConversionTime(dateStr, leadCreatedAtUtc) {
// 日付のみ → JSTのその日の 23:59:00 として扱う
let t = new Date(`${dateStr}T23:59:00+09:00`);
// 未来の日時にならないようにする
const now = new Date();
if (t > now) t = now;
// クリック(≒リード作成)より前の日時にならないようにする
const created = new Date(leadCreatedAtUtc);
if (t <= created) t = new Date(created.getTime() + 60 * 60 * 1000);
return toJstString(t);
}
// 2026-07-15T23:59:00+09:00 の形にする
function toJstString(date) {
const jst = new Date(date.getTime() + 9 * 60 * 60 * 1000);
const s = jst.toISOString().slice(0, 19); // "2026-07-15T23:59:00"
return `${s}+09:00`;
}
function toCsv(rows) {
const header = [
"Google Click ID",
"Conversion Time",
"Conversion Value",
"Conversion Currency",
"Order ID",
];
const lines = rows.map((r) =>
[r.gclid, r.time, r.value ?? "", r.value ? "JPY" : "", r.orderId]
.map((v) => `"${String(v).replace(/"/g, '""')}"`)
.join(",")
);
return [header.join(","), ...lines].join("\n");
}
async function build(stage, dateField, since) {
const records = await fetchByDate(dateField, since);
const rows = [];
let skipped = 0;
for (const rec of records) {
const gclid = rec.gclid.value;
if (!gclid) {
skipped++; // GCLIDがないものは送れないのでスキップ
continue;
}
rows.push({
gclid,
time: toConversionTime(rec[dateField].value, rec.lead_created_at.value),
value: stage === "won" ? rec.won_amount.value : null,
orderId: `${rec.$id.value}-${stage}`,
});
}
const file = `conversions_${stage}.csv`;
writeFileSync(file, toCsv(rows));
console.log(
`[${stage}] 対象 ${records.length}件 / 出力 ${rows.length}件 / GCLIDなしでスキップ ${skipped}件 → ${file}`
);
}
const since = process.argv[2] ?? "2026-07-01";
await build("opportunity", "opportunity_date", since);
await build("won", "won_date", since);
実行するとこうなります。
$ node exportCsv.js 2026-07-01
[opportunity] 対象 55件 / 出力 23件 / GCLIDなしでスキップ 32件 → conversions_opportunity.csv
[won] 対象 17件 / 出力 8件 / GCLIDなしでスキップ 9件 → conversions_won.csv
(デモデータでの結果です)
出力されたCSVをスプレッドシートで開くと、こうなっています。
スキップが多く見えますが、デモデータにはMeta広告や自然検索から来たリードも混ざっているので、GCLIDがないのはある意味当然です。Google広告経由のリードだけで見たときにGCLIDがどれくらい埋まっているかは、別途チェックしておくと安心です。
データマネージャーで取り込む
出力したCSVの中身を、それぞれのコンバージョンアクション用のスプレッドシートに貼り付けます。
あとは、先ほど作ったコンバージョンアクションのデータソース設定で、スプレッドシートの列とGoogle広告側のフィールド(Google クリック ID、コンバージョンの日時、値、通貨、オーダーID)を対応づければOKです。
日時はタイムゾーン付きのISO 8601形式(2026-07-15T23:59:00+09:00)にしているので、タイムゾーンで迷うことはないはずです。
取り込んでから管理画面に反映されるまでは時間がかかります。新しく作ったコンバージョンアクションの場合、作成から数時間おいてからアップロードするよう案内されているので、作ってすぐに数字が出なくても焦らなくて大丈夫です。
今回はデモ環境のため、データマネージャーは設定画面での確認までで、実際の取り込み結果までは試せていません。
ハマりポイント
日付フィールドには時刻がない
「商談化日」「受注日」は日付フィールドなので、時刻を自分で決める必要があります。
今回はその日の23:59(JST)にしましたが、
- クリックより前の日時になる(リード作成と同じ日に商談化した場合など)
- 未来の日時になる(今日の日付の場合)
とエラーや取りこぼしの原因になるので、上のコードのように補正しています。
運用でちゃんと時刻まで持ちたいなら、最初から日時フィールドにしておくほうが楽です。
GCLIDが入っていないレコードが意外と多い
デモデータでも一部わざと抜いていますが、実際のアプリでもフォームの設定次第でGCLIDが保存されていないケースはよくあります。
スクリプトでスキップ件数を出しているのはそのためで、ここが多いならまずフォーム側を直すのが先です。
同じレコードを何度送っても大丈夫?
Google広告側では、コンバージョンアクション・日時・識別子が同じものは二重にカウントされません。
念のため、今回は レコード番号-ステージ を Order ID として入れています。毎回「7/1以降」のように期間をかぶらせて出力しても、重複を気にしなくて済みます。
クリックから時間が経ちすぎたものは取り込めない
GCLIDを使ったオフラインコンバージョンには、クリックからコンバージョンまでの期間に上限があります(データソースや設定によって異なります)。商談化までに時間がかかる商材の場合は、コンバージョンアクションの設定とあわせて確認しておいてください。
おわりに
CSV経由でも、一度流れを作ってしまえば「商談化したリードをGoogle広告に返す」こと自体はそこまで難しくありません。
ただ、毎回スクリプトを回してスプレッドシートに貼って……というのは地味に面倒で、担当者が忙しくなると止まりがちです。
自分はこのあたりを自動化するkintoneプラグインを個人で作っています。
あわせて、kintoneのCSVを読み込むだけで「GCLIDがどれくらい保存できているか」「どのステージを返すべきか」が分かる無料の分析ツールも準備中です。
進捗や、無料の分析ツールの公開はXで先にお知らせするので、よかったらフォローしてください
「うちはこう運用している」「ここでハマった」などあれば、コメントで教えてもらえるとうれしいです!
以上です!
参考
- オフライン コンバージョンのインポートについて - Google 広告 ヘルプ
- Google クリック ID(GCLID)を使用してオフライン コンバージョンを設定する - Google 広告 ヘルプ
- オフライン コンバージョンを設定する - Google 広告 ヘルプ
- インポートするデータを準備する - データマネージャー ヘルプ
- Google スプレッドシート - データマネージャー ヘルプ
- サポートされているデータソース - データマネージャー ヘルプ
- Changes to Offline Click Conversion Import Support in the Google Ads API - Google Ads Developer Blog
- Field mappings - Data Manager API
- レコードを一括取得する - cybozu developer network
- フィールド形式 - cybozu developer network
- kintone REST API Client(record)- GitHub


