KintoneとZoom PhoneやZoom Revenue Acceleratorとの連携 ~通話後の活動履歴登録編~

# はじめに
CybozuさんのKintoneでZoom Phoneをもっと便利に使うことができたら、と考えている方がいらっしゃればと思い、Kintoneの開発者向けテスト環境とZoom Phoneを連携させてみました。この記事はKintoneとZoom Phoneの連携についての3回に分けたシリーズの最終回となります。

:::note info
今回は、おそらくきっと多分めちゃ**長い**記事になるので、途中休憩とか挟みつつ行きます。
:::

- **Click to Call** : 顧客情報にある電話番号をもとに、Zoom Phoneで電話をかける方法を説明します。いくつか注意点もありますが、十分実現できる内容です。
- **着信時の顧客情報ポップアップ**：Zoom Phoneに着信があった際、自動的にその番号をKintoneで検索し、該当する顧客がいればその情報をウェブ画面上で提示する方法について説明します。
- ***今回→通話後の活動履歴登録***：通話終了後に顧客情報ページなどから活動履歴を手動で登録する代わりに、終了後に自動的に活動履歴を登録する方法を説明します。これは少し高度な技術と知識が必要となるかもしれません。

この記事は、基本的にZoom Phoneをインストールしているデスクトップ環境を前提に説明しています。モバイル環境では動作しない可能性があるため、あらかじめご理解ください。

# Kintone のアプリについて
前回に引き続き、Kintoneアプリストアにある「営業支援パック」を前提にお話しします。このアプリパックには、顧客管理アプリと活動履歴アプリが含まれています。

# はじめに
今回の、通話後の活動履歴登録では、前提としてウェブサーバが必要になります。ウェブサーバでは大枠として、以下の処理を行います。
1. Zoomからの通話完了に関する情報を受け取り
1. それを元にKintoneの顧客管理アプリで顧客名を検索し
1. さらにその情報をもとに、今度は活動履歴アプリに顧客名と活動内容を登録します

![1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/442226/0584c3ea-b3fe-4d0d-bbeb-73819f1ba580.png)

この図の`Processing Server`に相当するHTTPSを受けられるウェブサーバをご自身で用意し、ご自身で管理し、以下のようなコードを実行できるアプリケーションを立ち上げておく必要があるので、あらかじめご理解いただきつつ先へ進めればと思います。

# Webhookの通知受信

まずはZoom PhoneのWebhook受信です。今回は、以下の`phone.callee_call_log_completed`をトリガとして利用したいと思います。

https://developers.zoom.us/docs/api/phone/events/#tag/phone/POSTphone.callee_call_log_completed

サンプルレスポンスとして以下のようなJSONが提示されています。

```JSON
{
  "event": "phone.callee_call_log_completed",
  "payload": {
    "account_id": "TQTvjT52Tmi_wrhASNNOEw",
    "object": {
      "call_logs": [
        {
          "id": "4a4ed8ec-6be4-4e42-96e6-352a4396204d",
          "call_type": "voip",
          "caller_number": "1045",
        ....(中略)....
        }
      ],
      "user_id": "IqoQmDRqS-aIoXqV_FZ88w"
    }
  },
  "event_ts": 1623218132807
}
```
今回はこの中の`caller_number`を使ってみようと思います。E164形式で提示されるため、Kintone顧客管理アプリ側のTELレコードとの整合性に注意してください。

## Webhookアプリの作成
Zoom App Marketplaceにて、まずWebhook受信のためのWebhookアプリの作成を行います。

![Screenshot 2025-03-27 at 11.32.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/442226/3ef4da19-22b3-491e-bac7-0460f8c51b36.png)

`Build App`からアプリを作成します。どのアプリを選択しても良いですが、今回は簡単に`Webhook Only App`で試してみましょう。その他のアプリを選択した場合の手順についても気になる方がいらっしゃいましたらコメントでご質問いただくか、私にご連絡いただければ解説します。

Informationタブで必要情報を入力したら、Featureタブで以下のようにEvent Subscriptionをオンにして、`Add Event Subscription`をクリックして通知を追加します。

![Screenshot 2025-03-27 at 11.36.47.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/442226/f55a863c-579c-4b11-b4b0-98ffd840530c.png)

ここで、`Event notification endpoint URL` にご自身のHTTPSサーバのURLを入力します。`Validate`をクリックするとValidationが走ります。（後述）

![Screenshot 2025-03-27 at 11.41.05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/442226/0bca67ab-e236-41d8-9682-8b4a2ef4d3b4.png)

Add Eventsの部分では、Zoom Phoneタブの`Callee call log is completed` を追加してください。

## WebhookのValidation処理

ここで、先ほどのValidationについてですが、

https://developers.zoom.us/docs/api/webhooks/#validate-your-webhook-endpoint

このサポートページにある通り、"endpoint.url_validation"イベントがPOSTされた際に、ご自身のサーバで`plainToken`の値を、WebhookのSecret Tokenの値をSaltとしてハッシュ化し、`encryptedToken`として3秒以内にレスポンスを返す必要があります。

こちらにNodejsのサンプルを貼り付けておきます。

```javascript
const crypto = require('crypto')

// Webhook request event type is a challenge-response check
if(request.body.event === 'endpoint.url_validation') {
  const hashForValidate = crypto.createHmac('sha256', ZOOM_WEBHOOK_SECRET_TOKEN).update(request.body.payload.plainToken).digest('hex')

  response.status(200)
  response.json({
    "plainToken": request.body.payload.plainToken,
    "encryptedToken": hashForValidate
  })
}
```
これにより、Zoom側からのValidateが成功します。

合わせて、正しくZoomから届いたWebhook通知かどうかについては以下の部分を参考に、`x-zm-signature`の値を元に検証することができます。

https://developers.zoom.us/docs/api/webhooks/#verify-webhook-events

## 受信とパース
あとはWebhookとして受け取ったPOST通知を元に、switch/caseで処理すればOKです。以下は、nodejsで、expressを使って処理している例です。

```javascript
const express = require('express');
const bodyParser = require('body-parser');
const crypto = require('crypto');
const app = express();
const port = 4000;

const webhookToken = 'YOUR_WEBHOOK_TOKEN';

app.use(bodyParser.urlencoded({
    extended: true
    }));
app.use(bodyParser.json());


app.post('/', (req, res) => {
  const message = `v0:${req.headers['x-zm-request-timestamp']}:${JSON.stringify(req.body)}`;
  const hashForVerify = crypto.createHmac('sha256',webhookToken).update(message).digest('hex');
  const signature = `v0=${hashForVerify}`;

    var response;
    if (req.headers['x-zm-signature']=== signature ) {
        switch (req.body.event) {
          case 'endpoint.url_validation':
            console.log('endpoint.url_validation');
            const hashForValidate = crypto.createHmac('sha256', webhookToken).update(req.body.payload.plainToken).digest('hex');
            response = {
              message: {
                "plainToken": req.body.payload.plainToken,
                "encryptedToken": hashForValidate
              },
              status: 200
            };
            console.log(response.message);
            res.status(response.status);
            res.json(response.message);
            break;
            case 'phone.callee_call_log_completed':
              console.log('>>>>>>>Webhook Notification: ' + req.body.event);
              response = { message: 'OK', status: 200 };
              res.status(response.status);
              res.json(response);
              console.log(`Caller Number: ${req.body.payload.object.call_logs[0].caller_number}`);
              break;
          default:
            response = { message: 'OK', status: 200 };
            res.status(response.status);
            res.json(response);
            break;
          }
        }else{
          response = { message: 'OK', status: 200 };
          res.status(response.status);
          res.json(response);
        }
    
  });
  
  app.listen(port, () => console.log(`Example app listening on port ${port}!`));
```

これで試していただき、想定通り発信者番号がログに表示されればOKかと思います。

```
>>>>>>>Webhook Notification: phone.callee_call_log_completed
Caller Number: +8150XXXXXXXX
```

# 顧客管理アプリで顧客情報検索
ここからはそれほど複雑ではありません。NPMに [KintoneRestAPIClient](https://www.npmjs.com/package/@kintone/rest-api-client) が公開されているので、それを使っても良いですし、そのまま axios なり request なりでリクエストを投げても良いかと思います。
ここでは、上述のWebhookで取得した発信者番号をキーとして、顧客情報を取得する例を取り上げます。すでにたくさんのリファレンスがあるので、Qiitaや他で見ていただければと思います。ここでは簡単に提示します。

```javascript
const client = new KintoneRestAPIClient({
  baseUrl: 'https://YOURDOMAIN.cybozu.com',
  auth: {
    apiToken:'YOUR_API_TOKEN'
  },
});

// TELで検索して、該当レコードの「顧客名」を取得
const searchCustomer = async (caller_number) => {
  try {
    const query = `TEL = "${caller_number}"`;
    const params = {
      app: 3,//appIdは事前に確認
      query: query,
    };
    const res = await client.record.getRecords(params);
    if (res.records && res.records.length > 0) {
      const customerName = res.records[0]['顧客名'].value;
      console.log('顧客名:', customerName);
      return customerName;
    } else {
      console.log('該当するレコードが見つかりませんでした');
      return null;
    }
  } catch (err) {
    console.error(err);
  }
};
```
# 活動履歴アプリで履歴追加
最後に、先ほどの顧客名をもとに、活動履歴を追加していきます。こちらはどのように自動化したいかによって内容を変えていく必要があると思います。いかに人の手を介在させずに必要な情報を自動的に同期していくか、がこの実装の一番重要なポイントになってくると思います。
最もシンプルな例として、以下参考です。
```javascript
const postRecord = async (caller_number) => {
  const customerName = await searchCustomer(caller_number);
  try {
    const postData = {
      app: 4,//appIdは事前に確認
      record: {
        作成日時: { value: '2025-03-14T10:11:00Z' },
        顧客名: {value: customerName},
        商談メモ: { value: 'Zoom Phone 商談履歴アップデート' },
        対応内容: { value: '商談' },
      },
    };
    const resp = await client.record.addRecord(postData);
    console.log(resp);
  } catch (err) {
    console.error(err);
  }
};
```

ここではrecordに入れる内容を決めうちにしてしまっていますが、この部分を改めてZoomのAPIを使って追加情報を取得し、自動的に必要な情報を埋めるようにすることもできると思います。

例えば、Webhook通知の`revenue_accelerator.conversation_analysis_completed` 通知をトリガーとして、**GET** `/zra/conversations/{conversationId}/content_analysis` で**サマリーや次のステップを取得＆加工して Zoom Revenue Accelerator（ZRA）のデータをKintone側に自動でプッシュ**することも可能です。

![Screenshot 2025-05-14 at 16.45.34.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/442226/17e4f01a-03c4-4d7c-bc33-6f082014f87e.png)

Kintoneのフォームは自由度が高いので、こんな感じでフォームにID付きで専用の入力ボックスを用意しておけば、反映が簡単になるかと思います。

## 参考：複数のアプリにまたぐAPIキーの設定

ちなみに、今回複数のアプリに跨いで連携をしましたが、この場合Kintoneのアプリごとに発行されるAPIキーを複数設定する必要があります。これについては、こちらの記事を参考にしました。

https://cybozu.dev/ja/kintone/tips/development/customize/development-know-how/multiple-api-token-capabilities/

こんな感じで配列で処理すればいいみたいです。

```javascript
// 複数トークンの場合は配列として指定
const client = new KintoneRestAPIClient({
  baseUrl: 'https://YOURDOMAIN.cybozu.com',
  auth: {
    apiToken: [
      'API_KEY_FOR_APP_A',
      'API_KEY_FOR_APP_B'
    ]
  },
});
```

# まとめ
今回は3回にわたってKintoneとZoom Phoneの連携について説明してきました。もし反響があれば、他のCRMサービスとの連携についても試してみたいと思います。

もしこんな記事を書いてほしい、という記事があればご応募お寄せください。よろしくお願いs