Swimburger

Find your US Representatives and Congressional Districts with SMS and ASP.NET Core

Niels Swimberghe

Niels Swimberghe - - .NET

Follow me on Twitter, buy me a coffee

Find your Representatives & Congressional Districts with SMS and ASP.NET Core

This blog post was written for Twilio and originally published at the Twilio blog.

When someone runs for office in the United States of America (U.S.), they have to fulfill certain "Ballot Access Requirements" to become listed on the ballot. One of those requirements could be that you need a certain number of signatures per district. So when volunteers and campaign workers go out to gather signatures, it is instrumental to know which congressional district (CD) the signee belongs to. However, knowing which congressional district you are from is not common knowledge, especially in areas where the district cuts right through counties, cities, and neighborhoods. 

States redraw their congressional districts every decade after getting an updated population count from the Census Bureau. Each district is represented by a single U.S. representative in the U.S. House of Representatives. However, the manner in which these districts are drawn often result in unique and odd shapes which can be confusing.  This is why constituents living just a street apart sometimes live in different districts, and as a result, vote in different house races. 

This blog post won't discuss why U.S. congressional district maps are drawn the way they are, however, I highly recommend you look into this topic and learn how it affects political voting power.

When I gathered signatures, the first question I had to ask over and over again was "what congressional district are you from?", and 95% of the time, the answer was "I don't know". That's because in Northern Virginia, the border between the 10th and 11th congressional district cuts directly through some counties, cities, neighborhoods, and streets. 

Map of Virginia's 11th Congressional District
Map of Virginia's 11th Congressional District

The house.gov website has a tool to look up your congressional district and representative, but unfortunately, the tool is slow and painful to use, especially when you're on the go on mobile internet.

That's why instead of using web technology, I built a solution using connectivity technology that's more accessible, more reliable, and simpler. Using Twilio SMS, I built a phone number that you can text your address to and you will receive a response with your congressional district and representative. Let me walk you through how you can build this SMS bot using Twilio, C# .NET, and Google's Civic Information API.

Prerequisites #

You'll need a couple of things to follow along:

You can find the source code for this tutorial on GitHub. Use it as a reference if you run into any issues, or submit an issue if you need assistance.

Get Started with the Google Civic Information API #

This SMS bot will use the Google Civic Information API to look up the U.S. congressional districts and representatives. This API has a free tier as part of the Google Cloud Platform.

Go to the Google Cloud Console and create a new project. Once the project is created, select the project using the project dropdown in the top-left navigation bar.

You'll first need to enable the Civic Information API. Click on "ENABLE APIS AND SERVICES", then search for the "Civic Information API".

The "Enabled APIs & services" page in Google Cloud Platform. The page has a button to "ENABLE APIS AND SERVICES".

Click on the search result and then click ENABLE.

The API listing for the "Google Civic Information API" with an enable button.

Now that the API is enabled for your project, you'll need to create an API token to be able to authenticate when consuming the API from your .NET project. Navigate to the Credentials page, click on "CREATE CREDENTIALS" and then on "API key".

GCP Credentials page where the user clicks on the "CREATE CREDENTIALS" button and then on the "API key" menu item.

A modal will appear showing your API key. You will need this API key later, so copy and paste the API key somewhere safe. Next, click on the "Edit API key" link.

API key created modal with a copy button to copy the key and a link "Edit API key".

On the "​​Edit API key" page, you can configure the name and restrictions for your API key. This step isn't strictly required, but it is a good practice to follow the principle of least privilege, which means you should only give accounts and keys the level of access necessary to do their job.

In that spirit, find the "API restrictions" field and click on the "Restrict key" radio button.

Click on the newly appeared dropdown and select the "Google Civic Information API" option, and then hit Save.

API restrictions form where the "Restrict key" radio button is enabled and the "Google Civic Information API" is selected in the dropdown.

You're all done setting up your GCP project and API key. Let's go try out the API!

Try out the Civic Information API #

Open a new tab in your browser and navigate to the Civic Information API documentation for getting representatives by address.The representativeInfoByAddress will be the specific method that you'll be using for this project.

You can pass a couple of parameters to the method to filter down the representative information, most importantly the required address field. This address field expects a physical mail address, however, it can be a partial address. You can pass in just the ZIP code, the city and state, or a full address including the street and number.

The HTTP response will contain divisions, offices, and officials for the given address:

  • divisions are the political geographic areas that the address is within. Among these you would find the U.S. congressional districts.
  • offices are the political offices someone can be elected to within the returned divisions.
  • officials are the elected officials holding the returned offices.

By default, representativeInfoByAddress will return all divisions, offices, and officials that represent the given address, all the way from the highest office of the President down to the local offices.
To filter this down, you can pass in the levels and roles parameters. 

This bot will look for U.S. representatives, so to filter down to only return representatives, you'll need to pass in country into the levels parameter because the U.S. representative is a national office, and you'll need to pass in legislatorLowerBody to the roles parameter because U.S. Congress has two houses: the Senate is the upper body and the House of Representatives is the lower body.

Click the "Try it now" link at the top of the page and play around with the method.

Set the address field to 10600 Little Run Farm Ct, Vienna, the levels to country, and roles to legislatorLowerBody, and then click EXECUTE.

A form to try out the representativeInfoByAddress API method. The address field has an address, the levels field is set to country, and the roles field is set to legislatorLowerBody. The API result shows the details of a single U.S. Representative.

The response should have a single division, office, and official.

New congressional district maps are going into effect for the November 2022 elections, so the maps and data will change soon!

Now change the address field to Little Run Farm Ct, Vienna and click EXECUTE again.
This time no division, office, or official is returned. Why is that?

When you look up this street and overlay the congressional district maps, you can see that the border of district 10 and 11 cuts through this street. In fact, the border follows a creek that runs between house number 10600 and 10602.

A map with Virginia"s Congressional District 11 overlayed. On the map, you can see two houses on the street Little Run Farm Ct are part of District 10, and the rest of the street is part of District 11.

That's why the Civic Information API cannot determine the district when you pass in the street without a house number, and as a result, representativeInfoByAddress returns nothing.

The house.gov representative lookup tool will claim that house number 10600 and 10602 are both in Virginia's 11th congressional district which is incorrect (for the pre-2022 maps which are current at the time of writing this). Luckily, the Civic Information API returns the correct result.

This does not mean you always have to enter a full address, in fact when you put in the ZIP code 20301 without anything else, representativeInfoByAddress returns the information for the 8th congressional district. That's because the entire ZIP code is located within district 8, so the API doesn't need more information to determine the district.

Now that you're more familiar with the Civic Information API and specifically the representativeInfoByAddress method, it's time to consume the API from .NET!

Build an SMS bot to look up representatives #

This project will be built on ASP.NET Core Minimal APIs. Open a shell and run the following command to create the project and navigate into the project folder:

dotnet new web -o RepresentativeBot
cd RepresentativeBot

Open the project in your preferred editor.

Retrieve Representatives from the Civic Information API #

You can consume the Civic Information API using an HTTP client, but Google generates .NET libraries for all of its APIs including this one, which is convenient. You can install the library by adding the Google.Apis.CivicInfo.v2 NuGet package using the following command:

dotnet add package Google.Apis.CivicInfo.v2

Create a new file RepresentativeLookupClient.cs and add the following C# code:

using System.Net;
using Google;
using Google.Apis.CivicInfo.v2;
using Google.Apis.CivicInfo.v2.Data;
using Google.Apis.Services;
using static Google.Apis.CivicInfo.v2.RepresentativesResource;

namespace RepresentativeBot;

public class RepresentativeLookupClient
{
    private readonly CivicInfoService service;

    public RepresentativeLookupClient(string gcpApiKey)
    {
        service = new CivicInfoService(new BaseClientService.Initializer
        {
            ApplicationName = "US Representative Lookup",
            ApiKey = gcpApiKey
        });
    }

    public async Task<Representative> GetRepresentativeByAddress(string address)
    {
        var request = new RepresentativeInfoByAddressRequest(service)
        {
            Address = address,
            // Level = Country and Roles = LegislatorLowerBody filters down to U.S. Representatives
            Levels = RepresentativeInfoByAddressRequest.LevelsEnum.Country,
            Roles = RepresentativeInfoByAddressRequest.RolesEnum.LegislatorLowerBody
        };

        RepresentativeInfoResponse response;
        try
        {
            response = await request.ExecuteAsync();
        }
        catch (GoogleApiException e) when (e.HttpStatusCode == HttpStatusCode.BadRequest &&
                                           e.Error.Message == "Failed to parse address")
        {
            throw new FailedToParseAddressException(e);
        }

        // if the address did not resolve to a specific district,
        // for example, no district, or multiple districts
        // then response.Offices will be null
        if (response.Offices == null)
        {
            throw new RepresentativeNotFoundException();
        }

        // only one office, one division, and one official will be returned
        // the office of U.S. representative, the congressional district, and the elected official
        var office = response.Offices[0];
        var division = response.Divisions[office.DivisionId];
        var official = response.Officials[0];
        return new Representative
        {
            DistrictName = division.Name,
            RepresentativeName = official.Name,
            Party = official.Party,
            PhotoUrl = official.PhotoUrl
        };
    }
}

The RepresentativeLookupClient constructor accepts the GCP API key as the gcpApiKey parameter, which is then used to create a new CivicInfoService object that is stored into the service field.

The GetRepresentativeByAddress method accepts an address as a parameter and returns a Task that will resolve to a Representative object. In this method, a RepresentativeInfoByAddressRequest object is created passing in the service into the constructor and the address into the Address property.
The Levels and Roles property are hard-coded to Country and LegislatorLowerBody to filter down to U.S. representatives and congressional districts.

The API request is made using the request.ExecuteAsync method, which is surrounded by a try/catch block to handle some of the exceptions that can be thrown. When the API fails to parse the address, a custom exception of type FailedToParseAddressException is thrown. When no offices are returned, because the API couldn't determine the correct congressional district, or for some other reason, then a custom exception of RepresentativeNotFoundException is thrown. These two custom exceptions will be caught later to provide user-friendly messages to the end user.

However, if an office is returned, then a new Representative object is created with the data from the response.

The Representative object only has a couple of properties, however, feel free to extend the class and retrieve more information from the response object. The API retrieves a bunch of information like a physical address, an email address, a phone number, social media links, etc.

For example, here's a tutorial that uses the same API to retrieve contact information of the representatives and uses Twilio SMS and SendGrid to contact them, built on Twilio Studio and Functions.  

Create 3 new files named Representative.cs, FailedToParseAddressException.cs, and RepresentativeNotFoundException.cs and then update each file with the contents listed below.

Representative.cs:

namespace RepresentativeBot;

public class Representative
{
    public string DistrictName { get; set; }
    public string RepresentativeName { get; set; }
    public string Party { get; set; }
    public string PhotoUrl { get; set; }
}

FailedToParseAddressException.cs:

namespace RepresentativeBot;

public class FailedToParseAddressException : Exception
{
    public FailedToParseAddressException(Exception innerException) : base(innerException.Message, innerException)
    {
    }
}

RepresentativeNotFoundException.cs:

namespace RepresentativeBot;

public class RepresentativeNotFoundException : Exception
{
}

To use the RepresentativeLookupClient in your future API endpoints, you'll need to add it to ASP.NET Core's Dependency Injection (DI) container. Open your Program.cs file and add the highlighted lines to the existing code:

using RepresentativeBot;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped(provider =>
{
    var configuration = provider.GetRequiredService<IConfiguration>();
    var gcpApiKey = configuration["GcpApiKey"] ?? throw new Exception("GcpApiKey is not configured.");
    return new RepresentativeLookupClient(gcpApiKey);
});

var app = builder.Build();
...

The GCP API key you created earlier needs to be configured using the GcpApiKey configuration key. Since the API key is a secret, you should avoid hard-coding it or storing it in source control.You can use environment variables or a secure vault service, or for local development, you can use the .NET Secrets Manager also known as user secrets.

Run the following command to initialize user secrets in your project:

dotnet user-secrets init

Next, configure the GCP API key as a user secret:

dotnet user-secrets set GcpApiKey "[YOUR_GCP_API_KEY]"

Replace [YOUR_GCP_API_KEY] with the API key secret you copied earlier.

The RepresentativeLookupClient is now ready to be used! Feel free to add some code to quickly test it out, otherwise, move to the next step where you'll integrate the client into your minimal API.

Create your Twilio Webhook #

There's a useful library called Twilio.AspNet which will help you build Twilio webhooks.
Add the Twilio.AspNet.Core NuGet package to install the library:

dotnet add package Twilio.AspNet.Core

Then, open your Program.cs file and update it with the following code:

using RepresentativeBot;
using Twilio.AspNet.Core.MinimalApi;
using Twilio.TwiML;
using Twilio.TwiML.Messaging;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddScoped(provider =>
{
    var configuration = provider.GetRequiredService<IConfiguration>();
    var gcpApiKey = configuration["GcpApiKey"] ?? throw new Exception("GcpApiKey is not configured.");
    return new RepresentativeLookupClient(gcpApiKey);
});

var app = builder.Build();

app.MapPost("/message", async (
    HttpRequest request,
    HttpResponse response,
    IServiceProvider serviceProvider,
    ILogger<Program> logger
) =>
{
    var messagingResponse = new MessagingResponse();

    if (bool.Parse(request.Cookies["HasBeenGreeted"] ?? "False") == false)
    {
        response.Cookies.Append("HasBeenGreeted", "True");
        messagingResponse.Message("Welcome to the U.S. Representative lookup bot. Respond with your address.");
        return Results.Extensions.TwiML(messagingResponse);
    }
    
    // TODO: remove me
    messagingResponse.Message("YOU HAVE BEEN GREETED ALREADY!");

    return Results.Extensions.TwiML(messagingResponse);
});

app.Run();

The new /message endpoint accepts a couple of parameters that will be dependency injected.
The MessagingResponse object will help you construct TwiML to respond to an incoming message. 

The first message you will respond with, is an introductory message. To only introduce the bot once, a cookie is used to keep track of whether the sender has been greeted with the introduction. 

You can use cookies when responding to Twilio webhooks, however, the cookie will expire after 4 hours and you can only store one cookie. In this tutorial you only need to keep track of one thing using cookies, but if you need to keep track of more state you can use a session cookie instead and store state in session. Learn more about the cookie limitations with Twilio webhooks here.

The messagingResponse.Message method is used to create a TwiML Message verb that will respond to the sender. The Results.Extensions.TwiML method creates a TwiMLResult that will serialize the messagingResponse to XML and set the correct content-type header.

If the sender has already been greeted, the bot responds with "YOU HAVE BEEN GREETED ALREADY!".

However, this last message is a placeholder. Replace the placeholder with the following code:

var form = await request.ReadFormAsync().ConfigureAwait(false);
var body = form["Body"][0];

var representativeLookupClient = serviceProvider.GetRequiredService<RepresentativeLookupClient>();
try
{
    var representative = await representativeLookupClient.GetRepresentativeByAddress(body);
    messagingResponse.Message(
        $"Your representative is {representative.RepresentativeName} ({representative.Party})" +
        $", representing {representative.DistrictName}."
    );

    if (representative.PhotoUrl is not null)
    {
        messagingResponse.Append(new Message().Media(new Uri(representative.PhotoUrl)));
    }
}
catch (FailedToParseAddressException)
{
    messagingResponse.Message("The address you entered is invalid.");
}
catch (RepresentativeNotFoundException)
{
    messagingResponse.Message("Your representative could not be determined. " +
                              "This may be because there's no representative or multiple representatives for the given location. " +
                              "Try entering a more specific address.");
}
catch (Exception ex)
{
    logger.LogError(ex, "An unexpected error occurred when looking up representative");
    messagingResponse.Message("An unexpected error occurred.");
}

Once the sender has been greeted, the bot will assume that whatever message is sent next will be the address to return representative information for. The message that is sent will be stored in the form encoded "Body" parameter. 

After getting the body of the message, the bot will request an instance of RepresentativeLookupClient from the DI container, then the bot invokes the RepresentativeLookupClient.GetRepresentativeByAddress method passing in the body as a parameter.
If no exception occurs, two messages will be added. One message will describe the district and representative, and the other will return the image of the elected official, if an image is provided.

A TwiML Message with Media will send the media as an MMS message, not as an SMS. You can choose to create a single message with the representative information and the image which looks great!
However, I chose to create separate messages because of these reasons:

  • An SMS is smaller and will be transmitted to the user faster, giving them the information as quickly as possible. 
  • MMS uses mobile internet which may not be turned on, or there may be no mobile internet connectivity in rural areas.
  • MMS may not be properly configured or disabled.

Your project is ready! Start your project using dotnet run.

To quickly test out your /message endpoint, you can use the following PowerShell or Bash script in a new shell tab:

PowerShell:

Invoke-WebRequest [YOUR_LOCALHOST_URL]/message `
    -Method Post `
    -Body @{ Body = 'Hi' } `
    -ContentType 'application/x-www-form-urlencoded' `
    -SessionVariable SmsBotSession

Invoke-WebRequest [YOUR_LOCALHOST_URL]/message `
    -Method Post `
    -Body @{ Body = '10602 Little Run Farm Ct, Vienna' } `
    -ContentType 'application/x-www-form-urlencoded' `
    -WebSession $SmsBotSession

Bash:

curl -X POST [YOUR_LOCALHOST_URL]/message \
    -H "Content-Type: application/x-www-form-urlencoded" \
    -d "Body=Hi" \
    --cookie-jar SmsBotCookieJar

curl -X POST [YOUR_LOCALHOST_URL]/message \
    -H "Content-Type: application/x-www-form-urlencoded" \
    -d "Body=10602 Little Run Farm Ct, Vienna" \
    --cookie SmsBotCookieJar

rm SmsBotCookieJar

Replace [YOUR_LOCALHOST_URL] with one of the localhost URLs printed out when running your .NET project.

The first web request will be responded to with the following TwiML:

<?xml version="1.0" encoding="utf-8"?>
<Response>
  <Message>Welcome to the U.S. representative lookup bot. Respond with your address.</Message>
</Response>

The subsequent request will be responded to with the following TwiML:

<?xml version="1.0" encoding="utf-8"?>
<Response>
  <Message>Your representative is Gerald E. "Gerry" Connolly (Democratic Party), representing Virginia's 11th congressional district.</Message>
  <Message>
    <Media>http://bioguide.congress.gov/bioguide/photo/C/C001078.jpg</Media>
  </Message>
</Response>

If everything is working as expected, you can now configure your Twilio Phone Number to use your Minimal API endpoint as the SMS webhook.

Configure your Twilio SMS Webhook  #

Start your project if it isn't running yet (dotnet run) and take note of one of the localhost URLs printed out by the application. For Twilio to be able to send HTTP requests to your local web server, the server needs to become publicly accessible. ngrok is a free secure tunneling service that can make your local web servers public.

Run the following ngrok command in a separate shell:

ngrok http [YOUR_ASPNET_URL]

Replace [YOUR_ASPNET_URL] with the localhost URL from your .NET application. If you're using an HTTPS localhost URL, you'll need to authenticate ngrok.

The ngrok command will display an HTTPS Forwarding URL that makes your local web server public.

ngrok http command output showing information about the secure tunnel, most importantly the public forwarding URLs

Now it's time to update your Twilio Phone Number to send HTTP requests to your /message endpoint via the ngrok Forwarding URL. The URL should look something like https://1cc74f4c9f70.ngrok.io/message.

Go to the Active Phone Numbers section in the Twilio Console and click on your Twilio Phone Number.
This will take you to the configuration for the phone number. Find the Messaging section and under the "A MESSAGE COMES IN" label, set the dropdown to Webhook. In the text field next to it, enter your ngrok forwarding URL with /message appended to it. Select “HTTP POST” in the last dropdown. 

Messaging section in the Twilio Phone Number configuration page. Under the "A MESSAGE COMES IN" label, a dropdown is set to "Webhook", the text field next to it is configured to "https://eaa3c4359db8.ngrok.io/message", and the dropdown next to that is set to "HTTP POST".

Finally, click the Save button at the bottom of the page.

Test your SMS Twilio Bot #

To test your bot, you can either use a personal phone to send text messages, or you can use the Twilio Dev Phone to test.

Send a text message to your Twilio Phone Number with anything as the body. You should receive the greeting you configured. Once you've been greeted, reply with any address that you want to retrieve representative and district information for.

Conversation between an iPhone user and the SMS bot. The user says "Hi" and the bot responds with "Welcome to the U.S. Representative lookup bot. Respond with your address. The user sends an address and the bot responds with "Your representative is Gerald E. Connolly (Democratic Party) representing Virginia"s 11th congressional district. Then the bot sends an image of the representative.

Future improvements #

This bot is a great start, however, you can improve this solution in a couple of ways:

You can respond to the user with more information about the elected official and how to reach them. And instead of only returning U.S. representatives, you could return elected officials from every level.

Currently, anyone can send HTTP requests to your webhook and pretend to be Twilio. This bot doesn't respond with any sensitive information so the risk is lower, however, it is always a good idea to secure your webhooks using webhook signature validation.

In this tutorial, you manually spun up a secure tunnel using ngrok, but you could also do this programmatically so that a tunnel is created and your Twilio SMS webhook is updated whenever you start your .NET application.

Next steps #

If you made it this far, a big shout-out to you! 🎉

In this tutorial, you learned how to respond to incoming text messages with Twilio, but if you’re looking to learn more, you can also initiate text messages yourself using the Twilio SDK, or send emails using Twilio SendGrid.

If you found this useful, let me know and share what you're working on. I can't wait to see what you build!

Image credit: Matthew Geason has kindly allowed us to use his picture of the U.S. Capitol.

Related Posts

Related Posts