Back To Top

February 28, 2024

Booking Appointments with WhatsApp Flows

  • 3

You can create interactive messages with WhatsApp Flows that allow users to take actions right within the messaging app. You can make displays for user interaction with flows. For instance, you can make straightforward input forms to receive feedback or gather leads. You may even create intricate Flows that span several screens to schedule appointments.

This tutorial will show you how to create a Node.js application that lets users schedule appointments over WhatsApp Flows. Using the WhatsApp Business Platform, you will first build a Flow. Next, you will set up a webhook to get the response from the Flow and schedule the meeting.

 

Create a Flow

On the left-side menu of your WhatsApp Manager dashboard, select Account tools. Then, click Flows.

Click Create Flow, located in the top-right corner.

create flow graphic

 

In the dialog box that appears, fill in the details for the appointment Flow:

  • Name — Type BookAppointment, or choose another name you like.
  • Categories — Select Appointment booking.
  • Template — Choose Book an appointment. You’ll use the template because it contains the necessary elements for booking an appointment. These elements include screens for the appointment details, user details entry, appointment summary, and company terms display. You can further customize the template to suit your use case.
  • Book an appointment graphic
  • Click Submit to create the Flow.

    To the right of the Builder UI, you can see a preview of the Flow. The user can select the location and date of the appointment on the appointment screen. The user will enter their information on the details screen. The appointment booking summary is shown on the summary screen. The final screen displays the terms of the company.

    While you make edits, the Flow stays in draft form. Right now, it can only be shared for testing with your team. You’d have to publish it if you wanted to share it with a big audience. But after you publish, you can’t modify the Flow. Leave it as a draft for the time being and move on to the following stage, where you will still need to enter the endpoint URL for this appointment flow.

Configuring the Flow’s Endpoint

WhatsApp Flows lets you connect to an external endpoint. This endpoint can provide dynamic data for your Flow and control routing. It also receives user-submitted responses from the Flow.

For testing purposes, this article uses Glitch to host the endpoint. Using Glitch is entirely optional, and not required to use Flows. You can clone the endpoint code from GitHub and run it in any environment you prefer.

Access the endpoint code in Glitch and remix it to get your unique domain. To remix it, click Remix at the top of the page. A unique domain will appear as a placeholder in the input element on the right side of the Glitch page.

Before proceeding, let’s walk through the code. There are four JavaScript files in the src directory: encryption.jsflow.jskeyGenerator.js, and server.js. The entry file is server.js, so let’s look at it first.

server.js

The server.js file starts by configuring the Express application to use the express.json middleware to parse incoming JSON requests. Then, it loads the environment variables needed for the endpoint.

const { APP_SECRET, PRIVATE_KEY, PASSPHRASE, PORT = "3000" } = process.env;

APP_SECRET is used in signature verification. It helps you check whether a message is coming via WhatsApp and, therefore, is safe to process. You’ll add it to the .env file.

To access your APP_SECRET, navigate to your dashboard on the app on Meta for Developers. In the left navigation pane under App settings, choose Basic. Click Show under App secret and copy the secret. Then, return to Glitch, open the .env file, and create a variable named APP_SECRET with the value of the secret you copied.

PRIVATE_KEY helps decrypt the messages received. The PASSPHRASE will be used to verify the private key. Along with the private key, you also need its corresponding public key, which you’ll upload later. Never use the private keys for your production accounts here. Create a temporary private key for testing on Glitch, and then replace it with your production key in your own infrastructure.

Generate the public-private key pair by running the command below in the Glitch terminal. Replace <your-passphrase> with your designated passphrase. Access the Glitch terminal by clicking the TERMINAL tab at the bottom of the page.

node src/keyGenerator.js <your-passphrase>

Copy the passphrase and private key and paste them to the .env file. Click on the file labeled .env on the left sidebar, then click on ✏️ Plain text on top. Do not edit it directly from the UI, as it will break your key formatting.

After you set the environment variables, copy the public key that you generated and upload the public key via the Graph API.

The server.js file also contains a POST endpoint that performs different steps:

  • Checks that the private key is present:
       if (!PRIVATE_KEY) {
         throw new Error('Private key is empty. Please check your env variable 
"PRIVATE_KEY".');
       }
  • Validates the request signature using the isRequestSignatureValid function found at the bottom of the file:
if(!isRequestSignatureValid(req)) {
        // Return status code 432 if request signature does not match.
        // To learn more about return error codes visit: 
    https://developers.facebook.com/docs/whatsapp/flows/reference/error-codes#endpoint_error_codes
         return res.status(432).send();
       }
  • Decrypts incoming messages using the decryptRequest function found in the encryption.js file:
      let decryptedRequest = null;
      try {
         decryptedRequest = decryptRequest(req.body, PRIVATE_KEY, PASSPHRASE);
      } catch (err) {
      console.error(err);
      if (err instanceof FlowEndpointException) {
         return res.status(err.statusCode).send();
      }
      return res.status(500).send();
      }

     const { aesKeyBuffer, initialVectorBuffer, decryptedBody } = decryptedRequest;
     console.log("💬 Decrypted Request:", decryptedBody);
  • Decides what Flow screen to display to the user. You’ll look at the getNextScreen function in detail later.

const screenResponse = await getNextScreen(decryptedBody);

       console.log("👉 Response to Encrypt:", screenResponse);
  • Encrypts the response to be sent to the user:
res.send(encryptResponse(screenResponse, aesKeyBuffer, initialVectorBuffer));

encryption.js

This file contains the logic for encrypting and decrypting messages exchanged for security purposes. This tutorial won’t focus on the workings of the file.

keyGenerator.js

This file helps generate the private and public keys, as you saw earlier. As with the encryption.js file, this tutorial won’t explore the keyGenerator.js file in detail.

flow.js

The logic for handling the Flow is housed in this file. It starts with an object assigned the name SCREEN_RESPONSES. The object contains screen IDs with their corresponding details, such as the preset data used in the data exchanges. This object is generated from Flow Builder under “…” > Endpoint > Snippets > Responses. In the same object, you also have another ID, SUCCESS, that is sent back to the client device when the Flow is successfully completed. This closes the Flow.

The getNextScreen function contains the logic that guides the endpoint on what Flow data to display to the user. It starts by extracting the necessary data from the decrypted message.

const { screen, data, version, action, flow_token } = decryptedBody;

WhatsApp Flows endpoints usually receive three requests:

You can find their details in the endpoint documentation.

The function handles the health check and error notifications using if statements and responds accordingly, as shown in the snippet below:

// handle health check request
if (action === "ping") {
    return {
        version,
        data: {
            status: "active",
        },
    };
}

// handle error notification
if (data?.error) {
    console.warn("Received client error:", data);
    return {
        version,
        data: {
            acknowledged: true,
        },
    };
}

When a user clicks the Flow’s call to action (CTA) button, an INIT action fires. This action returns the appointment screen together with the data. It also disables the location, date, and time drop-downs to ensure the user fills out all fields.

For instance, the date drop-down is enabled only when the location drop-down is filled. The enabling and disabling of the fields are handled when a data_exchange request is received.

// handle initial request when opening the flow and display APPOINTMENT screen
if (action === "INIT") {
    return {
        ...SCREEN_RESPONSES.APPOINTMENT,
        data: {
            ...SCREEN_RESPONSES.APPOINTMENT.data,
            // these fields are disabled initially. Each field is enabled when previous fields are selected
            is_location_enabled: false,
            is_date_enabled: false,
            is_time_enabled: false,
        },
    };
}

For data_exchange actions, a switch case structure is used to determine what data to send back based on the screen ID. If the screen ID is APPOINTMENT, the drop-down fields are enabled only when the preceding drop-downs are selected.

// Each field is enabled only when previous fields are selected
 is_location_enabled: Boolean(data.department),
 is_date_enabled: Boolean(data.department) && Boolean(data.location),
 is_time_enabled: Boolean(data.department) && Boolean(data.location) && Boolean(data.date)

For the DETAILS screen, the titles of data object properties, such as location and department, are extracted from the SCREEN_RESPONSES.APPOINTMENT.data object. This code assumes there’s a valid match, so note that it may throw an error if no matching object is found.

Now, take an instance of the location object. The selection of the specific location object is determined by matching the id property of the objects in the array with the value of data.location.

const departmentName = SCREEN_RESPONSES.APPOINTMENT.data.department.find(
    (dept) => dept.id === data.department
).title;
const locationName = SCREEN_RESPONSES.APPOINTMENT.data.location.find(
    (loc) => loc.id === data.location
).title;
const dateName = SCREEN_RESPONSES.APPOINTMENT.data.date.find(
    (date) => date.id === data.date

).title;

The values are then concatenated and returned in the response to render the SUMMARY screen.

const appointment = `${departmentName} at ${locationName}
${dateName} at ${data.time}`;

const details = `Name: ${data.name}
Email: ${data.email}
Phone: ${data.phone}
"${data.more_details}"`;

return {
    ...SCREEN_RESPONSES.SUMMARY,
    data: {
        appointment,
        details,
        // return the same fields sent from client back to submit in the next step
        ...data,
    },
};

After the SUMMARY screen is submitted from the client, a success response is sent to the client device to mark the Flow as complete. The flow_token is a unique identifier that you can set when sending the Flow to the user.

// send success response to complete and close the flow
return {
    ...SCREEN_RESPONSES.SUCCESS,
    data: {
        extension_message_response: {
            params: {
                flow_token,
            },
        },
    },
};

The TERMS screen has no data to be exchanged, so the endpoint doesn’t handle it.

Prev Post

Building a Simple WhatsApp Flow

Next Post

Voice Notes and Video Calls: Elevating Conversations on WhatsApp

post-bars
Mail Icon

Newsletter

Get Every Weekly Update & Insights

3 thoughts on “Booking Appointments with WhatsApp Flows

Wonderful web site. Lots of useful info here. I’m sending it to a few friends ans additionally sharing in delicious. And obviously, thanks to your effort!

Reply

Its like you read my mind! You appear to know a lot about this, like you wrote the book in it or something. I think that you could do with some pics to drive the message home a little bit, but instead of that, this is fantastic blog. An excellent read. I will certainly be back.

Reply

I spent over three hours reading the internet today, and I haven’t come across any more compelling articles than yours. I think it’s more than worth it. I believe that the internet would be much more helpful than it is now if all bloggers and website proprietors produced stuff as excellent as you did.

Reply

Leave a Comment