This document describes the HTTP communication of LibreLinkUp which functions as follower app to receive cgm data. Some data in the responses were masked.
This dump was created on an android device with LibreLinkUp app. Capturing was done with HttpToolkit over adb.
The global api url is https://api.libreview.io. If you are placed in europe you can use https://api-eu.libreview.io instead.
The following list includes general purpose headers but also specific ones which are required to get correct responses:
'accept-encoding': 'gzip'
'cache-control': 'no-cache'
'connection': 'Keep-Alive'
'content-type': 'application/json'
The following headers are required and needs to be setted to get correct responses. There might be alternative values which are not known.
'product': 'llu.android'
'version': '4.2.1',
To get cgm data from the api it is required to fire at least three requests:
- Login and retrieve JWT Token
- Get connections of patients to get
patientId - Retrieve cgm data of specific
patient
This request expects credentials and will return a JWT Token which is required to call auth-needed endpoints.
Endpoint POST /llu/auth/login
Request Body
{
"email": "[email protected]",
"password": "$yOurVerySecretPasSw0rd!"
}
Response
{
"status": 0,
"data": {
"user": {
"id": "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",
"firstName": "John",
"lastName": "Doe",
"email": "[email protected]",
"country": "DE",
"uiLanguage": "de-DE",
"communicationLanguage": "de-DE",
"accountType": "pat",
"uom": "1",
"dateFormat": "2",
"timeFormat": "2",
"emailDay": [
1
],
"system": {
"messages": {
"firstUsePhoenix": 1652399492,
"firstUsePhoenixReportsDataMerged": 1652399492,
"lluGettingStartedBanner": 1652399555,
"lluNewFeatureModal": 1652399526,
"lluOnboarding": 1652399536,
"lvWebPostRelease": "3.9.47"
}
},
"details": {},
"created": 1652399492,
"lastLogin": 1653140180,
"programs": {},
"dateOfBirth": 627609600,
"practices": {},
"devices": {},
"consents": {
"llu": {
"policyAccept": 1652399485,
"touAccept": 1652399485
}
}
},
"messages": {
"unread": 0
},
"notifications": {
"unresolved": 0
},
"authTicket": {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjZlZGFjNDk2LWQyNGUtMTFlYy04ZTVkLTAyNDJhYzExMDAwMiIsImZpcnN0TmFtZSI6IkhhbGltZSBTZWxjdWsiLCJsYXN0TmFtZSI6Iktla2VjIiwiY291bnRyeSI6IkRFIiwicmVnaW9uIjoiZXUiLCJyb2xlIjoicGF0aWVudCIsInVuaXRzIjoxLCJwcmFjdGljZXMiOltdLCJjIjoxLCJzIjoibGx1LmFuZHJvaWQiLCJleHAiOjE2Njg2OTIzNTh9.MdEzdJ3NrpYS4WVAcuy87Gxzk7EJFHzCtei-y7_XXXX",
"expires": 1668692358,
"duration": 15552000000
},
"invitations": [
"xxxxxxxxx"
]
}
}
The JWT Token is present in data.authTicket.token and is valid for nearly 6 month which is quite long. This is extremly long and actually you cannot invalidate this token why you should not share it with anyone.
For the next requests you have to set this token in the headers:
'authorization': Bearer [YOUR_JWT_TOKEN]
To be able to retrieve cgm data you have to determine the patientId of the person who is sharing his data with you.
Endpoint GET /llu/connections
Response
{
"status": 0,
"data": [
{
"id": "xxxxx",
"patientId": "xxxxxxx",
"country": "DE",
"status": 2,
"firstName": "John",
"lastName": "Doe",
"targetLow": 70,
"targetHigh": 130,
"uom": 1,
"sensor": {
"deviceId": "",
"sn": "xxxxx",
"a": 1652400270,
"w": 60,
"pt": 4
},
"alarmRules": {
"c": true,
"h": {
"on": true,
"th": 130,
"thmm": 7.2,
"d": 1440,
"f": 0.1
},
"f": {
"th": 55,
"thmm": 3,
"d": 30,
"tl": 10,
"tlmm": 0.6
},
"l": {
"on": true,
"th": 70,
"thmm": 3.9,
"d": 1440,
"tl": 10,
"tlmm": 0.6
},
"nd": {
"i": 20,
"r": 5,
"l": 6
},
"p": 5,
"r": 5,
"std": {}
},
"glucoseMeasurement": {
"FactoryTimestamp": "5/21/2022 1:38:50 PM",
"Timestamp": "5/21/2022 3:38:50 PM",
"type": 1,
"ValueInMgPerDl": 91,
"TrendArrow": 3,
"TrendMessage": null,
"MeasurementColor": 1,
"GlucoseUnits": 1,
"Value": 91,
"isHigh": false,
"isLow": false
},
"glucoseItem": {
"FactoryTimestamp": "5/21/2022 1:38:50 PM",
"Timestamp": "5/21/2022 3:38:50 PM",
"type": 1,
"ValueInMgPerDl": 91,
"TrendArrow": 3,
"TrendMessage": null,
"MeasurementColor": 1,
"GlucoseUnits": 1,
"Value": 91,
"isHigh": false,
"isLow": false
},
"glucoseAlarm": null,
"patientDevice": {
"did": "2d97357e-d250-11ec-b409-0242ac110004",
"dtid": 40068,
"v": "3.3.1",
"ll": 65,
"hl": 130,
"u": 1653016896,
"fixedLowAlarmValues": {
"mgdl": 60,
"mmoll": 3.3
},
"alarms": false
},
"created": 1652399545
}
],
"ticket": {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjZlZGFjNDk2LWQyNGUtMTFlYy04ZTVkLTAyNDJhYzExMDAwMiIsImZpcnN0TmFtZSI6IkhhbGltZSBTZWxjdWsiLCJsYXN0TmFtZSI6Iktla2VjIiwiY291bnRyeSI6IkRFIiwicmVnaW9uIjoiZXUiLCJyb2xlIjoicGF0aWVudCIsInVuaXRzIjoxLCJwcmFjdGljZXMiOltdLCJjIjoxLCJzIjoibGx1LmFuZHJvaWQiLCJleHAiOjE2Njg2OTIzNTh9.MdEzdJ3NrpYS4WVAcuy87Gxzk7EJFHzCtei-y7_XXXX",
"expires": 1668692358,
"duration": 15552000000
}
}
The datapart includes all persons who are sharing their data with you. To retrieve cgm data you need data[0].patientId.
Now both prerequirements (JWT Token and Patient ID) are met and you can retrieve the cgm data.
Endpoint GET /llu/connections/{patientId}/graph
Response
{
"status": 0,
"data": {
"connection": {
"id": "xxxxxxx",
"patientId": "xxxxxxx",
"country": "DE",
"status": 2,
"firstName": "John",
"lastName": "Doe",
"targetLow": 70,
"targetHigh": 130,
"uom": 1,
"sensor": {
"deviceId": "",
"sn": "XXXXXXXXXX",
"a": 1652400270,
"w": 60,
"pt": 4
},
"alarmRules": {
"c": true,
"h": {
"on": true,
"th": 130,
"thmm": 7.2,
"d": 1440,
"f": 0.1
},
"f": {
"th": 55,
"thmm": 3,
"d": 30,
"tl": 10,
"tlmm": 0.6
},
"l": {
"on": true,
"th": 70,
"thmm": 3.9,
"d": 1440,
"tl": 10,
"tlmm": 0.6
},
"nd": {
"i": 20,
"r": 5,
"l": 6
},
"p": 5,
"r": 5,
"std": {}
},
"glucoseMeasurement": {
"FactoryTimestamp": "5/21/2022 1:38:50 PM",
"Timestamp": "5/21/2022 3:38:50 PM",
"type": 1,
"ValueInMgPerDl": 91,
"TrendArrow": 3,
"TrendMessage": null,
"MeasurementColor": 1,
"GlucoseUnits": 1,
"Value": 91,
"isHigh": false,
"isLow": false
},
"glucoseItem": {
"FactoryTimestamp": "5/21/2022 1:38:50 PM",
"Timestamp": "5/21/2022 3:38:50 PM",
"type": 1,
"ValueInMgPerDl": 91,
"TrendArrow": 3,
"TrendMessage": null,
"MeasurementColor": 1,
"GlucoseUnits": 1,
"Value": 91,
"isHigh": false,
"isLow": false
},
"glucoseAlarm": null,
"patientDevice": {
"did": "xxxxxxx",
"dtid": 40068,
"v": "3.3.1",
"ll": 65,
"hl": 130,
"u": 1653016896,
"fixedLowAlarmValues": {
"mgdl": 60,
"mmoll": 3.3
},
"alarms": false
},
"created": 1652399545
},
"activeSensors": [
{
"sensor": {
"deviceId": "xxxxxx",
"sn": "xxxxx",
"a": 1652400270,
"w": 60,
"pt": 4
},
"device": {
"did": "xxxxxx",
"dtid": 40068,
"v": "3.3.1",
"ll": 65,
"hl": 130,
"u": 1653016896,
"fixedLowAlarmValues": {
"mgdl": 60,
"mmoll": 3.3
},
"alarms": false
}
},
{
"sensor": {
"deviceId": "xxxxx",
"sn": "xxxxxx",
"a": 1652399154,
"w": 60,
"pt": 4
},
"device": {
"did": "xxxxxxxxx",
"dtid": 40068,
"v": "3.3.1",
"ll": 70,
"hl": 250,
"u": 1652399060,
"fixedLowAlarmValues": {
"mgdl": 60,
"mmoll": 3.3
},
"alarms": false
}
},
{
"sensor": {
"deviceId": "xxxxx",
"sn": "xxxxx",
"a": 1652391830,
"w": 60,
"pt": 4
},
"device": {
"did": "xxxxx",
"dtid": 40068,
"v": "3.3.1",
"ll": 70,
"hl": 250,
"u": 1652396851,
"fixedLowAlarmValues": {
"mgdl": 60,
"mmoll": 3.3
},
"alarms": false
}
}
],
"graphData": [
{
"FactoryTimestamp": "5/21/2022 1:39:50 AM",
"Timestamp": "5/21/2022 3:39:50 AM",
"type": 0,
"ValueInMgPerDl": 117,
"MeasurementColor": 1,
"GlucoseUnits": 1,
"Value": 117,
"isHigh": false,
"isLow": false
},
{
"FactoryTimestamp": "5/21/2022 1:44:51 AM",
"Timestamp": "5/21/2022 3:44:51 AM",
"type": 0,
"ValueInMgPerDl": 115,
"MeasurementColor": 1,
"GlucoseUnits": 1,
"Value": 115,
"isHigh": false,
"isLow": false
},
{
"FactoryTimestamp": "5/21/2022 1:49:50 AM",
"Timestamp": "5/21/2022 3:49:50 AM",
"type": 0,
"ValueInMgPerDl": 115,
"MeasurementColor": 1,
"GlucoseUnits": 1,
"Value": 115,
"isHigh": false,
"isLow": false
},
{
"FactoryTimestamp": "5/21/2022 1:54:51 AM",
"Timestamp": "5/21/2022 3:54:51 AM",
"type": 0,
"ValueInMgPerDl": 116,
"MeasurementColor": 1,
"GlucoseUnits": 1,
"Value": 116,
"isHigh": false,
"isLow": false
},
{
"FactoryTimestamp": "5/21/2022 1:59:50 AM",
"Timestamp": "5/21/2022 3:59:50 AM",
"type": 0,
"ValueInMgPerDl": 116,
"MeasurementColor": 1,
"GlucoseUnits": 1,
"Value": 116,
"isHigh": false,
"isLow": false
},
{
"FactoryTimestamp": "5/21/2022 2:04:50 AM",
"Timestamp": "5/21/2022 4:04:50 AM",
"type": 0,
"ValueInMgPerDl": 118,
"MeasurementColor": 1,
"GlucoseUnits": 1,
"Value": 118,
"isHigh": false,
"isLow": false
},
{
"FactoryTimestamp": "5/21/2022 2:09:50 AM",
"Timestamp": "5/21/2022 4:09:50 AM",
"type": 0,
"ValueInMgPerDl": 118,
"MeasurementColor": 1,
"GlucoseUnits": 1,
"Value": 118,
"isHigh": false,
"isLow": false
},
{
"FactoryTimestamp": "5/21/2022 2:14:51 AM",
"Timestamp": "5/21/2022 4:14:51 AM",
"type": 0,
"ValueInMgPerDl": 115,
"MeasurementColor": 1,
"GlucoseUnits": 1,
"Value": 115,
"isHigh": false,
"isLow": false
}
]
},
"ticket": {
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjZlZGFjNDk2LWQyNGUtMTFlYy04ZTVkLTAyNDJhYzExMDAwMiIsImZpcnN0TmFtZSI6IkhhbGltZSBTZWxjdWsiLCJsYXN0TmFtZSI6Iktla2VjIiwiY291bnRyeSI6IkRFIiwicmVnaW9uIjoiZXUiLCJyb2xlIjoicGF0aWVudCIsInVuaXRzIjoxLCJwcmFjdGljZXMiOltdLCJjIjoxLCJzIjoibGx1LmFuZHJvaWQiLCJleHAiOjE2Njg2OTIzNTl9.LK8Ejr2IDKGM7oiObVYMHC8HV2bPcv6obt7UiEFXXXX",
"expires": 1668692359,
"duration": 15552000000
}
}
The last measurement is present in glucoseMeasurement:
{
"FactoryTimestamp": "5/21/2022 1:38:50 PM",
"Timestamp": "5/21/2022 3:38:50 PM",
"type": 1,
"ValueInMgPerDl": 91,
"TrendArrow": 3,
"TrendMessage": null,
"MeasurementColor": 1,
"GlucoseUnits": 1,
"Value": 91,
"isHigh": false,
"isLow": false
}
Instead you can find historical measurements in graphData. The data has the same shape as above.
Selcuk Kekec
E-mail: [email protected]
@MrAda
I never used either of these (nor seen them used anywhere).
As you can see from the very first message in this gist, step 1 will normally1 return a response with your user details , looking something like
and it is this id which you need to encrypt for use with llu/connections etc.
Footnotes
I say normally, because sometimes the response from step 1 is a redirect to an intermediate step (like accepting new Terms and Conditions etc) and you need to perform another api post to complete these step(s) until you get the full response. If your software does not handle those intermediate steps, you may have to log into the official LibreLinkup app in order to get passed that stage. ↩