Skip to main content

Private URL Shortener


SMS Marketing is one of the most effective ways to reach consumers in a format they prefer, but every character counts when it comes to messaging. A common solution is to use a public URL shortener like,, or The problem with these public URL shorteners is that the carriers typically flag messages containing their URLs as spam since it is disallowed content!

This application shows how easy it is to set up your own private URL shortener allowing you to generate and redirect shortened URLs, keep track of their usage, and most importantly you will be following all of the carrier rules regarding shortened URLs!

What do I need to run this code?

View on Github here!

This application doesn't require much - you will need Python, the Flask framework, and a ngrok tunnel to reach your localhost or server to host it on.

How to Run Application

To run the application, execute export then run flask run.

You may need to use an SSH tunnel for testing this code – we recommend ngrok. After starting the tunnel, you can use the URL you receive from ngrok in your webhook configuration for your phone number.

Step by Step Code Content

This code repo has the following structure

Static -> This folder contains the style.css and SW logo
Templates -> This folder contains the shortUrls.html file
.env -> This will contain your hostname variable.
shortUrls.csv -> This is where the shortened URLs and data about their usage will live. This does not need to be edited - generating/deleting shortened URLs will happen on browser and their usage will update automatically on redirect to the original URL.

Setting up .env

We only need one environment variable - you will need your hostname to generate the URLs!


Step by step through

This application requires very little setup in order to handle all of your shortened URL needs for messaging. It can easily but customized in several spots which we will walk through step by step below. There are two functions - one to generate shortened URLs and one to delete shortened URLs. We will also have two Flask routes - one that redirects shortened URLs to the full URL and one that handles the shortened URL interface. The finished product will look like this:

The first function we will define will handle the generation of new short URLs from full URLs. The first step is to use pandas to read in the shortUrls.csv and assign the current numbers of rows as the object_id. This number will serve to encode our full URL using Pythons short_url implementation. We will then save some helpful data about this short URL to our dataframe and resave it to CSV.

# generate shortened URL using encoding and store in CSV
def generateShortenedURL(fullURL, keyword):
# read in csv using pandas
shortenedUrls = pd.read_csv('shortUrls.csv')
object_id = len(shortenedUrls)

shortened_url = f"{hostName}{keyword}/{short_url.encode_url(object_id, min_length=3)}"
shortenedUrls.loc[len(shortenedUrls.index)] = [fullURL, shortened_url,, 'Not Used Yet', 0]
shortenedUrls.to_csv('shortUrls.csv', index=None)
return shortened_url

The next function will serve to delete the shortened URL/full URL pair from the CSV. This one is super simple - we will read the CSV into a dataframe and locate the row with the matching full URL. When we find it, we will drop it from the dataframe and resave to CSV.

# delete shortened URL from CSV
def deleteShortenedURL(fullURL):
# read in csv using pandas
shortenedUrls = pd.read_csv('shortUrls.csv')

# delete row with matching fullURL
shortenedUrls.drop(shortenedUrls[shortenedUrls['Full URL'] == fullURL].index, inplace=True)
shortenedUrls.to_csv('shortUrls.csv', index=None)
return 'shortened URL deleted'

Next, we need a route that will look up the shortened URL and redirect to the full URL. By evaluating the characters at the end of the route, we will get the decoded object ID that was used to create the shortened URL. We will again read the CSV into a dataframe and set the fullURL to the URL at the index of the decoded_id. We will also update the Last Clicked and Times Clicked columns for this row to reflect that the URL was used. We will resave the dataframe to CSV and redirect to the full URL.

# handle inbound shortened url requests and redirect to full URL
@app.route("/<name>/<char>", methods=('GET', 'POST'))
def redirectShortCode(char, name):
decoded_id = short_url.decode_url(char)
shortenedUrls = pd.read_csv('shortUrls.csv')
fullURL = shortenedUrls.loc[decoded_id, 'Full URL']
shortenedUrls.loc[decoded_id, 'Last Clicked'] =
shortenedUrls.loc[decoded_id, 'Times Clicked'] = shortenedUrls.loc[decoded_id, 'Times Clicked'] + 1
shortenedUrls.to_csv('shortUrls.csv', index=None)

return redirect(fullURL, code=302)

The last part that we need is the default roue where you will be able to generate new URLs, view the table of shortened URL/full URL pairs, and delete URLs. In this route, we will look at the at the POST request variables and call the generateShortenedUrl() or deleteShortenedURL() function if we receive fullURL or delURL. keyword is optional as it will default to 'sc' if not provided. Lastly, we will read the current version of the CSV into a dataframe and read the URL names into a list. We will pass these data structures through to our HTML file using flask's render_template.

# handle url shortener
@app.route('/', methods=['POST', 'GET'])
def shortenedURLs():
# generate new shortened URL
if request.args.get('fullURL'):
fullURL = request.args.get('fullURL')
if request.args.get('keyword'):
keyword = request.args.get('keyword')
generateShortenedURL(fullURL, keyword)
keyword = 'sc'
generateShortenedURL(fullURL, keyword)

# delete shortened URL
if request.args.get('delURL'):
delURL = request.args.get('delURL')

data = pd.read_csv('shortUrls.csv')
urls = data['Full URL'].tolist()

return render_template('urlShortener.html', table=data.to_html(
classes=["table", "table-striped", "table-dark", "table-hover", "table-condensed", "table-fixed"], index=False),


This page is easily accomplished without too much effort using Jinja templates to accept data structures from Flask. Most of the HTML file is standard page content and will not be shown, however, the Jinja parts are shown below. The full code can be viewed in the Github repo.

As you can see in the <select> for deleting a shortened URL, we will use the list of filenames from our serverside code and iterate through them creating a <select> option for each value. We will also take the html table we created from the dataframe in our serverside code table=data.to_html( classes=["table", "table-striped", "table-dark", "table-hover", "table-condensed", "table-fixed"], index=False) and insert it directly into a <div>.

<!DOCTYPE html>
<html lang="en">
<!-- Page Cut From Here Forward, View Github Repo For Full Code -->
<div class="col-sm-4" style="text-align: center; background:#eee; height: 100vh;">
<h4>Delete a Shortened URL</h4>
<form action="/shortUrls">
<label for="deleteShortURL">
Select the URL you would like to delete.
<select id="deleteShortURL" name="delURL">
{% for url in urls %}
<option value="{{url}}">
{{ url }}
{% endfor %}
<input type="submit" value="Submit" style="background:#033ec3; color: white;">

<div class="col-md-8" style="background:#212529; height: 100vh;">
{{table | safe }}


Wrap up

If you want to make the most of your messaging and not get flagged by the carriers, using your own private URL shortener hosted on your server is the best way to do it. Clone the code from our GitHub repo to try it yourself!

Sign Up Here

If you would like to test this example out, you can create a SignalWire account and space here.

Please feel free to reach out to us on our Community Slack or create a Support ticket if you need guidance!