開発者向けニュースに戻る

WhatsApp Flowsを用いた予約受付: Node.jsをバックエンドに使用する

2024年2月27日作成者:Gafi G & Iryna Wagner

WhatsApp Flowsは、WhatsApp上だけでアクションが完結するインタラクティブなメッセージを構築できる機能です。Flowsでは、ユーザーのアクションを受け付ける画面を作成できます。例えば、リードの獲得やレビューの収集のためのシンプルな入力フォームなどです。加えて、複数の画面を使って予約の日時を設定する複雑なFlowsをデザインすることも可能です。

このガイドでは、WhatsApp Flowsで予約を受け付けるためのNode.jsアプリの構築方法について説明します。WhatsApp BusinessプラットフォームでFlowを作成し、続いて、そのFlowのレスポンスを受け取って予約を受け付けるWebhookを設定します。

前提条件

このチュートリアルを実行するには以下のものが必要です。

WhatsApp Flowを作成する

WhatsApp Flowを作成するには、Flows Builder (WhatsAppマネージャからアクセス可能)を使う方法とFlows APIを使う方法の2通りがありますが、このチュートリアルではFlows Builderを使用します。

Flowを作成する

WhatsAppマネージャダッシュボードの左側のメニューで[アカウントツール]を選択します。次に、[Flows]をクリックします。

WhatsAppマネージャの画像

右上の[Flowを作成]をクリックします。

[Flowを作成]の画像

表示されるダイアログボックスに、予約Flowの詳細を入力します。

  • 名前 — 「BookAppointment」またはその他の任意の名前を入力します。
  • カテゴリ — [予約]を選択します。
  • テンプレート — [予約する]を選択します。このテンプレートを選択するのは、予約に必要な要素が含まれているためです。例えば、予約の詳細を入力する画面、ユーザー情報を入力する画面、予約確認画面、会社の規約画面などです。このテンプレートは用途に合わせてさらにカスタマイズできます。
[予約する]の画像

[送信]をクリックしてFlowを作成します。

Builder UIの右側にFlowのプレビューが表示されます。予約画面では、予約の詳細(場所、日付など)を選択してもらいます。ユーザー情報画面では、ユーザーの情報を入力してもらいます。予約確認画面には予約の概要が表示されます。最後の画面には会社の規約が表示されます。

編集している間は、Flowは下書き状態のままです。この段階では、社内でのテスト目的の共有しかできません。広く共有するにはFlowを公開する必要があります。ただし、公開した後はFlowを編集することができません。この予約FlowにはまだエンドポイントURLを追加する必要があるので、ここでは下書きのままにしておき、次のエンドポイント設定ステップに進みましょう。

Flowのエンドポイントを設定する

WhatsApp Flowsには、外部のエンドポイントに接続できる機能があります。このエンドポイントからFlowに動的データを提供し、ルーティングをコントロールすることができます。ユーザーから送信されたレスポンスをFlowから受け取ることも可能です。

テストの目的上、この記事ではGlitchを使用してエンドポイントをホストします。Glitchの使用はまったくの任意であり、Flowsの使用に必須というわけでありません。GitHubからエンドポイントコードをクローンし、任意の環境で実行してかまいません。

Glitchのエンドポイントコードにアクセスし、それをリミックスして固有のドメインを得ます。リミックスするには、ページ上部の[Remix]をクリックします。Glitchページの右側の入力要素に、固有のドメインがプレースホルダーとして表示されます。

先に進む前にコードの中身を見ていきましょう。srcディレクトリには、encryption.jsflow.jskeyGenerator.jsserver.js、という4つのJavaScriptファイルがあります。エントリーファイルはserver.jsです。まずはこのファイルから見ていきます。

server.js

server.jsファイルは、express.jsonミドルウェアを使用して受信JSONリクエストを解析するために、まずExpressアプリケーションを設定します。次に、エンドポイントに必要な環境変数を読み込みます。

const { APP_SECRET, PRIVATE_KEY, PASSPHRASE, PORT = "3000" } = process.env;

署名認証にはAPP_SECRETを使用します。これにより、メッセージの送信元がWhatsAppであり、よって安全に処理できるかどうかを確認できます。これを.envファイルに追加します。

APP_SECRETにアクセスするには、Meta for Developersのアプリのダッシュボードを開きます。左側のナビゲーションパネルにある[アプリの設定]で、[ベーシック]を選択します。[app secret]の下で[表示]をクリックし、秘密鍵をコピーします。次に、Glitchに戻って.envファイルを開き、コピーした秘密鍵の値でAPP_SECRETという変数を作成します。

PRIVATE_KEYは、受信したメッセージを復号するためのものです。PASSPHRASEは、非公開鍵の検証に使用します。非公開鍵のほかに、それに対応する公開鍵も必要です。これは後ほどアップロードします。本番用アカウントの非公開鍵は絶対に使用しないでください。Glitchでのテスト用に仮の非公開鍵を作成し、その後、自社のインフラ内で本番用の鍵に差し替えてください。

Glitchのターミナルで以下のコマンドを実行して公開鍵と非公開鍵のペアを生成します。<your-passphrase>には、自身で決めたパスフレーズを使用してください。Glitchのターミナルにアクセスするには、ページ下部の[TERMINAL]タブをクリックします。

node src/keyGenerator.js <your-passphrase>

パスフレーズと非公開鍵をコピーし、.envファイルにペーストします。左側のサイドバーで拡張子が.envのファイルをクリックしてから、上部の[✏️ Plain text]をクリックします。鍵のフォーマットが崩れてしまうため、UIで直接編集しないでください

環境変数を設定したら、生成した公開鍵をコピーし、グラフAPIで公開鍵をアップロードします。

server.jsファイルには、以下の各ステップを実行するPOSTエンドポイントも含まれています。

  • 非公開鍵があることを確認します:
       if (!PRIVATE_KEY) { throw new Error('非公開鍵が空です。環境変数"PRIVATE_KEY"を確認してください。'); }
  • ファイルの下部にあるisRequestSignatureValid関数を使ってリクエストの署名を検証します:
if(!isRequestSignatureValid(req)) { // リクエストの署名が一致しない場合はステータスコード432を返します。// 返すエラーコードについて、詳しくはhttps://developers.facebook.com/docs/whatsapp/flows/reference/error-codes#endpoint_error_codesをご覧ください。 return res.status(432).send(); }
  • encryption.jsファイルにあるdecryptRequest関数を使って受信メッセージを復号します:
      let decryptedRequest = null; try { decryptedRequest = decryptRequest(req.body, PRIVATE_KEY, PASSPHRASE); } catch (err) { console.error(err); if (err instanceof FlowEndpointException) { return res.status(err.statusCode).send(); } return res.status(500).send(); } const { aesKeyBuffer, initialVectorBuffer, decryptedBody } = decryptedRequest; console.log("💬 復号されたリクエスト:", decryptedBody);
  • ユーザーに表示するFlow画面を決めます。getNextScreen関数については後ほど詳しく見ていきます。

const screenResponse = await getNextScreen(decryptedBody);

       console.log("👉 暗号化するレスポンス:", screenResponse);
  • ユーザーに送信されるレスポンスを暗号化します:
res.send(encryptResponse(screenResponse, aesKeyBuffer, initialVectorBuffer));

encryption.js

このファイルには、交換されるメッセージのセキュリティ確保を目的とした暗号化と復号のロジックが含まれます。このチュートリアルでは、このファイルの扱いについては取り上げません。

keyGenerator.js

このファイルは、前述したように非公開鍵と公開鍵の生成に使用します。encryption.jsファイルと同様、keyGenerator.jsファイルについてもここでは詳しくは取り上げません。

flow.js

Flowの処理ロジックはこのファイルに格納されています。SCREEN_RESPONSESという名前が割り当てられたオブジェクトから始まります。このオブジェクトには、画面IDとそれに対応する情報(データ交換で使用されるプリセットデータなど)が含まれます。このオブジェクトは、Flow Builderの[...] >[エンドポイント] > [スニペット] > [返信]で生成されます。この同じオブジェクトには、別のID「SUCCESS」もあります。これは、Flowが正常に完了したときにクライアントデバイスに返されます。これによってFlowが閉じます。

getNextScreen関数には、ユーザーに表示するFlowデータをエンドポイントに伝えるロジックが含まれます。この関数は、はじめに、復号されたメッセージから必要なデータを抽出します。

const { screen, data, version, action, flow_token } = decryptedBody;

WhatsApp Flowsのエンドポイントは通常、3つのリクエストを受信します。

これらの詳細についてはエンドポイントのドキュメントをご覧ください。

この関数は、以下のスニペットに示すとおり、if文を使ってヘルスチェックとエラー通知を処理し、適宜レスポンスします。

// ヘルスチェックリクエストを処理 if (action === "ping") { return { version, data: { status: "active", }, }; } // エラー通知を処理 if (data?.error) { console.warn("クライアントエラーを受信しました:", data); return { version, data: { acknowledged: true, }, }; }
        

ユーザーがFlowのCTAボタンをクリックすると、INITアクションが実行されます。このアクションは、データの入った予約画面を返します。ユーザーが確実にすべてのフィールドに入力するように、場所、日付、時刻のドロップダウンの無効化も行います。

例えば、日付ドロップダウンは場所ドロップダウンが入力されたときしか有効になりません。フィールドの有効化と無効化は、data_exchangeリクエストを受信したときに処理されます。

// Flowを開くときに最初のリクエストを処理し、APPOINTMENT画面を表示する if (action === "INIT") { return { ...SCREEN_RESPONSES.APPOINTMENT, data: { ...SCREEN_RESPONSES.APPOINTMENT.data, // 以下のフィールドは最初は無効化されています。各フィールドは、その1つ前のフィールドが選択されたときに有効になります。 is_location_enabled: false, is_date_enabled: false, is_time_enabled: false, }, }; }

data_exchangeアクションでは、画面IDに基づいて返すべきデータを判断するためにswitch case文を使用しています。画面IDがAPPOINTMENTの場合、各ドロップダウンフィールドは、その1つ前のドロップダウンが選択されたときしか有効になりません。

// 各フィールドは、その1つ前のフィールドが選択されたときのみ有効になる is_location_enabled: Boolean(data.department), is_date_enabled: Boolean(data.department) && Boolean(data.location), is_time_enabled: Boolean(data.department) && Boolean(data.location) && Boolean(data.date)

DETAILS画面では、データオブジェクトプロパティ(場所や部署など)のタイトルはSCREEN_RESPONSES.APPOINTMENT.dataオブジェクトから抽出されます。このコードは有効な一致があることを前提としているため、一致するオブジェクトが見つからなかった場合はエラーが起きる可能性があることに注意してください。

場所オブジェクトを例に見てみましょう。特定の場所オブジェクトの選択は、配列内のオブジェクトのidプロパティとdata.locationの値を照らし合わせることで判断されます。

const departmentName = SCREEN_RESPONSES.APPOINTMENT.data.department.find( (dept) => dept.id === data.department ).title; const locationName = SCREEN_RESPONSES.APPOINTMENT.data.location.find( (loc) => loc.id === data.location ).title; const dateName = SCREEN_RESPONSES.APPOINTMENT.data.date.find( (date) => date.id === data.date

).title;

続いて、値が連結されてレスポンスで返され、SUMMARY画面が生成されます。

const appointment = `${departmentName} at ${locationName} ${dateName} at ${data.time}`; const details = `Name: ${data.name} Email: ${data.email} Phone: ${data.phone} "${data.more_details}"`; return { ...SCREEN_RESPONSES.SUMMARY, data: { appointment, details, // 次のステップで送信してもらうために、クライアントから送信されたのと同じフィールドを返す ...data, }, };
        

SUMMARY画面でクライアントが送信を選択すると、完了レスポンスがクライアントデバイスに送信されてFlowが完了とマークされます。flow_tokenは、ユーザーにFlowを送信するときに設定できる固有の識別子です。

// 完了レスポンスを送信し、Flowを完了して閉じます send success response to complete and close the flow return { ...SCREEN_RESPONSES.SUCCESS, data: { extension_message_response: { params: { flow_token, }, }, }, };

TERMS画面には交換するデータがないため、エンドポイントはこの画面を処理しません。

Flowにエンドポイントを追加する

Glitchページの右上にある縦3点ドットメニューをクリックし、[Copy Link]を選択するとURLをコピーできます。リンクのコピーは右上の[Share]をクリックすることでも行えます。

Flowエディターに移動します。エディターの上部に表示されるブラウンのバナーで[設定]をクリックします。

ポップアップが表示されます。ここで、エンドポイントURI、ビジネスの電話番号、Meta for Developersのアプリを設定できます。必要な設定を済ませたら、次はヘルスチェックです。まず、インタラクティブプレビューを実行し、インタラクティブプレビュー設定の[最初の画面のデータをリクエスト][データをリクエスト]を選択します。これは、最初の画面のデータを取得するリクエストをエンドポイントに送信して、エンドポイントがアクティブであること、およびヘルスチェックが実行済みであることを検証するための設定です。

続いて、横3点ドットメニュー([...])をクリックし、[公開]を選択してFlowを公開します。action === "ping"によってエンドポイントにヘルスチェックリクエストが送信され、エンドポイントが設定されていることの検証が行われた後、公開されます。

エンドポイントの画像

Flowをテストする

設定が完了したら、もう一度WhatsApp Builder UIのインタラクティブプレビューに切り替えてFlowをテストします。表示されるポップアップで、電話番号を選択し、[最初の画面のデータをリクエスト][データをリクエスト]オプションを選択します。CTAボタンからFlowのテストを開始できるように、XアイコンをクリックしてFlowを閉じます。

[LOGS]タブをクリックしてGlitchのログを開きます。[Clear]をクリックしてログをクリアします。続いて、WhatsApp Builder UIのプレビューに戻ります。[Flowをプレビュー]をクリックします。次のようなものが表示されます。

[Flowをプレビュー]の画像

次に、Glitchのログに戻ります。復号されたリクエストの下に、INITアクション、Flowのトークン、その他の情報が表示されます。部署ドロップダウンが選択されたときにユーザーのFlowに返す、暗号化対象のレスポンスもあります。

復号されたリクエストの画像

部署の選択に進みます。is_location_enabledtrueに設定され、アクションがdata_exchangeに変わっています。

data_exchangeの画像

Flowのテストを継続し、Glitchのログでデータが変化することを確認します。ユーザーがモバイルデバイスからFlowを操作したときも、同じようなログが生成されます。

次のセクションでは、予約したユーザーに確認メッセージを送信するWebhookを作成します。

Webhookを設定する

ユーザーがFlowを完了すると、Flowの完了を伝えるメッセージが設定済みのWebhookに送信されます。このWebhookから、チャットのメッセージでユーザーに予約が完了したことを伝えます。エンドポイントと同様、テストにはGlitchを使用します。コードへのアクセスとリミックスは、こちらから行えます。

Glitchの使用はまったくの任意であり、Flowsの使用に必須というわけでありません。GitHubからエンドポイントコードをクローンし、任意の環境で実行してかまいません。

環境変数を設定する

環境変数を設定するには、Glitchで.envファイルを開きます。VERIFY_TOKENに任意の文字列を設定し、FLOW_IDを自分のFlowのIDにし、GRAPH_API_TOKENにWhatsApp Businessアカウントのアクセストークンを設定します。アクセストークンは、Meta for Developersのアプリのダッシュボードで、左側のナビゲーションパネルの[WhatsApp]セクションで[APIの設定]をクリックすることにより取得できます。

[APIの設定]の画像

表示されるページの[一時的なアクセストークン][コピー]をクリックします。キーを.envファイルにペーストします。

MetaダッシュボードでWebhookを設定する

Meta for Developersの開発者アカウントに移動し、左側のナビゲーションパネルの[WhatsApp][設定]メニューをクリックします。

[設定]の画像

[Webhook]カードで[編集]をクリックします。表示されるダイアログの[コールバックURL]フィールドにコピーしたGlitch URLをペーストし、末尾に「/webhook」を付けます。[トークンを認証]フィールドに、.envファイルのVERIFY_TOKEN変数のトークンを追加します。完了したら[確認して保存]をクリックします。ダイアログが閉じ、メイン画面に戻ります。[管理]をクリックし、messagesフィールドを確認します。これでWebhookの準備が整いました。

Webhookのコードの説明

コードには、POST /webhookGET /webhookの2つのルートがあります。GETは、Webhook認証リクエストを処理するルートです。提供されたトークンと定義済みの認証トークンを照合し、適切なステータスコードとチャレンジトークンを返します。

const verify_token = process.env.VERIFY_TOKEN; // Webhook認証リクエストのパラメーターを解析する let mode = req.query["hub.mode"]; let token = req.query["hub.verify_token"]; let challenge = req.query["hub.challenge"]; if (mode && token) { if (mode === "subscribe" && token === verify_token) { console.log("WEBHOOK_VERIFIED"); res.status(200).send(challenge); } else { res.sendStatus(403); } }

POST /webhookは、受信Webhook通知を処理するルートです。Webhookリクエストは、さまざまなペイロードを持つ可能性があります。そのため、以下のコードは、リクエストフィールドが未定義の場合に備えてそのフィールドに安全にアクセスすることで、メッセージとビジネスの電話番号を読み取ります。

const message = req.body.entry?.[0]?.changes[0]?.value?.messages?.[0]; const business_phone_number_id =

req.body.entry?.[0].changes?.[0].value?.metadata?.phone_number_id;

その後、受信リクエストが、「appointment」(予約)の単語を含む"text"タイプのメッセージかどうかを確認します。メッセージにその単語が含まれている場合はユーザーにFlowが送信されます。このFlowメッセージにはflow_action: "data_exchange"が含まれているため、Flowの起動時には、最初の画面とデータを取得するINITリクエストがエンドポイントに送信されます。

if ( message.type === "text" && // デモの都合上、ユーザーが「appointment」を含むメッセージを送信するたびにFlowメッセージが送信されるようになっています message.text.body.toLowerCase().includes("appointment") ) { // https://developers.facebook.com/docs/whatsapp/flows/gettingstarted/sendingaflow#interactive-message-parametersのドキュメントに従ってFlowを送信します await axios({ method: "POST", url: `https://graph.facebook.com/v18.0/${business_phone_number_id}/messages`, headers: { Authorization: `Bearer ${GRAPH_API_TOKEN}`, }, data: { messaging_product: "whatsapp", to: message.from, type: "interactive", interactive: { type: "flow", header: { type: "text", text: "こんにちは👋", }, body: { text: "空間をイメージチェンジしませんか?当社のエキスパートチームとの個別相談をご予約ください!", }, footer: { text: "予約に進むには以下のボタンをクリックしてください", }, action: { name: "flow", parameters: { flow_id: FLOW_ID, flow_message_version: "3", // flow_tokenをこのFlowメッセージの固有の識別子に差し替えてエンドポイントとWebhookでトラッキングします flow_token: "<FLOW_TOKEN_PLACEHOLDER>", flow_cta: "予約する", flow_action: "data_exchange", }, }, }, }, }); } ...

受信メッセージのタイプが"text"でない場合は、メッセージのタイプが"interactive"かどうかがチェックされます。インタラクティブなタイプ"nfm_reply"は、受信メッセージがFlowレスポンスであることを示します。その後、「予約が完了しました」というメッセージがユーザーに返されます。

... if ( message.type === "interactive" && message.interactive?.type === "nfm_reply" ) { // 確認メッセージを送信します await axios({ method: "POST", url: `https://graph.facebook.com/v18.0/${business_phone_number_id}/messages`, headers: { Authorization: `Bearer ${GRAPH_API_TOKEN}`, }, data: { messaging_product: "whatsapp", to: message.from, text: { body: "予約が完了しました" }, }, }); } ...

その後、ユーザー側に青いチェックマークが表示されるように受信メッセージを既読にします。

... // 受信メッセージを既読にします await axios({ method: "POST", url: `https://graph.facebook.com/v18.0/${business_phone_number_id}/messages`, headers: { Authorization: `Bearer ${GRAPH_API_TOKEN}`, }, data: { messaging_product: "whatsapp", status: "read", message_id: message.id, }, }); ...
        

利用体験

この例では、ビジネスの番号宛に「appointment」の単語を含むメッセージを送信したユーザーにFlowメッセージが送信されます。別のアクションの後にFlowを送信したり、Flowをメッセージテンプレートとして送信したりすることも可能です。

ユーザーには予約のCTAボタンを含むFlowメッセージが届き、ユーザーはそのボタンから情報を入力します。その後、Flowの操作を完了すると確認メッセージが届きます。

ユーザーへのFlowの送信の画像

このガイドでは、シームレスに予約を受け付けるWhatsApp Flowの設定方法について説明しました。Flow Builder UIを使って、ユーザーから予約情報を取得するフォームの作成方法をお分かりいただけたと思います。

Flowsなら外部の予約用ウェブサイトにユーザーを飛ばす必要がないため、顧客体験が向上します。WhatsAppの中だけで予約が完了するのでプロセスがシンプルです。WhatsApp Flowsでできるのは予約だけではありません。サービスに対する顧客の声を集めたり、キャンペーンやメーリングリストに登録できるようにしたりするためにも応用できます。WhatsApp Flowsは、エンドポイントで外部のAPIや他のアプリと接続できる柔軟性も備えています。

Flow Builder UIならWhatsApp Flowsの作成も簡単です。もちろん、Flow APIを使ってFlowsをプログラムで作成することも可能です。詳しくはWhatsApp Flowsのドキュメントをご覧ください。