Reverse engineering the Bird scooter API

Featured image

Introduction

I recently got a new Bird One scooter. I liked the fact that I could lock and unlock it with the Bird app. However, needing to open the app before riding the scooter and when parking it is annoying.

So I decided to reverse engineer the API so that I could connect it to the iOS Shortcuts app. This will allow me to use Siri, my Apple Watch, or even NFC tags.

Initially, I had assumed that when using the app to unlock, the app gets a token from Bird, and sends that token via Bluetooth to the scooter to unlock it.

However, upon seeing the API calls, I realized that Bird unlocks the scooter directly.

Bird scooters use a cellular connection to communicate with the Bird infrastructure. Based on this comment on scootertalk.org, Bird uses Verizon sim cards. However, based on some of the data I’ve seen in the API, I believe my scooter is connected to T-Mobile.

When using the app to unlock/lock the scooter, the app makes a request to the Bird API to unlock it. Bird then sends the command directly to the scooter over its cellular connection. To connect it to Siri, we need to intercept the API calls, and make those same calls ourselves.

I used mitmproxy to intercept the API calls.

When opening the Bird app we get lots of API calls:

mitmweb view, showing the api calls performed by the Bird app mitmweb view of the API calls

Once the requests stop coming, I unlock the scooter with the app. Now I see exactly which request unlocks it.

Screenshot of unlock request headers

It is a POST request to /bird/action/lock endpoint. I copied the headers and the body to Postman. I slowly removed headers to figure out which ones were required. I narrowed it down to these required headers:

  • Authorization: Obviously we need to authorize the request. The contents need to be a Bearer token. More on this later.

  • Content-Type: Without this we get a 415 error HTTP 415 Unsupported Media Type

  • Content-Length: Without it we get a 500 error

  • Host: Just the host of the API, which in this case is api-bird.prod.birdapp.com

  • platform: I was testing this on an iPhone, and it was set to ios, so I just left it.

  • app-version: I just left the version I saw in the request 4.122.0.

  • app-name: This was set to bird

The body contained two fields:

  • bird_id: This is the ID of the bird scooter we want to unlock/lock

  • lock: This is a boolean. If it is set to true it will lock the scooter. If set to false it will unlock it.

The response contains some interesting information about the scooter. (Note, I redacted any identifying information):

{
  "success": true,
  "bird": {
    "id": "<redacted>",
    "location": {
      "latitude": "<redacted>",
      "longitude": "<redacted>"
    },
    "code": "<redacted>",
    "down": false,
    "needs_inspection": false,
    "captive": false,
    "partner_id": "<redacted>",
    "battery_level": 100,
    "private_bird": {
      "id": "<redacted>",
      "user_id": "<redacted>",
      "bird_id": "<redacted>",
      "created_at": "<redacted>",
      "updated_at": "<redacted>",
      "ownership_kind": "owner"
    },
    "serial_number": "<redacted>",
    "model": "rf",
    "gps_at": "<redacted>",
    "tracked_at": "<redacted>",
    "locked": true,
    "ack_locked": true,
    "lifecycle": {
      "brain_state": "available"
    },
    "bluetooth": true,
    "cellular": true
  }
}

Authentication

Now that I figured out how to lock and unlock the scooter, the only missing piece was the authentication.

I was able to replay the request successfully, but when I decoded the bearer token I saw that it doesn’t last very long. This means I need to be able to generate new tokens.

Bird uses a standard refresh/access token system. There are many good explanations available online. The gist of it is that we don’t want the server to need to look up to see if the token is valid. That would require a database lookup before beginning each request. Instead, Json Web Tokens (JWT) can be used. Data can be encoded and signed directly in the token and validated without a lookup.

Seeing that the token in the unlock request was short-lived, means that there must be a long-lived token that is being used to generate the short-lived tokens. I needed to find it.

I was not going to wait around for the app to decide to refresh the token, so I need to trick it to refresh now. To force the app the get a new access token, I first force-quit the Bird app. I then changed the time on the phone to a couple of days into the future. This will cause the Bird app to think the access token has already expired and refresh it.

I was able to capture this exchange and get the refresh token. The request was a POST to:

https://api-auth.prod.birdapp.com/api/v1/auth/refresh/token

The bearer token in this request was the refresh token, which expires in a few months. All the headers were the same as the previous requests. The only additional header was device-id. This can be filled with anything, as long as it’s not left empty.

This returned:

{
  "access": "<redacted>",
  "refresh": "<redacted>"
}

The refresh token is an updated one, which extends the expiration date of the one we just used for this request. The access token is what we need to make our standard API requests.

I also wanted to see how I could get the initial refresh token without needing to man-in-the-middle every time the refresh token expires. I signed out of the app and signed back in. I was able to see a request to

POST https://api-auth.prod.birdapp.com/api/v1/auth/email
{
    "email": "<redacted"
}

My email address was included in the body of the request.

Making this request, triggers an email that contains a token that can be used to get the refresh token:

POST https://api-auth.prod.birdapp.com/api/v1/auth/magic-link/use
{
"token": "<redacted>"
}

This returns:

{
"access": "<redacted>",
"refresh": "<redacted>"
}

Siri shortcuts

Once I knew which API requests I needed to generate the access tokens, and lock/unlock the scooter, I created a Shortcut for it in the iOS Shortcuts app. I created one shortcut to generate the access token.

Shortcut screenshot

This shortcut can be called by other shortcuts to get the token.

This shortcut then uses the token to unlock the scooter.

Final notes

I’m pretty sure there is also an option for Bird to send an unlock token to the app to unlock the scooter even if the scooter does not have an internet connection. I’m saying this, because I noticed an API call which returned a token. However, I didn’t investigate this further, because it’s simpler for me to just do one API call and not need to snoop on the Bluetooth traffic to figure out how to unlock it that way.