Observed since 20 Oct, 16:08 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
Reverse-engineered Python client for the 70mai dashcam cloud API (eu-api.70mai.com)
A reverse-engineered Python client for the 70mai dashcam cloud API
(eu-api.70mai.com), built from a Postman-exported HTTPS capture of the
official iOS app and cross-checked against a decompiled Android APK.
It's not affiliated with, endorsed by, or supported by 70mai. The underlying API is undocumented and can change without notice. If someone from 70mai would like to contribute or collaborate please contact me at me@lorenzodeluca.dev
Used by homeassistant-70mai, a Home Assistant integration built on top of this client.
PyPI project name is 70maiclient (PyPI names can't start with a digit
in a way that's also a valid Python identifier, so the distribution
name differs from the import name below):
pip install 70maiclient
import uuid
from client70mai import MaiClient
client = MaiClient(uuid=str(uuid.uuid4()).upper())
client.login(email="your-email", plain_password="your-password")
devices = client.get_all_device_for_user()
print(devices)
detail = client.get_dashcam_detail(device_id="your-device-id")
print(detail)
client.logout()
See example.py for the same script standalone, and
console.py for an interactive rich-based CLI that
exercises every endpoint (Italian-language prompts):
pip install "70maiclient[console]"
python3 console.py
client70mai/client.py — MaiClient, the only class consumers use. Holds
the session, the device uuid, the auth token, and one method per API
endpoint.client70mai/signing.py — SignatureProvider protocol plus
BanyacKeyMd5SignatureProvider, the verified implementation of the
server's request-signing scheme.client70mai/exceptions.py — MaiError and subclasses for transport,
HTTP, response-parsing, auth, and signing failures.client70mai/__init__.py — public re-exports.example.py — minimal login → list devices → detail → logout script.console.py — interactive rich-based CLI over MaiClient for manually
exercising every endpoint (Italian-language prompts).re/ — the APK, extracted dex/native libs, and Ghidra/pyghidra scripts
used to reverse the signing algorithm. Not imported by client70mai,
and not shipped in the PyPI package.api-refs/ — captured traffic used as ground truth: Postman exports
(request-only, no response bodies) and *.proxymanlogv2 archives (a zip
of one JSON file per request, request/response bodyData fields
base64-encoded — these DO carry real response bodies, unlike the Postman
exports, and are how most "CONFIRMED response" notes in client.py were
derived). Not shipped in the PyPI package.There is no Authorization header or cookie anywhere in the captured
traffic. Every authenticated call instead sends, inside its JSON body:
uuid — per-install device UUID (caller-supplied, any valid UUID string).channel — constant 3001013 (CHANNEL_ID in client.py) on every
request, including unauthenticated ones.ts — client epoch-millisecond timestamp, unique per request.token — opaque session token from userLoginPwd/login(), reused
verbatim until logout().sig — md5(token + uuid + channel + ts + SECRET), SECRET recovered
from the Android app's native libbanyackey.so (see signing below).MaiClient._signed_body() assembles this envelope for every method except
login() and get_ts(). Calling any endpoint before login() raises
MaiNotAuthenticatedError.
login() uses a different, separately-verified construction for the
passWord field (md5("dashcam" + plain_password + SECRET)) and its own
sig (md5(deviceType + email + "null" + passWord + ts + SECRET)) — see
the signing.py module docstring for the full decompilation trail.
Every call returns the raw parsed JSON body as Dict[str, Any], matching
the confirmed MaiCommonResult<T> shape:
{"error": bool, "errorCode": int, "errorMessage": str, "resultBodyObject": T}
resultBodyObject's shape (T) is not unwrapped by the client. The
original Postman captures had no successful response bodies at all, so
every schema was originally a best-effort guess; a 2026-09 Proxyman
capture (api-refs/*.proxymanlogv2) since confirmed the real
resultBodyObject shape for many endpoints (each such method's docstring
in client.py says "CONFIRMED response" and gives the shape). Endpoints
without that note are still unconfirmed — treat any field access on their
response as unverified until you observe real traffic. login() is the
one method that always reads a specific field
(resultBodyObject.token), confirmed, because the client cannot function
without it.
extra dict of
endpoint-specific fields, pass it to self._signed_body(extra), POST via
self._post(path, body).SignatureProvider (protocol) and BanyacKeyMd5SignatureProvider
(implementation) rather than hardcoding it in client.py.getTutkToken/living endpoints backed by the ThroughTek/TUTK SDK, but
no request/response shape was captured or confirmed.Tagging a GitHub Release publishes to PyPI automatically via
.github/workflows/publish.yml (trusted publishing, no stored token).
Bump version in pyproject.toml before tagging.
GNU AGPLv3 © Lorenzo De Luca
pypistats · 37 observations shown. Missing days are unknown. Source category: overall: without mirrors.
| Date / source period | Value | Observed at |
|---|---|---|
| 2026-10-19 – 2026-10-19 | 3 | 2026-10-20T19:33:32.1258848Z |
| 2026-10-18 – 2026-10-18 | 15 | 2026-10-20T19:33:32.1258848Z |
| 2026-10-17 – 2026-10-17 | 5 | 2026-10-20T19:33:32.1258848Z |
| 2026-10-16 – 2026-10-16 | 4 | 2026-10-20T19:33:32.1258848Z |
| 2026-10-15 – 2026-10-15 | 1 | 2026-10-20T19:33:32.1258848Z |
| 2026-10-14 – 2026-10-14 | 3 | 2026-10-20T19:33:32.1258848Z |
| 2026-10-13 – 2026-10-13 | 4 | 2026-10-20T19:33:32.1258848Z |
| 2026-10-12 – 2026-10-12 | 4 | 2026-10-20T19:33:32.1258848Z |
| 2026-10-11 – 2026-10-11 | 7 | 2026-10-20T19:33:32.1258848Z |
| 2026-10-10 – 2026-10-10 | 5 | 2026-10-20T19:33:32.1258848Z |
| 2026-10-09 – 2026-10-09 | 1 | 2026-10-20T19:33:32.1258848Z |
| 2026-10-08 – 2026-10-08 | 11 | 2026-10-20T19:33:32.1258848Z |
| 2026-10-07 – 2026-10-07 | 7 | 2026-10-20T19:33:32.1258848Z |
| 2026-10-06 – 2026-10-06 | 2 | 2026-10-20T19:33:32.1258848Z |
| 2026-10-05 – 2026-10-05 | 6 | 2026-10-20T19:33:32.1258848Z |
| 2026-10-04 – 2026-10-04 | 5 | 2026-10-20T19:33:32.1258848Z |
| 2026-10-02 – 2026-10-02 | 3 | 2026-10-20T19:33:32.1258848Z |
| 2026-10-01 – 2026-10-01 | 2 | 2026-10-20T19:33:32.1258848Z |
| 2026-09-30 – 2026-09-30 | 6 | 2026-10-20T19:33:32.1258848Z |
| 2026-09-29 – 2026-09-29 | 5 | 2026-10-20T19:33:32.1258848Z |
| 2026-09-28 – 2026-09-28 | 4 | 2026-10-20T19:33:32.1258848Z |
| 2026-09-27 – 2026-09-27 | 11 | 2026-10-20T19:33:32.1258848Z |
| 2026-09-26 – 2026-09-26 | 2 | 2026-10-20T19:33:32.1258848Z |
| 2026-09-25 – 2026-09-25 | 4 | 2026-10-20T19:33:32.1258848Z |
| 2026-09-24 – 2026-09-24 | 4 | 2026-10-20T19:33:32.1258848Z |
| 2026-09-23 – 2026-09-23 | 7 | 2026-10-20T19:33:32.1258848Z |
| 2026-09-22 – 2026-09-22 | 2 | 2026-10-20T19:33:32.1258848Z |
| 2026-09-21 – 2026-09-21 | 5 | 2026-10-20T19:33:32.1258848Z |
| 2026-09-20 – 2026-09-20 | 4 | 2026-10-20T19:33:32.1258848Z |
| 2026-09-19 – 2026-09-19 | 8 | 2026-10-20T19:33:32.1258848Z |
| 2026-09-18 – 2026-09-18 | 12 | 2026-10-20T19:33:32.1258848Z |
| 2026-09-17 – 2026-09-17 | 2 | 2026-10-20T19:33:32.1258848Z |
| 2026-09-16 – 2026-09-16 | 8 | 2026-10-20T19:33:32.1258848Z |
| 2026-09-15 – 2026-09-15 | 8 | 2026-10-20T19:33:32.1258848Z |
| 2026-09-14 – 2026-09-14 | 12 | 2026-10-20T19:33:32.1258848Z |
| 2026-09-13 – 2026-09-13 | 97 | 2026-10-20T19:33:32.1258848Z |
| 2026-09-12 – 2026-09-12 | 87 | 2026-10-20T19:33:32.1258848Z |
| Version | Published | Direct dependencies | Open version |
|---|---|---|---|
| 0.3.0 | 13 Sept 2026 | 2 |
This describes our collected data; it is not a security assessment.
Shared repository data · GitHub
Observed since 20 Oct, 16:08 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
package:21a520d688dfa46f358e69bb842764e24d5b859e61c7ba3f24c439117f76208b