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 of the API calls
Once the requests stop coming, I unlock the scooter with the app. Now I see exactly which request unlocks it.
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 errorHTTP 415 Unsupported Media Type
Content-Length
: Without it we get a 500 errorHost
: Just the host of the API, which in this case isapi-bird.prod.birdapp.com
platform
: I was testing this on an iPhone, and it was set toios
, so I just left it.app-version
: I just left the version I saw in the request4.122.0
.app-name
: This was set tobird
The body contained two fields:
bird_id
: This is the ID of the bird scooter we want to unlock/locklock
: This is a boolean. If it is set totrue
it will lock the scooter. If set tofalse
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.
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.