他システムのデータ更新をSalesforceにリアルタイムに反映する方法：Webhook通知とShopify連携の実例

今回は、他システムのデータ更新をSalesforceにリアルタイムに反映する方法について、概要をご紹介いたします。

まず、Webhook通知をSalesforceで受け取り、データを更新する方法の概要を説明し、以降の章で詳細を解説していきます。具体的には、以下の構成で連載を進めていく予定です：

1. Webhookとは？APIとの違いと実際のビジネス現場での使用例
2. Webhook通知を処理するApex RESTのプログラム実例：Shopifyの注文結果をSalesforceに反映させる方法

それでは、本題に入りましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41156/ec5b2dae-8822-278d-8402-998b3b6ac9e6.png)

# Webhookの必要性：なぜAPIだけでは不十分なのか？
外部システムのデータ更新をリアルタイムで反映するには、APIだけでは不十分な場合があります。例えば、以下のようなシステムパターンやビジネスユースケースが考えられます：

- **リアルタイム性が求められるケース**
    - 例：Shopify（ECプラットフォーム）での注文情報をSalesforceに即時反映させたい場合、APIを定期的に叩いて情報を取得するよりも、注文が発生した瞬間にWebhookで通知を受け取る方が効率的です。
    - ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41156/ff8f1fb7-ab09-4f07-8863-b94d9d9b0877.png)

- **外部システム側でのイベント駆動が必要なケース**
    - 例：Stripe（決済システム）での取引完了や、チケット管理システムでのステータス変更など、特定のイベントが発生した時点で即座にSalesforceに反映させたい場合。
    - ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41156/ced4557b-482e-688b-5c09-0333e47ae70a.png)

- **セキュリティ上の制約がある場合**
    - 例：ファイアウォールの制限により、Salesforceから外部システムへの直接的なアクセスが困難な環境では、外部システムからのWebhook通知を受け取る方が実装しやすい場合があります。
    - ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41156/99fbc1c7-8fcf-5b98-961f-e8fd376c2016.png)

これらのケースでは、Salesforceから定期的にAPIを呼び出すよりも、外部システムからWebhookで通知を受け取る方が適しています。

# Shopifyを活用したWebhook連携の例

Shopifyは、世界中で広く使われているECプラットフォームです。Shopifyは豊富なWebhook機能を提供しており、これを活用することで、Salesforceと連携して効率的な顧客管理を実現できます。

Shopifyが提供する主なWebhookの種類には以下のようなものがあります：

1. 注文関連
    - 新規注文が作成された時
    - 注文の支払いが完了した時
    - 注文がキャンセルされた時
2. 顧客関連
    - 新規顧客が登録された時
    - 顧客情報が更新された時
    - 顧客が削除された時
3. 商品関連
    - 新規商品が作成された時
    - 商品情報が更新された時
    - 商品が削除された時

これらのWebhookを適切に活用することで、Shopifyでの様々なイベントをリアルタイムでSalesforceに反映させることができます。例えば、新規注文が入った際に即座にSalesforceにユーザー登録されている営業担当者に通知したりすることが可能になります。

# Apex RESTによるWebhook受信：シンプルで確実な方法

Shopify とSalesforceの連携する製品は、Shopifyが提供しているマーケットプレイスには既成の連携製品が販売されていますので、こちらを利用すればプログラム開発を行うことなくShopifyからSalesforceに販売状況を連携することが可能です(連携タイミングや間隔等の仕様は不明です)。

```
Salesforce Sync
https://apps.shopify.com/salesforce-sync?locale=ja
```

ですが、このような製品を購入をしなくても、ShopifyのWebhook通知をSalesforce側で保存すれば同じような連携を行うことが可能です。このように、既成の製品を使用しない場合や存在しない場合や、カスタマイズの自由度を高めたい場合には、Apex RESTを使用してWebhook通知を受信する方法が、シンプルで確実な選択肢です。

Salesforceは、このようなケースに対応するためにApex RESTという機能を提供しています。Apex RESTを使用すると、外部システムからのHTTPリクエストを受け取り、Apexコードで処理することができます。この方法には以下のような利点があります：

1. **カスタマイズの柔軟性**：受信したデータを自由に加工し、Salesforceオブジェクトに反映させることができます。
1. **セキュリティの確保**：多くのサービスでWebhook通知のリクエストのヘッダーに認証キーを設定しています。Apex REST側ではこのヘッダーの認証キーを使用して、簡単かつ安全に認証を実装できます。
1. **スケーラビリティ**：Salesforceのインフラストラクチャを利用するため、大量のリクエストにも対応可能です。
1. **開発の容易さ**：Apexを使用するため、Salesforce開発者にとって馴染みやすい実装方法です。


Apex RESTによるWebhook受信の基本的な流れは以下のようになります：

1. Apex RESTクラスを作成し、HTTPリクエストを受け付けるエンドポイントを定義します。
1. リクエストヘッダーからカスタムAuthorizationトークンを取得し、検証を行います(検証方法はサービスにより異なってきます)。
1. リクエストボディからデータを抽出し、必要な処理を行います。
1. 処理結果をSalesforceオブジェクトに反映させます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41156/f4716c7f-48d8-9560-f515-d8c04e552a09.png)

このような実装により、外部システムからのデータ更新をリアルタイムでSalesforceに反映させることが可能になります。セキュリティ面では、カスタムAuthorizationヘッダに保存された認証トークンを検証することで、不正なリクエストを防ぐことができます。

# Webhook通知を処理するApex RESTのプログラムの実例のご紹介。Shopifyの注文結果をSalesforceにリアルタイムに反映させるには。

前セクションでは、Webhookの概要とその必要性について説明しました。今回は、具体的にShopifyの注文データをSalesforceにリアルタイムに反映させる方法を、Apex RESTを使用して実装する方法をご紹介します。

# ShopifyとApex RESTのシステム構成

ShopifyとSalesforceを連携させるシステム構成は以下のようになります：

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41156/9c61b3c7-184b-0165-d9f7-c3f1441bf6eb.png)

1. Shopifyで注文イベントが発生
1. Shopifyが設定されたWebhookエンドポイント（Salesforce側のApex REST URL）にHTTPリクエストを送信
1. SalesforceのApex RESTがリクエストを受信し、正しいShopifyのリクエストかを検証する
1. 処理されたデータがSalesforceオブジェクトに反映される
1. Apex RESTの基本的なプログラム構成

# ShopifyのWebhook設定

まずShopifyに対しての設定を行いましょう。
Webhookの設定画面を開きます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41156/e59eed2b-a7c4-2b88-a686-5bd15425bc29.png)

この際にイベントが選べるようになっており、どのような時にShopifyから通知先に通知するかを選択できます。

今回は以下の設定を行います。
- **イベント** : 注文作成
- **フォーマット** : JSON
- **URL** : https://<<Salesforce組織>>.my.salesforce-sites.com/services/apexrest/ShopifyOrderWebhook/
- **Webhook APIのバージョン** : 2024-07

Webhookを作成すると、受取先のシステムが検証で使用する署名用のキーが発行されます(今回のApex RESTでも使用します)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41156/ca90a1b3-7d7c-d31e-fc94-3ed326566940.png)

また、ShopifyのWebhookはテスト送信ができるようになっています。今回の検証ではこちらを使用してデモを行います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41156/5ba8c3f8-aed3-4609-d8e2-77866a1bf002.png)

次にWebhook通知を受け取るSalesforce側の設定を行います。

# Salesforceのオブジェクト設定

次にShopifyのWebhookに対応する商品レコードを登録します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41156/ef1b4755-6c24-fc08-3b58-185d35ba347f.png)

次にShopifyの注文情報を保存するオブジェクトの登録をします。今回はWebhookの取り込みの例なので、シンプルに取引先責任者と商品の間に中間オブジェクトをカスタムオブジェクトで作成することにします。カスタム項目も、一旦は数量のみを定義するようにします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41156/1fc369ff-09c1-b601-9d08-2b5fa0c488b6.png)

# Apexクラスの作成
次にApex RESTとしての最低限のApex Classを作ります。このアノテーションを記載してApexクラスを作成して、後続の設定を行えばApex RESTが構築できるんです。とても便利ですよね。

```Java
@RestResource(urlMapping='/ShopifyOrderWebhook/*')
global without sharing class ShopifyOrderWebhookHandler {
    
    @HttpPost
    global static void handlePostRequest() {
      //ここにビジネスロジックを記載する。
    }    
}
```

# 検証処理
次に検証処理です。まずShopify側の公式サイトではどのように書いてあるのかを確認します。

> https://shopify.dev/docs/apps/build/webhooks/subscribe/https#step-2-validate-the-origin-of-your-webhook-to-ensure-its-coming-from-shopify
>
> こちらのサイトに仕様が記載されています。
> 
> Step 2: Validate the origin of your webhook to ensure it’s coming from Shopify
> 
> Before you respond to a webhook, you need to verify that the webhook was sent from Shopify. You can verify the webhook by calculating a digital signature.
> Each webhook includes a base64-encoded X-Shopify-Hmac-SHA256 field in the payload header, which is generated using the app’s client secret along with the data sent in the request.
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41156/0db46850-d3d2-4281-a3be-ce9371bebede.png)

Shopifyのリクエストヘッダーに含まれるHMAC (X-Shopify-Hmac-Sha256) と、リクエストボディと検証キー(上記を参照)を使って、算出した HMAC が同じ値になるかどうかを検証するというものです。

これをApex RESTに実装します。以下では、カスタムメタデータにShopifyの検証キーを保存しています。

```Java
@RestResource(urlMapping='/ShopifyOrderWebhook/*')
global without sharing class ShopifyOrderWebhookHandler {
    
    @HttpPost
    global static void handlePostRequest() {
        RestRequest req = RestContext.request;
        RestResponse res = RestContext.response;

        // リクエストボディを取得
        String requestBody = req.requestBody.toString();
        
        // Shopifyから送信されたHMAC
        String shopifyHmac = req.headers.get('X-Shopify-Hmac-Sha256');

        // カスタムメタデータから署名用のキーを取得
        Shopify_Settings__mdt settings = Shopify_Settings__mdt.getInstance('Default');
        String hmacKey = settings.HmacKey__c;
                
        // Shopifyの仕様に基づいた検証処理
		if (!validateShopifyRequest(requestBody, hmacKey, shopifyHmac)) {
            res.statusCode = 401;
            res.responseBody = Blob.valueOf('Unauthorized');
            return;
        }

        // これ以降に注文の作成処理を記載する。

    }    

    // Shopifyの仕様に基づいた検証処理
    private static Boolean validateShopifyRequest(String requestBody, String hmacKey, String shopifyHmac) {
        Blob hmacData = Crypto.generateMac('hmacSHA256', Blob.valueOf(requestBody), Blob.valueOf(hmacKey));
        String calculatedHmac = EncodingUtil.base64Encode(hmacData);
        
        return (shopifyHmac == calculatedHmac);
    }
}
```

次にこのApexがShopifyのWebhookから実行されるように設定します。サイトのサイトゲストユーザーの設定を使用します。サイトゲストユーザーはこの”注文作成処理”のみを行え、他の用途でのレコードの閲覧等は行えるようにすべきではないので、サイトゲストユーザープロファイルのApex実行権限のみを設定します(その為にwithout sharingにします)。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41156/c607ada9-069d-9629-6fb6-d1c58e346c7f.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41156/7ff021d0-6219-22bd-4318-ec4bff45f6b5.png)

# 注文作成の処理

前の処理で正しく検証がされた場合にのみ、注文作成の処理を実行します。今回の処理ではサンプルとして以下のようなApexクラスになります。

```Java
@RestResource(urlMapping='/ShopifyOrderWebhook/*')
global without sharing class ShopifyOrderWebhookHandler {
    
    @HttpPost
    global static void handlePostRequest() {
        RestRequest req = RestContext.request;
        RestResponse res = RestContext.response;

        // リクエストボディを取得
        String requestBody = req.requestBody.toString();
        
        // Shopifyから送信されたHMAC
        String shopifyHmac = req.headers.get('X-Shopify-Hmac-Sha256');

        // カスタムメタデータから署名用のキーを取得
        Shopify_Settings__mdt settings = Shopify_Settings__mdt.getInstance('Default');
        String hmacKey = settings.HmacKey__c;
                
        // Shopifyの仕様に基づいた検証処理
		if (!validateShopifyRequest(requestBody, hmacKey, shopifyHmac)) {
            res.statusCode = 401;
            res.responseBody = Blob.valueOf('Unauthorized');
            return;
        }
        
        // Webhook通知をMap<String, Object>に読み取る
        Map<String, Object> orderData = (Map<String, Object>) JSON.deserializeUntyped(requestBody);
        
        // 注文者のメールアドレスを取得
        String customerEmail = (String) ((Map<String, Object>) orderData.get('customer')).get('email');
        
        // 取引先責任者を検索
        Contact customerContact;
        List<Contact> contacts = [SELECT Id FROM Contact WHERE Email = :customerEmail LIMIT 1];
        if (!contacts.isEmpty()) {
            customerContact = contacts[0];
        }
        
        // 商品情報を取得
        List<Object> lineItems = (List<Object>) orderData.get('line_items');
        if (lineItems.isEmpty()) {
            res.statusCode = 400;
            res.responseBody = Blob.valueOf('No line items found');
            return;
        }
        
        //デモコードのため、注文内の１件目のみを処理
        Map<String, Object> firstItem = (Map<String, Object>) lineItems[0];        
        
        //skuを取得して商品を検索
        String sku = ((String) firstItem.get('sku')).trim();
        List<Product2> products = [SELECT Id FROM Product2 WHERE ProductCode = :sku LIMIT 1];
        if (products.isEmpty()) {
            res.statusCode = 400;
            res.responseBody = Blob.valueOf('Product not found');
            return;
        }
        Product2 product = products[0];
           
        // 注文レコードを作成
        Order__c newOrder = new Order__c(
            Number__c = (Integer) firstItem.get('quantity'),
            Contact__c = customerContact?.Id,
            Product__c = product.Id
        );
                
        try {
            insert newOrder;
            res.statusCode = 200;
            res.responseBody = Blob.valueOf('Order created successfully');
        } catch (Exception e) {
            res.statusCode = 500;
            res.responseBody = Blob.valueOf('Error creating order');
        }
    }
    
    // Shopifyの仕様に基づいた検証処理
    private static Boolean validateShopifyRequest(String requestBody, String hmacKey, String shopifyHmac) {
        Blob hmacData = Crypto.generateMac('hmacSHA256', Blob.valueOf(requestBody), Blob.valueOf(hmacKey));
        String calculatedHmac = EncodingUtil.base64Encode(hmacData);
        
        return (shopifyHmac == calculatedHmac);
    }
}
```

正しく動作するかをテストしてみましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41156/89c7ed85-1696-6c3d-7000-4d3702a3d6ae.png)

ShopifyのWebhookの画面からテスト送信を行います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41156/3741b97b-0df0-3fcb-379a-fce32eb98ef3.png)

無事に注文レコードがSalesforceに作成されました。作成者もサイトゲストユーザーになっているので、たしかに今のテスト送信で作成されたことがわかります。

# 最後に

今回は、ShopifyのWebhook通知をSalesforceで受け取り、Apex RESTを使用して処理する方法を詳しく解説しました。Salesforceでは、このようにApexを使用してAPIのエンドポイントを作成できるため、ShopifyのWebhookが求める検証処理も問題無く実装でき、Webhookに合わせた柔軟なデータ処理も実装することが可能です。

自社ブログでも色々とまとめておりますので、ぜひご覧くださいませ。

- [顧客事例はこちら](https://prazto.com/case/)
- [メディア記事はこちら](https://prazto.com/article/)
- [システム連携ポートフォリオはこちら](https://prazto.com/data-process/integration-portfolio/)
- [Tableauポートフォリオはこちら](https://prazto.com/tableau/dashboard-portfolio/)


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41156/0fc5d8ba-c38e-e851-5e89-751ba6656109.png)