Catalog PyPI
PACKAGE EXPLORERView in registry

70maiclient

PyPI

Reverse-engineered Python client for the 70mai dashcam cloud API (eu-api.70mai.com)

Versions collected1
Direct dependencies2From release 0.3.0
Last observed20 Oct, 16:08 UTC

README

Version 0.3.0 documentation

70maiclient

hacs_badge Version Downloads buy me a coffee

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.

Disclaimer

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.

Install

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

Usage

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

Layout

  • 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.

Authentication model

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.

Response envelope

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.

Extending the client

  • New endpoints follow the existing pattern: build an extra dict of endpoint-specific fields, pass it to self._signed_body(extra), POST via self._post(path, body).
  • Do not invent response-field extraction beyond what a real captured response confirms — raise/return the raw dict instead, as every existing method does.
  • If you reverse-engineer a new signing variant, add it to SignatureProvider (protocol) and BanyacKeyMd5SignatureProvider (implementation) rather than hardcoding it in client.py.
  • Live view (P2P streaming) is not implemented — static analysis found getTutkToken/living endpoints backed by the ThroughTek/TUTK SDK, but no request/response shape was captured or confirmed.

Releasing

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.

License

GNU AGPLv3 © Lorenzo De Luca

Downloads & popularity

3downloads
1-day window · source-reported counts19 Oct 2026
9748.50
2026-09-12: 87 · observed 2026-10-20T19:33:32.1258848Z · period 2026-09-12–2026-09-122026-09-13: 97 · observed 2026-10-20T19:33:32.1258848Z · period 2026-09-13–2026-09-132026-09-14: 12 · observed 2026-10-20T19:33:32.1258848Z · period 2026-09-14–2026-09-142026-09-15: 8 · observed 2026-10-20T19:33:32.1258848Z · period 2026-09-15–2026-09-152026-09-16: 8 · observed 2026-10-20T19:33:32.1258848Z · period 2026-09-16–2026-09-162026-09-17: 2 · observed 2026-10-20T19:33:32.1258848Z · period 2026-09-17–2026-09-172026-09-18: 12 · observed 2026-10-20T19:33:32.1258848Z · period 2026-09-18–2026-09-182026-09-19: 8 · observed 2026-10-20T19:33:32.1258848Z · period 2026-09-19–2026-09-192026-09-20: 4 · observed 2026-10-20T19:33:32.1258848Z · period 2026-09-20–2026-09-202026-09-21: 5 · observed 2026-10-20T19:33:32.1258848Z · period 2026-09-21–2026-09-212026-09-22: 2 · observed 2026-10-20T19:33:32.1258848Z · period 2026-09-22–2026-09-222026-09-23: 7 · observed 2026-10-20T19:33:32.1258848Z · period 2026-09-23–2026-09-232026-09-24: 4 · observed 2026-10-20T19:33:32.1258848Z · period 2026-09-24–2026-09-242026-09-25: 4 · observed 2026-10-20T19:33:32.1258848Z · period 2026-09-25–2026-09-252026-09-26: 2 · observed 2026-10-20T19:33:32.1258848Z · period 2026-09-26–2026-09-262026-09-27: 11 · observed 2026-10-20T19:33:32.1258848Z · period 2026-09-27–2026-09-272026-09-28: 4 · observed 2026-10-20T19:33:32.1258848Z · period 2026-09-28–2026-09-282026-09-29: 5 · observed 2026-10-20T19:33:32.1258848Z · period 2026-09-29–2026-09-292026-09-30: 6 · observed 2026-10-20T19:33:32.1258848Z · period 2026-09-30–2026-09-302026-10-01: 2 · observed 2026-10-20T19:33:32.1258848Z · period 2026-10-01–2026-10-012026-10-02: 3 · observed 2026-10-20T19:33:32.1258848Z · period 2026-10-02–2026-10-022026-10-04: 5 · observed 2026-10-20T19:33:32.1258848Z · period 2026-10-04–2026-10-042026-10-05: 6 · observed 2026-10-20T19:33:32.1258848Z · period 2026-10-05–2026-10-052026-10-06: 2 · observed 2026-10-20T19:33:32.1258848Z · period 2026-10-06–2026-10-062026-10-07: 7 · observed 2026-10-20T19:33:32.1258848Z · period 2026-10-07–2026-10-072026-10-08: 11 · observed 2026-10-20T19:33:32.1258848Z · period 2026-10-08–2026-10-082026-10-09: 1 · observed 2026-10-20T19:33:32.1258848Z · period 2026-10-09–2026-10-092026-10-10: 5 · observed 2026-10-20T19:33:32.1258848Z · period 2026-10-10–2026-10-102026-10-11: 7 · observed 2026-10-20T19:33:32.1258848Z · period 2026-10-11–2026-10-112026-10-12: 4 · observed 2026-10-20T19:33:32.1258848Z · period 2026-10-12–2026-10-122026-10-13: 4 · observed 2026-10-20T19:33:32.1258848Z · period 2026-10-13–2026-10-132026-10-14: 3 · observed 2026-10-20T19:33:32.1258848Z · period 2026-10-14–2026-10-142026-10-15: 1 · observed 2026-10-20T19:33:32.1258848Z · period 2026-10-15–2026-10-152026-10-16: 4 · observed 2026-10-20T19:33:32.1258848Z · period 2026-10-16–2026-10-162026-10-17: 5 · observed 2026-10-20T19:33:32.1258848Z · period 2026-10-17–2026-10-172026-10-18: 15 · observed 2026-10-20T19:33:32.1258848Z · period 2026-10-18–2026-10-182026-10-19: 3 · observed 2026-10-20T19:33:32.1258848Z · period 2026-10-19–2026-10-19
12 Sept19 Oct

pypistats · 37 observations shown. Missing days are unknown. Source category: overall: without mirrors.

View observed values
Date / source periodValueObserved at
2026-10-19 – 2026-10-1932026-10-20T19:33:32.1258848Z
2026-10-18 – 2026-10-18152026-10-20T19:33:32.1258848Z
2026-10-17 – 2026-10-1752026-10-20T19:33:32.1258848Z
2026-10-16 – 2026-10-1642026-10-20T19:33:32.1258848Z
2026-10-15 – 2026-10-1512026-10-20T19:33:32.1258848Z
2026-10-14 – 2026-10-1432026-10-20T19:33:32.1258848Z
2026-10-13 – 2026-10-1342026-10-20T19:33:32.1258848Z
2026-10-12 – 2026-10-1242026-10-20T19:33:32.1258848Z
2026-10-11 – 2026-10-1172026-10-20T19:33:32.1258848Z
2026-10-10 – 2026-10-1052026-10-20T19:33:32.1258848Z
2026-10-09 – 2026-10-0912026-10-20T19:33:32.1258848Z
2026-10-08 – 2026-10-08112026-10-20T19:33:32.1258848Z
2026-10-07 – 2026-10-0772026-10-20T19:33:32.1258848Z
2026-10-06 – 2026-10-0622026-10-20T19:33:32.1258848Z
2026-10-05 – 2026-10-0562026-10-20T19:33:32.1258848Z
2026-10-04 – 2026-10-0452026-10-20T19:33:32.1258848Z
2026-10-02 – 2026-10-0232026-10-20T19:33:32.1258848Z
2026-10-01 – 2026-10-0122026-10-20T19:33:32.1258848Z
2026-09-30 – 2026-09-3062026-10-20T19:33:32.1258848Z
2026-09-29 – 2026-09-2952026-10-20T19:33:32.1258848Z
2026-09-28 – 2026-09-2842026-10-20T19:33:32.1258848Z
2026-09-27 – 2026-09-27112026-10-20T19:33:32.1258848Z
2026-09-26 – 2026-09-2622026-10-20T19:33:32.1258848Z
2026-09-25 – 2026-09-2542026-10-20T19:33:32.1258848Z
2026-09-24 – 2026-09-2442026-10-20T19:33:32.1258848Z
2026-09-23 – 2026-09-2372026-10-20T19:33:32.1258848Z
2026-09-22 – 2026-09-2222026-10-20T19:33:32.1258848Z
2026-09-21 – 2026-09-2152026-10-20T19:33:32.1258848Z
2026-09-20 – 2026-09-2042026-10-20T19:33:32.1258848Z
2026-09-19 – 2026-09-1982026-10-20T19:33:32.1258848Z
2026-09-18 – 2026-09-18122026-10-20T19:33:32.1258848Z
2026-09-17 – 2026-09-1722026-10-20T19:33:32.1258848Z
2026-09-16 – 2026-09-1682026-10-20T19:33:32.1258848Z
2026-09-15 – 2026-09-1582026-10-20T19:33:32.1258848Z
2026-09-14 – 2026-09-14122026-10-20T19:33:32.1258848Z
2026-09-13 – 2026-09-13972026-10-20T19:33:32.1258848Z
2026-09-12 – 2026-09-12872026-10-20T19:33:32.1258848Z

Versions 1

Collected releases
VersionPublishedDirect dependenciesOpen version
0.3.013 Sept 20262

Package advisories

0
No matching advisories collected

This describes our collected data; it is not a security assessment.

Repository

PENDING
lorenzo-deluca/70maiclient

Shared repository data · GitHub

License detected in repository
Not collected

Metadata history

Meaningful changes
Current collected state Current

Observed since 20 Oct, 16:08 UTC

No superseded metadata states collected. Unchanged observations do not create new revisions.

Record identity

package:21a520d688dfa46f358e69bb842764e24d5b859e61c7ba3f24c439117f76208b