Files
arneandClaude Opus 5.5 6466f002e0 Add Baby Tracker client and Home Assistant integration
Python client for the Baby Tracker sync server, reverse engineered from
the Android app (4.53), and a HACS-installable Home Assistant integration
built on it. Each baby gets a log calendar and summary sensors, and actions
add entries that sync back to the group.

Reading has been run against the real server. Uploading and running inside
real Home Assistant have not.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-04 10:04:39 +00:00

11 KiB

Baby Tracker sync protocol

Derived from Baby Tracker for Android 4.53, mainly sync/SyncImple, utility/GsonUtils and database/BTDatabaseImple. Nothing here has been observed on the wire. It is what the app's code sends and expects. Open questions lists what the code cannot settle.

Conventions

  • Base URL: https://prodapp.babytrackers.com
  • Auth: a session cookie set by POST /session. No tokens or request signing.
  • JSON requests set Content-Type: application/json and charset: utf-8.
  • On a 401, the app logs in again and retries once.
  • After a re-login, the app compares AccountID with the stored one and turns sync off if they differ.

Endpoints

Method Path Body Success Other statuses the app handles
POST /session UserInfo 200, {"AccountID": n} 400/401 bad login, 307, 409
DELETE /session 200 401
POST /account/forgot_password see below 200 409
POST /device_token {"TokenNative": "...", "TokenAppType": 5} 200 or 201 401, 408
GET /account/device 200, [DeviceSyncInfo] 401
GET /account/transaction/{deviceUUID}/{lastSyncID} 200, [TransactionItem] 401
POST /account/transaction TransactionItem 201 401
GET /account/seed 200, zip file 401, 404 no seed
GET /account/picture/{fileName} 200, file 401, 404
POST /account/picture multipart picture 201 401, 408
POST /account/clonedata multipart clonedata 201 401, 408
POST /staging_account UserInfo 200 400/401, 409
POST /staging_account/seed multipart seed 201 401, 408
POST /staging_account/picture multipart picture 201 401, 408
PUT /staging_account 200, {"AccountID": n} 401, 408, 409, 424 no seed
POST /account_reset UserInfo 200 400/401, 409

Request bodies

// UserInfo
{
  "EmailAddress": "you@example.com",
  "Password": "plain text, protected only by TLS",
  "Device": {
    "DeviceUUID": "random UUID",
    "DeviceName": "free text",
    "DeviceOSInfo": "<Build.MODEL> OS <SDK_INT>"
  },
  "AppInfo": {"AccountType": 0, "AppType": 0}
}

// forgot_password. Language is Locale.getLanguage(), such as "en".
{"EmailAddress": "...", "Language": "en", "AppInfo": {"AccountType": 0, "AppType": 0}}

// DeviceSyncInfo
{"DeviceUUID": "...", "LastSyncID": 123}

// TransactionItem. Transaction is base64 of a record's JSON.
{"SyncID": 123, "OPCode": "0", "Transaction": "ewog..."}

Multipart format

The boundaries are fixed, and each body starts with a CRLF:

  • __BABY_TRACKER_BOUNDARY____Photo__, for pictures and clone data
  • __BABY_TRACKER_BOUNDARY____SEED__, for seeds
\r\n--<boundary>\r\n
Content-Disposition: form-data; name="picture"; filename="abc.jpg"\r\n
Content-Type: image/jpeg\r\n\r\n
<bytes>\r\n--<boundary>--\r\n

Seeds use name="seed"; filename="seed.zip". Clone data uses name="clonedata" with the device ID in the file name. Both have Content-Type: application/zip.

Flows

Sync

  1. GET /account/device.
  2. For each device other than this one, compare LastSyncID with the locally merged value (table MergedTransaction). If behind, or unknown, start from the local value or from 0.
  3. GET /account/transaction/{device}/{syncID} and merge. Repeat from the last SyncID while a page has at least 500 entries.
  4. Upload this device's pending entries oldest first, one POST each, and delete each after its 201.

Join a group

  1. Reset the local device ID.
  2. POST /session. A 400 or 401 means the group does not exist or the password is wrong.
  3. Upload the old local database as clone data. The app continues if this fails.
  4. GET /account/seed, unzip it, and replace the local database with EasyLog.db.
  5. Download pictures and thumbnails, then run a sync.

Create a group POST /staging_account, then POST /staging_account/seed, then each picture to /staging_account/picture, then PUT /staging_account.

Reset a group As create, but starting with POST /account_reset.

Records

The decoded Transaction value is JSON written by Gson, then UTF-8, then standard base64 with padding and no line breaks.

  • BCObjectType: the class name, such as "Diaper". It appears at the top level only. Nested objects such as baby do not have it.
  • Dates: "yyyy-MM-dd HH:mm:ss Z" in GMT, such as "2026-11-04 08:30:00 +0000". Parsers must also accept "yyyy-MM-dd hh:mm:ss a Z", including localized AM/PM markers such as nachm. or 午後, because the app does.
  • Enums: strings holding a number, such as "1".
  • Nulls: omitted.
  • Durations: whole minutes.
  • objectID: a random UUID.
  • timestamp: last modification time.
  • time: when the activity happened.
  • timezone: UTC offset in seconds, excluding DST.
  • newFlage: spelled that way. Merging ignores the incoming value.

An update or delete carries the whole record. The app applies records in log order, with no timestamp comparison in the activity merge path, so the last one merged wins. Replica instead ignores a change older than the one it holds, because it fetches logs device by device when rebuilding from the seed.

Common fields

Class Parent Fields
BCObject objectID, timestamp, newFlage
Activity BCObject time, timezone, note, pictureLoaded, pictureNote (list of Picture)
BabyActivity Activity baby
Baby BCObject name, dob, dueDay, gender (true is boy), pictureName
Picture BCObject fileName, activityID, attached, deleted, mediaType

Activities

Class Parent Fields
Diaper BabyActivity status, amount
Sleep BabyActivity duration
Bath, Joy, Journal BabyActivity none
Growth BabyActivity weight, length, head, deleted, queryIndex
Temperature BabyActivity temperature
Medication BabyActivity medicationSelection, amount (float)
Vaccine BabyActivity vaccineType
Milestone BabyActivity milestoneType, hasNewType
OtherActivity BabyActivity desc, duration
Feed BabyActivity none
Formula, Pumped Bottle, which is a Feed amount
OtherFeed Feed feedType, desc, amount, unit
Nursing Feed leftDuration, rightDuration, bothDuration, finishSide
NursingSession Nursing state, pairSessionState, detailed, leftTimestamps, rightTimestamps
Pump Activity (no baby) amount, leftAmount, rightAmount, leftDuration, rightDuration, sides, finishSide, label

Selections

Class Parent Fields
EditableSelection BCObject name, desc
OtherFeedSelection EditableSelection bottle
MedicationSelection EditableSelection prescription, amountPerTime, interval, unit
MilestoneSelection EditableSelection babyID, forAge, used
VaccineSelection EditableSelection forAge
OtherActivityDescription EditableSelection none

ConflictRecord holds winner and loser. Each is a full record, with its own BCObjectType.

Measures

A measure is {"englishMeasure": bool, "value": float}.

Measure Metric English
Volume ml fl oz
Weight kg lb
Length cm in
Temperature °C °F

Enums

Enum Values
OPCode 0 insert, 1 update, 2 delete, 3 relive, 4 conflict
DiaperStatus 0 wet, 1 poopy, 2 mixed, 3 dry
DiaperAmount 0 none, 1 light, 2 normal, 3 heavy, 4 overflow, 5 not set
NursingFinishSide 0 not set, 1 left, 2 right
PumpFinishSide 0 none, 1 left, 2 right
PumpSides 0 both, 1 left, 2 right
MediaType 0 picture, 1 video, 2 audio
NursingSessionState 0 start, 1 left running, 2 right running, 3 left paused, 4 right paused, 5 both stopped, 6 end

The app's statistics count a diaper with status wet and amount none as dry. The diaper() helper therefore defaults to amount "not set".

Pictures and seeds

  • A Picture record's file is <fileName>.jpg for a picture, <fileName>.mp4 for a video and plain <fileName> for audio. Its thumbnail is <fileName>__thumbnail.jpg.
  • A baby's picture is named <pictureName>.jpg.
  • Upload the picture files as well as the record that refers to them.
  • The seed zip contains the SQLite database EasyLog.db. Times in it are Unix seconds stored as floats. MergedTransaction(DeviceID, SyncID) says how far each device's log is already included.

Open questions

The app's code does not settle these. The client's choice is noted where it made one.

Question Client's behaviour
Does login add the device to the account? Assumed yes. Keep device_uuid stable.
Is lastSyncID exclusive? The app passes its last merged ID and logs "got old sync item" for repeats. Assumed exclusive. Entries at or below the cursor are dropped, so an inclusive server is also safe.
Does LastSyncID include a device's own uploads? The app counts SyncIDs locally, in an autoincrement table. Assumed yes. The next SyncID is LastSyncID + 1, then counted locally. The Home Assistant integration also relies on this to find its own log after a restart.
What does the server do with a repeated or out-of-order SyncID? Unknown. Errors are raised to the caller.
What does 307 on login mean? The app stores the credentials, returns internal code 6001 and turns sync off. No UI code handles 6001 specifically. Raises AccountStateError.
What does 409 mean on login and /account_reset? On /staging_account it most likely means the email is taken. Raises ConflictError.
Are there other response fields? The app ignores anything it does not declare. Ignored, except for transaction payloads, which are kept whole.
Must a nested baby be complete, or is objectID enough? Android stores only the ID, but iOS clients may differ. Sends the full baby record you pass in.
How do other platforms handle records? The server may also serve an iOS app, whose records could have types missing from GsonUtils. Unknown types decode normally. Android would reject them, so do not upload them to a group with Android devices.

Security notes

  • Certificates are validated normally. SyncImple.trustAllHosts() exists but nothing calls it.
  • The app stores the account password in shared preferences, DES encrypted with a key hardcoded in the APK. Anyone with access to the app's preferences can recover it.
  • The server takes the password itself rather than a hash of it, so TLS is its only protection in transit.