返回开发者新闻

Creating Surveys with WhatsApp Flows

2024年3月6日发布者:Gafi G & Iryna Wagner

WhatsApp Flows 优化并简化了企业收集客户数据的方式。您的企业可以从与客户的互动中轻松获取结构化信息,而客户则可在 WhatsApp 中获得良好的用户体验。WhatsApp Flows 非常适合用来收集潜在客户数据、开展调查、帮助客户预约、提交客户问题和疑虑等等。

最重要的是,您无需构建复杂的应用程序和后端,即可为客户提供所有这些选项:只需将 WhatsApp 用作前端,然后使用 Webhook 捕获 JSON 消息形式的响应、处理信息和检索所需数据即可。

本教程以一家虚构的公司为例,探讨如何使用 Webhooks 在 WhatsApp 上创建客户调查问卷。调查问卷将收集客户的反馈信息,例如客户是如何发现公司的,以及他们偏爱哪种类型的旅行,这样公司便可更好地为新老顾客提供服务。

通过 WhatsApp Flows 开展调查

前提条件

要继续操作,请确保您拥有以下内容:

操作流程

  1. 创建一个 Flask 应用。
  2. 编写 Python 代码以使用 WhatsApp Flows API 创建和发布 Flows。Python 代码还将使用云端 API 发送已发布的 Flow。
  3. 创建用于监听聊天消息的 Webhook。
  4. 运行应用程序。

如果您想预览项目,可以查看完整代码

通过 WhatsApp Flows API 创建调查问卷

创建 Flow 有两种方法:使用 Flow Builder UI 或 Flows API。本教程使用 Flows API 以编程方式创建调查问卷。

要构建使用服务器中动态数据的 Flow,您可以创建一个端点,该端点将调查问卷连接到您自己的服务器。您可以使用端点控制多个 Flow 屏幕之间的导航逻辑,从您的服务器填充 Flow 数据,或根据用户互动情况在屏幕上显示/隐藏组件。

将要讨论的调查问卷 Flow 示例不使用任何端点,因为该调查问卷 Flow 与服务器之间没有动态数据交换。您将使用聊天 Webhook 来捕获调查问卷中的信息。此外,您可以通过 WhatsApp 管理工具将 Flows 附加到消息模板

创建 Flask 应用

首先,创建一个 Flask 应用以便与 Flows API 互动。在您的终端运行以下命令来创建虚拟环境。

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 设置,如下方截图所示。

预览 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 已完成。它将关闭,有效负载将被发送到您的 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 有一个包含九道单选题的屏幕,如下所示。

调查问卷 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 正文是否存在后,仅当 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 中提取响应并将其发回给用户。由于从 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 开发者控制台中配置 Webhook 之前,需要运行应用程序。因此,请在终端中使用命令 flask --app main run --port 5000 来运行代码。如果一切设置正确,您应在终端中看到 * Running on http://127.0.0.1:5000 消息。

接下来,在终端中运行命令 ngrokhttp 5000 以获取映射到您应用程序的网址。复制该链接。

在 Meta 开发者控制台左侧导航窗口中的 WhatsApp 下方,点击配置

WhatsApp 配置

Webhook 图卡中,点击编辑。然后,在回调网址字段的对话框中,添加复制的网址并附加 /webhook。在验证口令字段中,添加 .env 文件的 TOKEN 变量中的口令。

完成后,点击验证并保存。对话框将关闭。点击管理并检查消息字段。此信息应与下图所示内容类似,其中包含回调网址验证口令下方的隐藏信息,以及 Webhook 字段下方列出的消息

WhatsApp 配置

Webhook 现已准备就绪。

运行应用程序

在新的终端实例中,运行下方的 cURL 命令来创建 Flow。

curl --location --request POST 'http://127.0.0.1:5000/create-flow'
        

在显示 Webhook 输出的终端实例中,您应该会看到一条类似于下图所示的消息。

运行应用程序

当用户给您的 WhatsApp 电话号码发消息时,他们会收到 Flow 作为回复,如下方截图所示。

Flow 调查问卷提示示例

完成调查问卷后,他们会收到包含答案的回复。

Flow 调查问卷回复示例

用户给您电话号码发送的任何消息都将触发 Flow。对于您的用例,可自定义代码以仅在特定情况下发送调查问卷 Flow,例如在用户与您聊天之后。

总结

WhatsApp Flows 可通过互动界面收集结构化数据(例如假设的旅游公司的调查问卷回复),以此来增强用户体验。这家公司只需创建一个 Flask 应用,使用 Python 代码来生成,并通过 WhatsApp Flows API 部署 Flows,设置 Webhook 以监听传入消息,并运行应用程序以启用调查问卷回复收集功能。

WhatsApp Flows 可让您的组织快速、轻松地收集数据,这有助于提高客户互动的完成率。您可以使用类似流程创建自己的调查问卷、提供客户支持、帮助他们进行预约等等。

继续探索 WhatsApp Flows 的潜力。马上试试