Build a Real-Time Support Chat with Go and Twilio Conversations

September 16, 2025
Written by
Oluyemi Olususi
Contributor
Opinions expressed by Twilio contributors are their own
Reviewed by

Build a Real-Time Support Chat with Go and Twilio Conversations

Customer support is a critical part of user experience, and in today’s world people expect instant communication with businesses. Real-time messaging solutions help bridge this gap by enabling agents and customers to engage in live conversations with ease. With the Twilio Conversations API and Go, you can build a secure and scalable live chat application that connects agents with customers through a shared conversation interface.

In this tutorial, we’ll build a real-time support system using Twilio Conversations and the Go programming language. We’ll focus on generating secure access tokens with Go, initializing the Twilio Conversations client on the front end, and creating or joining conversations based on identity. In Twilio, an identity is a unique string that represents and authenticates each participant in a conversation.

What you’ll learn

  • How to create access tokens for conversations using Go
  • How to initialize a Twilio Conversations client on the frontend
  • How to create or join a shared support conversation between an agent and a customer
  • How to differentiate message authors visually in the chat interface

By the end of this tutorial, you’ll have a working real-time live chat support demo built with Go and Twilio and understand how to extend it to suit your own business needs.

Prerequisites

You will need the following for this tutorial:

  • A basic understanding of and experience with Go (version 1.22.0 or newer)
  • A Twilio account (either free or paid). Sign up for a free trial account if you don’t have one
  • Node.js or any local static server (e.g., http-server) to serve HTML files
  • Ngrok (optional, but recommended for exposing localhost on the public internet)

Set up the project

To begin, create a new project directory, wherever you create your Go projects, and change into it by running the following commands:

mkdir realtime-support-demo
cd realtime-support-demo

While in the realtime-support-demo directory, create a new Go module by running the following command.

go mod init realtime-support-demo

This will create a go.mod file in the root of your project which defines the module path and lists all dependencies required by your module.

Install the project's dependencies

This project requires two packages:

Install them using the following command:

go get github.com/joho/godotenv github.com/twilio/twilio-go

Set the required environment variables

Create a .env file at the root of your project to store your Twilio credentials, and add the following keys to the file:

TWILIO_ACCOUNT_SID=<YOUR_TWILIO_ACCOUNT_SID>
TWILIO_AUTH_TOKEN=<YOUR_TWILIO_AUTH_TOKEN>
TWILIO_API_KEY=<YOUR_TWILIO_API_KEY>
TWILIO_API_SECRET=<YOUR_TWILIO_API_SECRET>
TWILIO_CONVERSATION_SERVICE_SID=<YOUR_TWILIO_CONVERSATION_SERVICE_SID>

Then, replace the placeholder values with the details retrieved from your Twilio Console, by following these steps:

  • Log in to the Twilio Console
  • Copy the Account SID and Auth Token from the Account Info panel
  • Replace <YOUR_TWILIO_ACCOUNT_SID> and <YOUR_TWILIO_AUTH_TOKEN> with the corresponding copied values
View Account Info

Next, to obtain the API Key and secret, scroll down and click Go to API Keys:

View API Keys

You will be redirected to the API keys & tokenspage listing all your created API keys. If you don’t already have an API key, click Create API Key and

  • Give it a name, such as "Real-time Support Chat" so that it's easy to identify
  • Leave the "Region" field with the default value
  • Choose Standard as the Key type

Then, click Create. After creation, your API key (SID) and secret will be shown like this:

API Key and API Secret
The secret will only be shown once, so don't reload or navigate away from the page until you've copied the information following the instructions below.

So, in .env, replace:

  • <YOUR_TWILIO_API_KEY> with the value of the SID field
  • <YOUR_TWILIO_API_SECRET> with the value of the Secret field

Check the box to indicate that you have copied the credentials and click Done.

Finally, obtain the Conversation Service ID from the Twilio Console as follows:

  • Go to Explore products > Conversations > Manage > Services
  • You can either use the "Default Conversations Service", or create a new one if you prefer
  • Copy the Service SID in the SID column and replace <YOUR_TWILIO_CONVERSATION_SERVICE_SID> in your .env file

Once these values are configured, you're ready to start building the real-time chat logic for your application.

Be sure to add the .env file to your .gitignore file to prevent these sensitive credentials from being exposed by Git if you publish your code to a public repository.

Set up token generation, conversation creation, and frontend routing

Twilio Conversations is a powerful API that enables users to have real-time chat experiences. It supports structured messaging workflows, participant identity, and message history across platforms.

In this section, you’ll create a handler that generates a secure token required by the Twilio Conversations client to authenticate each user (such as an agent or customer).

Start by creating a folder named handler and a file called handler.go inside it. Paste the following content into the file:

package handler

import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "os"
    "strings"
    "github.com/twilio/twilio-go"
    "github.com/twilio/twilio-go/client/jwt"
    conversations "github.com/twilio/twilio-go/rest/conversations/v1"
)

func TokenHandler(w http.ResponseWriter, r *http.Request) {
    identity := r.URL.Query().Get("identity")
    if identity == "" {
        http.Error(w, "Identity is required", http.StatusBadRequest)
        return
    }
    accountSid := os.Getenv("TWILIO_ACCOUNT_SID")
    apiKey := os.Getenv("TWILIO_API_KEY")
    apiSecret := os.Getenv("TWILIO_API_SECRET")
    serviceSid := os.Getenv("TWILIO_CONVERSATION_SERVICE_SID")
    if accountSid == "" || apiKey == "" || apiSecret == "" || serviceSid == "" {
        http.Error(w, "Internal server configuration error", http.StatusInternalServerError)
        return
    }
    params := jwt.AccessTokenParams{
        AccountSid:    accountSid,
        SigningKeySid: apiKey,
        Secret:        apiSecret,
        Identity:     identity,
    }
    jwtToken := jwt.CreateAccessToken(params)
    chatGrant := &jwt.ChatGrant{
        ServiceSid: serviceSid,
    }
    jwtToken.AddGrant(chatGrant)
    token, err := jwtToken.ToJwt()
    if err != nil {
        log.Printf("[INFO]: Failed to generate JWT: %v", err)
        http.Error(w, "Failed to generate token", http.StatusInternalServerError)
        return
    }
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(map[string]string{"token": token})
}

This handler takes in a user's identity, validates your Twilio credentials from environment variables, then generates a signed JWT with a ChatGrant to authorize access to your Twilio Conversation Service. The response is returned as a JSON object containing the token.

Next, update handler.go by pasting the following just below the TokenHandler() function:

func CreateConversationHandler(w http.ResponseWriter, r *http.Request) {
    client := twilio.NewRestClient()
    
    const uniqueName = "support-room-001"
    identity := r.URL.Query().Get("identity")
    
    convo, err := client.ConversationsV1.FetchConversation(uniqueName)
    if err != nil {
        log.Println("[INFO]: No existing conversation found, creating new one")
        createParams := &conversations.CreateConversationParams{}
        createParams.SetUniqueName(uniqueName)
        createParams.SetFriendlyName("Customer Support Room")
        convo, err = client.ConversationsV1.CreateConversation(createParams)
        if err != nil {
            http.Error(w, fmt.Sprintf("Failed to create conversation: %v", err), http.StatusInternalServerError)
            return
        }
    }

    convoSid := *convo.Sid
    log.Printf("[INFO]: Using conversation SID: %s", convoSid)
    if identity != "" {
        params := &conversations.CreateConversationParticipantParams{}
        params.SetIdentity(identity)
        _, err := client.ConversationsV1.CreateConversationParticipant(convoSid, params)
        if err != nil && !strings.Contains(err.Error(), "Participant already exists") {
            log.Printf("[WARNING]: Failed to add participant '%s': %v", identity, err)
        } else {
            log.Printf("[INFO]: Participant '%s' added or already exists", identity)
        }
    }

    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(map[string]string{"conversationSid": convoSid})
}

func ServeStaticFiles(w http.ResponseWriter, r *http.Request) {
    path := r.URL.Path
    switch path {
    case "/", "/customer":
        http.ServeFile(w, r, "static/customer.html")
    case "/agent":
        http.ServeFile(w, r, "static/agent.html")
    case "/styles.css":
        http.ServeFile(w, r, "static/styles.css")
    case "/chat.js":
        http.ServeFile(w, r, "static/chat.js")
    default:
        http.NotFound(w, r)
    }
}

The CreateConversationHandler() checks if a support conversation with a predefined unique name exists. If it doesn’t, it creates one. It also ensures that the user (agent or customer) is added to the conversation as a participant, ignoring errors if they already exist. This allows your frontend to seamlessly join the same conversation room for every session.

The ServeStaticFiles() handler maps frontend file paths to serve the appropriate HTML pages and assets based on the request. It ensures the correct chat interface (customer or agent) and the required styles and scripts are loaded.

This setup enables the entire loop: generate a token, create/join a conversation, and serve the UI — all in one place.

Check here on GitHub for the complete content of handler.go file in context.

Build the user interface

Now that the backend functionality has been implemented let’s build the user interface in which the agent and customer will interact. Begin by creating a folder named static and adding the following files inside it:

  • agent.html
  • customer.html
  • chat.js
  • styles.css

Create the agent chat page

Start with the agent.html file and paste the following content:

<!DOCTYPE html>
<html>
  <head>
    <title>Agent Support Chat</title>
    <script src="https://sdk.twilio.com/js/conversations/v2.3/twilio-conversations.min.js"></script>
    <script src="/chat.js"></script>
    <link rel="stylesheet" href="/styles.css" />
  </head>
  <body>
    <div class="chat-container">
      <h1>Agent Chat</h1>
      <div id="messages" class="messages"></div>
      <div class="input-area">
        <input id="messageInput" type="text" placeholder="Type a message..." />
        <button onclick="sendMessage()">Send</button>
      </div>
    </div>
    <script>
      initChat("agent");
    </script>
  </body>
</html>

This HTML file creates the interface for the support agent. It includes the Twilio Conversations JavaScript SDK, loads the shared chat logic via chat.js, and applies styles from styles.css. Once the page loads, it calls initChat("agent") to initialize the chat session for the agent.

Create the customer chat page

Next, paste the following content into the customer.html file:

<!DOCTYPE html>
<html>
  <head>
    <title>Customer Support Chat</title>
    <script src="https://sdk.twilio.com/js/conversations/v2.3/twilio-conversations.min.js"></script>
    <script src="/chat.js"></script>
    <link rel="stylesheet" href="/styles.css" />
  </head>
  <body>
    <div class="chat-container">
      <h1>Customer Chat</h1>
      <div id="messages" class="messages"></div>
      <div class="input-area">
        <input id="messageInput" type="text" placeholder="Type a message..." />
        <button onclick="sendMessage()">Send</button>
      </div>
    </div>
    <script>
      initChat("customer");
    </script>
  </body>
</html>

Just like the agent page, this customer interface loads the required scripts and styles. The only difference is that it identifies the user as "customer" when initializing the chat. This helps the backend differentiate between participants.

Define the shared chat logic

Open chat.js and add the following code:

let client;
let conversation;

async function initChat(identity) {
  console.log(`Starting chat initialization for ${identity}`);
  try {
    const res = await fetch(`/token?identity=${identity}`);
    if (!res.ok) throw new Error(`Failed to fetch token: ${res.statusText}`);
    const { token } = await res.json();
    client = new Twilio.Conversations.Client(token);
    console.log(`Twilio Conversations client created`);
    await new Promise((resolve, reject) => {
      client.on("stateChanged", (state) => {
        console.log(`Client state changed to: ${state}`);
        if (state === "initialized") resolve();
        if (state === "failed") reject(new Error("Client initialization failed"));
      });
      client.on("connectionError", (error) => {
        console.error(`Twilio client connection error: ${error.message}`);
      });
    });

    console.log(`Twilio client fully initialized`);
    
    const convoRes = await fetch(`/create-conversation?identity=${identity}`);
    if (!convoRes.ok) throw new Error(`Failed to create conversation`);
    const { conversationSid } = await convoRes.json();
    console.log(`Conversation SID: ${conversationSid}`);
    conversation = await client.getConversationBySid(conversationSid);
    console.log(`Joined conversation: ${conversation.sid}`);

    conversation.on("messageAdded", (message) => {
      const messagesDiv = document.getElementById("messages");
      const author = message.author;
      const isMe = author === client.user.identity;
      const messageClass = isMe ? "my-message" : "their-message";
      const msgEl = document.createElement("p");
      msgEl.className = messageClass;
      msgEl.innerHTML = `<strong>${author}:</strong> ${message.body}`;
      messagesDiv.appendChild(msgEl);
      messagesDiv.scrollTop = messagesDiv.scrollHeight;
      messagesDiv.scrollTop = messagesDiv.scrollHeight;
    });
  } catch (error) {
    console.error(`Chat initialization error:`, error);
  }
}

async function sendMessage() {
  const input = document.getElementById("messageInput");
  const message = input.value.trim();
  if (!message) return;
  input.value = "";
  try {
    if (!conversation) throw new Error("No conversation initialized");
    await conversation.sendMessage(message);
    console.log(`Message sent: ${message}`);
  } catch (err) {
    console.error(`Send message error: ${err.message}`);
  }
}

This script handles the chat flow. It begins by fetching a token from the "/token" endpoint using the provided identity, then creates a Twilio.Conversations.Client(). Once the client is initialized, it fetches or creates a conversation from the server and joins it. Messages sent and received are dynamically appended to the chat window, keeping both participants in sync.

Add styling for the chat interface

Paste the following into the styles.css file:

body {
  font-family: Arial, sans-serif;
  background-color: #f4f4f9;
  margin: 0;
  padding: 0;
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100vh;
}

.chat-container {
  width: 400px;
  background-color: white;
  border-radius: 10px;
  box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
  overflow: hidden;
  display: flex;
  flex-direction: column;
}

h1 {
  background-color: #007bff;
  color: white;
  margin: 0;
  padding: 15px;
  text-align: center;
  font-size: 1.5em;
}

.messages {
  flex-grow: 1;
  padding: 20px;
  overflow-y: auto;
  max-height: 400px;
}

.messages p {
  margin: 10px 0;
  padding: 10px;
  border-radius: 5px;
  max-width: 70%;
  word-wrap: break-word;
}

.messages p strong {
  color: inherit;
  font-weight: bold;
}

/* Differentiate customer and agent messages */
.messages p:nth-child(even) {
  align-self: flex-start;
}

.messages p:nth-child(odd) {
  align-self: flex-end;
  margin-left: auto;
}

.input-area {
  display: flex;
  padding: 10px;
  border-top: 1px solid #ddd;
}

.input-area input {
  flex-grow: 1;
  padding: 10px;
  border: 1px solid #ccc;
  border-radius: 5px 0 0 5px;
  outline: none;
}

.input-area button {
  padding: 10px 20px;
  background-color: #007bff;
  color: white;
  border: none;
  border-radius: 0 5px 5px 0;
  cursor: pointer;
  transition: background-color 0.3s;
}

.input-area button:hover {
  background-color: #0056b3;
}

.my-message {
  background-color: #0056b3;
  color: white;
  align-self: flex-end;
  margin-left: auto;
  padding: 10px;
  border-radius: 8px;
  max-width: 70%;
  margin: 8px 0;
}

.their-message {
  background-color: #e9ecef;
  color: #212529;
  align-self: flex-start;
  margin-right: auto;
  padding: 10px;
  border-radius: 8px;
  max-width: 70%;
  margin: 8px 0;
}

This provides the basic styling to distinguish messages between participants, formats the chat box, and enhances the overall user interface.

Put it all together

To bring everything together, create a main.go file in the root of your project and paste the following content into it:

package main

import (
    "log"
    "net/http"
    "os"

    "github.com/joho/godotenv"
    "github.com/settermjd/realtime-support-demo/handler"
)

func main() {
    if err := godotenv.Load(); err != nil {
        log.Fatal("[ERROR]: Error loading .env file")
    }
    serviceId := os.Getenv("TWILIO_CONVERSATION_SERVICE_SID")
    if serviceId == "" {
        log.Fatal("[ERROR]: TWILIO_CONVERSATION_SERVICE_SID is required")
    }
    

    log.Printf("[INFO]Using Conversation Service SID: %s", serviceId)

    mux := http.NewServeMux()
    mux.HandleFunc("/token", handler.TokenHandler)
    mux.HandleFunc("/create-conversation", handler.CreateConversationHandler)
    mux.HandleFunc("/", handler.ServeStaticFiles)
    log.Println("[INFO]: Server running at http://localhost:8080")
    log.Fatal(http.ListenAndServe(":8080", mux))
}

This Go program loads environment variables, verifies that essential credentials like the conversation service SID are set, and registers routes to serve the token generator, create conversation participants, and deliver static frontend files. Once the server starts, it’s ready to power your real-time support chat system.

Spin it up and see the magic

With everything set up, it’s time to test your application. Run the following command to start it:

go run main.go

The server will start on port 8080.

Now, open two browser tabs with the following URIs:

  • http://localhost:8080/agent
  • http://localhost:8080/customer

Try sending messages between the agent and the customer to verify that the chat experience works.

Chat between Agent and Customer

If you encounter an "Invalid Access Token" error, double-check that your JWT is generated correctly. Ensure it includes the correct identity, ChatGrant (with the correct service SID), and that it has not expired. Use jwt.io to decode and inspect your token for debugging purposes. Also, confirm that all required environment variables are correctly loaded.

That’s a wrap

This real-time support chat system built with Go and Twilio's Conversations API offers a solid starting point for teams looking to improve customer engagement through live messaging. It’s lightweight and easy to set up and demonstrates how seamlessly Go can integrate with Twilio’s robust messaging APIs.

While this demo keeps things simple with hardcoded roles and a single conversation, there’s plenty of room for growth. You can enhance it by supporting multiple chat rooms, storing messages in a database, integrating authentication, or adding notifications to alert agents of new message arrivals.

I hope you found this tutorial helpful. To explore more features, be sure to check out Twilio’s official documentation. You can find the full source code on GitHub to fork, extend, and adapt to your project. Happy coding!

Oluyemi is a tech enthusiast with a telecommunications engineering background who leverages his passion for solving everyday challenges by building web and mobile software. A full-stack Software Engineer and prolific technical writer. He shares his expertise through numerous technical articles while exploring new programming languages and frameworks. You can find him at: https://www.linkedin.com/in/yemiwebby/ and https://github.com/yemiwebby.

Communication icons created by Freepik - Flaticon.