How to Build a REST API for a Real Estate Agency with Twilio TaskRouter, Java, and Spring Boot

July 08, 2022
Written by
Pedro Lopes
Contributor
Opinions expressed by Twilio contributors are their own
Reviewed by

header - How to Build a REST API for a Real Estate Agency with Twilio TaskRouter, Java, and Spring Boot

Many businesses use a routing system to delegate a task to the correct agent that will execute it. An example is a real estate agency. In this kind of business, each client has an economic profile, and so each one can be served by a different type of salesperson.

The customer of a real estate agency is commonly called a lead. A lead is a potential buyer or renter of real estate. A lead with a high monthly income, and thus a high purchasing power, is better for the agency, and will be served by a more experienced salesperson. On the other hand, a lead with low purchasing power could be served by beginner or less experienced salespeople.

In this post, I will show you how to create a REST API for a real estate agency. This API will receive and delegate leads to the respective salespeople. Creating this API from scratch can become too complex, especially if it needs to scale for many users. The Twilio TaskRouter API will be utilized for convenient implementation.

Prerequisites

To follow this tutorial, you need to have the following installed and configured:

Set up the Task Router workspace

The Twilio Task Router workspace contains elements such as Workers, Workflows, and TaskQueues. We will look at each of these in detail in this section.

To set up a Task Router workspace:

  1. Go to the console and click "Create New Workspace".
  2. Name it Real State Agency, and leave the template as "Standard Workspace".
  3. Leave the "Callback URL" field blank and click "Save".

 

Add Workers to the Workspace

The Workers are the agents that execute Tasks. In the real estate agency, each Task is a real estate potential sale obtained from a lead. And each Worker is a salesperson that will take care of that potential sale.

In this agency, we will have three types of salespeople: level 1, level 2, and level 3. Each one will be associated with a different buyer profile using the following rules:

  • Level 1 Salespeople handle leads with monthly income between $3,000 and $6,000.
  • Level 2 Salespeople handle leads with monthly income between $6,000 and $10,000.
  • Level 3 Salespeople handle leads with monthly income greater than $10,000.

Each Salesperson is a Worker in the Workspace. The "Attributes" field identifies if the Worker is suitable for the Task received by Task Router. I have used the "leadTypes" field to identify the correct Worker. That means Level 1 Salespeople can pick any Task that maps to BUY_L1. The same applies to Level 2 and 3 Salespeople and BUY_L2 and BUY_L3 Tasks, respectively. To configure that, create three Workers as follows and leave its status offline for now:

  • Name: Level 1 Salespeople

Attributes {"leadTypes":["BUY_L1"]}

  • Name: Level 2 Salespeople

Attributes {"leadTypes":["BUY_L2"]}

  • Name: Level 3 Salespeople

Attributes {"leadTypes":["BUY_L3"]}

Add Task Queues for each Worker

Before creating the Task Queues, go to the Workflows tab and delete the default workflow that is already created there. Then, go back to the Task Queue screen and delete the already created Queue named Sample Queue. These two don’t have any use in this tutorial.

The purpose of a Task Queue is to receive and filter Tasks to the correct Worker. Create three TaskQueues (one for each Worker) named Level 1 Salesperson Queue, Level 2 Salesperson Queue, and Level 3 Salesperson Queue. The three Queues should share the same initial configuration as below:

  • Task Order: First-In-First-Out.
  • Reservation Activity: Offline.
  • Assignment Activity: Offline.
  • Max reserved workers: 1.

Each TaskQueue filters a Task to the specific Worker through a query language. The query language is configured in the "Queue Expression" field. This field is matched with the "Attributes" field of each Worker. You can check the complete guide to Queue Expressions here. For this example, assign the following expressions to the queues created earlier:

  • Name: Level 1 Salesperson Queue, Queue Expression leadTypes HAS "BUY_L1"
  • Name: Level 2 Salesperson Queue, Queue Expression leadTypes HAS "BUY_L2"
  • Name: Level 3 Salesperson Queue, Queue Expression leadTypes HAS "BUY_L3"

At this point your TaskQueues located inside the create Workflow should look like this:

list of 3 salesmen in the task queue

You can also see which Workers are associated with each Queue. To check that, navigate to the bottom of the Task Queue page. Under the "Matching Workers" tab, make sure that the correct Workers are listed here. For example, Level 3 Salespeople should appear in the Level 3 Salesperson Queue. To illustrate, check the images below how the matching workers in your Level 1 Salesperson Queue, Level 2 Salesperson Queue, and Level 3 Salesperson Queue should look like, respectively:

listing for worker at level 1

Level 1 Salesperson Queue

listing for worker at level 2

Level 2 Salesperson Queue

listing for worker at level 3

Level 3 Salesperson Queue

Create a Workflow for each task type

Workflows receive Tasks and delegate them to the correct Task Queue. The Workflow will capture the leads mapped by the Spring application created in the next section and send them to the correct Task Queue. To create Workflows, go to the homepage of your Workspace and click on Workflows. Create three Workflows configured as shown below. The other configuration options can be set as default for now.

  • Name: Level 1 Salesperson Workflow.
  • Name: Level 2 Salesperson Workflow.
  • Name: Level 3 Salesperson Workflow.

Now, for each Workflow, we need to associate a filter to associate the correct TaskQueue and Tasks with it. To do that, create three new filters by clicking on “Add a Filter”, one for each Workflow, configured as follows:

  • For the Level 1 Salesperson Workflow:

        TaskQueue: Level 1 Salesperson Queue

        Matching Tasks: leadType == "BUY_L1"

  • For the Level 2 Salesperson Workflow:

        TaskQueue: Level 2 Salesperson Queue

        Matching Tasks: leadType == "BUY_L2"

  • For the Level 3 Salesperson Workflow:

        TaskQueue: Level 3 Salesperson Queue

        Matching Tasks: leadType == "BUY_L3"

You can leave the other configuration options as is. Each of these Workflows matches the Tasks sent by our application. Then, each Workflow maps that Task to the correct TaskQueue. Finally, the TaskQueue will associate the Task with the correct Worker via Reservation. We’ll look at Reservation in detail in the next section. Make sure the created Workflows look as follows:

list of workflows for each of the three salesmen listed with 10 secs task reservation timeout

Create the application for the real estate agency

With Twilio's TaskRouter environment configured, it's time to write some code. Create a Spring application via the Spring initializer with the following parameters:

  • Build system: Maven
  • Spring Boot version: 2.6.x
  • Java version: 11
  • Dependencies: Spring Web and Lombok
  • Artifact: real-estate-agency

Click on Generate and save it on your computer. Go to the folder where you just saved the project, right-click on it, and extract it using your favorite tool.

Open your Intellij IDEA and open the project by clicking on File->Open, located at the top left corner of the screen. Choose the extracted folder. Then, you should be able to see the generated Spring application at your IDE.

We will use the Twilio Java Helper Library to facilitate the use of the TaskRouter API. To pull that library open the generated project in your IDE and add the following dependency to your pom.xml file:

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

Click on the Maven icon at the top right corner of the screen, and choose the Reload All Maven Projects to load the newly added dependency.

Set the environment variables

To use Twilio's TaskRouter API you need to set some values for authentication, such as SIDs and authentication tokens. The best practice is to always use environment variables. To set environment variables, add the following entries into your application.properties file located under the resources folder and replace them with the correct values from your account:

TWILIO_ACCOUNT_SID=<ACCOUNT_SID>
TWILIO_AUTH_TOKEN=<AUTH_TOKEN>
TWILIO_NUMBER=<TWILIO_NUMBER>
TWILIO_WORKSPACE_SID=<WORKSPACE_SID>
L1_WORKFLOW_SID=<L1_WORKFLOW_SID>
L2_WORKFLOW_SID=<L2_WORKFLOW_SID>
L3_WORKFLOW_SID=<L3_WORKFLOW_SID>

A SID is a unique String identifier. Every Twilio resource has a 34-character SID. You can use the first two characters of the SID to identify its type. For instance, Workspace SIDs start with the characters WS. Workflow SIDs start with the characters WW. Account SIDs start with the characters AC. You can find each of those values on the Twilio console.

The Twilio Account SID and Auth Token are a unique identification of your account and can be found on the homepage of your Twilio account console. By default, you will be given the option to acquire one phone number that can be used for free and can also be found on the homepage of the Twilio console. You can also buy more Twilio phone numbers as needed. In the application.properties file, you should give the phone number in E.164 format.

Create the REST API

The REST API receives a request that contains customer data and turns it into a lead. Then, it sends that lead (as a Task) to the TaskRouter. To create the endpoint that accepts lead requests, right-click on the root package, and click New->Package. Name that package as controller. In that same package, create a class called LeadController with the content below:

package com.realestateagency.controller;

import com.realestateagency.model.LeadRequest;
import com.realestateagency.service.LeadService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import static org.springframework.http.HttpStatus.NO_CONTENT;

@RestController
@RequestMapping("/leads")
@RequiredArgsConstructor
public class LeadController {

    private final LeadService leadService;

    @PostMapping("/send")
    public ResponseEntity<Void> sendLead(@RequestBody LeadRequest request) {
        return new ResponseEntity<>(leadService.generateTask(request), NO_CONTENT);
    }
}

The endpoint exposed at the path /leads/send returns a 204 (No Content) if the Task is successfully created.

To wrap our request body, create a model package inside the root package and add the LeadRequest class inside of it. Add the following content to that class:

package com.realestateagency.model;

import lombok.Data;

import java.math.BigDecimal;

@Data
public class LeadRequest {

    private BigDecimal netMonthlyIncome;
}

Create a package called service in the root package, and add the WorkflowConfiguration and LeadService classes inside of that package:

package com.realestateagency.service;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
@AllArgsConstructor
public class WorkflowConfiguration {

    private String attributes;

    private String workflowSid;
}
package com.realestateagency.service;

import com.realestateagency.model.LeadRequest;
import com.twilio.Twilio;
import com.twilio.rest.taskrouter.v1.workspace.Task;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.math.BigDecimal;

import static java.lang.String.format;
import static java.math.BigDecimal.valueOf;

@Service
@Slf4j
public class LeadService {

    private final String TWILIO_ACCOUNT_SID;
    private final String TWILIO_AUTH_TOKEN;
    private final String L1_WORKFLOW_SID;
    private final String L2_WORKFLOW_SID;
    private final String L3_WORKFLOW_SID;
    private final String TWILIO_WORKSPACE_SID;

    public LeadService(@Value("${TWILIO_ACCOUNT_SID}") String twilio_account_sid, @Value("${TWILIO_AUTH_TOKEN}") String twilio_auth_token, @Value("${L1_WORKFLOW_SID}") String l1_workflow_sid, @Value("${L2_WORKFLOW_SID}") String l2_workflow_sid, @Value("${L3_WORKFLOW_SID}") String l3_workflow_sid, @Value("${TWILIO_WORKSPACE_SID}") String twilio_workspace_sid) {
        TWILIO_ACCOUNT_SID = twilio_account_sid;
        TWILIO_AUTH_TOKEN = twilio_auth_token;
        L1_WORKFLOW_SID = l1_workflow_sid;
        L2_WORKFLOW_SID = l2_workflow_sid;
        L3_WORKFLOW_SID = l3_workflow_sid;
        TWILIO_WORKSPACE_SID = twilio_workspace_sid;
    }

    public Void generateTask(LeadRequest request) {
        Twilio.init(TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN);

        var configs = getWorflowConfiguration(request);

        var task = Task.creator(TWILIO_WORKSPACE_SID).setWorkflowSid(configs.getWorkflowSid()).setAttributes(configs.getAttributes()).create();

        log.info("Task created succesfully taskSid={} leadType={}", task.getSid(), configs.getAttributes());

        return null;
    }

    private WorkflowConfiguration getWorflowConfiguration(LeadRequest request) {

        var income = request.getNetMonthlyIncome();

        if (sendToL1Salespeople(income)) {
            return new WorkflowConfiguration("{\"leadType\":\"BUY_L1\"}", L1_WORKFLOW_SID);
        } else if (sendToL2Salespeople(income)) {
            return new WorkflowConfiguration("{\"leadType\":\"BUY_L2\"}", L2_WORKFLOW_SID);
        } else if (sendToL3Salespeople(income)) {
            return new WorkflowConfiguration("{\"leadType\":\"BUY_L3\"}", L3_WORKFLOW_SID);
        } else {
            throw new IllegalStateException(format("Net monthly income not supported: income=%s", request.getNetMonthlyIncome()));
        }
    }

    private boolean sendToL1Salespeople(BigDecimal income) {
        return (income.compareTo(valueOf(6000)) < 0) && (income.compareTo(valueOf(3000)) > 0);
    }

    private boolean sendToL2Salespeople(BigDecimal income) {
        return (income.compareTo(valueOf(10000)) < 0) && (income.compareTo(valueOf(6000)) > 0);
    }

    private boolean sendToL3Salespeople(BigDecimal income) {
        return (income.compareTo(valueOf(10000)) > 0);
    }
}

This class generates a lead and communicates with the TaskRouter API to assign the lead to the correct salesperson. That service class has the following structure:

  • The environment variables located in the application.properties file were injected using the @Value annotation and constructor dependency injection.
  • The public method generateTask authenticates the Twilio client using the Twilio.init method. Then,  it creates the Task and sends it to the correct Workflow using the Task.creator method.
  • The helper method getWorkflowConfiguration generates the correct Workflow SID and attributes for a lead.
  • The last three auxiliary methods sendToL1Salespeople, sendToL2Salespeople, and sendToL3Salespeople define the type of lead based on the customer's monthly income.

Create the endpoint to receive the Reservation callback

A Reservation is created when a Worker becomes available and a Task eligible for that Worker is pending. After creating the Reservation, TaskRouter will automatically send a request to an address that you define as a callback. That callback is called assignment_callback. You will see more about how to set up the Worker availability and callback address in the testing section.

To be able to receive and accept a reservation create a TaskRouterController class in the controller package with the following content:

package com.realestateagency.controller;

import com.realestateagency.service.ReservationService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;

@RestController
@RequestMapping("/taskrouter")
@RequiredArgsConstructor
public class TaskRouterController {

    private final ReservationService reservationService;

    @PostMapping("/assignment_callback")
    public void handlePost(HttpServletRequest request) {
        reservationService.handleCallback(request);
    }

}

Here’s what’s going on in that class:

  • The @RequestMapping(“/taskrouter”) and @PostMapping(“/assignment_callback”) annotations define the callback endpoint at the path /taskrouter/assignment_callback.
  • The handlePost method receives a request and passes it to the ReservationService.

The ReservationService receives the Reservation and automatically accepts it. When the Reservation is accepted, a Worker is finally able to work on that Task. To be able to accept the Reservation, create a class ReservationService inside the service package with the following content:


package com.realestateagency.service;

import javax.servlet.http.HttpServletRequest;

import com.twilio.Twilio;
import com.twilio.rest.taskrouter.v1.workspace.task.Reservation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import static com.twilio.rest.taskrouter.v1.workspace.task.Reservation.Status.*;

@Component
@Slf4j
public class ReservationService {

    private final String WORKSPACE_SID;

    private final String TWILIO_ACCOUNT_SID;

    private final String TWILIO_AUTH_TOKEN;

    public ReservationService(@Value("${TWILIO_WORKSPACE_SID}") String workspace_sid,
                              @Value("${TWILIO_ACCOUNT_SID}") String twilio_account_sid,
                              @Value("${TWILIO_AUTH_TOKEN}") String twilio_auth_token) {
        WORKSPACE_SID = workspace_sid;
        TWILIO_ACCOUNT_SID = twilio_account_sid;
        TWILIO_AUTH_TOKEN = twilio_auth_token;
    }

    public void handleCallback(HttpServletRequest request) {

        Twilio.init(TWILIO_ACCOUNT_SID, TWILIO_AUTH_TOKEN);

        final var taskSid = request.getParameter("TaskSid");

        final var reservationSid = request.getParameter("ReservationSid");

        Reservation.updater(WORKSPACE_SID, taskSid, reservationSid)
                .setReservationStatus(ACCEPTED)
                .update();

        log.info("Accepting reservation reservationSid={} taskSid={}", reservationSid, taskSid);
    }
}

The handle method contains three pieces of logic:

  • Twilio.init instantiates and authenticates the Twilio client.
  • The callback request contains useful data about the Reservation. To accept the reservation we need the taskSid and reservationSid from that request, those that uniquely identify the Task and the Reservation created, respectively.
  • The Reservation.updater updates the status of the Reservation to “accepted” using a synchronous request to TaskRouter. The request contains the taskSid, reservationSid and workspaceSid.

Test your API

To test the application we need a public URL to receive the callbacks. With ngrok you can create a public URL of the application running on localhost. Do the following to create a public URL using ngrok:

  1. Install ngrok on your computer
  2. Run the command ngrok http 8080 in a terminal
  3. The command outputs some information. Your public URL is the one pointing to https://localhost:8080. That URL looks similar to http://<random_sequence>.ngrok.io.

Keep in mind that every time you run the ngrok http 8080 command another URL will be generated at port 8080. Keep ngrok running in your terminal while you finish the next steps.

After setting up ngrok, go to the main class named RealEstateAgencyApplication and click the play button located at the left of the class name. Now, we have a server up and running. At this point, you should be able to see similar entries at the end of the logs in your IntelliJ IDEA:

022-06-21 22:23:22.267  INFO 84616 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 1201 ms
2022-06-21 22:23:22.650  INFO 84616 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2022-06-21 22:23:22.663  INFO 84616 --- [           main] c.r.RealEstateAgencyApplication          : Started RealEstateAgencyApplication in 2.471 seconds (JVM running for 3.148)

That means your application is running properly.

Now, grab the http:// URL created by ngrok and add the following path at the end of that URL: /taskrouter/assignment_callback. Your callback URL should look similar to:

http://<random_sequence>.ngrok.io/taskrouter/assignment_callback

To complete the test setup, go to the Workflows page in the Twilio Console. For each Workflow set that same Assignment Callback URL.

With the test setup done, it’s time to create some leads to be consumed by our API. With your server running, open up a new terminal window and run the following cURL command:

curl --location --request POST 'http://localhost:8080/leads/send' \
--header 'Content-Type: application/json' \
--data-raw '{
    "netMonthlyIncome": 7500
} '

That cURL command creates a Task that contains the lead information. In this case, that lead is going to be served by a Level 2 Salesperson since the net monthly income of that customer is between $6000 and $10000. A new Task should appear in the Tasks tab in your Twilio Workspace. No reservations are created for this Task since all Workers were initially set to "Offline".

To create a Reservation, go to the Workers tab in your Twilio Console and click on the Level 2 Salesperson Worker button. Change the activity field of that Worker from Offline to Available. TaskRouter will automatically create the Reservation between the Task and that Worker. Then, TaskRouter sends the callback request to our API to be handled by us. The status of that Reservation changes to ACCEPTED as soon as the callback request hits our API since we configured it that way.

We finally made it to the end of the Task lifecycle. At this point, the Worker is effectively working on that Task. Or, in other words, our customer (a lead) is being attended by one of our Salespeople.

If you want to check the API requests made go to the URL http://localhost:4040. At this address, you can check all requests made to the public ngrok address, including the TaskRouter callback requests.

Next steps

In this tutorial, we defined a REST API to generate a lead and send it to the Twilio TaskRouter via HTTP call. We have also handled the callback requests from TaskRouter to accept the reservations made. Some improvements can be made to this model:

  • Not always you’ll want to accept Reservations as soon as they arrive in your API, as we did. In a real-world scenario, you may want the accept reservations process decoupled from the handle assignment callback URL process. For example, you could process the Reservation received, validate the data, or send an SMS to the parties involved before accepting the Reservation.
  • For this tutorial, we used an HTTP call. But, there are other ways to create a Task. One example is by receiving a phone call from a customer. With TwiML you define instructions on how to handle phone calls from customers. So, you could configure TwiML to send those phone calls to the TaskRouter. And once there, the TaskRouter automatically assigns the correct Salesperson to that customer.

I’d love to see what you build with the Twilio Task Router!

Pedro Lopes is a backend engineer. He's enthusiastic about distributed systems, big data, and high-performance computing. He can be reached at LinkedIn.