Home
Blog
Creating Surveys with WhatsApp Flows

March 6, 2024

Creating Surveys with WhatsApp Flows

By Gafi G & Iryna Wagner

WhatsApp Flows optimizes and simplifies how your business collects customer data. Your organization can easily take in structured information from interactions with customers, who, in turn, enjoy a positive user experience within WhatsApp. WhatsApp Flows works well for lead generation data collection, conducting surveys, helping clients book appointments, submitting customer questions and concerns, and much more.

Best of all, you can offer your customers all these options without building a complex application and back end: Simply use WhatsApp as a front end and employ a webhook to capture the responses as JSON messages, process the information, and retrieve the data you need.

Using a fictitious company as an example, this tutorial explores setting up a customer survey on WhatsApp using webhooks. The survey will collect feedback like how the customer discovered the company and their preferred types of tours, so the company can better serve current and future clients.

Conducting a Survey with WhatsApp Flows

Prerequisites

To follow along, ensure you have the following:

The Process

  1. Create a Flask app.
  2. Write the Python code for creating and publishing the Flows using the WhatsApp Flows API. The Python code will also send the published Flow using the Cloud API.
  3. Create a webhook for listening to chat messages.
  4. Run the application.

If you’d like a preview of the project, you can view the complete code.

Creating a Survey with WhatsApp Flows API

There are two ways to create a Flow: using the Flow Builder UI or the Flows API. This tutorial uses the Flows API to set up the survey programmatically.

To build a Flow that uses dynamic data from your server, you could create an endpoint connecting the survey to your own server. Using an endpoint, you can control the navigation logic between the Flow screens, populate Flow data from your server, or show/hide components on the screen based on user interaction.

The example of a survey Flow that will be discussed does not use any endpoint since there is no dynamic data exchange between it and a server. You will use the chat webhook to capture the information from the survey. Additionally, you could attach Flows to a message template in WhatsApp Manager.

Create a Flask App

First, create a Flask app to interact with the Flows API. Run the following command in your terminal to create a virtual environment.

python -m venv venv
        

Then, use the command below to activate the environment.

source venv/bin/activate
        

Next, use the following command to install the required packages.

pip install requests flask python-dotenv
        

You’ll use Flask to create routes and interact with the Flows API, requests to send HTTP requests, and python-dotenv to load environment variables.

Now, create an environment file named .env and paste in the following information.

VERIFY_TOKEN =
ACCESS_TOKEN =
WHATSAPP_BUSINESS_ACCOUNT_ID =
PHONE_NUMBER_ID =
        

Assign the values based on your developer account information. You can use any string for VERIFY_TOKEN. The WHATSAPP_BUSINESS_ACCOUNT_ID and PHONE_NUMBER_ID variables are your account’s unique identifiers autogenerated by Meta. The ACCESS_TOKEN is for authenticating and authorizing the API requests.

To access this information from your Meta app’s dashboard, click WhatsApp > API Setup on the left navigation pane as in the screenshot below.

Preview API setup

Finally, in the same directory, create a file called main.py to contain the Python logic for creating the Flows and webhook.

Building the Flow

To build the Flow, first, add the following packages to main.py.

import os
import uuid
import requests
from dotenv import load_dotenv
from flask import Flask, request, make_response, json
        

Next, add the following code snippet to main.py to initialize the variables. The snippet also initiates Flask and calls the load_dotenv() method to help load the variables.

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}",
}
        

Then, add the following route to handle creating a 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)

The function calls the Flows endpoint (flow_base_url) while passing in the payload (flow_creation_payload) containing the name and the Flow category. Possible values for the category are: SIGN_UP, SIGN_IN, APPOINTMENT_BOOKING, LEAD_GENERATION, CONTACT_US, CUSTOMER_SUPPORT, SURVEY, or OTHER.

Replace <FLOW-NAME> with your desired name —for example, survey_flow.

After the code creates the Flow, it extracts the created_flow_id for uploading its JSON body.

Uploading the Flow’s JSON Components

Create a survey.json file with these contents. The JSON contains the Flow’s structure.

Then, paste the following code into the main.py file.

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())

That function uploads the JSON data from survey.json to the Flow assets endpoint.

In the code snippet below, when the user triggers the click action, it initiates on-click-action, capturing data within the payload. The "name": "complete" field indicates that the Flow is complete. It will be closed and the payload will be sent to your webhook server.

...
"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}"
    }
}

...
        

The values within the payload objects may correspond to Flow components (resembling element names in HTML forms) or data objects. The keys associated with these payload values are called names, similar to how you assign variables in programming languages.

The data-source elements also contain IDs acting as keys to the values. The code sends these IDs for the choices. For example, if the user chooses Likely for the data-source below, the code sends 1. Once you receive the data, you can match up the data sources.

...
{
    "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"
        }
    ]
}
...
        

The Flow has one screen containing nine questions with multiple choices, like below.

Survey flow

You can explore the JSON elements’ details in the developer documentation.

Publishing the Flow

Next, paste the function below into the main.py file to add the logic for publishing the Flow. A published Flow is production-ready, so you won’t be able to make further changes.

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)
        

The function invokes the publishing endpoint while passing in the Flow ID.

Sending the Flow

Paste the following function into the main.py file to send the Flow to a WhatsApp user. The function calls the Cloud API’s messages endpoint while passing in the Flow payload.

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")
        

The Flow payload contains the Flow details. The action.parameters.flow_token field lets you pass a unique identifier for the Flow message that will transmit from the client to your webhook once the Flow is complete. For this tutorial, you’ll use a random ID (uuid). The code sets the action.parameters.flow_action_payload.screen as SURVEY_SCREEN which is the ID of the screen you want to display when the user clicks action.parameters.flow_cta.

Setting the Webhook

The webhook logic is fairly straightforward. It has two functions, webhook_get and webhook_post, that handle GET and POST requests respectively. The code uses the GET request when adding the webhook to your Meta app. It returns the request’s hub.challenge when successful. The POST request prints the message payload to the terminal.

@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)
        

The POST request extracts and processes the message payload. Because the code only caters to the message payload, it raises errors upon capturing any other payload. For this reason, you use an if statement to check if there is a messages body. After checking that the messages JSON body is present, another check is carried out to extract the sender's phone number only if there is a text body in the messages payload.

@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)
        

Additionally, you should use the following helper function called flow_reply_processor to extract the response from the Flow and send it back to the user. Since the Flow response contains the selected option ID when capturing data from RadioButtonsGroups, the function matches the IDs with corresponding string values.

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")
        

The function sends the response using the Cloud API text message endpoint.

The application needs to be running before you configure the webhook in the Meta for Developers console. Therefore, use the command flask --app main run --port 5000 in your terminal to run the code. You should see a * Running on http://127.0.0.1:5000 message in your terminal if everything is set up correctly.

Next, run the command ngrokhttp 5000 in your terminal to get a URL that maps to your application. Copy that link.

In the Meta for Developers console, under WhatsApp in the left navigation pane, click Configuration.

WhatsApp Configuration

In the Webhook card, click Edit. Then, in the dialog box in the Callback URL field, add the copied URL and append /webhook. In the Verify token field, add the token from your .env file’s TOKEN variable.

When finished, click Verify and save. The dialog box closes. Click Manage and check the messages field. The information should look similar to the image below, with the Callback URL, hidden information under Verify token, and messages listed under Webhook fields.

WhatsApp Configuration

The webhook is now ready.

Running the Application

In a new terminal instance, run the cURL command below to create a Flow.

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

In the terminal instance that displays your webhook output, you should see a message similar to the one shown below.

Running application

When a user sends a message to your WhatsApp number, they receive the Flow as a reply, as in the screenshot below.

Flow survey prompt example

They receive a response with their answers after completing the survey.

Flow survey response example

Any message a user sends to your number triggers the Flow. For your use case, customize your code to only send the survey Flow in specific situations, for example, after a user has chatted with your business.

Conclusion

WhatsApp Flows enhance the user experience through interactive interfaces to collect structured data, such as survey responses for a hypothetical tour company. The business just creates a Flask app, implements Python code to generate, and deploys Flows via the WhatsApp Flows API, sets up a webhook to listen to incoming messages, and runs the application to enable the survey response collection.

WhatsApp Flows let your organization gather data quickly and simply, which can help improve completion rates for customer interactions. Use a similar process to set up your own surveys, offer customer support, help them book appointments, and more.

Continue exploring the potential of WhatsApp Flows. Try it now.


Get our newsletter

Sign up for monthly updates from Meta for Developers.

Sign up