【GAS】HubSpot API を使って、コンタクト担当者が未割り当てのコンタクト一覧を、チャットに投稿する方法


熱中症のリスクが高まってくる季節、皆様対策はできていますでしょうか。
野球場でのビールは醍醐味の一つですが、アルコール飲料は水分補給にはなりませんのでお気を付けください!


ドーモ。イノベーションLAB 改め、テクノロジー・コンサルティング課のハヤシです。
今年度から、主に提案や技術検証、時々社内の技術教育を行う人になりました。
社内では「なんかいつも野球場かライヴハウスに行っている人」と思わせておいて、実は技術も大好きです。
こちらでは、私が検証した技術や社内で共有した技術情報などをご紹介していきます。




未処理タスク、忘れがちですよね。チャットを使ってリマインドできたら最高ですよね。
今回は、以下の内容を GAS(Google Apps Script)でやってみようと思います。

  • HubSpot API を使って未割り当てのコンタクト一覧を取得
  • Webhook を使ってSlackに投稿する


※投稿処理に使用するのは Webhook なので、対象の Webhook URL がわかれば Google Chat などへの投稿にも変更できます。
参考)
(Google Chat のスレッドの概念が当時からかわりましたが、基本的にこのまま応用できます。)


GAS についてのファーストステップはこちらをご覧ください。

Webhook とはなんぞや、という方はこちらのブログで入門してみましょう!

1. 前提条件

1-1. 必要なもの

  • Google アカウント
    • 無料版で OK
  • HubSpot アカウント
    • 無料版で OK
  • ブラウザ
  • Slack アカウント
    • 無料版で OK
    • 投稿先に使うのみなので、必須ではないです。

2. GAS のスクリプトを作成

2-1. 最初のスクリプト作成

まずは単純なスクリプトを作成します。


Google Driveで、プロジェクトファイルを保管したいフォルダを開いた状態で、 新規 -> その他 -> Google Apps Script を選択します

※Google Apps Script がない場合は「アプリを追加」で追加しておきましょう


警告が表示されたら、内容を確認して、「スクリプトを作成」をクリックします。
警告にあるように、対象のドライブを別ユーザと共有していた場合、そのユーザも編集・実行できてしまいます。
必要な場合以外は共有ドライブでスクリプトを作成しないよう気を付けてください。

ファイルが開いたら、プロジェクト名をクリックして変更しておきます。


また、デフォルトで表示されているコードを、以下のようにしてみましょう

function myFunction() {
  console.log('Hello World!');
}

これは、「Hello World!」という文字をログに出力するだけのプログラムです。

保存マークをクリックして保存し、実行マークをクリックで実行します。
ログが出力されました。

3. HubSpot のデータを取得

3-1. 対象のデータを確認

これから取得するデータを確認しておきます。
HubSpot のコンタクトのうち、担当者が設定されていないものを取得したいと思います。

サンプルデータとしてコンタクトを登録し、一部に担当者を設定しました。

これらのデータを取得するための情報を考えます。
通知するのは、担当者が設定されていないコンタクトの

  • コンタクトID(キー情報)
  • 氏名(姓 + 名)

のみとします。

3-2. Contact API でデータを取得

3-2-1. 非公開アプリ作成

HubSpot のデータを GAS から取得するために、 Contact API を使用します。
そのためには、 API にアクセスするための認証情報が必要です。
ここでは、対象の HubSpot アカウントに専用の非公開アプリを作成してアクセストークンを取得する方法で実施します。

まずは、コンタクトを取得したい HubSpot アカウントで以下を開きます。

設定 -> 連携 -> 非公開アプリ -> 「非公開アプリを作成」をクリック

基本情報タブで、アプリの名前を設定します。わかりやすいものにしましょう。
ロゴや説明は、任意で変更してください。

「スコープ」タブで権限を設定します。
「contact」で検索し、「crm.objects.contacts.read」の読み取りをチェックします。

設定ができたら、右上の「アプリを作成」をクリックします。

確認ダイアログが表示されたら、「作成を続行」をクリックします。

トークンが表示されるので、「トークンを表示」と「コピー」をクリックしてコピーしておきます。
「閉じる」でダイアログは閉じておいてください。

※このトークンが漏れるとコンタクトデータを取得されてしまいますので、厳重に管理して下さい。

3-2-2. アクセストークンを設定

GAS に戻り、さきほどコピーしたトークンをスクリプトプロパティに設定します。
セキュリティ情報はソースコードではなく、スクリプトプロパティなどに設定して外部から取得するようにしましょう。

歯車の設定マークをクリックし、最下部の「スクリプトプロパティを追加」をクリックすると入力欄が現れます。

入力欄に、以下を入力します

  • プロパティ: ACCESS_TOKEN
  • 値: 先ほどコピーしたトークン

入力したら、「スクリプトプロパティを保存」で保存しておきましょう。

※トークンをコピーし損ねてしまった方は、「非公開アプリ」から対象アプリの「アクセストークンを表示」で表示できます。

エディタに戻って、以下の内容に書き換えましょう。

// スクリプトプロパティを取得
const props = PropertiesService.getScriptProperties().getProperties();

function myFunction() {
  console.log('Hello World!');
  console.log(props.ACCESS_TOKEN);
}

保存して実行すると、ログにアクセストークンが出力されます。

3-2-3. コンタクト一覧を取得

いよいよ API でデータを取得していきます。
ドキュメントはこちらを参照します。
CRM API|コンタクト|HubSpot(ハブスポット)

こちらを見ると、単純なコンタクトデータの取得は GET /crm/v3/objects/contacts でよさそうです。ベースの URL は https://api.hubapi.com なので、 https://api.hubapi.com/crm/v3/objects/contacts に GET しましょう。

また、アクセスするのには先ほど取得したアクセストークンも必要です。

GAS では UrlFetchApp を使うので上記のドキュメントどおりにはなりませんが、コンタクトデータのうち id と firstname, lastname を取得するスクリプトは以下のようになります。

const BASE_URL = 'https://api.hubapi.com';
// スクリプトプロパティを取得
const props = PropertiesService.getScriptProperties().getProperties();

function myFunction() {
  const endpoint = `${BASE_URL}/crm/v3/objects/contacts`;
  // トークンを指定して GET する
  const options = {
    'method': 'get',
    'headers' : {
      'Content-Type' : 'application/json',
      'Authorization': 'Bearer '+ props.ACCESS_TOKEN
    },
  };
  // リクエスト
  const response = UrlFetchApp.fetch(endpoint, options);
  // レスポンスを取得
  const responseContent = JSON.parse(response.getContentText());

  console.log(responseContent['results'].map(v => {
    return `${v.id}: ${v.properties.firstname} ${v.properties.lastname}`
  }));
}

保存して実行すると、承認を促すダイアログが出ますので、「権限を確認」をクリックします。

実行するアカウントを選択します

内容を確認の上、「許可」をクリックしてください。

※UrlFetchApp で外部サービス(今回は HubSpot)へアクセスすることになるため、許可を求められています。

実行すると、以下の結果がログに出力されました。

一見するとよさそうですが、実際には 22 件あるコンタクトのうち、 10 件しか取得されていません。これについて対処していきます。

3-2-4. 大量データを全件取得

HubSpot のデータ取得 API は、デフォルトでは全件のデータを取得できるとは限りません。
全件取得するには... AWS の API などで慣れている方はピンとくると思いますが、ページングが必要です。
API 実行時にページング指定をして、取得範囲を指定することで全件取得していきます。


さきほどの API のリファレンス中、 Response -> Example に、 paging に関するレスポンスがあります。
この「after」が、次ページの情報を取得するためのキーになります。

これを使ったソースコードはこちら。

const BASE_URL = 'https://api.hubapi.com';
// スクリプトプロパティを取得
const props = PropertiesService.getScriptProperties().getProperties();

function myFunction() {
  const endpoint = `${BASE_URL}/crm/v3/objects/contacts`;
  // トークンを指定して GET する
  const options = {
    'method': 'get',
    'headers' : {
      'Content-Type' : 'application/json',
      'Authorization': 'Bearer '+ props.ACCESS_TOKEN
    },
  };

  // ---------- ↓追加↓ ----------
  let contacts = [];
  let after = undefined;

  do {
    let url = endpoint;
    if (after) {
      url += `?after=${after}`;
    }

    // リクエスト
    const response = UrlFetchApp.fetch(url, options);
    // レスポンスを取得
    const responseContent = JSON.parse(response.getContentText());

    // スプレッド構文を使ってコンタクトを追加
    contacts = [...contacts, ...responseContent['results']];

    // 次のページの情報を取得(responseContent['paging']['next']['after'] が存在しない = 最終ページ)
    after = responseContent.paging?.next?.after || null;
  } while (after);
  // ---------- ↑追加↑ ----------

  console.log(contacts.map(v => {
    return `${v.id}: ${v.properties.firstname} ${v.properties.lastname}`
  }));
}

実行すると、こうなります。

順不同ですが、 22 件、取得できていますね。

3-3. 担当者が設定されていないコンタクトに絞り込み

現在はコンタクトが全て取得されているので、これを 担当者が設定されていないもの に絞り込んでみましょう。

こちらは少し躓きやすいので、順を追って説明していきます。

3-3-1. 担当者のプロパティーキーを探す

まずは、絞り込みを行うために「担当者」がどういったプロパティー(※)なのかを調べる必要があります。
※「プロパティー」とは、 HubSpot で情報を保存するフィールドのこと(「ID」や「Eメール」「初回取引作成日」など)です。
 GAS の「スクリプトプロパティ」とまぎらわしいですが、こちらは公式にならって「ー」有りで表記しています。
 参考: プロパティーの作成と編集


取得されたコンタクトのキー一覧からそれっぽいものを探してみましょう。

さきほどのコードを以下のように変更します。

  • myFunction 関数を main 関数に変更
  • myFunction 関数を作成して、 main 関数を呼び出すように
  • main 関数の呼び出しをコメント化
  • contacts で取得した 1 件目の properties を表示する getContactProperties 関数を作成
  • getContactProperties 関数を main 関数から呼び出し

このようなコードになりました。(一部略)

const BASE_URL = 'https://api.hubapi.com';
// スクリプトプロパティを取得
const props = PropertiesService.getScriptProperties().getProperties();

function myFunction() {
  getCntactProperties();
  // main();
}

function getCntactProperties() {
  const endpoint = `${BASE_URL}/crm/v3/objects/contacts`;
  // トークンを指定して GET する
  const options = {
    'method': 'get',
    'headers' : {
      'Content-Type' : 'application/json',
      'Authorization': 'Bearer '+ props.ACCESS_TOKEN
    },
  };

  const url = endpoint;
  // リクエスト
  const response = UrlFetchApp.fetch(url, options);
  // レスポンスを取得
  const responseContent = JSON.parse(response.getContentText());

  // プロパティー一覧を取得
  console.log(responseContent['results'][0].properties);
}

function main() {
  const endpoint = `${BASE_URL}/crm/v3/objects/contacts`;
  // トークンを指定して GET する
  const options = {
    'method': 'get',
    'headers' : {
      'Content-Type' : 'application/json',
      'Authorization': 'Bearer '+ props.ACCESS_TOKEN
    },
  };

  // ~~~~~ 略 ~~~~~
}

結果はこうなりました。

createdate ~ lastname の、 6 つのプロパティーが表示されています。デフォルトだと、これらのプロパティーしか取得対象になっていないようです。担当者 ID は含まれてなさそうに見えますね。

それでは他のプロパティーはどうすればよいかと探してみたところ...
こちらのドキュメントのなかほどに、オブジェクトのプロパティー一覧を探す方法があります。
CRM API|コンタクト|HubSpot(ハブスポット)

利用可能な全てのプロパティーを確認するには、GETリクエストを/crm/v3/properties/contactsに送信して、アカウントのコンタクトプロパティーのリストを取得します。詳しくはプロパティーAPIをご参照ください。

この API を使ってコンタクトプロパティーのリストを取得し、「hubspot_owner_id」が担当者 ID であることを突き止めました。
※見つける過程は省略しますが、興味のある方は試行錯誤してみてください

3-3-2. 担当者IDを取得対象に含める

それでは、「hubspot_owner_id」を表示してみましょう。
コードを以下のように変更します

const BASE_URL = 'https://api.hubapi.com';
// スクリプトプロパティを取得
const props = PropertiesService.getScriptProperties().getProperties();

function myFunction() {
  main();
}

function main() {
  const endpoint = `${BASE_URL}/crm/v3/objects/contacts`;
  // トークンを指定して GET する
  const options = {
    'method': 'get',
    'headers' : {
      'Content-Type' : 'application/json',
      'Authorization': 'Bearer '+ props.ACCESS_TOKEN
    },
  };

  let contacts = [];
  let after = undefined;

  // プロパティーリストを作成
  const properties = ['firstname', 'lastname', 'hubspot_owner_id'];
  const baseQueryParams = properties.map(prop => `properties=${prop}`);

  do {
    let url = endpoint;
    const queryParams = [...baseQueryParams];

    if (after) {
      queryParams.push(`after=${after}`);
    }

    // クエリパラメータを結合
    url += `?${queryParams.join('&')}`;

    // リクエスト
    const response = UrlFetchApp.fetch(url, options);
    // レスポンスを取得
    const responseContent = JSON.parse(response.getContentText());

    // スプレッド構文を使ってコンタクトを追加
    contacts = [
      ...contacts,
      ...responseContent['results']
    ];

    // 次のページの情報を取得(responseContent['paging']['next']['after'] が存在しない = 最終ページ)
    after = responseContent.paging?.next?.after || null;
  } while (after);

  // ---------- 変更:responseContent['results'] -> contacts
  // ---------- 変更:v.properties.hubspot_owner_id を追加
  console.log(contacts.map(v => {
    return `${v.id}: ${v.properties.firstname} ${v.properties.lastname} ${v.properties.hubspot_owner_id}}`
  }));
}

実行してみると、結果はこうなります。
最後に表示されているのが、担当者の ID です。設定されていない場合は、 null になっているようですね。

3-3-2. 担当者IDが設定されていないコンタクトに絞り込む

前回までで、「User16」~「User20」のユーザにのみ担当者が設定されていることがわかりました。
それでは、フィルタをかけてそれ以外を取得してみようと思います。


コンタクトを取得している部分に、フィルタを追加しました。

    // スプレッド構文を使ってコンタクトを追加
    contacts = [
      ...contacts,
      // 追加: hubspot_owner_id に値があるものを除外
      ...responseContent['results'].filter(contact => !contact.properties.hubspot_owner_id)
    ];

実行してみると、さきほど確認した 5 件は対象からはずれていました。これで取得の絞り込みはできました。

※ただし、今回は取得したデータを加工する形でのフィルタです。
 参考までに、 HubSpot API での検索は POST /crm/v3/objects/contacts/search を使って以下のように書きます。

const BASE_URL = 'https://api.hubapi.com';
// スクリプトプロパティを取得
const props = PropertiesService.getScriptProperties().getProperties();

function myFunction() {
  main();
}

function main() {
  const endpoint = `${BASE_URL}/crm/v3/objects/contacts/search`;
  const payload = {
    'after': null,
    // 取得するプロパティーリスト
    'properties': [
      'firstname', 'lastname', 'hubspot_owner_id',
    ],
    // 検索条件
    'filterGroups': [
      {
        "filters": [
          {
            "propertyName": "hubspot_owner_id",
            "operator": "NOT_HAS_PROPERTY"
          }
        ]
      }
    ],
  }
  // トークンと条件を指定して GET する
  const baseOptions = {
    'method': 'post',
    'headers' : {
      'Content-Type' : 'application/json',
      'Authorization': 'Bearer '+ props.ACCESS_TOKEN
    },
    'payload': JSON.stringify(payload)
  };

  let contacts = [];
  let after = undefined;

  do {
    let url = endpoint;
    const options = { ...baseOptions };

    if (after) {
      // ページングが必要な場合はセット
      const newPayload = JSON.parse(options.payload);
      newPayload.after = after;
      options.payload = JSON.stringify(newPayload);
    }

    // リクエスト
    const response = UrlFetchApp.fetch(url, options);
    // レスポンスを取得
    const responseContent = JSON.parse(response.getContentText());

    // スプレッド構文を使ってコンタクトを追加
    contacts = [
      ...contacts,
      ...responseContent['results']
    ];

    // 次のページの情報を取得(responseContent['paging']['next']['after'] が存在しない = 最終ページ)
    after = responseContent.paging?.next?.after || null;
  } while (after);

  console.log(contacts.map(v => {
    return `${v.id}: ${v.properties.firstname} ${v.properties.lastname} ${v.properties.hubspot_owner_id}}`
  }));
}

4. チャットに投稿する

4-1. 投稿するための Webhook の準備(Slack 編)

ここからは、投稿先の設定をしていきます。今回は Slack に Webhook で投稿しようと思います。
こちらの手順を参考に、簡略化してお伝えします。
Sending messages using incoming webhooks | Slack
※Google Workspace のチャットに投稿したい方は、こちらを参考になさってください。
【GAS】Google Apps Script で Webhook を使って Google Chat に投稿する(スレッド指定方法も) - シー・エス・エス イノベーションラボ(ブログ)


ページ内の Create your Slack app をクリックします。

Create an App をクリックします。

From scratch をクリックします。

以下を設定し、 Create App をクリックします。

  • App Name:わかりやすい名前(今回は HubSpotSample01 としました)
  • Pick a workspace to develop your app in:投稿する先のワークスペースを選択

左上の表示が今作成したアプリ名であることを確認し、 Incoming Webhooks をクリックします。

Activate Incoming Webhooks を On にして、有効化します。

Add New Webhook to Workspace をクリックします。

さきほど選択した ワークスペース名への許可を求められるので、投稿したいチャンネルを選択して Allow をクリックします。

Webhook URL が発行されます。 Copy でコピーすることができます。この後使いますので、控えておきましょう。

4-2. チャットに投稿する処理の追加

作成した GAS から WebHook を実行する処理を関数化して追記しておきます。

※「<Webhook URL>」の部分は先ほどコピーした URL に書き換えてください(スクリプトプロパティにするのが理想ですが、割愛します)。

// ~~~~~前略~~~~~

function sendMessage(message) {
  const webhookurl = '<Webhook URL>';

  const data = {
    'text': message
  };
  const options = {
    'method': 'post',
    'headers' : {
      'Content-Type': 'application/json; charset=UTF-8'
    },
    'payload': JSON.stringify(data)
  };
  // Webhook URL に POST
  var response = UrlFetchApp.fetch(webhookurl, options);
  // レスポンスをログ出力
  Logger.log(response.getContentText());
}


4-3. チャットに投稿する内容を整形

現在 console.log で出力している内容をチャットに投稿できるようにしてみましょう。

const BASE_URL = 'https://api.hubapi.com';
// スクリプトプロパティを取得
const props = PropertiesService.getScriptProperties().getProperties();

function myFunction() {
  const message = main();
  sendMessage(message);
}

function main() {
  // ~~~~~略~~~~~

  const contactList = contacts.map(v => {
    return `${v.id}: ${v.properties.firstname} ${v.properties.lastname}`
  });

  const messageList = [
    '担当者が設定されていないコンタクト一覧',
    '',
    'ID: 名 姓',
    ...contactList
  ]

  return messageList.join('\r\n');
}

function sendMessage(message) {
  // ~~~~~略~~~~~
}

4-4. チャットに投稿

最終的にはこうなりました。
※<Webhook URL> は適切な値に置き換えてください

const BASE_URL = 'https://api.hubapi.com';
// スクリプトプロパティを取得
const props = PropertiesService.getScriptProperties().getProperties();

function myFunction() {
  const message = main();
  sendMessage(message);
}

function main() {
  const endpoint = `${BASE_URL}/crm/v3/objects/contacts/search`;
  const payload = {
    'after': null,
    // 取得するプロパティーリスト
    'properties': [
      'firstname', 'lastname', 'hubspot_owner_id',
    ],
    // 検索条件
    'filterGroups': [
      {
        "filters": [
          {
            "propertyName": "hubspot_owner_id",
            "operator": "NOT_HAS_PROPERTY"
          }
        ]
      }
    ],
  }
  // トークンと条件を指定して GET する
  const baseOptions = {
    'method': 'post',
    'headers' : {
      'Content-Type' : 'application/json',
      'Authorization': 'Bearer '+ props.ACCESS_TOKEN
    },
    'payload': JSON.stringify(payload)
  };

  let contacts = [];
  let after = undefined;

  do {
    let url = endpoint;
    const options = { ...baseOptions };

    if (after) {
      // ページングが必要な場合はセット
      const newPayload = JSON.parse(options.payload);
      newPayload.after = after;
      options.payload = JSON.stringify(newPayload);
    }

    // リクエスト
    const response = UrlFetchApp.fetch(url, options);
    // レスポンスを取得
    const responseContent = JSON.parse(response.getContentText());

    // スプレッド構文を使ってコンタクトを追加
    contacts = [
      ...contacts,
      ...responseContent['results']
    ];

    // 次のページの情報を取得(responseContent['paging']['next']['after'] が存在しない = 最終ページ)
    after = responseContent.paging?.next?.after || null;
  } while (after);

  const contactList = contacts.map(v => {
    return `${v.id}: ${v.properties.firstname} ${v.properties.lastname}`
  });

  const messageList = [
    '担当者が設定されていないコンタクト一覧',
    '',
    'ID: 名 姓',
    ...contactList
  ]

  return messageList.join('\r\n');
}

function sendMessage(message) {
  const webhookurl = '<Webhook URL>';

  const data = {
    'text': message
  };
  const options = {
    'method': 'post',
    'headers' : {
      'Content-Type': 'application/json; charset=UTF-8'
    },
    'payload': JSON.stringify(data)
  };
  // Webhook URL に POST
  var response = UrlFetchApp.fetch(webhookurl, options);
  // レスポンスをログ出力
  Logger.log(response.getContentText());
}

実行すると、Slack にはこのように投稿されました。

あとはお好きに、見やすく整形すれば OK です!

5. 最後に

GAS から外部 API を呼ぶ処理について解説してみました。
いつまでも担当者が設定されないコンタクトがないように、 GAS の「トリガー」などを使って定期実行してもよさそうです。


今回は HubSpot でしたが、 Google や Salesforce など様々なサービスの API でもぜひ試してみてください。

それでは、よい自動化ライフを。

6. 参考

HubSpot API | はじめに
Apps Script  |  Google for Developers

この記事を書いた人

2024年交流戦 DeNA - 楽天@ハマスタ
ニックネーム:ハヤシ
最近行った野球場:ロッテ浦和球場、県営大宮公園野球場
一言:ニンテンドーダイレクトで NRS を発症しかけました