Using Shortcuts to Check Santander Cycles

TfL's Santander Cycles have been my favorite way to get around central London since I first tried them during Covid lockdowns. The annual subscription (with a student discount!) means I can make short journeys faster than on a bus, sometimes even faster than on the tube. But I had one problem when making the journey into LSE – often I would arrive to find every docking station completely full, meaning I had to pull out my phone to search for the nearest available docking space. That's not the most elegant thing to do when you're pulled over on the side of a busy road straddling a bike, especially when the app turns out to be frozen. I wanted to find an easier way to check for empty spaces near my destination.

In this post I show how to get the number of bikes and empty docks at any station using the TfL API and then show the steps to do this in the Shortcuts app. The Shortcut can be run from the Shortcuts app, from your iPhone or iPad's home screen as a standalone app or within a widget, from Siri, from your Mac's task bar, or even from a complication on your Apple Watch face. If you'd like to use these shortcuts yourself, here are the iCloud sharing links:

The TfL API

TfL has developed a unified API that provides real-time information about its services to anyone with the appropriate programming know-how. The API provides a BikePoint endpoint that returns information about all docking stations. Let's pull this information and see what the JSON data for a single bikepoint looks like:

using HTTP, JSON3
r = HTTP.get("https://api.tfl.gov.uk/BikePoint")
bikepoints = r.body |> String |> JSON3.read
bp = first(bikepoints)
JSON3.pretty(bp)
{
    "$type": "Tfl.Api.Presentation.Entities.Place, Tfl.Api.Presentation.Entities",
    "id": "BikePoints_1",
    "url": "/Place/BikePoints_1",
    "commonName": "River Street , Clerkenwell",
    "placeType": "BikePoint",
    "additionalProperties": [
        {
            "$type": "Tfl.Api.Presentation.Entities.AdditionalProperties, Tfl.Api.Presentation.Entities",
            "category": "Description",
            "key": "TerminalName",
            "sourceSystemKey": "BikePoints",
            "value": "001023",
            "modified": "2024-03-13T10:32:20.74Z"
        },
        {
            "$type": "Tfl.Api.Presentation.Entities.AdditionalProperties, Tfl.Api.Presentation.Entities",
            "category": "Description",
            "key": "Installed",
            "sourceSystemKey": "BikePoints",
            "value": "true",
            "modified": "2024-03-13T10:32:20.74Z"
        },
        {
            "$type": "Tfl.Api.Presentation.Entities.AdditionalProperties, Tfl.Api.Presentation.Entities",
            "category": "Description",
            "key": "Locked",
            "sourceSystemKey": "BikePoints",
            "value": "false",
            "modified": "2024-03-13T10:32:20.74Z"
        },
        {
            "$type": "Tfl.Api.Presentation.Entities.AdditionalProperties, Tfl.Api.Presentation.Entities",
            "category": "Description",
            "key": "InstallDate",
            "sourceSystemKey": "BikePoints",
            "value": "1278947280000",
            "modified": "2024-03-13T10:32:20.74Z"
        },
        {
            "$type": "Tfl.Api.Presentation.Entities.AdditionalProperties, Tfl.Api.Presentation.Entities",
            "category": "Description",
            "key": "RemovalDate",
            "sourceSystemKey": "BikePoints",
            "value": "",
            "modified": "2024-03-13T10:32:20.74Z"
        },
        {
            "$type": "Tfl.Api.Presentation.Entities.AdditionalProperties, Tfl.Api.Presentation.Entities",
            "category": "Description",
            "key": "Temporary",
            "sourceSystemKey": "BikePoints",
            "value": "false",
            "modified": "2024-03-13T10:32:20.74Z"
        },
        {
            "$type": "Tfl.Api.Presentation.Entities.AdditionalProperties, Tfl.Api.Presentation.Entities",
            "category": "Description",
            "key": "NbBikes",
            "sourceSystemKey": "BikePoints",
            "value": "7",
            "modified": "2024-03-13T10:32:20.74Z"
        },
        {
            "$type": "Tfl.Api.Presentation.Entities.AdditionalProperties, Tfl.Api.Presentation.Entities",
            "category": "Description",
            "key": "NbEmptyDocks",
            "sourceSystemKey": "BikePoints",
            "value": "12",
            "modified": "2024-03-13T10:32:20.74Z"
        },
        {
            "$type": "Tfl.Api.Presentation.Entities.AdditionalProperties, Tfl.Api.Presentation.Entities",
            "category": "Description",
            "key": "NbDocks",
            "sourceSystemKey": "BikePoints",
            "value": "19",
            "modified": "2024-03-13T10:32:20.74Z"
        },
        {
            "$type": "Tfl.Api.Presentation.Entities.AdditionalProperties, Tfl.Api.Presentation.Entities",
            "category": "Description",
            "key": "NbStandardBikes",
            "sourceSystemKey": "BikePoints",
            "value": "7",
            "modified": "2024-03-13T10:32:20.74Z"
        },
        {
            "$type": "Tfl.Api.Presentation.Entities.AdditionalProperties, Tfl.Api.Presentation.Entities",
            "category": "Description",
            "key": "NbEBikes",
            "sourceSystemKey": "BikePoints",
            "value": "0",
            "modified": "2024-03-13T10:32:20.74Z"
        }
    ],
    "children": [
    ],
    "childrenUrls": [
    ],
    "lat": 51.529163,
    "lon": -0.10997
}

There's a lot going on here, but the important things we observe are:

  • The unique id field will let us query the API for just this station

  • The commonName field gives the name of the station as it appears in the app

  • The information we want (the number of bikes/E-bikes and empty docking points) live inside the additionalProperties field

The structure of the additionalProperties field is a bit odd in that the keys are actually values themselves, so we can't access them by name. Let's get the index of each key-value pair:

for (i, property) in enumerate(bp.additionalProperties)
    println("($i) ", property.key, " => ", property.value)
end
(1) TerminalName => 001023
(2) Installed => true
(3) Locked => false
(4) InstallDate => 1278947280000
(5) RemovalDate => 
(6) Temporary => false
(7) NbBikes => 7
(8) NbEmptyDocks => 12
(9) NbDocks => 19
(10) NbStandardBikes => 7
(11) NbEBikes => 0

So the indices we care about are [8, 10, 11].

function print_bike_point(bp)
    println("$(bp.commonName) (id: $(bp.id))")
    println("Number of standard bikes: $(bp.additionalProperties[10].value)")
    println("Number of E-bikes: $(bp.additionalProperties[11].value)")
    println("Number of empty docks: $(bp.additionalProperties[8].value)")
end

print_bike_point(bp)
River Street , Clerkenwell (id: BikePoints_1)
Number of standard bikes: 7
Number of E-bikes: 0
Number of empty docks: 12

Now that we know how to get the information we want for any station, we need to find the IDs of the stations we want to include in our search. The API provides a BikePoint/Search endpoint where we can search stations by name:

r = HTTP.get(
    "https://api.tfl.gov.uk/BikePoint/Search";
    query=["query" => "Lincoln's Inn Fields"]
)
@show search_results = r.body |> String |> JSON3.read
search_results = (r.body |> String) |> JSON3.read = JSON3.Object[{
                  "$type": "Tfl.Api.Presentation.Entities.Place, Tfl.Api.Presentation.Entities",
                     "id": "BikePoints_809",
                    "url": "/Place/BikePoints_809",
             "commonName": "Lincoln's Inn Fields, Holborn",
              "placeType": "BikePoint",
   "additionalProperties": [],
               "children": [],
           "childrenUrls": [],
                    "lat": 51.516277,
                    "lon": -0.118272
}]

Now that we have the ID, we can use the BikePoint/{id} endpoint to request data for that bike point only:

base_url = "https://api.tfl.gov.uk/BikePoint/BikePoints_"
station_id = "809"
r = HTTP.get(base_url * station_id)
r.body |> String |> JSON3.read |> print_bike_point
Lincoln's Inn Fields, Holborn (id: BikePoints_809)
Number of standard bikes: 23
Number of E-bikes: 0
Number of empty docks: 5

Accessing the API through Shortcuts

Apple's Shortcuts app is able to read JSON objects as dictionaries, so with a little manipulation (and a lot of dragging and dropping) we can replicate the results of the above API call. I wanted to be able to run my Shortcut for multiple destinations (e.g. work, home, etc.), so I separated the Shortcut into two: an inner Shortcut to receive a list of stations and return a list of messages, and an outer Shortcut to pass a list of stations to the inner Shortcut and display the results as an alert.

The Inner Shortcut: Accessing the API

The steps to follow are:

  1. Set the base URL

  2. Add the station ID to the URL

  3. Request the data from the API

  4. Parse the response into a dictionary

  5. Extract the station's commonName

  6. Extract the additionalProperties as a list

    1. Get the number of standard bikes at index 10

    2. Get the number of E-bikes at index 11

    3. Get the number of empty docks at index 8

  7. Combine the results into text that can be displayed

Setting the Base URL

Setting the base URL

Constructing the API Call

We begin looping over every item in the list:

Constructing the API Call

Parsing the API Response

Parsing the API Response

Getting the Station Name

Getting the Station Name

Getting the Station Properties

Because we want to access the additionalProperties dictionary multiple times, we have to create a variable for it:

Getting the Station Properties

Scripting functionality in Shortcuts is fairly limited, so I had to hardcode the next three steps rather than using something like a loop:

Imgur

Imgur

Imgur

Constructing the Output

Now I combine everything into a single message. I leave an empty line at the top so that when the Shortcut returns multiple stations they are separated by a space. I chose to represent each number with an emoji since it saves visual space, allowing all three numbers to fit into one line on my watch.

When you tell Siri to run the shortcut, the results are read aloud. This can sound pretty confusing with the output text I currently have written, but you can tweak the text output to make it sound more natural when spoken if you prefer to run the Shortcut through Siri.

Constructing the Output

The Outer Shortcut: Making a List of Stations

Constructing a list of stations you want to be included in the Shortcut can be a bit difficult, depending on how you approach it. I used the map in the mobile app to find the names of the stations I wanted to include and looked up the ID for each station. You can do this using the API, but I found it quicker to look up the XML feed and search for the station name using my browser (the number you need for the shortcut is the <id> field, not <terminalName>).

Once we've done that, the Shortcut itself is very simple:

Outer Shortcut

Running the Shortcut produces a native alert:

Outer Shortcut Result

The great advantage of this Shortcut is that these results are incredibly accessible because Shortcuts are built into iOS. You can add the Shortcut to your home screen as an icon, run it from a widget, access it from your Mac's task bar, or even run it from a complication on your watch face:

Outer Shortcut Result on Apple Watch

I use this Shortcut pretty much every time I leave the house, and it's saved me from riding up to a full station more times than I can count. If you'd like to try them out for yourself, here are the iCloud sharing links:

CC BY-SA 4.0 Jack Shannon. Last modified: March 13, 2024. Website built with Franklin.jl and the Julia programming language.