Skip to content

Instantly share code, notes, and snippets.

@khskekec
Last active January 31, 2026 01:55
Show Gist options
  • Select an option

  • Save khskekec/6c13ba01b10d3018d816706a32ae8ab2 to your computer and use it in GitHub Desktop.

Select an option

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
@gjkoolen
Copy link

Thanks @sgmoore
In addition: after Oct 8, I had to start my LibreLinkUp Android app, to read and accept the new terms of use.
After that, I got correct responses from my https requests.

@MrAda
Copy link

MrAda commented Nov 1, 2025

Thanks @sgmoore In addition: after Oct 8, I had to start my LibreLinkUp Android app, to read and accept the new terms of use. After that, I got correct responses from my https requests.

Gosh that worked! my code is working again.

@MrAda
Copy link

MrAda commented Nov 1, 2025

@sgmoore I like that you have a simpler interface. I will look into it.

@MrAda
Copy link

MrAda commented Nov 1, 2025

@sgmoore I got things to work again. Your suggestion helped a lot. Do I new that new header "Account-Id" for every command after logging in or only on connections?

@sgmoore
Copy link

sgmoore commented Nov 1, 2025

@MrAda

I use "Account-Id" for every command after logging in.

@rollkuo
Copy link

rollkuo commented Nov 5, 2025

Hi! I'm building an app to get the LibreLinkUp data but it can only obtain ~142 records. Does anyone know if I can get my full history of my CGM data? Also, I started using my first CGM in Taipei a month ago, but I returned to the US and activated my 2nd CGM with region setting as US. Why do my returned records still have a timestamp in the TPE time zone? How can I fix it?

@MrAda
Copy link

MrAda commented Nov 5, 2025

@rollkuo I don't think any of us have figured out to get all the data programmatically yet. You can go to LibreLinkUp on the web and download your csv data and then incorporate that into your app. Not optimal but it is what is available. You may have noticed that when you poll to get data from the API it is around 30 minutes in the past, no current data. I complained to Abbott that if I was being watched by a friend and I was plunging in Blood Glucose I could be in a serious medical event which could be tragic.

@sgmoore
Copy link

sgmoore commented Nov 5, 2025

You may have noticed that when you poll to get data from the API it is around 30 minutes in the past, no current data

The data you get from the /llu/connections/{patientId}/graph endpoint should contain the current reading (or more accurately the last blood glucose value uploaded) in data/Connection/glucoseMeasurement or data/Connection/glucoseItem (And before you ask, I don't know what the difference between these two is)

@Mynuggets-dev
Copy link

Mynuggets-dev commented Dec 9, 2025

Hi! I'm building an app to get the LibreLinkUp data but it can only obtain ~142 records. Does anyone know if I can get my full history of my CGM data? Also, I started using my first CGM in Taipei a month ago, but I returned to the US and activated my 2nd CGM with region setting as US. Why do my returned records still have a timestamp in the TPE time zone? How can I fix it?

na looks like they do sterlizing and compression in the backend for librelink too and will only keep 42-45 reading in the api ( ive been making one for librelink2 and collecting current reading and storing to compile the raw data myself, using the older data in the api isnt raw data and i noticed in liber2 the highs / lows are not recorded correctly they get compressed eg 3.2 > 3.4 etc to make a pretty graph )

@liubquanti
Copy link

Hello everyone, and thank you to the author for this dump. Thanks to it, I spent some time developing an application similar to LibreLinkUp for Windows, and today I decided to create a public release so that anyone can install the application. So if you are interested, you can check it out. Please note that to install the MSIX package, you must first install the certificate.

@valdemarvictorleitecarvalho
Copy link

valdemarvictorleitecarvalho commented Jan 20, 2026

Hello everyone! I’m developing an application to present at a national competition here in Brazil on January 31st, but I’m running into an issue when trying to connect to the API. For some reason, I’m unable to authenticate and retrieve data. I always receive the error "No token returned", because the API never seems to return the authentication token from the server. Could someone please help me understand what might be wrong and how I can fix my code?

Thanks a lot!

@liubquanti @Mynuggets-dev @sgmoore @MrAda @gjkoolen @xfoguet @SteveCEvans @adamlounds

`const corsHeaders = {
  'Access-Control-Allow-Origin': '*',
  'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
}

async function sha256(message: string) {
    const msgBuffer = new TextEncoder().encode(message);
    const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer);
    const hashArray = Array.from(new Uint8Array(hashBuffer));
    return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
}

Deno.serve(async (req) => {
  if (req.method === 'OPTIONS') return new Response('ok', { headers: corsHeaders })

  try {
    const { email, password, region = "ae" } = await req.json()

    if (!email || !password) throw new Error("Email e senha obrigatórios")

    const baseUrl = `https://api-${region}.libreview.io`

    const commonHeaders = {
      "product": "llu.android",
      "version": "4.16.0", 
      "accept-encoding": "gzip, deflate, br",
      "cache-control": "no-cache",
      "connection": "keep-alive",
      "content-type": "application/json",
      "pragma": "no-cache",
    }

    console.log(`[LibreFix] 1. Login em ${baseUrl}...`)

    const loginResp = await fetch(`${baseUrl}/llu/auth/login`, {
      method: "POST",
      headers: commonHeaders,
      body: JSON.stringify({ email, password })
    })

    if (!loginResp.ok) {
        throw new Error(`Erro Login: ${loginResp.status} - Verifique credenciais`)
    }

    const loginData = await loginResp.json()
    
    const token = loginData.data?.authTicket?.token
    const userId = loginData.data?.user?.id 

    if (!token || !userId) {
        throw new Error("Login falhou: Token ou User ID não retornados.")
    }

    const accountIdHash = await sha256(userId);
    
    console.log(`[LibreFix] Hash gerado: ${accountIdHash.substring(0, 10)}...`)

    const authHeaders = {
        ...commonHeaders,
        "Authorization": `Bearer ${token}`,
        "Account-Id": accountIdHash 
    }

    console.log("[LibreFix] 2. Buscando conexões...")
    const connResp = await fetch(`${baseUrl}/llu/connections`, {
      headers: authHeaders 
    })
    
    const connData = await connResp.json()
    
    if (!connData.data || connData.data.length === 0) {
        throw new Error("Nenhum sensor vinculado. Use uma conta de SEGUIDOR (LibreLinkUp).")
    }
    
    const patientId = connData.data[0].patientId

    console.log(`[LibreFix] 3. Buscando dados...`)
    const graphResp = await fetch(`${baseUrl}/llu/connections/${patientId}/graph`, {
      headers: authHeaders
    })
    
    const graphData = await graphResp.json()
    const measurements = graphData.data?.connection?.glucoseMeasurement?.measurementData

    if (!measurements || measurements.length === 0) {
        throw new Error("Conexão feita, mas sem dados recentes.")
    }

    const latest = measurements[measurements.length - 1]

    console.log("[LibreFix] SUCESSO!", latest.Value)

    return new Response(
      JSON.stringify({ 
        value: latest.Value, 
        trend: latest.TrendArrow, 
        date: latest.FactoryTimestamp,
        isHigh: latest.isHigh,
        isLow: latest.isLow
      }),
      { headers: { ...corsHeaders, 'Content-Type': 'application/json' }, status: 200 }
    )

  } catch (error) {
    const msg = error instanceof Error ? error.message : String(error)
    console.error("ERRO FATAL:", msg)
    return new Response(
      JSON.stringify({ error: msg }),
      { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
    )
  }
})` 

@sgmoore
Copy link

sgmoore commented Jan 20, 2026

Could someone please help me understand what might be wrong and how I can fix my code?

The response to the .../llu/auth/login request does not always return a token. You need to check the value for data\status and if this is 4, look to see if you need to perform an extra step (or several extra steps) until you get a status 0 and a token.

These steps are usually to read and accept new terms and conditions and there appear to be lots of third-party apps that do not handle them correctly and rely on the user logging into the official LibreLinkUp app and performing the necessary steps there and that clears the account so that the next logon goes straight to providing a token.

See https://gist.github.com/khskekec/6c13ba01b10d3018d816706a32ae8ab2?permalink_comment_id=5749295#gistcomment-5749295

I think my C# code at https://dotnetfiddle.net/OWr8wO should handle this, but is not recommended as it does not do it the proper way. (It is the equivalent of saying that I have read and agree with the new terms and conditions without actually reading them.)

@valdemarvictorleitecarvalho

I think my C# code at https://dotnetfiddle.net/OWr8wO should handle this, but is not recommended as it does not do it the proper way. (It is the equivalent of saying that I have read and agree with the new terms and conditions without actually reading them.)

This helped me a lot. I wouldn’t have realized it on my own. Thank you very much for the explanation and guidance.
At the moment, the person whose data I’m collecting is not entering glucose values in LibreLinkUp, but I was able to successfully complete the login, retrieve the connections, and fetch the graph data.

I sincerely appreciate your time, attention, and the help you provided!

@Mynuggets-dev
Copy link

Mynuggets-dev commented Jan 31, 2026

Hello everyone! I’m developing an application to present at a national competition here in Brazil on January 31st, but I’m running into an issue when trying to connect to the API. For some reason, I’m unable to authenticate and retrieve data. I always receive the error "No token returned", because the API never seems to return the authentication token from the server. Could someone please help me understand what might be wrong and how I can fix my code?

Thanks a lot!

@liubquanti @Mynuggets-dev @sgmoore @MrAda @gjkoolen @xfoguet @SteveCEvans @adamlounds

`const corsHeaders = {
  'Access-Control-Allow-Origin': '*',
  'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
}

async function sha256(message: string) {
    const msgBuffer = new TextEncoder().encode(message);
    const hashBuffer = await crypto.subtle.digest('SHA-256', msgBuffer);
    const hashArray = Array.from(new Uint8Array(hashBuffer));
    return hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
}

Deno.serve(async (req) => {
  if (req.method === 'OPTIONS') return new Response('ok', { headers: corsHeaders })

  try {
    const { email, password, region = "ae" } = await req.json()

    if (!email || !password) throw new Error("Email e senha obrigatórios")

    const baseUrl = `https://api-${region}.libreview.io`

    const commonHeaders = {
      "product": "llu.android",
      "version": "4.16.0", 
      "accept-encoding": "gzip, deflate, br",
      "cache-control": "no-cache",
      "connection": "keep-alive",
      "content-type": "application/json",
      "pragma": "no-cache",
    }

    console.log(`[LibreFix] 1. Login em ${baseUrl}...`)

    const loginResp = await fetch(`${baseUrl}/llu/auth/login`, {
      method: "POST",
      headers: commonHeaders,
      body: JSON.stringify({ email, password })
    })

    if (!loginResp.ok) {
        throw new Error(`Erro Login: ${loginResp.status} - Verifique credenciais`)
    }

    const loginData = await loginResp.json()
    
    const token = loginData.data?.authTicket?.token
    const userId = loginData.data?.user?.id 

    if (!token || !userId) {
        throw new Error("Login falhou: Token ou User ID não retornados.")
    }

    const accountIdHash = await sha256(userId);
    
    console.log(`[LibreFix] Hash gerado: ${accountIdHash.substring(0, 10)}...`)

    const authHeaders = {
        ...commonHeaders,
        "Authorization": `Bearer ${token}`,
        "Account-Id": accountIdHash 
    }

    console.log("[LibreFix] 2. Buscando conexões...")
    const connResp = await fetch(`${baseUrl}/llu/connections`, {
      headers: authHeaders 
    })
    
    const connData = await connResp.json()
    
    if (!connData.data || connData.data.length === 0) {
        throw new Error("Nenhum sensor vinculado. Use uma conta de SEGUIDOR (LibreLinkUp).")
    }
    
    const patientId = connData.data[0].patientId

    console.log(`[LibreFix] 3. Buscando dados...`)
    const graphResp = await fetch(`${baseUrl}/llu/connections/${patientId}/graph`, {
      headers: authHeaders
    })
    
    const graphData = await graphResp.json()
    const measurements = graphData.data?.connection?.glucoseMeasurement?.measurementData

    if (!measurements || measurements.length === 0) {
        throw new Error("Conexão feita, mas sem dados recentes.")
    }

    const latest = measurements[measurements.length - 1]

    console.log("[LibreFix] SUCESSO!", latest.Value)

    return new Response(
      JSON.stringify({ 
        value: latest.Value, 
        trend: latest.TrendArrow, 
        date: latest.FactoryTimestamp,
        isHigh: latest.isHigh,
        isLow: latest.isLow
      }),
      { headers: { ...corsHeaders, 'Content-Type': 'application/json' }, status: 200 }
    )

  } catch (error) {
    const msg = error instanceof Error ? error.message : String(error)
    console.error("ERRO FATAL:", msg)
    return new Response(
      JSON.stringify({ error: msg }),
      { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
    )
  }
})` 

try something like this

  "Access-Control-Allow-Origin": "*",
  "Access-Control-Allow-Headers":
    "authorization, x-client-info, apikey, content-type",
  "Access-Control-Allow-Methods": "POST, OPTIONS",
};

async function sha256Hex(message: string) {
  const msgBuffer = new TextEncoder().encode(message);
  const hashBuffer = await crypto.subtle.digest("SHA-256", msgBuffer);
  const hashArray = Array.from(new Uint8Array(hashBuffer));
  return hashArray.map((b) => b.toString(16).padStart(2, "0")).join("");
}

// Simple in-memory token cache (keyed by email+region)
// If you deploy on serverless with cold starts, use Deno KV or your DB.
type Session = {
  token: string;
  accountIdHash: string;
  userId: string;
  region: string;
  baseUrl: string;
  updatedAt: number;
};

const sessionCache = new Map<string, Session>();

function cacheKey(email: string, region: string) {
  return `${region}::${email.toLowerCase().trim()}`;
}

function commonHeaders() {
  return {
    product: "llu.android",
    version: "4.16.0",
    "accept-encoding": "gzip, deflate, br",
    "cache-control": "no-cache",
    connection: "keep-alive",
    "content-type": "application/json",
    pragma: "no-cache",
  } as Record<string, string>;
}

async function login(email: string, password: string, region: string): Promise<Session> {
  const baseUrl = `https://api-${region}.libreview.io`;

  const resp = await fetch(`${baseUrl}/llu/auth/login`, {
    method: "POST",
    headers: commonHeaders(),
    body: JSON.stringify({ email, password }),
  });

  if (!resp.ok) {
    // Avoid leaking details; keep it useful.
    throw new Error(`Erro Login: ${resp.status} - Verifique credenciais/região`);
  }

  const loginData = await resp.json();

  const token = loginData?.data?.authTicket?.token as string | undefined;
  const userId = loginData?.data?.user?.id as string | undefined;

  if (!token || !userId) {
    throw new Error("Login falhou: Token ou User ID não retornados.");
  }

  const accountIdHash = await sha256Hex(userId);

  return {
    token,
    userId,
    accountIdHash,
    region,
    baseUrl,
    updatedAt: Date.now(),
  };
}

async function authedFetch(
  session: Session,
  input: string,
  init: RequestInit,
  relogin: () => Promise<Session>,
): Promise<{ resp: Response; session: Session }> {
  const makeHeaders = (s: Session) => ({
    ...commonHeaders(),
    Authorization: `Bearer ${s.token}`,
    "Account-Id": s.accountIdHash,
    ...(init.headers ?? {}),
  });

  // First try
  let resp = await fetch(input, { ...init, headers: makeHeaders(session) });

  // Token invalid/expired: refresh once and retry
  if (resp.status === 401 || resp.status === 403) {
    const fresh = await relogin();
    resp = await fetch(input, { ...init, headers: makeHeaders(fresh) });
    return { resp, session: fresh };
  }

  return { resp, session };
}

Deno.serve(async (req) => {
  if (req.method === "OPTIONS") return new Response("ok", { headers: corsHeaders });

  try {
    const { email, password, region = "ae" } = await req.json();

    if (!email || !password) throw new Error("Email e senha obrigatórios");

    const key = cacheKey(email, region);

    // Get cached session or login
    let session = sessionCache.get(key);
    if (!session) {
      session = await login(email, password, region);
      sessionCache.set(key, session);
    }

    // Helper to re-login and update cache
    const relogin = async () => {
      const fresh = await login(email, password, region);
      sessionCache.set(key, fresh);
      return fresh;
    };

    // 1) Connections
    const connUrl = `${session.baseUrl}/llu/connections`;

    const connResult = await authedFetch(session, connUrl, { method: "GET" }, relogin);
    session = connResult.session;

    if (!connResult.resp.ok) {
      throw new Error(`Erro conexões: ${connResult.resp.status}`);
    }

    const connData = await connResult.resp.json();

    if (!Array.isArray(connData?.data) || connData.data.length === 0) {
      throw new Error("Nenhum sensor vinculado. Use conta SEGUIDOR (LibreLinkUp).");
    }

    const patientId = connData.data[0]?.patientId;
    if (!patientId) throw new Error("Conexão inválida: patientId ausente.");

    // 2) Graph
    const graphUrl = `${session.baseUrl}/llu/connections/${patientId}/graph`;

    const graphResult = await authedFetch(session, graphUrl, { method: "GET" }, relogin);
    session = graphResult.session;

    if (!graphResult.resp.ok) {
      throw new Error(`Erro dados: ${graphResult.resp.status}`);
    }

    const graphData = await graphResult.resp.json();

    const measurements =
      graphData?.data?.connection?.glucoseMeasurement?.measurementData;

    if (!Array.isArray(measurements) || measurements.length === 0) {
      throw new Error("Conexão feita, mas sem dados recentes.");
    }

    const latest = measurements[measurements.length - 1];

    return new Response(
      JSON.stringify({
        value: latest.Value,
        trend: latest.TrendArrow,
        date: latest.FactoryTimestamp,
        isHigh: latest.isHigh,
        isLow: latest.isLow,
      }),
      { headers: { ...corsHeaders, "Content-Type": "application/json" }, status: 200 },
    );
  } catch (error) {
    const msg = error instanceof Error ? error.message : String(error);
    return new Response(JSON.stringify({ error: msg }), {
      status: 400,
      headers: { ...corsHeaders, "Content-Type": "application/json" },
    });
  }
});

@Mynuggets-dev
Copy link

Mynuggets-dev commented Jan 31, 2026

image this is one i have been making and use to watch a friend i care for, i just compile the data from the raw ' current reading ' and log them every 1min 24/7

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