leapmotor-api
Unofficial Python client for the Leapmotor vehicle cloud API.
Extracted from the leapmotor-ha Home Assistant integration to provide a reusable, framework-agnostic library.
Acknowledgments
Special thanks to Jakob Kern for the impressive reverse engineering work on the Leapmotor application and for generously sharing his work.
Installation
pip install leapmotor-api
Certificates
The library requires the Leapmotor app certificate and private key to authenticate API requests. Download them from the dedicated repository:
wget https://github.com/markoceri/leapmotor-certs/archive/refs/tags/v1.0.0.zip
unzip v1.0.0.zip
The extracted folder contains app_cert.pem and app_key.pem. Pass their paths to the client via app_cert_path and app_key_path.
Quick Start
from leapmotor_api import LeapmotorApiClient
client = LeapmotorApiClient(
username="user@example.com",
password="password",
app_cert_path="/path/to/app_cert.pem",
app_key_path="/path/to/app_key.pem",
)
client.login()
vehicles = client.get_vehicle_list()
for vehicle in vehicles:
status = client.get_vehicle_status(vehicle)
print(f"{vehicle.vin} — Battery: {status.battery.soc}% — Range: {status.battery.expected_mileage} km")
client.close()
Async Usage
from leapmotor_api import LeapmotorApiClient
from leapmotor_api.async_client import AsyncLeapmotorApiClient
sync_client = LeapmotorApiClient(
username="user@example.com",
password="password",
app_cert_path="/path/to/app_cert.pem",
app_key_path="/path/to/app_key.pem",
)
client = AsyncLeapmotorApiClient(sync_client)
await client.login()
vehicles = await client.get_vehicle_list()
status = await client.get_vehicle_status(vehicles[0])
await client.close()
Vehicle Status
get_vehicle_status() returns a VehicleStatus dataclass with typed sub-objects:
| Sub-object | Key fields |
|---|---|
status.battery |
soc, expected_mileage, charge_state, is_charging, is_charge_fast_gun_insert, is_charge_slow_gun_insert, charging_power_kw, battery_power, dump_energy_kwh, charge_plan.* |
status.driving |
speed, total_mileage, gear_status, is_parked |
status.location |
latitude, longitude, latitude_signed, longitude_signed |
status.climate |
ac_switch, ac_setting, outdoor_temp, interior_temp, ac_operate_mode, ac_air_volume, ac_cooling_and_heating |
status.doors |
is_locked, bbcm_back_door_status |
status.windows |
left_front_window_percent, right_front_window_percent, sun_shade |
status.tires |
front_left_kpa, front_right_kpa, rear_left_kpa, rear_right_kpa (raw), *_bar, *_psi, all_bar, all_psi, all_ok |
status.connectivity |
bluetooth_state, hotspot_state |
status.seat_comfort |
driver_seat_heating, driver_seat_ventilation, steering_wheel_heating |
status.security |
vehicle_security_active, sentry_mode, roof_opening |
status.ignition |
bcm_key_position_on1, bcm_key_position_on3 |
Top-level convenience properties: status.is_locked, status.is_plugged, status.is_charging, status.is_parked, status.is_driving, status.is_regening, status.tire_pressure_bar, status.tire_pressure_psi.
All fields are T | None — they are populated only when the vehicle reports the corresponding signal.
For raw API data, use get_vehicle_raw_status() or access status.raw.
Charging History
from datetime import date, timedelta
result = client.get_charging_daily_detail(
vehicle.vin,
start_time=date.today() - timedelta(days=90),
end_time=date.today(),
timezone="GMT+01:00",
page_size=50,
)
for record in result.records:
charge_kind = "DC (fast)" if record.is_fast_charge else "AC (normal)"
print(f"{record.start_datetime} -> {record.end_datetime} {charge_kind} {record.energy_kwh} kWh")
ChargeRecord fields: start_ts, end_ts, charge_type (ChargeType.AC / ChargeType.DC), energy_kwh, longitude, latitude, timezone.
Convenience properties: start_datetime, end_datetime, duration_seconds, is_fast_charge.
Energy Consumption Statistics
# Last week energy breakdown (driving, A/C, other)
breakdown = client.get_consumption_last_week_breakdown(vehicle)
print(f"Driver: {breakdown.driver_ec} kWh, AC: {breakdown.ac_ec} kWh, Other: {breakdown.other_ec} kWh")
print(f"Total: {breakdown.total_ec} kWh")
# Six-week consumption history and ranking
weekly_rank = client.get_consumption_weekly_rank(vehicle)
print(f"Rank: {weekly_rank.rank.rank}, Avg: {weekly_rank.rank.hundred_km_ec} kWh/100km")
for week in weekly_rank.weekly:
print(f" {week.week_start} ~ {week.week_end}: {week.hundred_km_ec} kWh/100km")
Both cloud statistics also accept an arbitrary window. Pass timezone-aware datetimes; the client converts them to the unit each endpoint expects:
from datetime import datetime, timedelta, timezone
end = datetime.now(timezone.utc)
start = end - timedelta(days=7)
# Energy split for any window (resolution goes down to about 15-20 minutes)
breakdown = client.get_consumption_breakdown(vehicle, start=start, end=end)
# Lifetime energy plus per-day mileage and energy over the window
history = client.get_mileage_energy_history(vehicle, start=start, end=end)
print(f"Lifetime energy: {history.total_energy_kwh} kWh over {history.total_mileage_km} km")
for day in history.days:
print(f" {day.day}: {day.mileage_km} km, {day.energy_kwh} kWh")
total_energy_kwh is the precise lifetime figure (standby included). The per-day energy_kwh is coarse: the cloud often rounds it to an integer.
Vehicle Permissions
The Vehicle object exposes the Leapmotor 3-tier permission system as typed enums:
| Field | Type | Description |
|---|---|---|
vehicle.abilities |
list[VehicleAbility] |
Hardware feature flags (what the vehicle can do) |
vehicle.rights |
list[VehicleRight] |
Remote command permissions (what the account can execute) |
vehicle.module_rights |
list[ModuleRight] |
Macro sharing categories |
from leapmotor_api import VehicleAbility, VehicleRight
vehicle = vehicles[0]
# Check capabilities
if vehicle.has_right(VehicleRight.WINDOWS):
client.open_windows(vehicle.vin)
if vehicle.has_ability(VehicleAbility.NAVIGATION):
client.send_destination(vehicle.vin, ...)
# Inspect permissions
for right in vehicle.rights:
print(f"{right.name} ({right.value}): {right.description}")
Remote commands automatically log a warning when the vehicle may lack the required permission, but still proceed (the server enforces permissions authoritatively).
Car Sharing
A car the owner shares from the official app reaches the second account as an invitation. Until it is accepted the car is missing from get_vehicle_list() and every vehicle call fails with No such permission. The recipient side is exposed here:
for invitation in client.get_share_invitations():
print(invitation.vin, invitation.car_type, invitation.owner_nickname, invitation.module_rights)
client.accept_share_invitation(invitation) # or client.reject_share_invitation(invitation)
# The car now shows up as a shared vehicle, with the rights the owner granted
shared = [v for v in client.get_vehicle_list() if v.is_shared]
Creating or revoking a share (the owner side) is not covered.
Software Updates (OTA)
get_software_info() returns the installed software version and, when the cloud offers one, the OTA update waiting to be installed, with its release notes and packages. The information is available only to the account that owns the car: for an account the car is shared with, the cloud answers No such permission and the call raises LeapmotorPermissionError.
from leapmotor_api import LeapmotorPermissionError
try:
software = client.get_software_info(vehicle)
except LeapmotorPermissionError:
print("Only the owner account sees the software version")
else:
if software.update_available:
new = software.new_version
print(f"Update {new.version}: {new.download_size / 1e9:.2f} GB in {len(new.packages)} packages")
for note in new.release_notes_for("it-IT"):
print(f"- {note.title}: {note.content}")
Each SoftwarePackage carries a signed download_url; it is kept out of repr(), keep it out of logs too.
Downloading the packages
The packages can be downloaded from the Leapmotor CDN, one at a time or all together. The CDN serves them encrypted (served_file_name ends with .encrypt): they cannot be installed by hand, and md5 / sha256 do not match the downloaded bytes, so only the size is checked. An interrupted download resumes from the .part file on the next call.
from pathlib import Path
new = client.get_software_info(vehicle).new_version
target = Path("ota") / new.version
target.mkdir(parents=True, exist_ok=True)
# One package, with progress
package = min(new.packages, key=lambda p: p.file_size or 0)
path = client.download_software_package(package, target, progress=lambda done, total: print(done, total))
# The whole update (~1.9 GB on a T03); packages already complete are skipped
files = client.download_software_update(new, target)
The link expires (package.link_expires_at); call get_software_info() again for a fresh one. Downloading the packages does not start the update on the car.
Remote Control
Remote actions require the vehicle PIN (operation_password):
client = LeapmotorApiClient(..., operation_password="1234")
client.login()
client.lock_vehicle("WLM...")
client.unlock_vehicle("WLM...")
client.open_trunk("WLM...")
client.close_trunk("WLM...")
client.find_vehicle("WLM...")
client.hotspot("WLM...")
client.autopark("WLM...")
client.open_windows("WLM...")
client.close_windows("WLM...")
client.ac_on("WLM...")
client.ac_off("WLM...")
client.ac_switch("WLM...")
client.quick_cool("WLM...")
client.quick_heat("WLM...")
client.windshield_defrost("WLM...")
client.open_sunshade("WLM...")
client.close_sunshade("WLM...")
client.battery_preheat("WLM...")
client.battery_preheat_off("WLM...")
client.sentry_mode_on("WLM...")
client.sentry_mode_off("WLM...")
client.start_charging("WLM...")
client.stop_charging("WLM...")
client.steering_wheel_heat_on("WLM...")
client.steering_wheel_heat_off("WLM...")
client.fuel_heating_on("WLM...")
client.fuel_heating_off("WLM...")
client.rearview_mirror_heat_on("WLM...")
client.rearview_mirror_heat_off("WLM...")
client.healthy_charging_on("WLM...")
client.healthy_charging_off("WLM...")
client.on3_on("WLM...")
client.on3_off("WLM...")
client.ble_key_restart("WLM...")
client.music("WLM...", operation="play")
client.video("WLM...", operation="play")
client.fota_download("WLM...", task_id=123)
client.fota_install("WLM...", task_id=123)
client.fota_schedule("WLM...", task_id=123, schedule_time="2026-05-13T10:00:00")
client.rear_seats("WLM...", seat_info="fold")
client.prepare_car("WLM...", params={"temperature": "24"})
# Schedule prepare-car (cmd 361) — full-state replacement; [] cancels all
client.set_prepare_car_schedule("WLM...", controls=[{
"datacontent": {"air_condition": {"mode": "cold", "temperature": "18", "circle": "in",
"windlevel": "7", "wshld": "1", "operate": "manual",
"position": "all", "enable": True}},
"days": [1, 2, 3, 4, 5], "enable": True,
"set_id": "ios_<hash><epoch>", "start_time": "2026-06-17 07:30:00",
}])
client.cancel_prepare_car_schedule("WLM...")
client.seat_adjust("WLM...", params={"position": "driver"})
client.piloted_parking("WLM...", params={"action": "start"})
client.set_speed_limit("WLM...", value="80")
client.seat_heat("WLM...", position=3, level=3)
client.seat_ventilation("WLM...", position=3, level=2)
client.open_sunroof("WLM...")
client.close_sunroof("WLM...")
client.set_charge_limit("WLM...", charge_limit_percent=80)
client.set_charge_schedule(
"WLM...",
enabled=True,
soc_limit=80,
start_time="23:00",
end_time="07:00",
cycles="1,2,3,4,5,6,7",
circulation=1,
)
client.unlock_charger("WLM...")
Configuration
| Parameter | Type | Default | Description |
|---|---|---|---|
username |
str |
— | Leapmotor account email |
password |
str |
— | Leapmotor account password |
app_cert_path |
str |
— | Path to app certificate PEM |
app_key_path |
str |
— | Path to app private key PEM |
operation_password |
str | None |
None |
Vehicle PIN (required for remote control) |
language |
str |
"en-GB" |
API language (en-GB, it-IT, de-DE, fr-FR, …) |
verify_ssl |
bool |
False |
Verify server TLS certificate |
base_url |
str |
DEFAULT_BASE_URL |
API base URL |
timeout |
int |
30 |
HTTP timeout in seconds |
device_id |
str | None |
None |
Custom device ID (auto-generated if omitted) |
Token refresh is handled automatically — all methods detect expired tokens and transparently refresh or re-login. See token_refresh() for manual control.
Documentation
- API Reference — endpoints, cryptography and remote commands
- Vehicle Model Differences — API response format and signal IDs across vehicle models
Contributing
Interested in contributing? Read the contributing guide for development setup, testing, and PR guidelines.
License
This project is licensed under the GNU Affero General Public License v3.0.
Metadata
Release files for leapmotor-api 0.6.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| leapmotor_api-0.6.0.tar.gz | 142.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| leapmotor_api-0.6.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 220.1 kB
Release files / leapmotor_api-0.6.0.tar.gz
| Download URL | leapmotor_api-0.6.0.tar.gz |
|---|---|
| Size | 142.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
29325d98bb7966124ce5785bf170f5afb08ac1897f9fa4dc277f81f41e7be2dd
|
|
BLAKE2b-256 checksum How to use checksums |
f33e081aee3f729269b1a4e40a954c26ed9576da3098a8493c33722fca140d9b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 13, 2026.
Transparency logRelease files / leapmotor_api-0.6.0-py3-none-any.whl
| Download URL | leapmotor_api-0.6.0-py3-none-any.whl |
|---|---|
| Size | 78.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
3d304d9a8033b6dcbc47d3cd5bcfd143e380327e02479bfe3800e76e9c1c8813
|
|
BLAKE2b-256 checksum How to use checksums |
ee42c4fd68cbc8c5e8cb7fca93e41f304ddcd3cc097355cea91faefd6cc859f0
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 13, 2026.
Transparency log