Reverse Engineering the BMW i3 API


I'm really enjoying driving the BMW i3.

I'd love to have it tweet its driving efficiency, or upload its location to my server, or let me turn on its air-conditioning when the temperature gets too warm - there are a hundred interesting things to do with the car's data. The official app has some of these features - but is slow, ugly, and a pain to use.

BMW used to have an API available for hackathons, but they shut it down.

Right! Only one thing to do - HACK THE MAINFRAME*!


*No mainframes were harmed in the writing of this blog post

Aim

The ultimate aim is to get access to all the car's functions which are exposed by the API - so I can do something like this:

Equipment

For this relatively simple bit of work you will need...

With Packet Capture installed, close all other apps, start the logging, then log in to the i Remote app. Click around on a few functions, then flip back to the Packet Capture app.

You'll see a bunch of API calls which have been captured.
BMW API-fs8

Click on one and you'll see the full API call, plus the JSON response. I've stripped out some of my personal details.
BMW API Details-fs8

The most important thing you'll need is the Authorization: Bearer token. This is a 32 character alphanumeric string.

Description

The latest version can always be found on GitHub.

These API calls are designed to allow you to interact with your BMW i3. They were reverse engineered from the official BMW i Remote Android app.

Your use of these API calls is entirely at your own risk. They are neither officially provided nor sanctioned.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Servers

There are three API servers.

  • https://b2vapi.bmwgroup.cn:8592 China
  • https://b2vapi.bmwgroup.us USA
  • https://b2vapi.bmwgroup.com Europe / Rest of World

Authorisation

In order to authenticate against the API you will need to be registered on BMW's Connected Drive service.

You will need:

  1. Your ConnectedDrive registered email address.
  2. Your ConnectedDrive registered password.
  3. The i Remote API Key.
  4. The i Remote API Secret.

You can get the i Remote details from either decompiling the Android App or from intercepting communications between your phone and the BMW server. This is left as an exercise for the reader ☺

Firstly, we use Basic authentication. That means taking the API Key and Secret and Base64 encoding them.

So key:secret becomes a2V5OnNlY3JldA==

We also need to send the following parameters as

  • Content-Type: application/x-www-form-urlencoded
 grant_type=password
&username=whatever%40example.com
&password=p4ssw0rd
&scope=remote_services+vehicle_data

Here's how to do it with curl:

curl \
   -H "Authorization: Basic a2V5OnNlY3JldA==" \
   -H "Content-Type: application/x-www-form-urlencoded" \
   -d "grant_type=password&username=whatever%40example.com&password=p4ssw0rd&scope=remote_services+vehicle_data" \
   "https://b2vapi.bmwgroup.com/webapi/oauth/token/"

If everything has worked, you should get back the following JSON:

{
  "access_token": "RCQ1hLP4AFaUBW9BjcPUN3i4WgkwF90R",
  "token_type": "Bearer",
  "expires_in": 28800,
  "refresh_token": "7WgKmEJ2kD1ydl9Hefp01eS8qDGzKnzjeORpA6vtsoFIEanz",
  "scope": "vehicle_data remote_services"
}

You must include

  • Authorization: Bearer RCQ1hLP4AFaUBW9BjcPUN3i4WgkwF90R

in your headers with every request.

The expires_in is in seconds - giving you 8 hours before you have to renew the token.

I've no idea what the refresh_token is for. Once the access_token expires, you can simply re-authenticate and gain a new one.

API

You must include

  • Authorization: Bearer RCQ1hLP4AFaUBW9BjcPUN3i4WgkwF90R

in your headers with every request.

Get Vehicle Data

  • /webapi/v1/user/vehicles/
    • Remember to include the Authorization: Bearer header.

The most important thing here is the VIN - Vehicle Identification Number. You'll need that for all the other API calls as well as the Authorization Bearer.

Response

{
    "vehicleStatus": {
        "vin": "WAB1C23456V123456",
        "mileage": 1234,
        "updateReason": "VEHICLE_SHUTDOWN_SECURED",
        "updateTime": "2015-10-30T18:45:04+0100",
        "doorDriverFront": "CLOSED",
        "doorDriverRear": "CLOSED",
        "doorPassengerFront": "CLOSED",
        "doorPassengerRear": "CLOSED",
        "windowDriverFront": "CLOSED",
        "windowDriverRear": "CLOSED",
        "windowPassengerFront": "CLOSED",
        "windowPassengerRear": "CLOSED",
        "trunk": "CLOSED",
        "rearWindow": "INVALID",
        "convertibleRoofState": "INVALID",
        "hood": "CLOSED",
        "doorLockState": "SECURED",
        "parkingLight": "OFF",
        "positionLight": "OFF",
        "remainingFuel": 8.9,
        "remainingRangeElectric": 73,
        "remainingRangeElectricMls": 45,
        "remainingRangeFuel": 126,
        "remainingRangeFuelMls": 78,
        "maxRangeElectric": 134,
        "maxRangeElectricMls": 83,
        "fuelPercent": 99,
        "maxFuel": 9,
        "connectionStatus": "DISCONNECTED",
        "chargingStatus": "INVALID",
        "chargingLevelHv": 58,
        "lastChargingEndReason": "UNKNOWN",
        "lastChargingEndResult": "FAILED",
        "position": {
            "lat": 51.123456,
            "lon": -1.2345678,
            "heading": 211,
            "status": "OK"
        },
        "internalDataTimeUTC": "2015-10-30T18:47:44"
    }
}

Values

  • mileage is in Km.
  • remainingFuel is in Litres.
  • maxRangeElectric is in Km.
  • maxRangeElectricMls is in miles.
  • chargingLevelHv is the percentage of charge left in the (High voltage?) battery.
  • maxFuel is in Litres.
  • heading is in degrees.

Valid chargingStatuses appear to be:

  • CHARGING
  • ERROR
  • FINISHED_FULLY_CHARGED
  • FINISHED_NOT_FULL
  • INVALID
  • NOT_CHARGING
  • WAITING_FOR_CHARGING

Valid connectionStatuses appear to be:

  • CHARGING_DONE
  • CHARGING_INTERRUPED [sic]
  • CHARGING_PAUSED
  • CHARGIN_STARTED [sic]
  • CYCLIC_RECHARGING
  • DOOR_STATE_CHANGED
  • NO_CYCLIC_RECHARGING
  • NO_LSC_TRIGGER
  • ON_DEMAND
  • PREDICTION_UPDATE
  • TEMPORARY_POWER_SUPPLY_FAILURE
  • UNKNOWN
  • VEHICLE_MOVING
  • VEHICLE_SECURED
  • VEHICLE_SHUTDOWN
  • VEHICLE_SHUTDOWN_SECURED
  • VEHICLE_UNSECURED

Get Last Trip

Shows the details about your most recent trip.

  • /webapi/v1/user/vehicles/:VIN/statistics/lastTrip
    • Where :VIN is your vehicle's VIN.
    • Remember to include the Authorization: Bearer header.

Response

{
   "lastTrip":{
      "efficiencyValue":0.53,
      "totalDistance":141,
      "electricDistance":100.1,
      "avgElectricConsumption":16.6,
      "avgRecuperation":2,
      "drivingModeValue":0,
      "accelerationValue":0.39,
      "anticipationValue":0.81,
      "totalConsumptionValue":0.79,
      "auxiliaryConsumptionValue":0.66,
      "avgCombinedConsumption":1.9,
      "electricDistanceRatio":71,
      "savedFuel":0,
      "date":"2015-12-01T20:44:00+0100",
      "duration":124
   }
}

Values

Distances appear to be in Kilometres rather than miles, so be sure to adjust accordingly. Multiply by 0.621371 to get miles.

  • totalDistance is in Km.
  • electricDistance is in Km.
  • avgElectricConsumption is in kWh/100Km.
  • avgRecuperation is in kWh/100Km.
  • duration is in minutes.

To convert kWh/100Km to Miles/kWh.

1 / (0.01609344 * avgElectricConsumption)

Get Charging Times

Shows when the car is scheduled to charge.

  • /webapi/v1/user/vehicles/:VIN/chargingprofile
    • Where :VIN is your vehicle's VIN.
    • Remember to include the Authorization: Bearer header.

Response

{
   "weeklyPlanner":{
      "climatizationEnabled":true,
      "chargingMode":"DELAYED_CHARGING",
      "chargingPreferences":"CHARGING_WINDOW",
      "timer1":{
         "departureTime":"07:30",
         "timerEnabled":true,
         "weekdays":[
            "MONDAY"
         ]
      },
      "timer2":{
         "departureTime":"13:00",
         "timerEnabled":false,
         "weekdays":[
            "SATURDAY"
         ]
      },
      "timer3":{
         "departureTime":"08:00",
         "timerEnabled":false,
         "weekdays":[

         ]
      },
      "overrideTimer":{
         "departureTime":"07:30",
         "timerEnabled":false,
         "weekdays":[
            "MONDAY"
         ]
      },
      "preferredChargingWindow":{
         "enabled":true,
         "startTime":"05:02",
         "endTime":"17:31"
      }
   }
}

Values

  • departureTime appears to be the car's local time.

Get Vehicle Destinations

Shows the destinations you've previously sent to the car.

  • /webapi/v1/user/vehicles/:VIN/destinations
    • Where :VIN is your vehicle's VIN.
    • Remember to include the Authorization: Bearer header.

Response

{
   "destinations":[
      {
         "lat":51.53053283691406,
         "lon":-0.08362331241369247,
         "country":"UNITED KINGDOM",
         "city":"LONDON",
         "street":"PITFIELD STREET",
         "type":"DESTINATION",
         "createdAt":"2015-09-25T08:06:11+0200"
      }
   ]
}

Values

  • An array of locations.

Get All Trip Details

Shows the statistics for all trips taken in the vehicle.

  • /webapi/v1/user/vehicles/:VIN/statistics/allTrips
    • Where :VIN is your vehicle's VIN.
    • Remember to include the Authorization: Bearer header.

Response

{
    "allTrips": {
        "avgElectricConsumption": {
            "communityLow": 0,
            "communityAverage": 16.33,
            "communityHigh": 35.53,
            "userAverage": 14.76
        },
        "avgRecuperation": {
            "communityLow": 0,
            "communityAverage": 3.76,
            "communityHigh": 14.03,
            "userAverage": 2.3
        },
        "chargecycleRange": {
            "communityAverage": 121.58,
            "communityHigh": 200,
            "userAverage": 72.62,
            "userHigh": 135,
            "userCurrentChargeCycle": 60
        },
        "totalElectricDistance": {
            "communityLow": 1,
            "communityAverage": 12293.65,
            "communityHigh": 77533.6,
            "userTotal": 3158.66
        },
        "avgCombinedConsumption": {
            "communityLow": 0,
            "communityAverage": 1.21,
            "communityHigh": 6.2,
            "userAverage": 0.36
        },
        "savedCO2": 87.58,
        "savedCO2greenEnergy": 515.177,
        "totalSavedFuel": 0,
        "resetDate": "1970-01-01T01:00:00+0100"
    }
}

Values

  • chargecycleRange is in Km.
  • totalElectricDistance is in Km.

I'm not sure what units of the other values are.

Get Range Map

Generate a polyline displaying the predicted range of the vehicle.

  • /webapi/v1/user/vehicles/:VIN/rangemap
    • Where :VIN is your vehicle's VIN.
    • Remember to include the Authorization: Bearer header.

Response

{
    "rangemap": {
        "center": {
            "lat": 51.123456,
            "lon": -1.2345678
        },
        "quality": "AVERAGE",
        "rangemaps": [
            {
                "type": "ECO_PRO_PLUS",
                "polyline": [
                    {
                        "lat": 51.6991281509399,
                        "lon": -2.00423240661621
                    },
                    {
                        "lat": 51.6909098625183,
                        "lon": -1.91526889801025
                    },
                    ...
                ]
            },
            {
                "type": "COMFORT",
                "polyline": [
                    {
                        "lat": 51.7212295532227,
                        "lon": -1.7363977432251
                    },
                    {
                        "lat": 51.6991496086121,
                        "lon": -1.73077583312988
                    },
                    ...
                ]
            }
        ]
    }
}

Values

  • ECO_PRO_PLUS driving using the efficient Eco mode.
  • COMFORT driving using comfort mode.

Sending information to the car

Sending information to the car is slightly complicated.

Your app communicates with the API, the API then communicates with the car's 3G modem, then you have to wait for a response.

If your car is in poor coverage, you can expect significant latency. Often much higher than a typical timeout will allow for.

At a basic level, you can just send a request - for example to lock the doors, or set off-peak charging.

Get Request status

Shows the status of a POSTed request

  • /webapi/v1/user/vehicles/:VIN/serviceExecutionStatus?serviceType=:SERVICE
    • Where :VIN is your vehicle's VIN.
    • Remember to include the Authorization: Bearer header.

Response

{
   "executionStatus":{
      "serviceType":"DOOR_LOCK",
      "status":"EXECUTED",
      "eventId":"[email protected]"
   }
}

Values

Valid statuses are:

  • DELIVERED
  • EXECUTED
  • INITIATED
  • NOT_EXECUTED
  • PENDING
  • TIMED_OUT

The following are valid :SERVICE types, but may not be supported by your vehicle.

  • CHARGE_NOW
  • CHARGING_CONTROL
  • CLIMATE_CONTROL
  • CLIMATE_NOW
  • DOOR_LOCK
  • DOOR_UNLOCK
  • GET_ALL_IMAGES
  • GET_PASSWORD_RESET_INFO
  • GET_VEHICLES
  • GET_VEHICLE_IMAGE
  • GET_VEHICLE_STATUS
  • HORN_BLOW
  • LIGHT_FLASH
  • LOCAL_SEARCH
  • LOCAL_SEARCH_SUGGESTIONS
  • LOGIN
  • LOGOUT
  • SEND_POI_TO_CAR
  • VEHICLE_FINDER

POST a command

Instructs the car to perform an action.

  • /webapi/v1/user/vehicles/:VIN/executeService
    • Where :VIN is your vehicle's VIN.
    • Remember to include the Authorization: Bearer header.

Available Commands

These commands are all available via the API, but may not be supported by your vehicle.

These are just what I've discovered so far.

Data must be POSTed to the server.

Initiate Charging

If the vehicle is plugged in, but not charging (due to an off peak setting?) it is possible to force the car to charge.

  • serviceType=CHARGE_NOW

Start Climate Control

This will activate climate control within your vehicle.

It appears to be limited to the last temperature you set when you were in the car. I can't find a way to instruct the car to reach a specific temperature.

  • serviceType=CLIMATE_NOW

Lock the doors

Performs central locking.

  • serviceType=DOOR_LOCK

Unlock the doors

This will unlock all the doors on your vehicle.

Please use extreme caution when sending this command. Ensure that you are in sight of the vehicle and are able to lock it if needed.

  • serviceType=DOOR_UNLOCK

Flash the headlights

If you can't find the vehicle, or need to illuminate something in its vicinity, you can briefly activate the headlights.

  • serviceType=LIGHT_FLASH&count=2
    • I assume that count relates to the number of seconds to keep the light on?

Charging Schedule

Set the peak / off peak charging schedule.

  • serviceType=CHARGING_CONTROL
    • I haven't bothered to figure this out, but the error it returns should give you some pointers:
{
   "error":{
      "code":500,
      "description":"(SmartPhoneUtil-A-102) Bad value(s) for parameter(s): Invalid chargingProfile, expected weeklyPlanner or twoTimesTimer"
   }
}

Vehicle Finder

  • serviceType=VEHICLE_FINDER
    • I'm not sure what this does.

Response

An example response for all POST commands:

{
    "executionStatus": {
        "serviceType": "LIGHT_FLASH",
        "status": "INITIATED",
        "eventId": "[email protected]"
    }
}

What's Next

Using these commands you should be able to replicate the functionality of the official app.

As you can see, I've managed to get my car to Tweet:

It would be lovely if BMW decided to open up an official API so that people could fiddle with their cars. The API seems secure and there's limited scope for damaging the vehicles.

I've added all the documentation to GitHub. Please raise an issue or send a Pull Request with any changes.

36 thoughts on “Reverse Engineering the BMW i3 API

  1. What you described is pretty standard oAuth architecture. refresh_token is there to be able to refresh your access_token after it expires without having to send username and password again.

    1. Hi quentin - I've done the same thing as you. Perhaps I am missing something or not following correctly, but I'm not seeing api key/secret in my packet capture. Any hints?

      1. Hi Justin - I don't have it in front of me, but if memory serves, the key and secret are used to get a token which is valid for a certain period. I used the token that the app was using, initially, for the API, but also printed out its expiry time, which turned out not to be very distant.

        The app will have the same expiry time, so I kept it using the proxy and the first time after the expiry, the key and secret were visible in the exchange.

        Hope that helps!

  2. it's really good to see you doing this. But it is lamentable - outrageous even - that BMW make it so hard. Two questions arise for me. 1. What sort of consumer pressure can BMW owners/drivers exert to get BMW to give them an open API on a car they've bought? and 2. Are there legality issues eg in US under DMCA and similar legislation, also under new emerging legislation such as TTP?

    Longer term surely open source cars will prevail over closed systems.

    1. Hi William,

      1. Most consumers won't care - API access is a non-issue. The best way to make consumers ask for this is by providing features which rely on this access. For example "VW's cars can do XYZ, why can't BMW's?".

      2. I am not a lawyer - I'm also based in the UK, not the USA. I suggest contacting the EFF or writing to your local politicians.

      And, yes, let's hope Open Source wins out in the end!

  3. Terence, thank you for all this. Quick question, how can I get valid API Key & API Secret if I don't have an iPhone or Android (Windows all the way here) Since no iRemote app for Windows, I was planning to build my own.

  4. Great work Terence, although I've been unable to capture the authentication tokens as it appears BMW have now implemented some form of certificate pinning within their app. As now once the app is opened, multiple sessions are established with b2vapi.bmwgroup.com but no data is transfered - as if TLS is failing in handshake stages. Any ideas how I can get hold of the API key and secret?
    Cheers,

      1. Hi, on the GitHub I only found GET Commands.
        I need POST commands to send serviceType=CLIMATE_NOW.
        Thank you.
        Regards.

        1. That may be my fault - I re-engineered some of Terence's original code, and I only did GET. I can't remember whether his original had POSTs as well - it wouldn't be difficult to add.

  5. Hi Terrence, is there a discussion group set up anywhere for this?
    I've made great progress on my own app but ran into a problem. After getting the vehicle status, once I retrieve that info it doesn't seem to change when I refresh that data unless I restart my app. Is there any possibility that a time stamp needs to be included too to trigger a new vehicle status update? Looking at the iPhone iRemote traffic it uses this type of URL for its status requests:
    https://b2vapi.bmwgroup.us/webapi/v1/user/vehicles/MyVIN/status?deviceTime=2016-01-03T17%3A46%3A46

    Thanks,
    Rick

      1. I may have a problem spotting where to go on GitHub but I just don't see any discussion/comment sections there. Can you point me specifically on where to go? I have a lot of questions and a lot of comments on things I've discovered in my C# app on Windows.

  6. Has anyone managed to packet-sniff or otherwise obtain the key & secret? Are these different for every device or can someone share them. I had no luck using tPacketCapture (jp.co.taosoftware.android.packetcapture) or Packet Capture (app.greyshirts.sslcapture)

    1. I think they're basically fixed for a specific app, but we don't really want to take the responsibility of sharing them. :-)

      What I did for my iOS phone in a similar situation was get a copy of the Charles proxy app (charlesproxy.com), run it on my laptop including SSL proxying, and on my phone (a) install the root SSL key that Charles can generate and (b) tell my phone to use the laptop as a proxy. Finally, if memory serves, when watching the current keys go by, look at their expiry time and after that has passed, fire up the app again and you'll see the key and secret being sent.

      There are ways you can do this with free open source software, but Charles is very handy if you do any kind of HTTP network monitoring and worth the price, in my opinion.

      1. Thank you Quentin. Seems a fantastic bit of software and one I am likely to purchase. I am testing on both my Android phone and my iPad. I have installed the SSL certificate after setting up the proxy connection. I can also see it making requests to the API endpoint but I am getting no data and Charles shows "SSLHandshake: Received fatal alert: certificate_unknown" via Android and "SSLHandshake: Remote host closed connection during handshake". Is this a case that the app has been updated since you managed to get it to work. As an aside, I am also using the normal Remote app rather than one for the i3 / i8 car.

        1. Just a quick update. It seems that the regular Remote app doesn't want to play nice with Charles Proxy. I had to download the i Remote app and that gave me what I needed. I can now make requests and retrieve the vehicle data for my M135i :) Thank you again

  7. One thing that I am interested in pulling is the energy consumed while charging. I can then register historical energy consumption and that allows me to make decisions about where to charge it, when, etc = save money. I can't see that data being exposed by BMW through this way, nor any other. Am I missing something?

    1. No, I don't think you can see the charging rate. Best you can do is measure the percentage of the battery every minute and work it out from there.

  8. I was able to successfully use your technique to have my home automation system pre-cool my BMW 535i on hot mornings. Now my kids don't get into a hot car to wait for me to take them to camp.

    Thank you so much!

  9. Pingback: BMW API – Notes
  10. I am using Charles and an iPhone with the iRemote app. I am unable to get the api keys. I get the following error in Charles: "Failure No request was made. Possibly the SSL certificate was rejected." I have the Charles certificate installed on the iPhone but the iRemote app does not seem to recognize it. I know it is installed properly because the proxy works for SSL sites from the Safari browser. Anyone have any ideas?

    By the way, great work on this.

  11. Hello Terence,

    this is fascinating stuff. I don't own an i3, but I am currently researching data to build an estimation model for the range of electric cars in real world settings. The car manufacturers are not easily giving these out and i found no public data for this task. I was wondering if you or anyone else was collecting regular output of the "status" API durig a trip and would be willing to share some trip histories (anonymized, of course) to an open data platform or your blog?
    I am mostly interested in the lat/long coords, combined with the state of charge, remaining fuel, a timestamp and mileage. I completely understand however if you do not want to give out coordinates near your place of residence. I then would still be interested in the remaining data, since my model could also work with a combination of mileage and charging state.
    Thank you in any case.

    Best regards,
    Martin

Leave a Reply

Your email address will not be published. Required fields are marked *

%d bloggers like this: