How I get Discord notifications for new ASUS ROG BIOS releases

Contents

Preface

Same problem as with AMD, different vendor.

I don’t like Armoury Crate, and I don’t like checking the ASUS support page every few weeks to see if there’s a new BIOS for my board.

Since the AMD chipset notifier was already running, turning it into a BIOS notifier was the next best thing I could think of lol

This post only covers what’s different.


What’s actually different

Comparing both repos, index.js, state.js and discord.js are almost identical.
The real difference is where the data comes from:

AMD notifier ROG notifier
Data source HTML page, parsed with regexes JSON API, the one the ASUS page uses itself
Config per board page URL page URL plus three IDs from the API request
Discord message version, release date, file size, changelog plus SHA-256 and a direct download link

So src/amd.js became src/asus.js, src/config.js got three new fields, and src/discord.js got two more fields in the embed.


Finding the API

The ASUS support page of a board, e.g. ROG STRIX X870E-E GAMING WIFI, doesn’t have the BIOS list in its HTML.

It loads it afterwards from an API. That’s the same request the worker sends.

To find it:

  1. Open the board’s helpdesk_bios page
  2. Open the dev tools (F12), switch to the Network tab and reload the page
  3. Filter for GetPDBIOS

There’s exactly one request:

https://rog.asus.com/support/webapi/ProductV2/GetPDBIOS?website=global&model=rog-strix-x870e-e-gaming-wifi&pdid=0&m1id=28607&cpu=&LevelTagId=231962&systemCode=rog

Three of those parameters are specific to the board:

Parameter Where to find it
model the slug in the page URL, between /motherboards/<series>/ and /helpdesk_bios/
m1id only in the API request
LevelTagId only in the API request

m1id and LevelTagId aren’t shown anywhere on the page and have nothing to do with the product name, so the network tab is the only place to get them.

Warning
The API isn’t documented by ASUS, so they can change it at any time.
If that happens, the check fails and a failure notification tells me about it.

The worker builds the same URL from the config:

function buildApiUrl(config) {
  const params = new URLSearchParams({
    website: "global",
    model: config.asusModel,
    pdid: "0",
    m1id: config.asusM1Id,
    cpu: "",
    LevelTagId: config.asusLevelTagId,
    systemCode: "rog"
  });

  return "https://rog.asus.com/support/webapi/ProductV2/GetPDBIOS?" + params.toString();
}

Why JSON beats scraping

On AMD’s side, the worker strips the page down to plain text and runs regexes over it.
That works… but every redesign of the page can break it.

Here the worker gets structured JSON and just reads fields.
There’s no markup that can change under it, only the API itself:

async function fetchJson(url) {
  const response = await fetch(url, {
    method: "GET",
    headers: {
      "user-agent": "Mozilla/5.0 rog-bios-notifier/1.0",
      "accept": "application/json"
    }
  });

  if (!response.ok) {
    throw new Error("Failed to load ASUS BIOS API. HTTP " + response.status);
  }

  return await response.json();
}

Picking the right BIOS

This is the response for my board, shortened to the fields the worker actually uses and to the three newest entries:

{
  "Result": {
    "Obj": [
      {
        "Name": "BIOS",
        "Files": [
          {
            "Version": "2503",
            "IsRelease": "1",
            "ReleaseDate": "2026/09/22",
            "FileSize": "18.32 MB",
            "sha256": "8714A81CE9F6701F93014EAEF96439BAAD926C0B60283F23EEC4319EF7C3C386",
            "Description": "\"1.Update to AGESA ComboAM5 PI 1.3.0.1d.<br/>2.Enhanced memory performance, system stability, and compatibility with CXMT memory chips for improved low-latency operation.<br/><br/>Before running the USB BIOS Flashback tool, please rename the BIOS file (A5557.CAP) using BIOSRenamer.\"",
            "DownloadUrl": {
              "Global": "/pub/ASUS/mb/BIOS/ROG-STRIX-X870E-E-GAMING-WIFI-ASUS-2503.ZIP"
            }
          },
          { "Version": "2402", "IsRelease": "1", "ReleaseDate": "2026/07/15" },
          { "Version": "2401", "IsRelease": "0", "ReleaseDate": "2026/06/26" }
        ]
      },
      { "Name": "Firmware" }
    ]
  }
}

Obj holds one group per download category, here BIOS and Firmware, and the worker only cares about BIOS.

Its Files list contains every BIOS release for the board, newest first.

Every entry also has an IsRelease flag.

When I wrote the worker, I only assumed there might be entries with "0". Looking at the full list later, 7 of the 29 builds for my board have one, like 2401, which was followed by 2402 three weeks later.

ASUS doesn’t document what the flag means, but none of those builds should trigger a notification if one ever ends up at the top.

So the worker takes the first entry marked as a release, and only falls back to the very first entry if none is:

export function extractLatestBios(json) {
  const groups = json && json.Result && json.Result.Obj;
  if (!Array.isArray(groups)) return null;

  const biosGroup = groups.find((group) => group.Name === "BIOS");
  if (!biosGroup || !Array.isArray(biosGroup.Files) || !biosGroup.Files.length) {
    return null;
  }

  return biosGroup.Files.find((file) => file.IsRelease === "1") || biosGroup.Files[0];
}

Cleaning up the data

Two fields need a bit of cleanup before they can go into a Discord message.

The changelog comes as HTML with <br> tags, and ASUS wraps the whole text in literal quote characters:

export function normalizeChangelog(rawDescription) {
  if (!rawDescription) return null;

  return rawDescription
    .replace(/<br\s*\/?>/gi, "\n")
    .replace(/<[^>]+>/g, "")
    .replace(/&nbsp;/gi, " ")
    .replace(/&amp;/gi, "&")
    .trim()
    .replace(/^"+|"+$/g, "")
    .trim();
}

The download URL is sometimes absolute and sometimes a path on ASUS’ CDN:

export function normalizeDownloadUrl(url) {
  if (!url) return null;
  return url.startsWith("http") ? url : "https://dlcdnets.asus.com" + url;
}

The version itself needs nothing.

BIOS versions are plain numbers like 2503, and compareVersions from the first part handles a single segment just fine.


What the Discord message adds

The embed is the same as in the first part, with three changes:

const link = data.downloadUrl || config.pageUrl;

// ...

if (data.sha256) {
  fields.push({ name: "SHA-256", value: "`" + data.sha256 + "`", inline: false });
}
  • The title links straight to the BIOS file instead of the support page, and only falls back to the page if there’s no download URL.
  • The SHA-256 checksum is in the message, so the download can be verified before flashing.
  • The title says “New BIOS released!” and the embed color is ROG red, 0xcc000e.

Adding another board

A target needs the three IDs from the API request on top of what the AMD version needs:

const TARGETS = [
  {
    id: "x870e-e-wifi",
    productName: "ROG STRIX X870E-E GAMING WIFI BIOS",
    pageUrl: "https://rog.asus.com/motherboards/rog-strix/rog-strix-x870e-e-gaming-wifi/helpdesk_bios/",
    asusModel: "rog-strix-x870e-e-gaming-wifi",
    asusM1Id: "28607",
    asusLevelTagId: "231962"
  }
];

If one of the three is missing, the worker refuses to run and says which target is incomplete.

Note
Board IDs tend to contain hyphens, but Cloudflare variable names can’t.
The worker replaces them with underscores, so id: "x870e-e-wifi" needs a secret named DISCORD_WEBHOOK_URL_X870E_E_WIFI.

Everything else, KV namespace, connecting the repo and the secret itself, works exactly like in the first part.


Final result

The worker checks every configured board once an hour.

When ASUS releases a new BIOS, one Discord message shows up with the version, release date, changelog, file size, checksum and a direct download link.

No Armoury Crate, no checking the support page.

You can find the project here: rog-bios-notifier

Cheers.