♻️ Flows#

The WhatsApp Flows are now the most exciting part of the WhatsApp Cloud API.

From developers.facebook.com:

WhatsApp Flows

WhatsApp Flows is a way to build structured interactions for business messaging. With Flows, businesses can define, configure, and customize messages with rich interactions that give customers more structure in the way they communicate.

You can use Flows to book appointments, browse products, collect customer feedback, get new sales leads, or anything else where structured communication is more natural or comfortable for your customers.

When you reading the official docs it’s looks very intimidating, but in fact it’s quite simple (by PyWa 😉).

For a real world example, you can check out the Sign up Flow Example.

The Flows are seperated to 4 parts:

  • Creating Flow

  • Sending Flow

  • Handling Flow requests and responding to them (Only for dynamic flow)

  • Getting Flow Completion message

Creating Flow#

First you need to create the flow, give it a name and set the categories by calling create_flow():

You can also create the flows using the WhatsApp Flow Builder.

 1from pywa import WhatsApp
 2from pywa.types import FlowCategory
 3
 4# WhatsApp Business Account ID (WABA) is required
 5wa = WhatsApp(..., business_account_id="1234567890123456")
 6
 7flow_id = wa.create_flow(
 8    name="My New Flow",
 9    categories=[FlowCategory.CUSTOMER_SUPPORT, FlowCategory.SURVEY]
10)
11print(wa.get_flow(flow_id))
12
13# FlowDetails(id='1234567890123456', name='My New Flow', status=FlowStatus.DRAFT, ...)

Now you can start building the flow structure.

A flow is collection of screens containing components. screens can exchange data with each other and with your server.

Flow can be static; all the components settings are predefined and no interaction is required from your server. Or it can be dynamic; your server can respond to screen actions and determine the next screen to display (or close the flow) and the data to provide to it.

Note

WORK IN PROGRESS

I really recommend you to read the Flow JSON Docs before you continue. A full guide will be added soon.

Every component on the FlowJSON, has a corresponding class in pywa.types.flows:

Category

Types

Static elements

TextHeading, TextSubheading, TextBody, TextCaption, Image

Collect data

Form, TextInput, TextArea, RadioButtonsGroup, CheckboxGroup, Dropdown, OptIn

Navigation

EmbeddedLink, Footer

here is an example of static flow:

simple_sign_up_flow.py#
 1static_flow = FlowJSON(
 2    screens=[
 3        Screen(
 4            id="SIGN_UP",
 5            title="Finish Sign Up",
 6            terminal=True,
 7            layout=Layout(
 8                type=LayoutType.SINGLE_COLUMN,
 9                children=[
10                    Form(
11                        name="form",
12                        children=[
13                            first_name := TextInput(
14                                name="first_name",
15                                label="First Name",
16                                input_type=InputType.TEXT,
17                                required=True,
18                            ),
19                            last_name := TextInput(
20                                name="last_name",
21                                label="Last Name",
22                                input_type=InputType.TEXT,
23                                required=True,
24                            ),
25                            email := TextInput(
26                                name="email",
27                                label="Email Address",
28                                input_type=InputType.EMAIL,
29                                required=True,
30                            ),
31                            Footer(
32                                label="Done",
33                                enabled=True,
34                                on_click_action=Action(
35                                    name=FlowActionType.COMPLETE,
36                                    payload={
37                                        "first_name": first_name.form_ref,
38                                        "last_name": last_name.form_ref,
39                                        "email": email.form_ref,
40                                    },
41                                ),
42                            ),
43                        ],
44                    )
45                ],
46            ),
47        )
48    ],
49)

Which is the equivalent of the following flow json:

simple_sign_up_flow.json#
 1{
 2  "version": "3.0",
 3  "screens": [
 4    {
 5      "id": "SIGN_UP",
 6      "title": "Finish Sign Up",
 7      "terminal": true,
 8      "layout": {
 9        "type": "SingleColumnLayout",
10        "children": [
11          {
12            "type": "Form",
13            "name": "form",
14            "children": [
15              {
16                "type": "TextInput",
17                "name": "first_name",
18                "label": "First Name",
19                "input-type": "text",
20                "required": true
21              },
22              {
23                "type": "TextInput",
24                "name": "last_name",
25                "label": "Last Name",
26                "input-type": "text",
27                "required": true
28              },
29              {
30                "type": "TextInput",
31                "name": "email",
32                "label": "Email Address",
33                "input-type": "email",
34                "required": true
35              },
36              {
37                "type": "Footer",
38                "label": "Done",
39                "on-click-action": {
40                  "name": "complete",
41                  "payload": {
42                    "first_name": "${form.first_name}",
43                    "last_name": "${form.last_name}",
44                    "email": "${form.email}"
45                  }
46                },
47                "enabled": true
48              }
49            ]
50          }
51        ]
52      }
53    }
54  ]
55}

And this is how it looks like on WhatsApp:

Static Sign Up Screen

Here is example of dynamic flow:

dynamic_sign_up_flow.py#
 1dynamic_flow = FlowJSON(
 2    data_api_version=utils.Version.FLOW_DATA_API,
 3    routing_model={},
 4    screens=[
 5        Screen(
 6            id="SIGN_UP",
 7            title="Finish Sign Up",
 8            terminal=True,
 9            data=[
10                first_name_helper_text := ScreenData(key="first_name_helper_text", example="Enter your first name"),
11                is_last_name_required := ScreenData(key="is_last_name_required", example=True),
12                is_email_enabled := ScreenData(key="is_email_enabled", example=False),
13            ],
14            layout=Layout(
15                type=LayoutType.SINGLE_COLUMN,
16                children=[
17                    Form(
18                        name="form",
19                        children=[
20                            first_name := TextInput(
21                                name="first_name",
22                                label="First Name",
23                                input_type=InputType.TEXT,
24                                required=True,
25                                helper_text=first_name_helper_text.data_key,
26                            ),
27                            last_name := TextInput(
28                                name="last_name",
29                                label="Last Name",
30                                input_type=InputType.TEXT,
31                                required=is_last_name_required.data_key,
32                            ),
33                            email := TextInput(
34                                name="email",
35                                label="Email Address",
36                                input_type=InputType.EMAIL,
37                                enabled=is_email_enabled.data_key,
38                            ),
39                            Footer(
40                                label="Done",
41                                on_click_action=Action(
42                                    name=FlowActionType.COMPLETE,
43                                    payload={
44                                        "first_name": last_name.form_ref,
45                                        "last_name": last_name.form_ref,
46                                        "email": email.form_ref,
47                                    },
48                                ),
49                            ),
50                        ],
51                    )
52                ],
53            ),
54        )
55    ],
56)

Which is the equivalent of the following flow json:

dynamic_sign_up_flow.json#
 1{
 2    "version": "3.0",
 3    "data_api_version": "3.0",
 4    "routing_model": {},
 5    "screens": [
 6        {
 7            "id": "SIGN_UP",
 8            "title": "Finish Sign Up",
 9            "data": {
10                "first_name_helper_text": {
11                    "type": "string",
12                    "__example__": "Enter your first name"
13                },
14                "is_last_name_required": {
15                    "type": "boolean",
16                    "__example__": true
17                },
18                "is_email_enabled": {
19                    "type": "boolean",
20                    "__example__": false
21                }
22            },
23            "terminal": true,
24            "layout": {
25                "type": "SingleColumnLayout",
26                "children": [
27                    {
28                        "type": "Form",
29                        "name": "form",
30                        "children": [
31                            {
32                                "type": "TextInput",
33                                "name": "first_name",
34                                "label": "First Name",
35                                "input-type": "text",
36                                "required": true,
37                                "helper-text": "${data.first_name_helper_text}"
38                            },
39                            {
40                                "type": "TextInput",
41                                "name": "last_name",
42                                "label": "Last Name",
43                                "input-type": "text",
44                                "required": "${data.is_last_name_required}"
45                            },
46                            {
47                                "type": "TextInput",
48                                "name": "email",
49                                "label": "Email Address",
50                                "input-type": "email",
51                                "enabled": "${data.is_email_enabled}"
52                            },
53                            {
54                                "type": "Footer",
55                                "label": "Done",
56                                "on-click-action": {
57                                    "name": "complete",
58                                    "payload": {
59                                        "first_name": "${form.first_name}",
60                                        "last_name": "${form.last_name}",
61                                        "email": "${form.email}"
62                                    }
63                                }
64                            }
65                        ]
66                    }
67                ]
68            }
69        }
70    ]
71}

And this is how it looks like on WhatsApp:

Dynamic Sign Up Screen

After you have the flow json, you can update the flow with update_flow_json():

1from pywa import WhatsApp
2
3wa = WhatsApp(...)
4
5wa.update_flow_json(flow_id, flow_json=flow_json)

The flow_json argument can be FlowJSON, a dict, json str, json file path or open(json_file) obj.

You can get the FlowDetails of the flow with get_flow() to see if there is validation errors needed to be fixed:

1from pywa import WhatsApp
2
3wa = WhatsApp(...)
4
5print(wa.get_flow(flow_id))
6
7# FlowDetails(id='1234567890123456', name='My New Flow', validation_errors=(...))

If you are working back and forth on the FlowJSON, you can do something like this:

 1from pywa import WhatsApp
 2from pywa.errors import FlowUpdatingError
 3from pywa.types.flows import *
 4
 5wa = WhatsApp(..., business_account_id="1234567890123456")
 6
 7flow_id = "123456789" # wa.create_flow(name="My New Flow") # run this only once
 8
 9your_flow_json = FlowJSON(...)  # keep edit your flow
10
11try:
12    wa.update_flow_json(flow_id, flow_json=your_flow_json)
13    print("Flow updated successfully!")
14except FlowUpdatingError:
15    print("Error updating flow")
16    print(wa.get_flow(flow_id).validation_errors)

This way you always know if there is validation errors that needed to be fixed.

To test your flow you need to sent it:

Sending Flow#

Note

WORK IN PROGRESS

Flow is just a FlowButton attached to a message. Let’s see how to send text message with flow:

 1from pywa import WhatsApp
 2from pywa.types import FlowButton
 3
 4wa = WhatsApp(...)
 5
 6wa.send_message(
 7    to="1234567890",
 8    text="Hi, You need to finish your sign up!",
 9    buttons=FlowButton(
10        title="Finish Sign Up",
11        flow_id="1234567890123456",  # The `static_flow` flow id from above
12        flow_token="AQAAAAACS5FpgQ_cAAAAAD0QI3s.",
13        mode=FlowStatus.DRAFT,
14        flow_action_type=FlowActionType.NAVIGATE,
15        flow_action_screen="SIGN_UP", # The screen id to open when the user clicks the button
16    )
17)

Let’s walk through the arguments:

  • title - The button title that will appear on the bottom of the message.

  • flow_id - The flow id that you want to send.

  • flow_token - A unique token you generate for each flow message. The token is used to identify the flow message when you receive a response from the user.

  • mode - If the flow is in draft mode, you must specify the mode as FlowStatus.DRAFT.

  • flow_action_type - The action to take when the user clicks the button. The action can be FlowActionType.NAVIGATE or FlowActionType.DATA_EXCHANGE. since this example is static flow, we will use FlowActionType.NAVIGATE.

  • flow_action_screen - The first screen id to display when the user clicks the button.

    If you don’t care about the dynamic example, you can skip to Getting Flow Completion message.

Handling Flow requests and responding to them#

This part is only for dynamic flow. here we will demonstrate how to handle the DATA_EXCHANGE requests and respond to them.

Note

Since the requests and responses can contain sensitive data, such as passwords and other personal information, all the requests and responses are encrypted using the WhatsApp Business Encryption.

Before you continue, you need to sign and upload the business public key. First you need to generate a private key and a public key:

Generate a public and private RSA key pair by typing in the following command:

>>> openssl genrsa -des3 -out private.pem 2048

This generates 2048-bit RSA key pair encrypted with a password you provided and is written to a file.

Next, you need to export the RSA Public Key to a file.

>>> openssl rsa -in private.pem -outform PEM -pubout -out public.pem

This exports the RSA Public Key to a file.

Once you have the public key, you can upload it using the set_business_public_key() method.

1from pywa import WhatsApp
2
3wa = WhatsApp(...)
4
5wa.set_business_public_key(open("public.pem").read())

Every request need to be decrypted using the private key. so you need to provide it when you create the WhatsApp object:

1from pywa import WhatsApp
2
3wa = WhatsApp(..., business_private_key=open("private.pem").read())

Now you are ready to handle the requests.

Just one more thing, the default decryption & encryption implementation is using the cryptography library, So you need to install it:

>>> pip3 install cryptography

Or when installing PyWa:

>>> pip3 install "pywa[cryptography]"

In dynamic flow, when the user perform an action with type of FlowActionType.DATA_EXCHANGE you will receive a request to your server with the payload and you need to determine if you want to continue to the next screen or complete the flow.

So in our dynamic example (dynamic_sign_up_flow.py), we have just one screen: SIGN_UP.

 1Screen(
 2    id="SIGN_UP",
 3    title="Finish Sign Up",
 4    terminal=True,
 5    data=[
 6        first_name_helper_text := ScreenData(key="first_name_helper_text", example="Enter your first name"),
 7        is_last_name_required := ScreenData(key="is_last_name_required", example=True),
 8        is_email_enabled := ScreenData(key="is_email_enabled", example=False),
 9    ],
10    ...
11)

The terminal argument is set to True which means that this screen can end the flow.

As you can see, this screen gets data that help it to be dynamic.

For example, we have TextInput that gets the user’s last name. We want to be able to decide if it’s required or not, so if we already have the user’s last name in our database, we don’t require it. This can be done by setting the required argument to a dynamic value taken from the data map. this data can be provided by the previous screen, by our server or when sending the flow.

We want to demonstrate how to handle dynamic flow with our server, so we will send the flow with action type of FlowActionType.DATA_EXCHANGE, So when the user clicks the button, we will receive a request to our server with the ation, flow_token and the screen which requested the data.

We need to tell WhatsApp to send the requests to our serve. update_flow_metadata():

1from pywa import WhatsApp
2
3wa = WhatsApp(...)
4
5wa.update_flow_metadata(
6    flow_id="1234567890123456",  # The `dynamic_flow` flow id from above
7    endpoint_uri="https://your-server.com/flow"
8)

Let’s send the flow. this time with an image:

 1from pywa import WhatsApp
 2from pywa.types import FlowButton, FlowActionType, FlowStatus
 3
 4wa = WhatsApp(...)
 5
 6wa.send_image(
 7    to="1234567890",
 8    image="https://t3.ftcdn.net/jpg/03/82/73/76/360_F_382737626_Th2TUrj9PbvWZKcN9Kdjxu2yN35rA9nU.jpg",
 9    caption="Hi, You need to finish your sign up!",
10    buttons=FlowButton(
11        title="Finish Sign Up",
12        flow_id="1234567890123456",  # The `dynamic_flow` flow id from above
13        flow_token="AQAAAAACS5FpgQ_cAAAAAD0QI3s.",
14        mode=FlowStatus.DRAFT,
15        flow_action_type=FlowActionType.DATA_EXCHANGE,  # This time we want to exchange data
16    )
17)

Here we set the flow_action_type to FlowActionType.DATA_EXCHANGE since we want to exchange data with the server. So, when the user opens the flow, we will receive a request to our server to provide the screen to open and the data to provide to it.

Let’s register a callback function to handle this request:

 1from pywa import WhatsApp
 2from pywa.types import FlowRequest, FlowResponse
 3
 4wa = WhatsApp(
 5    ...,
 6    business_private_key=open("private.pem").read(),  # provide your business private key
 7)
 8
 9@wa.on_flow_request(endpoint="/flow")  # The endpoint we set above
10def on_support_request(_: WhatsApp, req: FlowRequest) -> FlowResponse:
11    print(req.flow_token)  # use this to indentify the user who you sent the flow to
12    return FlowResponse(
13        version=req.version,
14        screen="SIGN_UP",  # The screen id to open
15        data={
16            "first_name_helper_text": "Please enter your first name",
17            "is_last_name_required": True,
18            "is_email_enabled": False,
19        },
20    )

We need to provide our business private key to decrypt the request and encrypt the response.

After that. we are registering a callback function to handle the request. The callback function will receive the FlowRequest object and should return FlowResponse object.

A callback function can be return or raise FlowTokenNoLongerValid or FlowRequestSignatureAuthenticationFailed to indicate that the flow token is no longer valid or the request signature authentication failed.

In our example, we returning our dynamic data to the SIGN_UP screen.

Of course, it can be more complex, if you have multiple screens, you can return data from them and then decide what screen to open next or complete the flow.

If you want example of more complex flow, you can check out the Sign up Flow Example.

Getting Flow Completion message#

Note

WORK IN PROGRESS

When the user completes the flow, you will receive a request to your webhook with the payload you sent when you completed the flow.

WhatApp recommends to send the user a summary of the flow response.

Here is how to listen to flow completion request:

1from pywa import WhatsApp
2from pywa.types import FlowCompletion
3
4wa = WhatsApp(...)
5
6@wa.on_flow_completion()
7def on_flow_completion(_: WhatsApp, flow: FlowCompletion):
8    print(f"The user {flow.from_user.name} just completed the {flow.token} flow!")
9    print(flow.response)

The .response attribute is the payload you sent when you completed the flow.