Skip to content

Instantly share code, notes, and snippets.

@khskekec
Last active February 4, 2026 08:30
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
@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

@gjkoolen
Copy link

gjkoolen commented Feb 2, 2026

Hello everyone. I wrote a Python script that reads out my glucose level every 5 minutes, to send me a Telegram message when my sugar is not following a (very simple) model. So it warns me when I have to use insulin. I use this for more than a year and it helped me reduce my HbA1C significantly. The script runs on a Raspberry Pi Zero. The only drawback is that every time I eat (and thus use insulin), I send a Telegram message to the bot. This information is needed for the model.
This glucose checker is very useful when for instance the whole night my level is high, but just under the alarm level. I will be awakened by the (annoying) Telegram message receive sound. Or when glucose level starts rising again 2 hours after a meal. Also then I am alerted and can compensate with insulin, to remain well under the alarm level.
The code consists of 2 services: the glucose checker service and Telegram bot service. Connected by an MQTT broker.
It is on my wish list to train an ML model, based on glucose data from the past.
If anyone is interested in the code, or in having more information, please let me know.

@Mynuggets-dev
Copy link

Mynuggets-dev commented Feb 2, 2026 via email

@Mynuggets-dev
Copy link

Mynuggets-dev commented Feb 2, 2026 via email

@gjkoolen
Copy link

gjkoolen commented Feb 2, 2026

@Mynuggets-dev Good questions. My Python script only stores data of the last 30 minutes, and uses that to determine if all is well. No DB needed. The Telegram bot is only to bridge between my house MQTT and external Telegram. The bot only relays between my phone and the Python script (via Telegram and MQTT). I got diabetes when I was 43 (in 2009).
Before each meal I administer insulin, and then usually it goes up and then down to normal level after 3 hrs. However, sometimes at t=2 hrs the Python script notices that the level goes not down anymore, but up again (depending on meal). In such cases I get a message, I compensate, and the final curve looks very good. My doctor asked me how I got my levels so flat, and I told him I use control techniques (is also my job) and this Python script.
Same at nighttime: usually very good, but sometimes I miscalculate, and then it's good to receive a message so I can compensate.
Freestyle Libre has a "bug" in my opinion: if my level is above alarm level before I go to sleep, I compensate (of course), but if I don't compensate enough, it can remain the whole night above the alarm level without raising any alarm. Abbott should automatically repeat any alarm after 3 hours in my opinion. Also for such cases this Python script is good.
Hope this explains :-)

@Mynuggets-dev
Copy link

@Mynuggets-dev Good questions. My Python script only stores data of the last 30 minutes, and uses that to determine if all is well. No DB needed. The Telegram bot is only to bridge between my house MQTT and external Telegram. The bot only relays between my phone and the Python script (via Telegram and MQTT). I got diabetes when I was 43 (in 2009). Before each meal I administer insulin, and then usually it goes up and then down to normal level after 3 hrs. However, sometimes at t=2 hrs the Python script notices that the level goes not down anymore, but up again (depending on meal). In such cases I get a message, I compensate, and the final curve looks very good. My doctor asked me how I got my levels so flat, and I told him I use control techniques (is also my job) and this Python script. Same at nighttime: usually very good, but sometimes I miscalculate, and then it's good to receive a message so I can compensate. Freestyle Libre has a "bug" in my opinion: if my level is above alarm level before I go to sleep, I compensate (of course), but if I don't compensate enough, it can remain the whole night above the alarm level without raising any alarm. Abbott should automatically repeat any alarm after 3 hours in my opinion. Also for such cases this Python script is good. Hope this explains :-)

yeah in the librelinkup i found they do like data sanitizing and its compressing high / low readings ( 20mins time for insulin for the fat cells etc so 30min could be half the issues for a good source of data ) that's why i just log the readings raw data and compile it myself, all my analytics are python functions i created based on Australian standards etc ( i like the pretty look but advice not to use as a direct refence, and i have handling incase trying to view more data than i have etc eg
image
and i have discord accounts that can link to a bot etc
image
and working on widgets too, i get alot of ideas from posts like these and issues, so thanks to anyone that posts for help or ideas 💕

@Mynuggets-dev
Copy link

ive been trying to add something to auto log mealtimes based on levels, but thats hard cause if you do it right it shouldnt detect the rapid changes 🤔

@Mynuggets-dev
Copy link

feel free to try it, only thing i ask is please don't use it like a source of truth, it can be flawed with my ' skill level ' https://diabetes.mynuggets.dev/

@gjkoolen
Copy link

gjkoolen commented Feb 4, 2026

@Mynuggets-dev Great that you exposed your scripting with others with your website! It looks good. Nice example for other developers. I hope one day I will provide machine-learned alerting service to others. At the moment I am too busy with other things. Keep going!

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