
WhatsApp Flowsは、顧客データの取得を最適化および簡素化できる機能です。顧客とのやり取りから構造化された情報を簡単に取得し、それによってWhatsApp内で良好な顧客体験を提供することができます。相性の良い用途は、リード獲得のためのデータ取得や、アンケートの実施、顧客向けの予約サービスの提供、顧客からの問い合わせの受け付けなどですが、そのほかにもさまざまな応用が利きます。
最大の特長は、これらすべてを、複雑なアプリケーションもバックエンドも構築せずに顧客に提供できる点です。WhatsAppをフロントエンドとして使い、あとは、Webhookを設定して応答をJSONメッセージとして捕捉し、情報を処理し、必要なデータを取り出すだけです。
このチュートリアルでは、架空の会社を例に、Webhookを使ってWhatsApp上の顧客アンケートを設定する方法を見ていきます。アンケートでは、既存顧客と将来の顧客へのサービス改善のため、どうやって会社を見つけたか、好きなタイプの旅行は何かなどのフィードバックを集めることにします。
このチュートリアルの実行に必要なものは以下のとおりです。
このプロジェクトをプレビューする場合は、こちらにコードの全文があります。
Flowを作成するには、Flow Builder UIを使う方法とFlows APIを使う方法の2通りがありますが、このチュートリアルではFlows APIを使ってアンケートをプログラム的に作成します。
サーバーからの動的データを使用するFlowを構築する場合は、アンケートをそのサーバーに接続するエンドポイントを作成できます。エンドポイントを作成すれば、Flowの画面間のナビゲーションロジックを制御したり、サーバーからのFlowデータを自動入力したり、ユーザーのインタラクションに基づいて画面上のコンポーネントの表示/非表示を切り替えたりできます。
ここで取り上げるアンケートFlowの例では、Flowとサーバー間で動的データを交換しないためエンドポイントは使用しません。アンケートからの情報の捕捉にはチャットWebhookを使用します。また、WhatsAppマネージャでメッセージテンプレートにFlowsを付けることも可能です。
はじめに、Flows APIを扱うFlaskアプリを作成します。ターミナルで以下のコマンドを実行して仮想環境を作成してください。
python -m venv venv
その後、以下のコマンドを使って仮想環境をアクティブ化します。
source venv/bin/activate
次に、以下のコマンドを使って必要なパッケージをインストールします。
pip install requests flask python-dotenv
Flaskは、ルートの作成、およびFlows APIや、HTTPリクエストを送信するリクエスト、環境変数を読み込むpython-dotenvの処理に使用します。
次に、拡張子.envの環境ファイルを作成し、以下の情報をペーストします。
VERIFY_TOKEN =
ACCESS_TOKEN =
WHATSAPP_BUSINESS_ACCOUNT_ID =
PHONE_NUMBER_ID =
お使いの開発者アカウントの情報に基づいて値を割り当ててください。VERIFY_TOKENはどのような文字列でもかまいません。変数WHATSAPP_BUSINESS_ACCOUNT_IDとPHONE_NUMBER_IDは、Metaによって自動生成されるアカウント固有の識別子です。The ACCESS_TOKENは、APIリクエストの認証および承認用です。
Metaアプリのダッシュボードからこの情報にアクセスするには、以下のスクリーンショットのように、左側のナビゲーションパネルで[WhatsApp] > [APIの設定]の順にクリックします。

最後に、同じディレクトリに、FlowsとWebhookを作成するPythonロジックを入れるファイル「main.py」を作成します。
Flowを構築するには、まず、以下のパッケージをmain.pyに追加します。
import os
import uuid
import requests
from dotenv import load_dotenv
from flask import Flask, request, make_response, json
次に、以下のコードスニペットをmain.pyに追加して変数を初期化します。このスニペットはFlaskも初期化し、変数を読み込むためのload_dotenv()メソッドも呼び出します。
app = Flask(__name__)
load_dotenv()
PHONE_NUMBER_ID = os.getenv('PHONE_NUMBER_ID')
VERIFY_TOKEN = os.getenv('VERIFY_TOKEN')
ACCESS_TOKEN = os.getenv('ACCESS_TOKEN')
WHATSAPP_BUSINESS_ACCOUNT_ID = os.getenv('WHATSAPP_BUSINESS_ACCOUNT_ID')
created_flow_id = ""
messaging_url = f"https://graph.facebook.com/v18.0/{PHONE_NUMBER_ID}/messages"
auth_header = {"Authorization": f"Bearer {ACCESS_TOKEN}"}
messaging_headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {ACCESS_TOKEN}",
}
その後、Flowの作成を処理する以下のルートを追加します。
@app.route("/create-flow", methods=["POST"])
def create_flow():
flow_base_url = (
f"https://graph.facebook.com/v18.0/{WHATSAPP_BUSINESS_ACCOUNT_ID}/flows"
)
flow_creation_payload = {"name": "<FLOW-NAME>", "categories": '["SURVEY"]'}
flow_create_response = requests.request(
"POST", flow_base_url, headers=auth_header, data=flow_creation_payload
)
try:
global created_flow_id
created_flow_id = flow_create_response.json()["id"]
graph_assets_url = f"https://graph.facebook.com/v18.0/{created_flow_id}/assets"
upload_flow_json(graph_assets_url)
publish_flow(created_flow_id)
print("FLOW CREATED!")
return make_response("FLOW CREATED", 200)
except:
return make_response("ERROR", 500)この関数は、Flowsエンドポイント(flow_base_url)を呼び出し、名前とFlowカテゴリを含むペイロード(flow_creation_payload)を渡します。カテゴリとして使える値は、SIGN_UP、SIGN_IN、APPOINTMENT_BOOKING、LEAD_GENERATION、CONTACT_US、CUSTOMER_SUPPORT、SURVEY、またはOTHERです。
<FLOW-NAME>には任意の名前(例えば「survey_flow」)を入れます。
このコードは、Flowを作成した後、そのJSONボディをアップロードするためのcreated_flow_idを取り出します。
これらのコンテンツを含むsurvey.jsonファイルを作成します。このJSONはFlowの構造を含みます。
その後、以下のコードをmain.pyファイルにペーストします。
def upload_flow_json(graph_assets_url):
flow_asset_payload = {"name": "flow.json", "asset_type": "FLOW_JSON"}
files = [("file", ("survey.json", open("survey.json", "rb"), "application/json"))]
res = requests.request(
"POST",
graph_assets_url,
headers=auth_header,
data=flow_asset_payload,
files=files,
)
print(res.json())
この関数は、survey.jsonのJSONデータをFlowアセットエンドポイントにアップロードします。
以下は、ユーザーのクリックアクションによってon-click-actionを初期化し、ペイロード内のデータを捕捉するコードスニペットです。"name": "complete"フィールドはFlowの完了を示します。Flowが閉じられ、ペイロードがWebhookサーバーに送信されます。
...
"on-click-action": {
"name": "complete",
"payload": {
"source": "${form.source}",
"tour_type": "${form.tour_type}",
"tour_quality": "${form.tour_quality}",
"decision_influencer": "${form.decision_influencer}",
"tour_guides": "${form.tour_guides}",
"aspects_enjoyed": "${form.aspects_enjoyed}",
"improvements": "${form.improvements}",
"recommend": "${form.recommend}",
"return_booking": "${form.return_booking}"
}
}
...
ペイロードオブジェクト内の値は、Flowのコンポーネント(HTML形式の要素名に似たもの)またはデータオブジェクトに対応します。これらのペイロード値に関連付けられているキーは名前と呼ばれ、プログラミング言語で割り当てる変数のようなものです。
data-source要素には、値のキーとして振る舞うIDも含まれます。このコードは、選ばれた選択肢のIDを送信します。例えば、以下のdata-sourceでLikelyが選ばれた場合は1を送信します。このデータは、受信後にデータソースとマッチさせます。
...
{
"type": "RadioButtonsGroup",
"required": true,
"name": "return_booking",
"data-source": [
{
"id": "0",
"title": "Very likely"
},
{
"id": "1",
"title": "Likely"
},
{
"id": "2",
"title": "Undecided"
},
{
"id": "3",
"title": "Unlikely"
},
{
"id": "4",
"title": "Very likely"
}
]
}
...
このFlowには、以下のように多肢選択式の質問を9つ含む画面が1つあります。

JSON要素の詳細については開発者向けドキュメントをご覧ください。
次に、以下の関数をmain.pyファイルにペーストして、Flowをパブリッシュするロジックを追加します。パブリッシュ後のFlowは送信可能な状態になり、それ以上変更を加えることができなくなります。
def publish_flow(flow_id):
flow_publish_url = f"https://graph.facebook.com/v18.0/{flow_id}/publish"
requests.request("POST", flow_publish_url, headers=auth_header)
この関数は、パブリッシュエンドポイントを呼び出してFlow IDを渡します。
FlowをWhatsAppユーザーに送信する以下の関数をmain.pyファイルにペーストします。この関数は、クラウドAPIのメッセージエンドポイントを呼び出し、Flowペイロードを渡します。
def send_flow(flow_id, recipient_phone_number):
# Generate a random UUID for the flow token
flow_token = str(uuid.uuid4())
flow_payload = json.dumps(
{
"type": "flow",
"header": {"type": "text", "text": "Survey"},
"body": {
"text": "Your insights are invaluable to us – please take a moment to share your feedback in our survey."
},
"footer": {"text": "Click the button below to proceed"},
"action": {
"name": "flow",
"parameters": {
"flow_message_version": "3",
"flow_token": flow_token,
"flow_id": flow_id,
"flow_cta": "Proceed",
"flow_action": "navigate",
"flow_action_payload": {"screen": "SURVEY_SCREEN"},
},
},
}
)
payload = json.dumps(
{
"messaging_product": "whatsapp",
"recipient_type": "individual",
"to": str(recipient_phone_number),
"type": "interactive",
"interactive": json.loads(flow_payload),
}
)
requests.request("POST", messaging_url, headers=messaging_headers, data=payload)
print("MESSAGE SENT")
FlowペイロードにはFlowの詳細が含まれます。action.parameters.flow_tokenフィールドでは、Flowの完了時に顧客からWebhookに転送されるFlowメッセージの固有識別子を渡すことができます。このチュートリアルではランダムなID (uuid)を使用します。このコードでは、action.parameters.flow_action_payload.screenをSURVEY_SCREENに設定しています(action.parameters.flow_ctaがクリックされたときに表示する画面のID)。
Webhookのロジックは単純明快です。webhook_getとwebhook_postという2つの関数があり、それぞれがGETリクエストとPOSTリクエストを処理します。以下のコードでは、GETリクエストを使ってMetaアプリにWebhookを追加します。成功した場合はリクエストのhub.challengeを返します。POSTリクエストは、ターミナルにメッセージペイロードを出力します。
@app.route("/webhook", methods=["GET"])
def webhook_get():
if (
request.args.get("hub.mode") == "subscribe"
and request.args.get("hub.verify_token") == VERIFY_TOKEN
):
return make_response(request.args.get("hub.challenge"), 200)
else:
return make_response("Success", 403)
POSTリクエストは、メッセージペイロードを取り出して処理します。このコードはメッセージペイロードにしか対応していないため、ほかのペイロードが捕捉されたときはエラーが出ます。このため、if文を使ってmessagesボディの有無をチェックします。JSONのmessagesボディがあることがチェックできたら、また別のチェックを行って、messagesペイロードにtextボディがある場合にのみ送信者の電話番号を取り出します。
@app.route("/webhook", methods=["POST"])
def webhook_post():
# checking if there is a messages body in the payload
if (
json.loads(request.get_data())["entry"][0]["changes"][0]["value"].get(
"messages"
)
) is not None:
"""
checking if there is a text body in the messages payload so that the sender's phone number can be extracted from the message
"""
if (
json.loads(request.get_data())["entry"][0]["changes"][0]["value"][
"messages"
][0].get("text")
) is not None:
user_phone_number = json.loads(request.get_data())["entry"][0]["changes"][
0
]["value"]["contacts"][0]["wa_id"]
send_flow(created_flow_id, user_phone_number)
else:
flow_reply_processor(request)
return make_response("PROCESSED", 200)
また、以下のヘルパー関数flow_reply_processorを使用して、Flowからレスポンスを取り出してユーザーに返します。このFlowレスポンスはRadioButtonsGroupsからのデータ捕捉時に選択されていた選択肢のIDを含むため、この関数でそのIDに対応する文字列を得ます。
def flow_reply_processor(request):
flow_response = json.loads(request.get_data())["entry"][0]["changes"][0]["value"][
"messages"
][0]["interactive"]["nfm_reply"]["response_json"]
flow_data = json.loads(flow_response)
source_id = flow_data["source"]
tour_type_id = flow_data["tour_type"]
tour_quality_id = flow_data["tour_quality"]
decision_influencer_id = flow_data["decision_influencer"]
tour_guides_id = flow_data["tour_guides"]
aspects_enjoyed_id = flow_data["aspects_enjoyed"]
improvements_id = flow_data["improvements"]
recommend_id = flow_data["recommend"]
return_booking_id = flow_data["return_booking"]
match source_id:
case "0":
source = "Online search"
case "1":
source = "Social media"
case "2":
source = "Referral from a friend/family"
case "3":
source = "Advertisement"
case "4":
source = "Others"
match tour_type_id:
case "0":
tour_type = "Cultural tour"
case "1":
tour_type = "Adventure tour"
case "2":
tour_type = "Historical tour"
case "3":
tour_type = "Wildlife tour"
match tour_quality_id:
case "0":
tour_quality = "1 - Poor"
case "1":
tour_quality = "2 - Below Average"
case "2":
tour_quality = "3 - Average"
case "3":
tour_quality = "4 - Good"
case "4":
tour_quality = "5 - Excellent"
match decision_influencer_id:
case "0":
decision_influencer = "Positive reviews"
case "1":
decision_influencer = "Pricing"
case "2":
decision_influencer = "Tour destinations offered"
case "3":
decision_influencer = "Reputation"
match tour_guides_id:
case "0":
tour_guides = "Knowledgeable and friendly"
case "1":
tour_guides = "Knowledgeable but not friendly"
case "2":
tour_guides = "Friendly but not knowledgeable"
case "3":
tour_guides = "Neither of the two"
case "4":
tour_guides = "I didn’t interact with them"
match aspects_enjoyed_id:
case "0":
aspects_enjoyed = "Tourist attractions visited"
case "1":
aspects_enjoyed = "Tour guide's commentary"
case "2":
aspects_enjoyed = "Group dynamics/interaction"
case "3":
aspects_enjoyed = "Activities offered"
match improvements_id:
case "0":
improvements = "Tour itinerary"
case "1":
improvements = "Communication before the tour"
case "2":
improvements = "Transportation arrangements"
case "3":
improvements = "Advertisement"
case "4":
improvements = "Accommodation quality"
match recommend_id:
case "0":
recommend = "Yes, definitely"
case "1":
recommend = "Yes, but with reservations"
case "2":
recommend = "No, I would not"
match return_booking_id:
case "0":
return_booking = "Very likely"
case "1":
return_booking = "Likely"
case "2":
return_booking = "Undecided"
case "3":
return_booking = "Unlikely"
reply = (
f"Thanks for taking the survey! Your response has been recorded. This is what we received:\n\n"
f"*How did you hear about our tour company?*\n{source}\n\n"
f"*Which type of tour did you recently experience with us?*\n{tour_type}\n\n"
f"*On a scale of 1 to 5, how would you rate the overall quality of the tour?*\n{tour_quality}\n\n"
f"*What influenced your decision to choose our tour company?*\n{decision_influencer}\n\n"
f"*How knowledgeable and friendly were our tour guides?*\n{tour_guides}\n\n"
f"*What aspects of the tour did you find most enjoyable?*\n{aspects_enjoyed}\n\n"
f"*Were there any aspects of the tour that could be improved?*\n{improvements}\n\n"
f"*Would you recommend our tour company to a friend or family member?*\n{recommend}\n\n"
f"*How likely are you to book another tour with us in the future?*\n{return_booking}"
)
user_phone_number = json.loads(request.get_data())["entry"][0]["changes"][0][
"value"
]["contacts"][0]["wa_id"]
send_message(reply, user_phone_number)
After the extraction, the following send_message function sends the responses to the sender.
def send_message(message, phone_number):
payload = json.dumps(
{
"messaging_product": "whatsapp",
"to": str(phone_number),
"type": "text",
"text": {"preview_url": False, "body": message},
}
)
requests.request("POST", messaging_url, headers=messaging_headers, data=payload)
print("MESSAGE SENT")
この関数は、クラウドAPIのテキストメッセージエンドポイントを使ってレスポンスを送信します。
Meta for DevelopersコンソールでWebhookを設定する前に、アプリケーションを実行しておく必要があります。そこで、ターミナルでコマンド「flask --app main run --port 5000」を使ってコードを実行します。すべて正しく設定されていれば、ターミナルに「* Running on http://127.0.0.1:5000」のメッセージが表示されるはずです。
次に、ターミナルでコマンド「ngrokhttp 5000」を実行してアプリケーションのURLを取得します。そのリンクをコピーします。
Meta for Developersコンソールを開き、左側のナビゲーションパネルにある[WhatsApp]の下で[設定]クリックします。

[Webhook]カードで[編集]をクリックします。その後、[コールバックURL]フィールドのダイアログボックスに、先ほどコピーしたURLをペーストして末尾に/webhookを付けます。[トークンを認証]フィールドに、.envファイルのTOKEN変数のトークンを追加します。
完了したら[確認して保存]をクリックします。ダイアログボックスが閉じます。[管理]をクリックし、messagesフィールドを確認します。以下の画像のように、[コールバックURL]の情報、[トークンを認証]の情報(非表示)、[Webhookフィールド]の情報(messages)が表示されるはずです。

これでWebhookの準備は完了です。
ターミナルの新しいインスタンスで、以下のcURLコマンドを実行してFlowを作成します。
curl --location --request POST 'http://127.0.0.1:5000/create-flow'
Webhookの出力が表示されたターミナルのインスタンスに、以下の画像のようにメッセージが表示されるはずです。

ビジネスのWhatsApp番号にメッセージを送信したユーザーには、以下のスクリーンショットのようにFlowが返信として届きます。

アンケートの回答後には、自分の回答結果が返信で届きます。

このFlowは、ユーザーからビジネスの番号に送信されたすべてのメッセージに対してトリガーされます。用途に応じて、ユーザーがビジネスとチャットした後などの特定の状況にのみアンケートFlowが送られるようにコードをカスタマイズしてください。
WhatsApp Flowsでは、この架空の旅行会社がアンケートで回答を集めたように、構造化データを収集できるインタラクティブなインターフェイスによって優れたユーザー体験を提供できます。必要な作業は、Flaskアプリの作成と、生成用のPythonコードの実装、WhatsApp Flows API経由でのFlowsの展開、受信メッセージを待ち受けるWebhookの設定、アンケートの回答を集めるアプリケーションの実行だけです。
WhatsApp Flowsではデータ収集の迅速化と簡素化が可能で、それによって顧客の回答完了率の向上につなげることができます。ここで紹介したプロセスを参考にして、アンケートやカスタマーサポート、予約サービスなどを実装してみてください。
WhatsApp Flowsを使ってできることはまだまだあります。今すぐお試しください。
月刊ニュースレターに登録して、Meta for Developersの最新情報をチェックしましょう。