How to Create a WhatsApp E-commerce Bot with Twilio and Go

June 10, 2025
Written by
Popoola Temitope
Contributor
Opinions expressed by Twilio contributors are their own
Reviewed by

How to Create a WhatsApp Ecommerce Bot with Twilio and Go

In today’s digital era, businesses increasingly leverage messaging platforms like WhatsApp to enhance customer engagement. As one of the most popular messaging apps, WhatsApp allows businesses to effectively utilize its chat features to reach more customers directly within the app.

In order to streamline the online shopping experience, ecommerce businesses can offer their services or products to their customers via a WhatsApp bot. Customers can interact with the bot to view available products, add and remove items from their cart, and review their cart list.

In this tutorial, you will learn how to create an interactive WhatsApp ecommerce bot for your customers using Go and the Twilio WhatsApp Business API.

How the application will work

The WhatsApp ecommerce bot will provide a seamless shopping experience, allowing customers to browse products and manage their cart using predefined commands to communicate with the application backend. These are:

  • ADD TO CART: Adds an item to the user's cart
  • REGISTER: Registers an account for the user, required before they can start shopping
  • REMOVE FROM CART: Removes an item from their shopping cart
  • VIEW CART: Views all items in their shopping cart
  • VIEW PRODUCTS: Views all available products

Requirements

To complete this tutorial, you will need the following:

Create a new Go project

To get started, let's create a new Go project. To do that, open your terminal, navigate to the directory where you want to create the project, and run the commands below to set it up.

mkdir twilio-whatsapp-ecommerce
cd twilio-whatsapp-ecommerce
go mod init twilio-whatsapp-ecommerce

After initializing the project, open the project folder in your preferred Go IDE or code editor.

Set the required environment variables

Let's create a .env file to store the application's credentials as environment variables. To do this, inside the project's root directory, create a .env file, and add the following configuration to it.

DB_CONNECTION=<db_username>:<db_password>@tcp(localhost:3306)/ecommerce
FORWARDING_URL=<forwarding-url>/uploads/
TWILIO_ACCOUNT_SID=<twilio_account_sid>
TWILIO_AUTH_TOKEN=<twilio_auth_token>
TWILIO_WHATSAPP_NUMBER=whatsapp:+<twilio_whatsapp_number>

Now, in the application environment variables above, replace the <db_username> and <db_password> placeholders with your MySQL database username and password, respectively.

Create the application's database schema

Next, you need to create database tables to store application data; these are: ''customer'', ''products'', and ''carts''. To do this, log in to your MySQL database server, create a new database named "ecommerce". Then, run the SQL code below in your MySQL client to create the three tables.

CREATE TABLE customers (
  ID int(11) NOT NULL PRIMARY KEY AUTO_INCREMENT,
  Fullname varchar(99) NOT NULL DEFAULT '',
  PhoneNo varchar(99) NOT NULL DEFAULT ''
);

CREATE TABLE products (
  ID int(11) NOT NULL PRIMARY KEY AUTO_INCREMENT,
  productName varchar(99) NOT NULL DEFAULT '',
  productPrice varchar(99) NOT NULL DEFAULT '',
  productImage varchar(999) NOT NULL DEFAULT ''
);

CREATE TABLE `carts` (
  ID int(11) NOT NULL PRIMARY KEY AUTO_INCREMENT,
  customerID int(11) NOT NULL,
  productID int(11) NOT NULL
);

The SQL query above will create three tables:

  • customers: This stores the customer's phone number and full name.
  • products: This stores details of available products, including the product ID, name, price, and image name.
  • carts: This stores customer orders by linking customer IDs to the products they have added to their cart.

Retrieve your Twilio credentials

Now, let’s retrieve your Twilio Account SID and Auth Token. To do this, log in to your Twilio Console dashboard. You will find them under the Account Info section, as shown in the screenshot below.

Screenshot showing Twilio account info with Account SID, Auth Token, and trial phone number.

Copy the credentials, then replace the <twilio_account_sid> and <twilio_auth_token> placeholders in .env with them.

Connect to the Twilio WhatsApp Sandbox

To connect your test WhatsApp number to the Twilio WhatsApp Sandbox, navigate to Explore Products > Messaging > Try it out > Send a WhatsApp message, from the Twilio Console dashboard menu, as shown in the screenshot below.

Screenshot of Twilio console showing instructions to connect to WhatsApp Sandbox with a QR code and phone number.

Copy the displayed Twilio WhatsApp number and replace the <twilio_whatsapp_number> placeholder in .env with it. Now, follow the instructions on the Twilio "Try WhatsApp" page by sending the provided join message to the Twilio WhatsApp number, as shown in the screenshot below.

Smartphone screen displaying a WhatsApp chat with Twilio Sandbox on a dark theme interface.

Install the official Twilio Go Helper Library

Next, you need to install the Twilio Go helper library, which simplifies interactions with Twilio services such as Twilio's Business WhatsApp API (and Programmable Messaging, SMS, and Verify) using the commands below.

go get github.com/twilio/twilio-go

Install the other required dependencies

Next, let’s install the MySQL driver to connect to the database server, GoDotEnv to load environment variables into the application, and Gorilla/Mux to handle routing. Install these dependencies by running the command below.

go get github.com/go-sql-driver/mysql github.com/joho/godotenv github.com/gorilla/mux

Create the WhatsApp ecommerce bot logic

Now, let’s develop the application logic that allows customers to register an account, browse available products, add and remove items from their cart, and view their cart items via WhatsApp messages.

To implement these functionalities, inside the project’s root directory, create a file named main.go, and add the following code.

package main

import (
	"database/sql"
	"fmt"
	"html/template"
	"io"
	"log"
	"net/http"
	"os"
	"path/filepath"
	"strconv"
	"strings"

	_ "github.com/go-sql-driver/mysql"
	"github.com/gorilla/mux"
	"github.com/joho/godotenv"
	"github.com/twilio/twilio-go"
	apiV2010 "github.com/twilio/twilio-go/rest/api/v2010"
)

var (
	accountSid           string
	authToken            string
	twilioWhatsAppNumber string
	dns                  string
	db                   *sql.DB
)

func handleWhatsAppMessage(w http.ResponseWriter, r *http.Request) {
	if err := r.ParseForm(); err != nil {
		http.Error(w, "Invalid request", http.StatusBadRequest)
		return
	}

	userNumber := r.FormValue("WaId")
	body := strings.TrimSpace(strings.ToUpper(r.FormValue("Body")))
	if strings.HasPrefix(body, "REGISTER ") {
		fullName := strings.TrimSpace(body[9:])
		if fullName == "" {
			sendWhatsAppMessage(userNumber, "Invalid format. Please reply with 'REGISTER <Full Name>'.")
			return
		}
		if registerUser(userNumber, fullName) {
			sendWhatsAppMessage(userNumber, "You have been registered successfully! 🎉")
		} else {
			sendWhatsAppMessage(userNumber, "Registration failed. Please try again.")
			return
		}
	}

	if !isUserRegistered(userNumber) {
		sendWhatsAppMessage(userNumber, "You are not registered with our WhatsApp ecommerce bot. \n Please reply with 'REGISTER <Full Name>' to register.")
		return
	}

	if strings.HasPrefix(body, "ADD-TO-CART ") {
		productIDStr := strings.TrimSpace(body[12:])
		if productIDStr == "" {
			sendWhatsAppMessage(userNumber, "Invalid format. Please use 'ADD-TO-CART <Product ID>'.")
			return
		}
		productID, err := strconv.Atoi(productIDStr)
		if err != nil {
			sendWhatsAppMessage(userNumber, "Invalid Product ID. Please enter a valid number.")
			return
		}
		if addToCart(userNumber, productID) {
			sendWhatsAppMessage(userNumber, fmt.Sprintf("✅ Product (ID: %d) has been added to your cart!", productID))
		} else {
			sendWhatsAppMessage(userNumber, "Failed to add product to cart. Please try again.")
		}
		return
	}

	if strings.HasPrefix(body, "REMOVE-FROM-CART ") {
		productIDStr := strings.TrimSpace(body[17:])
		if productIDStr == "" {
			sendWhatsAppMessage(userNumber, "Invalid format. Use 'REMOVE-FROM-CART <Product ID>'.")
			return
		}
		productID, err := strconv.Atoi(productIDStr)
		if err != nil {
			sendWhatsAppMessage(userNumber, "Invalid Product ID. Please enter a valid number.")
			return
		}
		if removeFromCart(userNumber, productID) {
			sendWhatsAppMessage(userNumber, fmt.Sprintf("🗑️ Product (ID: %d) has been removed from your cart.", productID))
		} else {
			sendWhatsAppMessage(userNumber, "Failed to remove product from cart. Ensure it exists in your cart and try again.")
		}
		return
	}

	if body == "VIEW-PRODUCTS" {
		sendProductList(userNumber)
		return
	}

	if body == "VIEW-CART" {
		sendCartDetails(userNumber)
		return
	}

	sendWhatsAppMessage(userNumber, "Thanks for your message! Reply with:\n\n"+
		"'VIEW-PRODUCTS' to see available items.\n"+
		"'VIEW-CART' to check your cart.\n"+
		"'ADD-TO-CART <Product ID>' to add an item to your cart.\n"+
		"'REMOVE-FROM-CART <Product ID>'")
}

func sendProductList(to string) {
	baseURL := os.Getenv("FORWARDING_URL")
	rows, err := db.Query("SELECT ID, productName, productPrice, productImage FROM products")
	if err != nil {
		sendWhatsAppMessage(to, "Sorry, we couldn't fetch the product list at the moment.")
		return
	}
	defer rows.Close()
	hasProducts := false

	for rows.Next() {
		hasProducts = true
		var id int
		var name, price, imagePath string
		if err := rows.Scan(&id, &name, &price, &imagePath); err != nil {
			continue
		}
		imageFile := filepath.Base(imagePath)
		imageURL := fmt.Sprintf("%s%s", baseURL, imageFile)
		message := fmt.Sprintf("*🛒 %s*\n📌 *Product ID:* %d\n💲 *Price:* %s\n\nTo add this item to your cart, reply with:\n👉 *ADD-TO-CART %d*",
			name, id, price, id)
		sendWhatsAppMediaMessage(to, message, imageURL)
	}

	if !hasProducts {
		sendWhatsAppMessage(to, "No products are available at the moment.")
	}
}

func removeFromCart(phone string, productID int) bool {
	var customerID int
	err := db.QueryRow("SELECT ID FROM customers WHERE PhoneNo = ?", phone).Scan(&customerID)
	if err != nil {
		return false
	}

	res, err := db.Exec("DELETE FROM carts WHERE customerID = ? AND productID = ? LIMIT 1", customerID, productID)
	if err != nil {
		return false
	}
	rowsAffected, _ := res.RowsAffected()
	return rowsAffected > 0
}

func addToCart(phone string, productID int) bool {
	var customerID int
	err := db.QueryRow("SELECT ID FROM customers WHERE PhoneNo = ?", phone).Scan(&customerID)
	if err != nil {
		log.Printf("Error fetching customer ID: %v", err)
		return false
	}

	_, err = db.Exec("INSERT INTO carts (customerID, productID) VALUES (?, ?)", customerID, productID)
	if err != nil {
		log.Printf("Error adding product to cart: %v", err)
		return false
	}
	return true
}

func sendWhatsAppMediaMessage(to, message, mediaURL string) error {
	client := twilio.NewRestClientWithParams(twilio.ClientParams{
		Username: accountSid,
		Password: authToken,
	})
	params := &apiV2010.CreateMessageParams{}
	params.SetTo("whatsapp:" + to)
	params.SetFrom(twilioWhatsAppNumber)
	params.SetBody(message)
	params.SetMediaUrl([]string{mediaURL})

	_, err := client.Api.CreateMessage(params)
	if err != nil {
		log.Printf("Error sending WhatsApp media message: %v", err)
	}
	return err
}

func sendCartDetails(to string) {
	var customerID int
	err := db.QueryRow("SELECT ID FROM customers WHERE PhoneNo = ?", to).Scan(&customerID)
	if err != nil {
		sendWhatsAppMessage(to, "We couldn't retrieve your cart. Please try again.")
		return
	}

	rows, err := db.Query(`
		SELECT p.ID, p.productName, p.productPrice 
		FROM carts c 
		JOIN products p ON c.productID = p.ID 
		WHERE c.customerID = ?`, customerID)
	if err != nil {
		sendWhatsAppMessage(to, "We couldn't retrieve your cart. Please try again.")
		return
	}
	defer rows.Close()

	var cartItems []string
	var totalPrice float64
	for rows.Next() {
		var id int
		var name string
		var price float64
		if err := rows.Scan(&id, &name, &price); err != nil {
			log.Printf("Error scanning cart item: %v", err)
			continue
		}
		totalPrice += price
		cartItems = append(cartItems, fmt.Sprintf("🛒 *%s* (ID: %d) - 💲%.2f", name, id, price))
	}
	if len(cartItems) == 0 {
		sendWhatsAppMessage(to, "Your cart is empty. Add items using 'ADD-TO-CART <Product ID>'.")
	} else {
		message := fmt.Sprintf(
			"🛍️ *Your Cart:*\n%s\n\n💰 *Total: $%.2f*\n\nReply with 'REMOVE-FROM-CART <Product ID>' to remove item from cart",
			strings.Join(cartItems, "\n"), totalPrice,
		)
		sendWhatsAppMessage(to, message)
	}
}

func isUserRegistered(phone string) bool {
	var exists bool
	query := "SELECT EXISTS(SELECT 1 FROM customers WHERE PhoneNo = ?)"
	err := db.QueryRow(query, phone).Scan(&exists)
	if err != nil {
		log.Printf("Database error: %v", err)
		return false
	}
	return exists
}

func registerUser(phone, name string) bool {
	query := "INSERT INTO customers (PhoneNo, FullName) VALUES (?, ?)"
	_, err := db.Exec(query, phone, name)
	if err != nil {
		log.Printf("Error inserting user: %v", err)
		return false
	}
	return true
}

func sendWhatsAppMessage(to, message string) error {
	client := twilio.NewRestClientWithParams(twilio.ClientParams{
		Username: accountSid,
		Password: authToken,
	})
	params := &apiV2010.CreateMessageParams{}
	params.SetTo("whatsapp:" + to)
	params.SetFrom(twilioWhatsAppNumber)
	params.SetBody(message)

	_, err := client.Api.CreateMessage(params)
	if err != nil {
		log.Printf("Error sending WhatsApp message: %v", err)
	}
	return err
}

func addProductHandler(w http.ResponseWriter, r *http.Request) {
	type PageData struct {
		SuccessMessage string
	}

	if r.Method == "POST" {
		err := r.ParseMultipartForm(10 << 20)
		if err != nil {
			http.Error(w, "File too large", http.StatusBadRequest)
			return
		}
		productName := r.FormValue("productName")
		productPrice := r.FormValue("productPrice")

		file, handler, err := r.FormFile("productImage")
		if err != nil {
			http.Error(w, "Error uploading file", http.StatusBadRequest)
			return
		}
		defer file.Close()
		cleanFilename := strings.ReplaceAll(handler.Filename, " ", "_")
		filePath := filepath.Join("uploads", cleanFilename)

		dst, err := os.Create(filePath)
		if err != nil {
			http.Error(w, "Error saving file", http.StatusInternalServerError)
			return
		}
		defer dst.Close()

		_, err = io.Copy(dst, file)
		if err != nil {
			http.Error(w, "Error writing file", http.StatusInternalServerError)
			return
		}
		query := "INSERT INTO products (productName, productPrice, productImage) VALUES (?, ?, ?)"
		_, err = db.Exec(query, productName, productPrice, filePath)
		if err != nil {
			http.Error(w, "Database error", http.StatusInternalServerError)
			return
		}
		renderTemplate(w, "templates/add-product.html", PageData{SuccessMessage: "Product added successfully!"})
		return
	}
	renderTemplate(w, "templates/add-product.html", PageData{})
}

func renderTemplate(w http.ResponseWriter, tmpl string, data interface{}) {
	tmplParsed, err := template.ParseFiles(tmpl)
	if err != nil {
		http.Error(w, "Error loading template", http.StatusInternalServerError)
		return
	}
	tmplParsed.Execute(w, data)
}

func main() {
	if err := godotenv.Load(); err != nil {
		log.Println("Warning: No .env file found")
	}
	accountSid = os.Getenv("TWILIO_ACCOUNT_SID")
	authToken = os.Getenv("TWILIO_AUTH_TOKEN")
	twilioWhatsAppNumber = os.Getenv("TWILIO_WHATSAPP_NUMBER")
	dns = os.Getenv("DB_CONNECTION")
	var err error

	db, err = sql.Open("mysql", dns)
	if err != nil {
		log.Fatalf("Error connecting to database: %v", err)
	}
	defer db.Close()

	if _, err := os.Stat("uploads"); os.IsNotExist(err) {
		os.Mkdir("uploads", os.ModePerm)
	}

	r := mux.NewRouter()
	r.HandleFunc("/add-product", addProductHandler).Methods("POST", "GET")
	r.HandleFunc("/webhook", handleWhatsAppMessage).Methods("POST")
	r.PathPrefix("/uploads/").Handler(http.StripPrefix("/uploads/", http.FileServer(http.Dir("uploads"))))
	log.Println("Server started at http://localhost:8080")
	log.Fatal(http.ListenAndServe(":8080", r))
}

In the above code, we:

  • Import all the necessary application packages, such as MySQL, Twilio, and GoDotEnv, etc.
  • The handleWhatsAppMessage() function processes incoming WhatsApp bot commands such as "VIEW-PRODUCTS", "ADD-TO-CART", "REMOVE-FROM-CART", and "VIEW-CART" and replies with appropriate messages.
  • The sendProductList() function sends the list of available products to the user whenever they send the "VIEW-PRODUCTS" command. The product image is sent along with the product details using the sendWhatsAppMediaMessage() function.
  • The addToCart(), removeFromCart(), and sendCartDetails() functions manage cart items by adding, removing, and displaying them based on the bot commands received from the customer.
  • The main() function is the application's entry point, where the environment variables are loaded, the database connection is established to the MySQL database server, and the uploads directory is made publicly accessible using the http.FileServer() method. This allows the uploaded product images to be accessible by the Twilio endpoint. Finally, the server is started using the http.ListenAndServe() method.

Add the application template

Next, let’s create the "add-product" interface to allow the admin to add new products to the database. To do this, inside the project’s root folder, create a new folder named templates. Inside this new folder, create a file named add-product.html and add the following code to it.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Add Product</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container mt-5">
    <h2 class="text-center">Add New Product</h2>
    {{if .SuccessMessage}}
    <div class="alert alert-success" role="alert">
        {{.SuccessMessage}}
    </div>
    {{end}}
    <form action="/add-product" method="POST" enctype="multipart/form-data" class="p-4 border rounded bg-light">
        <div class="mb-3">
            <label for="productName" class="form-label">Product Name</label>
            <input type="text" class="form-control" id="productName" name="productName" required>
        </div>
        <div class="mb-3">
            <label for="productPrice" class="form-label">Product Price</label>
            <input type="number" class="form-control" id="productPrice" name="productPrice" step="0.01" required>
        </div>
        <div class="mb-3">
            <label for="productImage" class="form-label">Product Image</label>
            <input type="file" class="form-control" id="productImage" name="productImage" accept="image/*" required>
        </div>
        <button type="submit" class="btn btn-primary">Add Product</button>
    </form>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

Configure the Twilio WhatsApp Webhook

To enable the application to process incoming WhatsApp messages, you need to configure the Twilio WhatsApp webhook. First, let’s make the Go application accessible over the internet using ngrok. To do so, run the command below.

ngrok http http://localhost:8080

This will generate a forwarding URL in your terminal. Copy it as shown in the screenshot below.

Terminal screenshot with Ngrok session details, showing online status and forwarding address.

In your application .env file, replace the <forwarding-url> placeholder with the generated forwarding URL.

Next, on the Twilio "Try WhatsApp" page, click on Sandbox Settings and configure the Sandbox with the following settings.

  • When a message comes in: paste the generated forwarding URL and append "/webhook"
  • Method: "POST"

Then, click the Save button to save the settings, as shown in the screenshot below.

Twilio Sandbox settings screen showing webhook URLs and HTTP methods for incoming messages and status callbacks.

Test the application

Finally, let’s test the WhatsApp ecommerce bot to ensure that the application functions as expected. To do this, let’s start the application development server using the command.

go run main.go

After starting the development server, open http://localhost:8080/add-product in your browser to add new products as shown in the screenshot below.

A web form for adding new products with fields for product name, price, and image upload, and a button to add the product.

Next, send a message from your WhatsApp number to the Twilio WhatsApp number. Interact with the ecommerce bot to register an account, browse available products, and add or remove items from your cart by sending the commands below to the Twilio WhatsApp number:

  • "REGISTER <FullName>": Send this command to register your ecommerce shopping account
  • "VIEW-PRODUCTS": Use this command to view available products
  • "ADD-TO-CART <Product_ID>": Use this command to add a product to your cart by replacing <Product_ID> with the actual product ID
  • "REMOVE-FROM-CART <Product ID>": Use this command to remove a product from your cart
  • "VIEW-CART": Use this command to view the items in your cart

The screenshot below shows how the WhatsApp ecommerce bot works.

Smartphone displaying a chat interface with a shoe product listing and options to view or add to cart.

That’s how to create a WhatsApp ecommerce bot with Twilio and Go

Building a WhatsApp ecommerce bot with Twilio and Go enables businesses to automate customer interactions, making shopping more convenient. In this tutorial, you have learned how to create an interactive WhatsApp ecommerce bot using the Twilio WhatsApp API and Go.

Popoola Temitope is a mobile developer and a technical writer who loves writing about frontend technologies. He can be reached on LinkedIn.

Ecommerce icons created by mangsaabguru on Flaticon.