1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

kintoneの商談化レコードを、Google広告のオフラインコンバージョンとして送るまで(データマネージャー対応)

1
Posted at

はじめに

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-client
  • dotenv
  • Google広告アカウントの管理者権限
  • Google広告の自動タグ設定が有効になっていること
    自動タグ設定はGCLIDを使ったオフラインコンバージョンの前提条件なので、まだの場合は先に有効にしておきます。

kintone側は、こんな感じのリード管理アプリを想定しています(検証用に作ったデモアプリで、ダミーデータを500件ほど入れています)。

フィールド名 フィールドコード タイプ
会社名 company_name 文字列(1行)
メール email リンク(メールアドレス)
電話 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広告側の準備

まず、取り込み先のコンバージョンアクションを作ります。

  1. Google広告の管理画面で「目標」→「コンバージョン」→「概要」
  2. 「+ 新しいコンバージョン アクション」
  3. 「オフライン ソースからのコンバージョン データ」を選ぶ
  4. 接続方法は「直接接続」を選び、データソースとしてGoogleスプレッドシートを指定
    今回は「商談化」と「受注」でコンバージョンアクションを分けました。
    データマネージャーはスプレッドシートの最初のシートしか読まないので、アクションごとにスプレッドシート(またはファイル)を分けておくと後で混乱しません。

スプレッドシートには編集者権限、Google広告アカウントには管理者権限が必要です。

kintoneから商談化・受注のレコードを取得する

今回使うのは、こんな感じのリード管理アプリです。

asdasdaswe.PNG

デモ用のリード管理アプリ(データはすべてダミーです)

kintoneのAPIトークンは、アプリの設定 →「APIトークン」から発行します。今回は読み取りだけなので「レコード閲覧」だけにチェックを入れておけば十分です。
トークンを発行・変更したあとは「アプリを更新」を押さないと反映されないので注意してください(これで一回ハマりました)。

.env はこんな感じ。

.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" を追加しておきます。

取得部分のコードです。

fetchLeads.js
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行目は見出しにしておく必要があります。

コードはこんな感じです。

exportCsv.js
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

(デモデータでの結果です)

222231.PNG

出力されたCSVをスプレッドシートで開くと、こうなっています。

asdasdasdasda.PNG

スキップが多く見えますが、デモデータには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で先にお知らせするので、よかったらフォローしてください

X:https://x.com/Shoma116167

「うちはこう運用している」「ここでハマった」などあれば、コメントで教えてもらえるとうれしいです!

以上です!

参考

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?