Group SMS with Twilio and Java

August 20, 2021
Written by
Reviewed by

Group SMS with Twilio and Java

Life's complicated enough without having to forward messages between friends and family members to make sure everyone is up to date with what's going on. In this post I'll share how I set up a Twilio number that I gave as "my" phone number to my kids' schools so that all the messages sent to and from that number are automatically forwarded to both me and my wife, and either of us can reply. I expect you can think of situations in your own life where this could be handy: package deliveries, party planning, appointment reminders, the list is long.

In this post I'll use Java, but the same approach would work in any language which you can build a web app in. If you're comfortable with JavaScript then SMS Forwarding to Multiple Numbers on Code Exchange would be a great starting point.

What are we building?

Everything here is based around a single Twilio phone number, and a group of you and your family members or friends who have cell phones. You can give the Twilio number to others as if it is your own number.

We'll set up the Twilio number so that when someone texts it the message will be forwarded on to everyone in your group. I'll use two group members in this example, but the code is designed so that you can use as many as you want.

Diagram of a phone sending an SMS to a Twilio number which is forwarded on to two other phones

When someone from outside your group (the left-hand phone) sends an SMS to the Twilio number, the message will be forwarded to everyone in the group (the phones on the right). The messages will appear to have been sent from the Twilio number, so the real sender's phone number will be added at the start of the message.

When anyone in your group replies to the Twilio number, they should put the real destination number at the start of the message, which will be removed before the message is forwarded on. Everyone in your group will get a copy of the message, too:

Diagram of one of the phones from the previous diagram replying to the SMS. The message is sent on to all participants.

Note that if you want to send a message from one group member to all others, you could do this by prefixing the message with your own number.

Prerequisites

To build this you will need:

Using Twilio Programmable Messaging

To tell Twilio how to behave in response to incoming SMS we will use webhooks. When a message comes into our phone number, Twilio will make an HTTP request to a URL we provide. We will build and app to send instructions back in the HTTP response to tell Twilio what to do next.

Same diagram as above with the addition of an HTTP request/response from Twilio to "your app"

The instructions in the HTTP response are written in TwiML, including multiple Message tags which tell Twilio to send new text messages. The content and destination for those messages depend on who sent the incoming message and what they said; attributes which are part of the HTTP request. Read on to see how to build this app using Java and Spring Boot

Building the app

Spring Boot is the most popular framework for building web apps in Java. I like to start new projects with the Spring Initializr. If you want to follow along with coding, use this link which sets up the same options as I used, or find the finished project on GitHub.

Download, unzip and open the generated project in your IDE. There will be a single class in src/main/java, in the com.example.smsgroupbroadcast package, called SmsGroupBroadcastApplication. You won't need to edit that class but it has a main method which can be used to run the application.

In the same package, create a new class called SmsHandler, in a new file called SmsHandler.java. To keep things from getting too complicated we'll put all our code in that class. By the time we're done it will be about 100 lines long.

Start with the code which we need to run at startup:


@RestController
public class SmsHandler {

   private final Set<String> groupPhoneNumbers;

   public SmsHandler() {
       groupPhoneNumbers = Set.of(System.getenv("GROUP_PHONE_NUMBERS").split(","));
   }

// more code will go in here

}

[this code including imports on GitHub]

The @RestController annotation tells Spring that this class should be scanned for methods that can handle HTTP requests. We'll be writing one soon.

The Set<String> groupPhoneNumbers on line 4 is initialized in the constructor by reading an environment variable whose value is a comma-separated string of phone numbers in E.164 format. These numbers should be your cell phone and the phones of everyone else in your group (everyone on the right side of the diagrams above). You can set environment variables directly in your IDE:

Screenshot showing how to set environment variables in IntelliJ IDEA

IntelliJ IDEA Environment Variable configuration

A method for handling HTTP requests

To make it easier to write the correct TwiML we will use the Twilio Java helper library. Add the following snippet into the <dependencies> section of pom.xml, the Maven config file which is at the top level of your project:

<dependency>
  <groupId>com.twilio.sdk</groupId>
  <artifactId>twilio</artifactId>
  <version>8.18.0</version>
</dependency>

We always recommend using the latest version of the Twilio Helper Libraries. At the time of writing the latest version is 8.18.0 and you can check for newer versions at mvnrepository.com.

You may have to tell your IDE to reload Maven changes at this point. Then, add this code to your SmsHandler class:


@RequestMapping(
    value = "/sms",
    method = {RequestMethod.GET, RequestMethod.POST},
    produces = "application/xml")
@ResponseBody
public String handleSmsWebhook(
        @RequestParam("From") String fromNumber,
        @RequestParam("To")   String twilioNumber,
        @RequestParam("Body") String messageBody) {

    List<Message> outgoingMessages;

    if (groupPhoneNumbers.contains(fromNumber)) {
            outgoingMessages = messagesSentFromGroup(fromNumber, twilioNumber, messageBody);

    } else {
            outgoingMessages = messagesSentToGroup(fromNumber, twilioNumber, messageBody);
    }

    MessagingResponse.Builder responseBuilder = new MessagingResponse.Builder();
    outgoingMessages.forEach(responseBuilder::message);
    return responseBuilder.build().toXml();
}

[this code with imports on GitHub]

This method starts with a lot of annotations, which are recognised by Spring:

  • @RequestMapping tells Spring that this method should be call for GET and POST requests to /sms, and that the Content-type on the response is application/xml.
  • @ResponseBody tells Spring that the return value from this method should be used as the body of the HTTP response.
  • The @RequestParam annotations tell Spring to extract the named parameters from the HTTP request and pass them as arguments to the method. This works for both GET and POST even though the parameters are in different parts of the HTTP request.

The body of the method creates a list of Message objects which is populated differently depending on whether the incoming SMS that triggered this webhook is from a group member or not (line 12). We'll define the messagesSentFromGroup and messagesSentToGroup methods in a moment, but first notice how the list of Messages is added to a MessagingResponse using forEach on lines 19-21.

Dealing with messages from non group members

If the groupPhoneNumbers.contains(fromNumber) check in the method above returns false then we know that the message has come from someone outside of our group. In this case the messagesSentToGroup method is called to get a list of Message objects representing a copy of the incoming message to be forwarded to each group member:

private List<Message> messagesSentToGroup(String fromNumber, String twilioNumber, String messageBody) {

    List<Message> messages = new ArrayList<>();

    String finalMessage = "From " + fromNumber + " " + messageBody;
    groupPhoneNumbers.forEach(groupMemberNumber ->
                messages.add(createMessageTwiml(groupMemberNumber, twilioNumber, finalMessage))
    );

    return messages;
}

[this code with imports on GitHub]

We build up the finalMessage and add a Message to the list for each group member. I created a small helper method called createMessageTwiml to turn the Twilio helper library's builder-pattern code into a one-liner. I felt that was worth doing as we will build Message objects a few times in this class. That method looks like this:

private Message createMessageTwiml(String to, String from, String body) {
    return new Message.Builder()
        .to(to)
        .from(from)
        .body(new Body.Builder(body).build())
        .build();
}

[this code with imports on GitHub]

Messages from group members

When a group member sends a message to the Twilio number, they should preface it with the real destination number:

An SMS saying "+4477xxxx Thank you!"

The "Thank you!" part of the message will be sent on from the Twilio number, to the number at the start of the message. Everyone in the group will get a copy of it, too. To do this, the messagesSentFromGroup method has to split up the message body, check if it starts with a phone number (sending a helpful reminder back if it doesn't), and build a list of outbound messages, like this:


private List<Message> messagesSentFromGroup(String fromNumber, String twilioNumber, String messageBody) {
    List<Message> messages = new ArrayList<>();

    String[] messageParts = messageBody.split("\\s+", 2);

    String e164Regex = "\\+[0-9]+";
    if (messageParts.length != 2 || !messageParts[0].matches(e164Regex)) {
        return List.of(createHowToMessage(fromNumber, twilioNumber));
    }

    String realToNumber = messageParts[0];
    String realMessageBody = messageParts[1];

    // add the message to the non-group recipient
    messages.add(
        createMessageTwiml(realToNumber, twilioNumber, realMessageBody)
    );

    // send a copy of the message to everyone in the group except the sender
    String groupCopyMessage = "To " + realToNumber + " " + realMessageBody;
    groupPhoneNumbers.forEach(groupMemberNumber -> {
        if (!groupMemberNumber.equals(fromNumber)) {
            messages.add(
                createMessageTwiml(groupMemberNumber, twilioNumber, groupCopyMessage));
        }
    });

    return messages;
}

private Message createHowToMessage(String fromNumber, String twilioNumber){
    return createMessageTwiml(fromNumber, twilioNumber,
        "To send a message to someone outside your group, " +
        "don't forget to include the destination phone number at the start, " +
        "eg '+44xxxx Ahoy!'");
}

[this code with imports on GitHub]

The messagesSentFromGroup method might seem like a lot of code, but it splits roughly in half. Lines 4-9 deal with splitting the input and checking to see if it starts with a phone number. The e164Regex tests for a + followed by numbers, which corresponds to E.164 formatting for phone numbers.

The rest of messagesSentFromGroup builds up a list of all the messages we need to send and returns it.

Finally there's a separate method for createHowToMessage, which I thought was worth breaking out to keep the longer method more readable.

Code Complete

The SmsHandler class is complete, so the code is finished. For reference, the full class is on GitHub.

Running your code locally

The easiest way to start the app is by using your IDE to run the main method in the SmsGroupBroadcasterApplication class that we saw earlier. If you prefer using a command line terminal then run ./mvnw spring-boot:run from the top level of the project. Either way remember to set the GROUP_PHONE_NUMBERS environment variable.

Once the app has started up, you can browse to http://localhost:8080/sms?From=__from__&To=__to__&Body=__body__ and you should see a response like:

<Response>
  <Message from="__to__" to="GROUP_MEMBER_1">
    <Body>From __from__ __body__</Body>
  </Message>
  <Message from="__to__" to="GROUP_MEMBER_2">
    <Body>From __from__ __body__</Body>
  </Message>
</Response>

GROUP_MEMBER_1 and GROUP_MEMBER_2 will be the numbers in your GROUP_PHONE_NUMBERS environment variable.

Using your code with Twilio

For Twilio to be able to use your app for its webhooks, it will need a public URL. There are a lot of ways to deploy Java code online, but for simplicity while you're working on it I recommend using ngrok.

After installing ngrok you can run ngrok http 8080 and you will see an https Forwarding URL which you will need to set for when "a message comes in" on your phone number config page. Don't forget to add the path of /sms onto the URL:

screenshot of setting the "when a message comes in" webhook in the Twilio console.

You can also set the webhook URL for a phone number using the Twilio CLI:

twilio phone-numbers:update <PHONE_NUMBER> --sms-url=<URL>

if the URL is a localhost address, Twilio CLI will create an ngrok tunnel for you.

If you want to test how this works before giving out the number for real, either recruit some friends with phones or use other Twilio numbers to try it out. Once you're happy with it, move the app to an always-on public cloud so that you don't have to keep your dev machine running 24/7. That's outside of the scope of this post but Spring's documentation on packaging and deployment has a lot of options. You only need to serve one HTTP request per SMS sent to your Twilio number so the requirements are very low.

Further credit

There are a lot of enhancements you could make to this app, for example:

  • For large groups, it might be useful to include the "From" number in the copy messages.
  • Include an "address book" in your app so you can put the name instead of number when sending to non-group recipients.
  • There are no checks to make sure that HTTP requests actually came from Twilio. It's not a huge problem for this app because if anyone else calls your app they'll get some TwiML back, but it won't cost you anything. If you want to add this check, I wrote Securing your Twilio webhooks in Java which shows how.

Whatever you're building with Twilio, I'd love to hear about it. Get in touch with me @MaximumGilliard on Twitter or mgilliard@twilio.com. Happy coding!