How to build a WhatsApp bot for to-do lists using MessageBird’s Programmable Conversations API

Marcel Corso

MessageBird recently launched Programmable Conversations. It lets companies blend communications platforms like WhatsApp, Messenger and SMS into their systems — using a single API.

I wanted to give it a whirl, so I built a WhatsApp bot to-do list, because who doesn’t need an automated to-do list to help organize their day? It may sound complicated, but it was actually easy, and I’d like to tell you all about it.

Now, I work at MessageBird, so I could just dive in and start building. If you try this, you’ll need to request early access. But once you’re set up with a WhatsApp channel, you can log on to the Dashboard on the MessageBird website and get started.

The first thing I did was read the docs. I learned that, in order to get messages from the bot, I would have to use a webhook. This meant that my bot would need to be accessible from the internet. Since I was just starting to code it, I decided to use ngrok. It creates a tunnel from the public internet to your dear localhost port 5007. Engage!

ngrok http 5007 -region eu -subdomain todobot

Next, I needed to do a call to the Programmable Conversations API to create the webhook. It’s a POST to and it looks something like this:

func main() {// define the webhook json payload
       wh := struct {
               Events    []string `json:"events"`
               ChannelID string   `json:"channelId"`
               URL       string   `json:"url"`
       } {// we would like to be notified on the URL
               URL:       "",
               // whenever a message gets created
               Events:    []string{"message.created"},
               // on the WhatsApp channel with ID
               ChannelID: "23a780701b8849f7b974d8620a89a279",
       }// encode the payload to json
       var b bytes.Buffer
       err := json.NewEncoder(&b).Encode(&wh)
       if err != nil {
       }// create the http request and set authorization header
       req, err := http.NewRequest("POST", "", &b)
       req.Header.Set("Authorization", "AccessKey todo-your-access-key")
       req.Header.Set("Content-Type", "application/json")// fire the http request
       client := &http.Client{}
       resp, err := client.Do(req)
       if err != nil {
       defer resp.Body.Close()// is everything ok?
       body, _ := ioutil.ReadAll(resp.Body)
       if resp.StatusCode >= http.StatusBadRequest {
               panic(fmt.Errorf("Bad response code from api when trying to create webhook: %s. Body: %s", resp.Status, string(body)))
       } else {
               log.Println("All good. response body: ", string(body))

Sweet. Now the Conversations API is going to do a POST request to: whenever a new message gets created on the WhatsApp channel you set up earlier.

This is what a webhook payload looks like:

          "text":"add buy milk"

We want to answer those messages. Let’s start by echoing them, what do you say?

// define the structs where we'll parse the webhook payload intype whPayload struct {
       Conversation conversation `json:"conversation"`
       Message      message      `json:"message"`
       Type         string       `json:"type"`
}type message struct {
       ID        string  `json:"id"`
       Direction string  `json:"direction"`
       Type      string  `json:"type"`
       Content   content `json:"content"`
}type content struct {
       Text string `json:"text"`
}type conversation struct {
       ID string `json:"id"`
}func main() {
      http.HandleFunc("/create-hook", createHookHandler)
      log.Fatal(http.ListenAndServe(*httpListenAddress, nil))
}// createHookHandler is an http handler that will handle webhook requests
func createHookHandler(w http.ResponseWriter, r *http.Request) {
       // parse the incoming json payload
       whp := &whPayload{}
       err := json.NewDecoder(r.Body).Decode(whp)
       if err != nil {
               log.Println("Err: got weird body on the webhook")
               fmt.Fprintf(w, "Internal Server Error")
       }if whp.Message.Direction != "received" {
               // you will get *all* messages on the webhook. Even the ones this bot sends to the channel. We don't want to answer those.
               fmt.Fprintf(w, "ok")
       }// echo: respond what we get
       err = respond(whp.Conversation.ID, whp.Message.Content.Text)

       if err != nil {
               log.Println("Err: ", err)
               fmt.Fprintf(w, "Internal Server Error")return
       fmt.Fprintf(w, "ok")

Now, for the interesting part. Do a POST request to:

“<conversationID>/messages” to answer the request.

func respond(conversationID, responseBody string) error {

       u := fmt.Sprintf("", conversationID)msg := message{
               Content: content{
                       Text: responseBody,
               Type: "text",
       }var b bytes.Buffer
       err := json.NewEncoder(&b).Encode(&msg)
       if err != nil {
               return fmt.Errorf("Error encoding buffer: %v", err)
       }req, err := http.NewRequest("POST", u.String(), &b)
       req.Header.Set("Authorization", "AccessKey todo-your-access-key")
       req.Header.Set("Content-Type", "application/json")client := &http.Client{}
       resp, err := client.Do(req)
       if err != nil {
               return err
       defer resp.Body.Close()body, _ := ioutil.ReadAll(resp.Body)
       if resp.StatusCode != http.StatusCreated {
               return fmt.Errorf("Bad response code from api when trying to create message: %s. Body: %s", resp.Status, string(body))
       }log.Println("All good. Response body: ", string(body))
       return nil

There. This is all you need to create a bot that acts like 5-year-old human.

Now, let’s make a push towards building the whole to-do list. First, modify the createHookHandler function a bit so it calls the new handleMessage function instead of respond.

func createHookHandler(w http.ResponseWriter, r *http.Request) {
       err = handleMessage(whp)

handle will simplistically parse the messages, do some work, and pick the response. Let’s look at the “add” command:

func handleMessage(whp *whPayload) error {
       // every conversation has a todo list
       list := manager.fetch(whp.Conversation.ID)
       // parse the command from the message body: it's the first word
       text := whp.Message.Content.Text
       text = regexp.MustCompile(" +").ReplaceAllString(text, " ")
       parts := strings.Split(text, " ")
       command := strings.ToLower(parts[0])
       // default message
       responseBody := "I don't understand. Type 'help' to get help."
       switch command {
       case "add":
               if len(parts) < 2 {
                       return respond(whp.Conversation.ID, "err... the 'add' command needs a second param: the todo item you want to save. Something like 'add buy milk'.")
               // get the item from the message body
               item := strings.Join(parts[1:], " ")list.add(item)
               responseBody = "added."
       return respond(whp.Conversation.ID, responseBody)

Here, we set up:list := manager.fetch(whp.Conversation.ID). Basically, “manager” is a concurrency safe map that maps conversation IDs to to-do lists.

A to-do list is a concurrency safe string slice. All in memory!

Another important thing! You can archive conversations. In some applications, like CRMs, it’s important to keep track of certain interactions — to track the effectiveness of customer support employees, for example. The Conversations API lets you archive a conversation to “close” the topic. If the user/customer sends another message, the Conversations API will open a new topic automatically.

Also. Doing PATCH request to{id} with the right status on the body allows you to archive the conversation with that id. We do this with the “bye” command:

case "bye":
               responseBody = "bye!"

archiveConversation will do the PATCH request and manager.close(whp.Conversation.ID) will remove the to-do list conversation.

But hey, Programmable Conversations is an omni-channel solution. What if you wanted to reuse the code of the bot for a different platform, like WeChat? How would you go about it?

Just create a new webhook to target that channel! A webhook that sends requests to the same url we used for WhatsApp!

This will work because the handler code always uses the conversationID from the webhook payload to answer the messages instead of a hardcoded channelID. MessageBird’s Conversations API will automatically determine the channel for the conversation to send your message over.

Do you want to build your own bot? Take a look at the full code on Github:, request early access to WhatsApp via this link and start building directly. Happy botting!