この記事はGROOVE X Advent Calendar 2024の15日目の記事です
qiita.com
こんにちは、LOVOTの業務システムを開発する業務を行っている尾銭です。
弊社では顧客基盤にSalesforceを利用しており、これまでMAツールとしては、Marketoを活用していたのですが、今年Account Engagement (AE) にリプレイスを実施しました。
移行の理由は割愛いたしますが、いくつかのMAツールを比較していく中で、Salesforceとの双方向データ連携が標準仕様となっているAEに価値を感じ、リプレイスすることにいたしました。
しかし、Marketoでは外部からAPIによりメール配信を行うことができたものの、AEでは、API経由でメール配信をすることができなく、一部は業務システムを改修してPardot以外からメール配信を行う運用が発生してしまっていました。
そんな状況だったので、2024年10月にようやくリリースされたAEのAPIメール送信機能は個人的にかなり嬉しかったです。
さっそく検証を行い、使用感をまとめてみました。
リリースノートはこちらをご参照ください:AE APIメール送信の変更点
help.salesforce.com
APIリファレンスはこちら:Account Engagement APIリファレンス
developer.salesforce.com
AEのAPIメール送信機能でできるようになったこと
柔軟なシナリオ作成
AEでは外部の情報を元にメール配信を行う場合、AEにカスタムオブジェクトとしてデータを同期させて、同期させたデータをもとにメール配信を行う必要があります。
しかし、プロスペクトに対して関連レコードがN件存在するようなリレーションに対するシナリオ配信やコンテンツへのレコード項目の埋め込みがかなり困難という罠があります。
(実はAE移行前には気づかなかった落とし穴、、)
例えば弊社では、お客様が複数のLOVOTを契約いただいていると、お客様に対して複数の契約が紐づくことになるのですが、各契約の更新日付のNヶ月前に契約更新のお知らせを、メール本文に契約プラン名、契約更新日を埋め込んで配信することができませんでした。
しかし、外部からAPIでメール配信ができるとなると、例えばSalesforce側のフローでメール配信を行うことで高度にパーソナライズされたメール配信を簡単に行うことができるようになります。
メールコンテンツの一元管理
複数システムでお客様とのメールコミュニケーションを行っていると、お客様に配信しているコンテンツ全体を把握することが難しくなっていきます。
弊社でもコンテンツの内容を一覧のドキュメントにまとめたりしていますが、外部システムから配信しているオペレーショナルメールの一部はコンテンツの修正にエンジニア側の作業が必要になるものもあり、カスタマーサクセスチームなどで完結できていないものもあります。
しかし、外部からAPIでメール配信ができるとなると、外部システムでロジックを構築する部分はエンジニア、AEでメールコンテンツを管理するのはビジネス側と役割を分担できるようになるので、運用効率が向上すると同時に、コンテンツの整合性を保ちやすくなります。
実際に使用してみた
今回はAEでできなかったメール配信を自由度の高いSalesforceフローで行うために実装してみました。
実際にAPIを実行する処理はApexで実装する必要があります。
事前準備
まずはAPIを実行するときの認証情報を取得するために接続アプリケーションを追加します。
接続アプリケーションの設定
追加した接続アプリケーションでclient_idとclient_secretを取得します。
コンシューマの詳細を管理ボタンをクリック
次はAPI実行のためのSalesforceユーザが必要になります。
ユーザライセンスはIdentityで問題ありません。
ただしPardotへのアクセス権限が必要っぽいので、権限セット、権限セットライセンスでAccount Engagementを追加してください。
(Salesforce IntegrationライセンスではAccount Engagementの権限セットを追加できずにAPIを実行できませんでした)
Salesforceユーザ
権限セット
これでAPI実行するSalesforceユーザのIDとPasswordが用意できれば準備OKです。
Apexでの実装例
フローで呼び出し可能にするために以下の引数を指定できるようにしています。
事前準備で用意した、接続アプリケーションのclient_idとclient_secret、SalesforceユーザのIDとPasswordをセットしてaccess_tokenを取得しています。
(実装例では指定ログイン情報、外部ログイン情報に認証情報、認証情報のエンドポイントをセットした実装になっております)
引数名
説明
name
任意の文字列。EngamenentHistoryに表示される
prospectId
送信対象のプロスペクト(見込み顧客)のID
campaignId
AE側キャンペーンのID
emailTemplateId
AE側メールテンプレートID。指定する場合は以下の引数は不要
htmlMessage
メールの本文としてHTML。emailTemplateIdを指定しない場合のみ設定
textMessage
メールの本文のプレーンテキスト。emailTemplateIdを指定しない場合のみ設定
isOperational
オペレーショナルメールかどうかを指定。オペレーショナルメールの場合はunsubscribedリンクが不要。emailTemplateIdを指定しない場合のみ設定
senderName
メール送信者名。emailTemplateIdを指定しない場合のみ設定
senderAddress
メール送信者アドレス。emailTemplateIdを指定しない場合のみ設定
public with sharing class AccountEngagement {
public class EmailRequest {
@InvocableVariable (label=' name ' )
public String name;
@InvocableVariable (label=' prospectId ' )
public Integer prospectId;
@InvocableVariable (label=' campaignId ' )
public Integer campaignId;
@InvocableVariable (label=' emailTemplateId ' )
public Integer emailTemplateId;
@InvocableVariable (label=' subject ' )
public String subject;
@InvocableVariable (label=' htmlMessage ' )
public String htmlMessage;
@InvocableVariable (label=' textMessage ' )
public String textMessage;
@InvocableVariable (label=' isOperational ' )
public Boolean isOperational;
@InvocableVariable (label=' senderName ' )
public String senderName;
@InvocableVariable (label=' senderAddress ' )
public String senderAddress;
}
@InvocableMethod
public static void processEmailRequests(List<EmailRequest> emailRequests) {
for (EmailRequest emailRequest : emailRequests) {
try {
sendEmail(emailRequest);
} catch (Exception e) {
System.debug(' Error processing email: ' + e.getMessage());
}
}
}
private static String getToken() {
String REQUEST_BODY = ' grant_type=password&client_id={0}&client_secret={1}&username={2}&password={3} ' ;
HttpRequest req = new HttpRequest();
Http httpRootObj = new Http();
req.setEndpoint(' callout:AEAuth ' );
req.setHeader(' Content-Type ' , ' application/x-www-form-urlencoded ' );
req.setMethod(' POST ' );
req.setBody(String.format(REQUEST_BODY, new String[]{' {!$Credential.AE.client_id} ' , ' {!$Credential.AE.client_secret} ' , ' {!$Credential.AE.username} ' , ' {!$Credential.AE.password} ' }));
HttpResponse res = httpRootObj.send(req);
if (res.getStatusCode() != 200 ) {
throw new EmailException(' Access token取得エラー。status code= ' + res.getStatusCode() + ' 。response body= ' + res.getBody());
}
return res.getBody();
}
private static void sendEmail(EmailRequest emailRequest) {
try {
String accessToken = getAccessToken();
Map<String, Object> reqJson = new Map<String, Object>();
reqJson.put(' name ' , emailRequest.name);
reqJson.put(' prospectId ' , emailRequest.prospectId);
reqJson.put(' campaignId ' , emailRequest.campaignId);
if (emailRequest.emailTemplateId != null ) {
reqJson.put(' emailTemplateId ' , emailRequest.emailTemplateId);
} else {
reqJson.put(' subject ' , emailRequest.subject);
reqJson.put(' htmlMessage ' , ' <html><body> ' + emailRequest.htmlMessage + ' </body></html> ' );
reqJson.put(' textMessage ' , emailRequest.textMessage);
reqJson.put(' isOperational ' , emailRequest.isOperational);
addSenderOptions(reqJson, emailRequest);
}
sendHttpRequest(reqJson, accessToken);
} catch (Exception e) {
throw new EmailException(' Error sending email: ' + e.getMessage());
}
}
private static void addSenderOptions(Map<String, Object> reqJson, EmailRequest emailRequest) {
List<Map<String, String>> senderOptionList = new List<Map<String, String>>();
Map<String, String> senderOption = new Map<String, String>();
senderOption.put(' type ' , ' general_user ' );
senderOption.put(' name ' , emailRequest.senderName);
senderOption.put(' address ' , emailRequest.senderAddress);
senderOptionList.add(senderOption);
reqJson.put(' senderOptions ' , senderOptionList);
}
private static String getAccessToken() {
Map<String, Object> mp = (Map<String, Object>) JSON.deserializeUntyped(getToken());
return String.valueOf(mp.get(' access_token ' ));
}
private static void sendHttpRequest(Map<String, Object> reqJson, String accessToken) {
HttpRequest req = new HttpRequest();
req.setMethod(' POST ' );
req.setHeader(' Authorization ' , ' Bearer ' + accessToken);
req.setEndpoint(' callout:AEApi/api/v5/objects/emails ' );
req.setBody(JSON.serialize(reqJson));
HttpResponse res = new Http().send(req);
if (res.getStatusCode() != 201 ) {
throw new EmailException(' Failed to send email. Status code= ' + res.getStatusCode() + ' , response body= ' + res.getBody());
}
}
public class EmailException extends Exception {}
}
外部ログイン情報
外部ログイン情報のプリンシパル
認証エンドポイントの指定ログイン情報
PardotAPIのエンドポイントの指定ログイン情報
フローでの呼び出し例
上記Apexをデプロイすると以下のようにフローで呼び出せるようになります。
フローで参照するイメージ
メールコンテンツはフローのテキストテンプレートを利用して設定します。
コンテンツを埋め込む際には、HTMLエンコード処理を行わないと予期しない表示崩れが発生してしまう点に注意です。
メールコンテンツの設定イメージ
HTMLエンコード例
これでフローからApexで実装したAE APIのコールアウトができるようになりました。
実際に運用するときにはプラットフォームイベントを間にいれるなどイベント駆動アーキテクチャにするとより安定的な運用ができると思います。
AEのAPIメール送信機能の使用感とメリット
可視化の向上
いままではお客様に配信されたメール内容についてメールサーバのログをエンジニアが確認しないとわからないものもあったのですが、AE経由で配信することで、LOVOTのサポートチームがメール配信状況や開封状況をリアルタイムで把握できるようになりました。
これにより業務負荷が軽減されるとともに、迅速な対応が可能になるのではないかと期待できます。
EngagementHistory
柔軟な運用
プロスペクトに対して関連レコードがN件存在するようなリレーションに対するシナリオ配信の困難さについては、Salesforceフロー側の自由度により完全に解決できそうです。
AEへのデータ連携のハードルがあり、AEで配信することを諦めていたオペレーショナルメールについても、AEに集約させやすくなったと思います。
現時点での課題
CCの指定ができない
AEでの配信はメールのCC機能が現状ではサポートされていません。
取引先責任者などにCC項目を追加してメール通知を行っている場合、CCのメールアドレスはプロスペクトとして登録されていないものもあると思うので、AEで配信しようとするならば、事前に取引先責任者として登録するか、取引先責任者に登録していないメールアドレスは登録できない、とするような制御が新たに必要となってきます。
このため、特定のユースケースでは考慮が必要です。
APIでメールテンプレートに外部システムのデータをセットできない
MarketoではメールテンプレートにプレースホルダーをセットしておけばAPI経由で差し込む項目を自由に定義できたのですが、現状のAE APIではメールテンプレートの指定かメール本文をまるまる指定するかのどちらかしかできないようです。。
このためAEでのメールコンテンツの完全な一元管理は現状は難しいことがわかりました。
初期設定の煩雑さ
API機能を初めて導入する際には、Salesforceの認証設定はマストで必要になるので、ある程度のSalesforce管理者としての知識が求められます。
また組織によっては、AEのSandbox環境を契約していないと思いますので、テストや検証がすこし大変になると思います。
(弊社もSandbox環境ありません、、)
まとめ
いかがでしたか。
いろいろ課題もありますが、個人的にはAEのAPIメール配信機能のリリースは2024年で一番うれしかったリリースの一つです。
私のチームではお客様にLOVOTとのより良い暮らしを提供できるよう日々業務を行っているのですが、まだまだ伸びしろだらけです。
各チームでメンバー積極採用中なので、ぜひGROOVE Xで一緒に働いてみませんか。
recruit.jobcan.jp