Legacy Countdown StopId / Stop_Code_LBSL from Unified API


I was wondering if there was a way to get the StopId / Stop_Code_LBSL from the Unified API.

The stop sequence csv https://tfl.gov.uk/tfl/syndication/feeds/bus-stops.csv I have found very unreliable, there are dozens of naptans missing from real bus stops.

I was wondering if there was a way to get codes from Unified api to future proof my apps from deprecation of stop sequence csv / countdown api.

Thanks in advance

This is not a comprehensive list just some examples of real world naptans missing on stop csv :



My experience of using that CSV file is that it’s not really best used as a primary source, it’s more a “useful cross-reference file”.

If you want all of the currrent data, the thing to do is probably get a list of all the bus service numbers from https://api.tfl.gov.uk/line/mode/bus/status and then loop though them all (with a small pause, say my inner hacker) https://api.tfl.gov.uk/line/<route_id>/stoppoints to get the stop points.

That’s great but if I want for example to find stop code 26747 from 490000073K via unified api is there any way to do that?


Is this the “SMS code” for the bus stop?

https://api.tfl.gov.uk/stoppoint/490000073K would be the obvious place to start, but the depreciated SMS codes aren’t shown. I don’t think that TfL really want people using them: they are very costly to the end user.

No not the SMS code or ICS code, it’s in the legacy countdown api but I can’t find it on the Unified.

Compare countdown api I posted with https://api.tfl.gov.uk/stoppoint/490000073K you can’t find the 26747 anywhere.

Hi @willa

The ID that you mention here is known as CALMS ID internally and this is the Surface Transport ID for the stop. It’s not something that we currently consume in our databuild or output via the API.

I’ll discuss with @theochapple on her return from leave what the plans are for this data.


The CALMS id is actually visible in the Working Timetable files for timing points used for scheduling purposes (interestingly it is visible for ALL stops in the dump of expired working timetables made available last year). The NAPTAN id is not included.

On the other hand the Journey Planner files available via Datastore only have the NAPTAN id.

It is useful sometimes to be able to marry up the codes used in the different sources and the stops csv file made that possible for the most part.

There is always the live countdown interface

http://countdown.api.tfl.gov.uk/interfaces/ura/stream_V1?ReturnList=StopCode2,StopPointIndicator,DestinationName,EstimatedTime,ExpireTime,VehicleID,StopID (with your generated LiveBusNNNNNN username and password - it’s an HTTPD digest )

Is the “StopCode2” that it can provide the CALMS ID?#

I know it means watching the live service until you have all the codes you need. It does have a LineID parameter, so you can just poll each service you don’t have all the codes for, rather than having to consume and process every single iBus datapoint.


(my PHP code to curl to this interface)

namespace xxx\mtrCrossrail\services\tflDigest;
class digestCommon
const DEBUGMODE = false;
const LINEID = "UL16,UL51,90";
public $isAlreadyRunning = null;

public function setAlreadyRunning($isAlreadyRunning)
    $this->isAlreadyRunning = $isAlreadyRunning;

public function startDigest($strURL = "http://countdown.api.tfl.gov.uk/interfaces/ura/stream_V1",
                            $strUsername = "LiveBus<id>",
                            $strPassword = "<password>",
                            $arrPostParameters = ["LineID" => self::LINEID,
                                "ReturnList" => "StopCode2,StopPointIndicator,DestinationName,EstimatedTime,ExpireTime,VehicleID,StopID"], $intAuth = CURLAUTH_DIGEST, $strFunctionName = "on_curl_write")
    if (self::DEBUGMODE) {
        ini_set('display_errors', '1');
    if (is_array($arrPostParameters)) {
        $arrPostParameters = http_build_query($arrPostParameters);
    echo PHP_EOL . __FUNCTION__ . " is sending " . strlen(var_export($arrPostParameters, true)) . " bytes to {$strURL}";
    echo PHP_EOL . PHP_EOL . PHP_EOL;
    $arrOptions = [CURLOPT_URL => $strURL,
        CURLOPT_HEADER => false,
        CURLOPT_RETURNTRANSFER => $strFunctionName !== "",
        CURLOPT_SSL_VERIFYPEER => substr($strURL, 0, 5) === "https",    // for https
        CURLOPT_USERPWD => $strUsername . ":" . $strPassword,
        CURLOPT_HTTPAUTH => $intAuth,
        CURLOPT_POST => true,
        CURLOPT_POSTFIELDS => $arrPostParameters];
    if ($strFunctionName != "") {
        $arrOptions = $arrOptions + [CURLOPT_WRITEFUNCTION => array($this, $strFunctionName)];
    $resCurlHandle = curl_init();
    curl_setopt_array($resCurlHandle, $arrOptions);
    try {
        $RawResponce = curl_exec($resCurlHandle);
        // validate CURL status
        if (curl_errno($resCurlHandle)) {
            throw new \Exception(curl_error($resCurlHandle), 500);
        // validate HTTP status code (user/strPassword credential issues)
        $intStatusCode = curl_getinfo($resCurlHandle, CURLINFO_HTTP_CODE);
        if ($intStatusCode != 200) {
            throw new \Exception("Response with Status Code [" . $intStatusCode . "].", 500);
    } catch (\Exception $ex) {
        if ($resCurlHandle != null) {
        throw new \Exception($ex);
    if ($resCurlHandle !== null) curl_close($resCurlHandle);
    return $RawResponce;


Thanks. Column 1 most certainly is the CALMS id. They come in a range of shapes and sizes, some numeric, some alphanumeric (e g BPnnnn). The differences may have some internal significance but once it is there, a code is a code.

You had me worried with columns 3 until I realised it was the bus destination rather than the stop name!

The third identifier, of course, is the SMS code. Can this be got on the same data line by extending what you have done?


1 Like


Yes, you can loop though all the bus service numbers in turn.

It’s worth remembering that this data is being used to send to countdown service indicators on bus stops, so the service will only work when the buses are running, so you might need some special cron-code to find Night Buses, School buses and Sunday Specials.

The HTTP service generates the whole of the current state of a single line and then only spits out changes that iBus calculates on the fly. If a bus runs “on time” then it won’t generate anything new. If it’s stuck in traffic, it will generate new data. The indicators at bus-stops aren’t terminals, they are addressable displays that know the time.

Let me know how you get on.

Brian - This is where I display my ignorance, never having used the Countdown feed directly. I have an id and password that I use (for example, for this forum, if I ever need to provide them) but I have no idea whether this is the same as LiveBus etc that needs to go in the php script.

I ought to be able to do these things but to be honest, I was more interested in whether the three IDs - NAPTAN, CALMS and SMS - were available in one place. The Journey Planner Update document that Gerard Butler used to post here shows all three codes for new or changed stops (SMS codes are still being allocated to new stops, it seems) but every other source seems to have just one or two of the three.

Hi jamesevans thanks for your response.

It would be great if all the data points in the countdown api were available via unified (for other people as who need other stuff in addtion to CALMS id), just so it was possible to migrate away from this system completely. As I understand the streaming version for this countdown api is no longer available (while there is a signlar one for the unified).