Skip to content

Instantly share code, notes, and snippets.

@khskekec
Last active April 28, 2024 04:30
Show Gist options
  • Star 47 You must be signed in to star a gist
  • Fork 10 You must be signed in to fork a gist
  • Save khskekec/6c13ba01b10d3018d816706a32ae8ab2 to your computer and use it in GitHub Desktop.
Save khskekec/6c13ba01b10d3018d816706a32ae8ab2 to your computer and use it in GitHub Desktop.
HTTP dump of Libre Link Up used in combination with FreeStyle Libre 3
@smpurkis
Copy link

I've made some progress. I had to change the version from 4.2.1 to 4.7.0 and now I am getting a response. Unfortunately, the response from the /llu/connections/{patientId}/graph endpoint, has an empty list for graph data.

@LitteulBlack
Copy link

@smpurkis: Is just a information, unfortunately that can’t directly help you, but is working great for me. I have all data of the graph, see below:

'graphData': [
    {
        'FactoryTimestamp': '9/14/2023 10:04:01 AM', 
        'Timestamp': '9/14/2023 12:04:01 PM', 
        'type': 0, 
        'ValueInMgPerDl': xxx, 
        'MeasurementColor': 1, 
        'GlucoseUnits': 1, 
        'Value': xxx, 
        'isHigh': False, 
        'isLow': False}, 
    {
        'FactoryTimestamp': '9/14/2023 10:19:00 AM', 
        'Timestamp': '9/14/2023 12:19:00 PM', 
        'type': 0, 
        'ValueInMgPerDl': xxx, 
        'MeasurementColor': 1, 
        'GlucoseUnits': 1, 
        'Value': xxx, 
        'isHigh': False, 
        'isLow': False
    },
.... 

@wtluke
Copy link

wtluke commented Sep 15, 2023

Thanks for this - excellent. I too had to change the version to 4.7. Some Python if anyone wants it:

import json

# Constants
BASE_URL = "https://api.libreview.io"  # or "https://api-eu.libreview.io" for Europe
HEADERS = {
    'accept-encoding': 'gzip',
    'cache-control': 'no-cache',
    'connection': 'Keep-Alive',
    'content-type': 'application/json',
    'product': 'llu.android',
    'version': '4.7'
}

# Function to log in and retrieve JWT token
def login(email, password):
    endpoint = "/llu/auth/login"
    payload = {
        "email": email,
        "password": password
    }
    
    response = requests.post(BASE_URL + endpoint, headers=HEADERS, json=payload)
    response.raise_for_status()
    data = response.json()
    print(data)
    token = data.get('data', []).get("authTicket", []).get("token", [])  # Access the "token" key from the response JSON
    print(token)
    return token

# Function to get connections of patients
def get_patient_connections(token):
    endpoint = "/llu/connections"  # This is a placeholder, you'll need to replace with the actual endpoint
    headers = {**HEADERS, 'Authorization': f"Bearer {token}"}
    
    response = requests.get(BASE_URL + endpoint, headers=headers)
    response.raise_for_status()
    return response.json()

# Function to retrieve CGM data for a specific patient
def get_cgm_data(token, patient_id):
    endpoint = f"/llu/connections/{patient_id}/graph"  # This is a placeholder, replace with the actual endpoint
    headers = {**HEADERS, 'Authorization': f"Bearer {token}"}
    
    response = requests.get(BASE_URL + endpoint, headers=headers)
    response.raise_for_status()
    return response.json()

# Main Function
def main():
    email = ""  # Replace with your actual email
    password = ""   # Replace with your actual password

    token = login(email, password)
    patient_data = get_patient_connections(token)
    
    patient_id = patient_data['data'][0]["patientId"]
    cgm_data = get_cgm_data(token, patient_id)
    
    print(cgm_data)

if __name__ == "__main__":
    main()

@smpurkis
Copy link

smpurkis commented Oct 19, 2023

P.S This was in response to a comment that has been deleted

If you have no patient ID returned from the get_patient_connections(token) function, then the first possibilities that jump to my mind are:

  1. You aren't using a LibreLinkUp account, this is a different from the FreeStyle LibreLink account. The linkup account is only used to view the freestyle account.
    • The fix, download the LibreLinkUp app, sign up an account (with different email from FreeStyle LibreLink account) and from the FreeStyle LibreLink account invite the LibreLinkUp account. Then retry with the python script using LibreLinkUp account details.
  2. You are using a LibreLinkUp account, but you haven't been connected to anyone's FreeStyle LibreLink account to be able to view them.
    • The fix, get whoever you would like to view to connect your LibreLinkUp account email from their FreeStyle LibreLink app, under Connections in the sidebar (if I recall).

The patient ID should be in the format of xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx a mix of numbers and lower case letters.

Just checked the endpoint using https://github.com/smpurkis/libre-api-pg/tree/old and it ran successfully for me.

@pavlolohvinov
Copy link

Hello. Could you please help me. I want to port the protocol to c++ so that it can be used with esp32. At the first stage, I decided to check the protocol using "postman". I got the following result. Can you tell what I'm doing wrong?
image

@Hynesman
Copy link

Hynesman commented Dec 7, 2023

Hello. Could you please help me. I want to port the protocol to c++ so that it can be used with esp32.

Thats a great Idea!! I did this for Dexcom Follow and was actually just looking to see if anyone has done it for librelinkup.
Here is what i have done. not sure it will help but I am interested in seeing what you come up with.
https://github.com/Hynesman/Dexcom_Follow

@maplerock
Copy link

Hello. Could you please help me. I want to port the protocol to c++ so that it can be used with esp32. At the first stage, I decided to check the protocol using "postman". I got the following result. Can you tell what I'm doing wrong? image

You need to change your endpoint to https://api-eu2.libreview.io/llu/auth/login

@Diegosmgq44
Copy link

Diegosmgq44 commented Dec 20, 2023

I can generate correctly the jwt token, but if i try to access another endpoint i have the next message. Can you tell me what I`m doing wrong? Thanks! Token is correctly generate ;)
image

@tarsjoris
Copy link

tarsjoris commented Dec 28, 2023

I can generate correctly the jwt token, but if i try to access another endpoint i have the next message. Can you tell me what I`m doing wrong? Thanks! Token is correctly generate ;)

I can't see the image, but if the login response contains status 4 instead of 0, it means that the terms and conditions need to be accepted first. If so, logging out and back in again in the official app will fix this.

@deelo55
Copy link

deelo55 commented Jan 18, 2024

Hi, I only seem to be able to access a small amount of historical data. Is there a way to get all historical data going back to the first reading (e.g. a few years ago)?

@jamescowie
Copy link

When using the login API I constantly get:

{"message":"Method Not Allowed"}

Any ideas on why?

@voljumet
Copy link

Hi, I only seem to be able to access a small amount of historical data. Is there a way to get all historical data going back to the first reading (e.g. a few years ago)?

I'm also looking for this

@Burak50
Copy link

Burak50 commented Jan 22, 2024

When using the login API I constantly get:

{"message":"Method Not Allowed"}

Any ideas on why?

I receive this message only when my link is structured like this: https://apiUrl/connections/{{patientId}}/

The correct version should be: https://apiUrl/connections/{{patientId}}/graph

edit: You wrote that you get it during login; I overlooked it. What does your link look like?

@ATouhou
Copy link

ATouhou commented Jan 25, 2024

Works like a charm! Thank you very much!

@BrandonGoodman
Copy link

This is great information. Thank you for sharing, @khskekec

Has anyone determined the possible values from the data.connection.glucoseMeasurement.TrendArrow field?
mostly just see 3 in my limited testing which seems to be ➡. My guess would be something like this:

  • 1 = ⬆
  • 2 = ↗️
  • 3 = ➡
  • 4 =↘️
  • 5 = ⬇️

Can anyone confirm? I could have it backward for all I know right now.

I also wonder what to expect to see populated in data.connection.glucoseMeasurement.TrendMessage. All I get there is null.

@maplerock
Copy link

I've got the below which I think is correct.

$trendArrow = match($freeStyleJson['data']['connection']['glucoseMeasurement']['TrendArrow']) {
            1 => '⬇️',
            2 => '↘️',
            3 => '➡️',
            4 => '↗️',
            5 => '⬆️',
        };

@gui-dos
Copy link

gui-dos commented Jan 26, 2024

LibreLinkUp is written in NativeScript. After beautifying bundle.js you can easily find out the enum definition:

function(e) {
    e[e.NotDetermined = 0] = "NotDetermined", e[e.FallingQuickly = 1] = "FallingQuickly", e[e.Falling = 2] = "Falling", e[e.Stable = 3] = "Stable", e[e.Rising = 4] = "Rising", e[e.RisingQuickly = 5] = "RisingQuickly"
}(a || (a = {}))

@BrandonGoodman
Copy link

so I did have them reversed. Thank you both for the corrections

@Burak50
Copy link

Burak50 commented Jan 28, 2024

FYI: for future users: https://api-eu.libreview.io does not work, for me anyway, the solution was https://api-de.....

The only question is still whether this is only the case in Germany or also in other European countries

My guess is that only the countries France, Germany, Italy, Netherlands, Spain, United Kingdom and United States of America work (because that's the countries listed on freestylelibre.com in the language selection)

wCsowDI 2

eu:
gq8gBh9 1

de:
G2hwAgy 1

@ATouhou
Copy link

ATouhou commented Jan 28, 2024

Denmark works with https://api-eu.libreview.io .
I can confirm the trendarrows :)

@ATouhou
Copy link

ATouhou commented Jan 28, 2024

LibreLinkUp is written in NativeScript. After beautifying bundle.js you can easily find out the enum definition:

function(e) {
    e[e.NotDetermined = 0] = "NotDetermined", e[e.FallingQuickly = 1] = "FallingQuickly", e[e.Falling = 2] = "Falling", e[e.Stable = 3] = "Stable", e[e.Rising = 4] = "Rising", e[e.RisingQuickly = 5] = "RisingQuickly"
}(a || (a = {}))

@gui-dos Where did you locate the librelinkup source file build.js? Could you provide instruction on where to find it and also share the file? :) Thank you

@mrniceboot
Copy link

I think you can do it something like this, however I'm certainly not an expert.
adb shell pm list packages #to figure out the package name
adb shell pm path [package name] #to list the apk paths
adb shell cp [path] /storage/emulated/0/Download #to copy the apk to the download folder
adb pull /storage/emulated/0/Download/[apk name] #to pull the apk to the local machine

Use something like jadx to open up the downloaded APK.
Find the bundle.js in Resources/assets/app/bundle.js
Then beautify the bundle.js with any reasonable tool (perhaps beautify.io) and go looking for whatever you want to find.

This is probably not the best way to do it, but it does work...

@OnizukaJS
Copy link

OnizukaJS commented Feb 6, 2024

Hi everyone,
I'm trying to retrieve data on my react/ts app but facing CORS issue.

The https://api-eu.libreview.io/llu/auth/login call is failing and I can see different errors on the console:

  • Refused to set unsafe header "accept-encoding"

  • Refused to set unsafe header "connection"

  • Access to XMLHttpRequest at 'https://api-eu.libreview.io/llu/auth/login' from origin 'http://localhost:5173' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.

I followed this example:
https://github.com/DiaKEM/libre-link-up-api-client

import axios from 'axios'
import { LibreCgmData } from './types/client'
import { ActiveSensor, Connection, GlucoseItem } from './types/connection'
import { ConnectionsResponse, Datum } from './types/connections'
import { LoginRedirectResponse, LoginResponse } from './types/login'
import { AE, CountryResponse, RegionalMap } from './types/countries'
import { GraphData } from './types/graph'
import { mapData, trendMap } from './utils'

const LIBRE_LINK_SERVER = 'https://api-eu.libreview.io'

type ClientArgs = {
  username: string
  password: string
  connectionIdentifier?: string | ((connections: Datum[]) => string)
}

type ReadRawResponse = {
  connection: Connection
  activeSensors: ActiveSensor[]
  graphData: GlucoseItem[]
}

type ReadResponse = {
  current: LibreCgmData
  history: LibreCgmData[]
}

const urlMap = {
  login: '/llu/auth/login',
  connections: '/llu/connections',
  countries: '/llu/config/country?country=DE',
}
 
export const LibreLinkUpClient = ({
  username,
  password,
  connectionIdentifier
}: ClientArgs) => {
  let jwtToken: string | null = null
  let connectionId: string | null = null

  const instance = axios.create({
    baseURL: LIBRE_LINK_SERVER,
    headers: {
      'accept-encoding': 'gzip',
      'cache-control': 'no-cache',
      connection: 'Keep-Alive',
      'content-type': 'application/json',
      product: 'llu.android',
      version: '4.7.0',
    }
  })
  instance.interceptors.request.use(
    config => {
      if (jwtToken && config.headers) {
        // eslint-disable-next-line no-param-reassign
        config.headers.authorization = `Bearer ${jwtToken}`
      }

      return config
    },
    e => e,
    { synchronous: true }
  )

  const login = async (): Promise<LoginResponse> => {
    console.log("HELLO")
    const loginResponse = await instance.post<LoginResponse | LoginRedirectResponse>(urlMap.login, {
      email: username,
      password
    })
    console.log("response", loginResponse)

    if (loginResponse.data.status === 2) throw new Error('Bad credentials.')

    if ((loginResponse.data as LoginRedirectResponse).data.redirect) {
      const redirectResponse = loginResponse.data as LoginRedirectResponse
      const countryNodes = await instance.get<CountryResponse>(
        urlMap.countries
      )
      const targetRegion = redirectResponse.data.region as keyof RegionalMap
      const regionDefinition: AE | undefined = countryNodes.data.data.regionalMap[targetRegion]

      if (!regionDefinition) {
        throw new Error(
          `Unabkle to find region '${redirectResponse.data.region}'. Available nodes are ${Object.keys(countryNodes.data.data.regionalMap).join(', ')}`
        )
      }

      instance.defaults.baseURL = regionDefinition.lslApi
      return login()
    }
    jwtToken = (loginResponse.data as LoginResponse).data.authTicket.token

    return loginResponse.data as LoginResponse
  }

  const loginWrapper =
    <Return>(func: () => Promise<Return>) =>
    async (): Promise<Return> => {
      try {
        if (!jwtToken) await login()
        return func()
      } catch (e) {
        await login()
        return func()
      }
    }

  const getConnections = loginWrapper<ConnectionsResponse>(async () => {
    const response = await instance.get<ConnectionsResponse>(
      urlMap.connections
    )

    return response.data
  })

  const getConnection = (connections: Datum[]): string => {
    if (typeof connectionIdentifier === 'string') {
      const match = connections.find(
        ({ firstName, lastName }) =>
          `${firstName} ${lastName}`.toLowerCase() ===
          connectionIdentifier.toLowerCase()
      )

      if (!match) {
        throw new Error(
          `Unable to identify connection by given name '${connectionIdentifier}'.`
        )
      }

      return match.patientId
    }
    if (typeof connectionIdentifier === 'function') {
      const match = connectionIdentifier.call(null, connections)

      if (!match) {
        throw new Error(`Unable to identify connection by given name function`)
      }

      return match
    }

    return connections[0].patientId
  }

  const readRaw = loginWrapper<ReadRawResponse>(async () => {
    if (!connectionId) {
      const connections = await getConnections()

      connectionId = getConnection(connections.data)
    }

    const response = await instance.get<GraphData>(
      `${urlMap.connections}/${connectionId}/graph`
    )

    return response.data.data
  })

  const read = async (): Promise<ReadResponse> => {
    const response = await readRaw()

    return {
      current: mapData(response.connection.glucoseMeasurement),
      history: response.graphData.map(mapData),
    }
  }

  const observe = async () => {
    // @todo
  }

  let averageInterval: NodeJS.Timeout

  const readAveraged = async (
    amount: number,
    callback: (
      average: LibreCgmData,
      memory: LibreCgmData[],
      history: LibreCgmData[]
    ) => void,
    interval = 15000
  ) => {
    let mem: Map<string, LibreCgmData> = new Map()

    averageInterval = setInterval(async () => {
      const { current, history } = await read()
      mem.set(current.date.toString(), current)

      if (mem.size === amount) {
        const memValues = Array.from(mem.values())
        const averageValue = Math.round(
          memValues.reduce((acc, cur) => acc + cur.value, 0) / amount
        )
        const averageTrend =
          trendMap[
            parseInt(
              (
                Math.round(
                  (memValues.reduce(
                    (acc, cur) => acc + trendMap.indexOf(cur.trend),
                    0
                  ) /
                    amount) *
                    100
                ) / 100
              ).toFixed(0),
              10
            )
          ]

        mem = new Map()
        callback.apply(null, [
          {
            trend: averageTrend,
            value: averageValue,
            date: current.date,
            isHigh: current.isHigh,
            isLow: current.isLow,
          },
          memValues,
          history,
        ])
      }
    }, interval)

    return () => clearInterval(averageInterval)
  }

  return {
    observe,
    readRaw,
    read,
    readAveraged,
    login,
  }
}

Then, on my App.tsx I'm just calling the function like this:

(async function () {
    const username = import.meta.env.LIBRE_LINK_UP_USER_NAME
    const password = import.meta.env.LIBRE_LINK_UP_USER_PASSWORD
    const libreClient = LibreLinkUpClient({
      username,
      password,
      connectionIdentifier: 'IDENTIFIER'
    })
    const data = await libreClient.read()
    console.log("data", data)
  })()

@ATouhou
Copy link

ATouhou commented Feb 6, 2024

CORS error is local browser error. You can download an extension on your browser to disable CORS protection error. When you upload to remote server or host on a server the error should disappear

@faysal-ali
Copy link

Hi, I only seem to be able to access a small amount of historical data. Is there a way to get all historical data going back to the first reading (e.g. a few years ago)?

Any luck with this?

@ATouhou
Copy link

ATouhou commented Feb 10, 2024

@faysal-ali
I was able to access more data than described in this Gist, using the "/logbook" endpoint. Same headers and setup as "/graph" url.
The data available from that is the same as is shown in the app under "log book / Logbog (danish)".
My daughter is newly diagnosed and her data only goes back to 27/1, so I do not know if the data extends further than weeks/months.
Let me know if I can be of help :)

@ATouhou
Copy link

ATouhou commented Feb 10, 2024

@mrniceboot I was looking at the wrong APK file. Manually selecting the right one, worked wonders :-D !
I've uploaded it here: https://gist.github.com/ATouhou/deaf79038a6e2083b73be41c413e392f

@Burak50
Copy link

Burak50 commented Feb 11, 2024

@faysal-ali @ATouhou @voljumet @deelo55

I took a closer look at this, in general only 90* are possible backwards.
In addition to the actual header, two parameters must be passed in each case

"numPeriods": the return is divided into several blocks to make it clearer (default: 5)
"period" The days that are to be returned (default: 90)

*I only took the 90 days from libreview.com, but more is also possible, at least the API returns more than 90 days, in my case it is more than 400 days (if I set my period to 100)

API URL: https://api.libreview.io/glucoseHistory?numPeriods=5&period=90

@Burak50
Copy link

Burak50 commented Feb 11, 2024

FYI: for future users: https://api-eu.libreview.io does not work, for me anyway, the solution was https://api-de.....

The only question is still whether this is only the case in Germany or also in other European countries

My guess is that only the countries France, Germany, Italy, Netherlands, Spain, United Kingdom and United States of America work (because that's the countries listed on freestylelibre.com in the language selection)

wCsowDI 2

eu: gq8gBh9 1

de: G2hwAgy 1

I have also been able to solve this, the assumption that only specific lengths are available is taboo.
You can see: Countrie List

@ATouhou
Copy link

ATouhou commented Feb 12, 2024

That Seems to return data that is available on libreview and not librelinkup. So in my case, unless I use the patients login id / token, it will return no data . ( {
"status": 0,
"data": {
"lastUpload": 0,
"lastUploadCGM": 0,
"lastUploadPro": 0,
"reminderSent": 0,
"devices": [],
"periods": []
}, ) .

However, I will save it in my PostMan collection. Might come in handy in the future. Thanks @Burak50

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment