首頁
部落格
Creating Surveys with WhatsApp Flows

2024年3月6日

Creating Surveys with WhatsApp Flows

發佈者: Gafi G & Iryna Wagner

WhatsApp Flows 最佳化並簡化企業商家蒐集顧客資料的方式。您的組織可以輕鬆地從與顧客的互動中取得結構化資訊,而顧客又可以在 WhatsApp 中享受正面的用戶體驗。WhatsApp Flows 非常適合用於蒐集名單型廣告資料、進行問卷調查、協助客戶預約、提交顧客問題和疑慮等等。

最棒的是,您可以為顧客提供上述所有選項,而無需打造複雜的應用程式和後端:只需使用 WhatsApp 作為前端,並使用 Webhook 來擷取回應作為 JSON 訊息、處理資訊,及檢索所需的資料。

本教學以虛構公司為例,探討如何使用 Webhooks 在 WhatsApp 上設定顧客問卷調查。該問卷調查將蒐集顧客如何發現該公司以及偏好的的旅行類型等意見回饋,讓該公司能夠為目前和未來的客戶提供更好的服務。

運用 WhatsApp Flows 進行問卷調查

必要條件

若要繼續操作,請確保您具備以下條件:

流程

  1. 建立 Flask 應用程式。
  2. 使用 WhatsApp Flows API 撰寫用於建立和發佈 Flows 的 Python 程式碼。Python 程式碼也會使用雲端 API 傳送已發佈的 Flow。
  3. 建立用於監聽聊天訊息的 Webhook。
  4. 執行應用程式。

如果您想預覽該專案,可以瀏覽完整程式碼

運用 WhatsApp Flows API 建立問卷調查

建立 Flow 有兩種方法:使用 Flow 建立工具 UI 或 Flows API本教學導覽使用 Flows API 以程式設計方式設定問卷調查。

若要建立使用伺服器中的動態資料的 Flow,您可以建立一個端點,將問卷調查連結至自己的伺服器。使用端點,您可以控制 Flow 畫面之間的瀏覽邏輯、從伺服器填入 Flow 資料,或根據用戶互動在畫面上顯示/隱藏元件。

將要討論的問卷調查 Flow 範例不使用任何端點,因為它與伺服器之間沒有可交換的動態資料。您將使用聊天 Webhook 來擷取問卷調查中的資訊。此外,您可以在 WhatsApp 管理工具中將 Flow 附加到訊息範本

建立 Flask 應用程式

首先,建立與 Flows API 互動的 Flask 應用程式。在終端機中執行以下指令來建立虛擬環境。

python -m venv venv
        

然後,使用以下指令啟動環境。

source venv/bin/activate
        

接著,使用以下指令安裝所需的套件。

pip install requests flask python-dotenv
        

您將使用 Flask 建立路徑並與 Flows API 互動,使用 requests 傳送 HTTP 要求,並使用 python-dotenv 載入環境變數。

現在,建立名為 .env 的環境檔案並貼上以下資訊。

VERIFY_TOKEN =
ACCESS_TOKEN =
WHATSAPP_BUSINESS_ACCOUNT_ID =
PHONE_NUMBER_ID =
        

根據您的開發人員帳號資訊指派值。您可以針對 VERIFY_TOKEN 使用任何字串。WHATSAPP_BUSINESS_ACCOUNT_IDPHONE_NUMBER_ID 變數是 Meta 針對您的帳號自動產生的不重複識別碼。The ACCESS_TOKEN 是用於驗證和授權 API 要求。

若要從 Meta 應用程式的主控板存取此資訊,請點擊左側導航面板中的 WhatsApp > API 設定,如下方螢幕截圖所示。

最後,在同一目錄中,建立一個名為 main.py 的檔案,以在其中包含用於建立 Flows 和 Webhook 的 Python 邏輯。

建立 Flow

若要建立 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_UPSIGN_INAPPOINTMENT_BOOKINGLEAD_GENERATIONCONTACT_USCUSTOMER_SUPPORTSURVEYOTHER

<FLOW-NAME> 取代為您想要的名稱(例如,survey_flow)。

程式碼建立 Flow 後,就會擷取 created_flow_id 以用於上傳其 JSON 內文。

上傳 Flow 的 JSON 元件

使用這些內容建立 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())

此函數會將 JSON 資料從 survey.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 元素也包含作為值的密鑰來使用的編號。程式碼會傳送這些編號以供選擇。舉例來說,如果用戶為下方的 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 有一個包含九個單選題的畫面,如下所示。

您可以在開發人員文件中探索 JSON 元素的詳細資料。

發佈 Flow

接下來,將下方的函數貼到 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 編號中傳遞時會叫用發佈端點。

傳送 Flow

將以下函數貼到 main.py 檔案中,以將 Flow 傳送給 WhatsApp 用戶。該函數在傳遞 Flow 裝載時會呼叫雲端 API 的訊息端點。

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 訊息的專屬識別碼,該訊息將會在 Flow 完成後從客戶端傳輸到您的 Webhook。在本教學導覽中,您將會使用隨機編號(uuid)。程式碼將 action.parameters.flow_action_payload.screen 設定為 SURVEY_SCREEN,這是用戶點擊 action.parameters.flow_cta 時您想要顯示的畫面的編號。

設定 Webhook

Webhook 邏輯相當簡單。它擁有兩個函數:webhook_getwebhook_post,分別處理 GETPOST 要求。將 Webhook 新增至 Meta 應用程式時,程式碼會使用 GET 要求。成功時,它會回傳要求的 hub.challengePOST 要求會將訊息裝載列印到終端機。

@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 內文。檢查 messages JSON 內文是否存在後,只有在 text 裝載中有 messages 內文時,才會執行另一項檢查以擷取傳送者的電話號碼。

@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 的回應並傳回給用戶。由於從 RadioButtonsGroups 擷取資料時,Flow 回應會包含選取的選項編號,因此該函數會比對編號與相對應的字串值。

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,以取得對應到您的應用程式的網址,並複製該連結。

在 Meta for Developers 主控台左側導覽面板中的 WhatsApp 下方,點擊設定

Webhook 頁籤卡中,點擊編輯。然後,在對話方塊的回呼網址欄位中,新增複製的網址並附加 /webhook。在驗證權杖欄位中, 新增您 .env 檔案的 TOKEN 變數中的權杖。

完成時點擊驗證並儲存。對話方塊會關閉。點擊管理並檢查 messages 欄位。該資訊看起來應如下圖所示,其中包含 Callback URLVerify token 下方有隱藏資訊,以及 Webhook fields 下方列出 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 的每月最新資訊。

立即訂閱