Wellbian Device Status API
Device Status API
Wellbian · Weather XRPL DePIN

Device Status API

Read-only API for showing reward balance, server connection, and outdoor air quality on the meter's display. The device queries it on its own - no user input.

v1 · 2026-08-28 · for device firmware developers

Addresses to burn into firmware

These are the factory defaults. All four live under wellbianlabs.io so the backend can be swapped without touching shipped units. Do not burn a vendor hostname.

PurposeAddress
Measurement uploadhttps://api.wellbianlabs.io/v1/c?<base64>
Reward & link statushttps://api.wellbianlabs.io/v1/device?serial=<SERIAL>&k=<TOKEN>&format=text
Outdoor air qualityhttps://api.wellbianlabs.io/v1/outdoor?serial=<SERIAL>&format=text (no token)
Firmware updatefota.wellbianlabs.io

Serial and token are not compiled in - they are written to the device over BLE at setup. An installer provisions each unit with the Wellbian factory tool, which sends SERIAL,<serial> and TOKEN,<token> over BLE and stores them in device settings (NVS), not the firmware image. The token is already unique per unit - it is issued by the server at provisioning time, not typed by hand. Only the four addresses above belong in firmware as compile-time constants; read the serial and token from settings, never hardcode them.

Compatibility with shipped v6.04 units. Units already in the field upload to the vendor host (…supabase.co/functions/v1/c?) with a shared legacy token, and that path stays alive. Only firmware built to accept the TOKEN BLE command gets a unique per-unit token through the addresses above - older images that ignore the command fall back to the legacy shared token.

Overview

The meter's serial and upload token are written into device settings via BLE provisioning at setup, not compiled into the firmware. With those two values already in settings, the meter can fetch the owner's reward balance and whether the server is receiving data, and show them on screen - no factory reflash per unit.

GET https://api.wellbianlabs.io/v1/device

Nothing for the user to enter. Only values the device already has - no key-entry screen, no pairing flow. The firmware implements fetch and display, nothing more.

This path is read-only. It cannot withdraw, transfer, or change settings.

Quick start

curl
curl "https://api.wellbianlabs.io/v1/device?serial=IARAW2600126&k=<TOKEN>&format=text"
Response · text/plain
OK 1
NET 1
LASTMIN 1
TODAY 902
SERVER Connected
WITHDRAWABLE 4.878074
CLAIMABLE 4.878074
PENDING 4.868819
CLAIMED 4.887346
TOTAL 14.634240
UNIT WLBN
LINKED 1
ACCRUING 1
NEXTSET 522
HOLDH 24

Only these two lines need to reach the screen.

Device display
WITHDRAWABLE  4.878074 WLBN
SERVER        Connected

SERVER is either Connected or Disconnected - fixed English strings, so the firmware neither decides nor translates. Use the other keys only for a detail view.

k is the upload token written into device settings via BLE provisioning at setup - not a value compiled into the firmware image. Legacy v6.04 units instead carry a shared token baked into the AT*ICT*HTTPGET=…/functions/v1/iot/%s?k= string; that path is described separately above. Either way, the end user never sees or enters it.

LINKED 0 means the unit has no owner yet. The user has not redeemed it, so every reward field is 0. Show something like "Not registered" instead of a zero balance.

Auth · request parameters

NameRequiredDescription
serialyesThe unit's own serial
kyesUpload token, written to device settings via BLE provisioning at setup (never compiled into firmware). May also be sent as the X-Device-Token header
formatnotext for line-per-key plain text; omit for JSON

Note. Measurement upload currently goes to …/functions/v1/c?<base64>, which carries no k=. Status queries do need it, so keep the token in settings and attach it to these requests only.

Response · text format

One KEY SPACE VALUE per line, readable with strtok/sscanf without a JSON parser. Line order is not guaranteed - look up by key. New lines may be added, so ignore keys you do not know.

KeyTypeMeaning
OK0/11 = success. On 0, the next line is ERROR
SERVERstringFor display. Connected / Disconnected - print as-is
WITHDRAWABLEdecimalFor display. Amount withdrawable now. Same value as CLAIMABLE, named separately so there is no doubt what to show
NET0/1Whether the server is receiving this unit's data. 1 if seen within 5 minutes
LASTMINintMinutes since last reception. -1 if never
TODAYintReadings received today (KST)
CLAIMABLEdecimalWithdrawable now, for this unit
PENDINGdecimalAccrued but still inside the hold window
CLAIMEDdecimalWithdrawn to date
TOTALdecimalSum of the three above - lifetime earnings of this unit
UNITstringReward unit. Currently WLBN
LINKED0/1Whether an owner is linked. 0 means not yet redeemed, so rewards are all 0
ACCRUING0/1Whether it is accruing right now - linked and receiving data today
NEXTSETintMinutes until the next settlement. Settlement runs once a day at KST midnight
HOLDHintHold hours between accrual and withdrawal

Amounts are for this unit alone. The serial identifies the unit and only that unit's rewards are returned. If the owner registered several units, the others are not mixed in - each unit shows its own number.

The owner's combined total belongs in the app or My Page, not on the device.

Right after registration the amount is 0. Rewards are finalised once a day at KST midnight, so a user who registers today sees TOTAL 0.000000 until then. A bare zero reads as a fault.

That is what ACCRUING and NEXTSET are for. When ACCRUING 1 the unit is earning normally even at zero, so show this instead of an amount:

Screen right after registration
// ACCRUING 1 · TOTAL 0 · NEXTSET 262
Accruing   1,155 min
Settles in 4h 22m

// After settlement - ACCRUING 1 · TOTAL 4.887346
4.89 WLBN
Connected

Watch the decimals. Rewards carry six decimal places. On a narrow display round TOTAL to 2–3 decimals, and internally prefer double or scaled integers (×10⁶) over float.

Response · JSON format

Omit format to get JSON. Use it on a roomier MCU or in an app.

GET /v1/device?serial=…&k=…
{
  "ok": true,
  "serial": "IARAW2600126",
  "linked": true,
  "display": { "server": "Connected", "withdrawable": 4.878074, "unit": "WLBN" },
  "reward": {
    "currency": "WLBN",
    "claimable": 4.878074,
    "pending": 4.868819,
    "claimed": 4.887346,
    "total": 14.634240,
    "holdHours": 24
  },
  "network": { "online": true, "lastSeenMin": 1, "todayCount": 902 },
  "accruing": true,
  "nextSettlementInMin": 522,
  "serverTime": "2026-08-28T…Z"
}

The wallet address is never returned. There is no reason to show it on the device, and leaving it out means a leaked upload token cannot identify the owner.

Errors

CodeHTTPCause · what the device should do
BAD_TOKEN401Wrong upload token - check firmware settings. Retrying will not help. Does not apply to outdoor queries
NO_SERIAL400Serial parameter missing
UNKNOWN_DEVICE404Serial not registered on the server - unit was never provisioned
RATE_LIMITED429Too many requests - retry shortly

With format=text errors look like this:

Error response · text
OK 0
ERROR BAD_TOKEN

Outdoor air quality

Returns the outdoor air for the unit's neighbourhood. Shown next to the indoor readings, it lets the user decide whether to ventilate.

GET https://api.wellbianlabs.io/v1/outdoor

No token needed - the serial alone is enough. The values are public air-quality data (redistributed from KMA and the Ministry of Environment) and carry nothing about rewards or wallets. The server already knows the location, so no coordinates either.

The response format matches /v1/device, so reuse the same parser. A 120 requests/minute limit guards against abuse - the recommended 5–10 minute polling interval will never reach it.

curl
curl "https://api.wellbianlabs.io/v1/outdoor?serial=IARAW2600126&format=text"
Response · text/plain
OK 1
PM10 40.3
PM25 28.7
GRADE 2
TEMP 29.5
HUMI 68
AREA Guro 3-dong
AGE 8
KeyTypeMeaning
PM10decimalCoarse particulate ㎍/㎥. -1 if unavailable
PM25decimalFine particulate ㎍/㎥. -1 if unavailable
GRADE1–41 Good 2 Moderate 3 Unhealthy 4 Very unhealthy. Follows the worse of PM10 and PM2.5
TEMPdecimalOutdoor temperature ℃. -999 if unavailable - not -1, which would collide with sub-zero readings
HUMIintOutdoor humidity %. -1 if unavailable
AREAstringNeighbourhood name
AGEintMinutes since observation. The source updates every 10 minutes, so expect 0–15

Poll every 5–10 minutes. The source refreshes on a 10-minute cycle, so a faster poll returns the same value. There is no need to match the reward-status interval.

Outdoor errors

CodeHTTPCause · what the device should do
DISABLED503Outdoor delivery is off or unconfigured - leave the outdoor area blank
NO_LOCATION503Server has no location for this unit - resolves once it is registered
NO_DATA502No reading for that area yet - retry shortly

Reward and connection display keep working even when outdoor fails. The two APIs are independent, so blank only the outdoor area and leave the rest on screen. If a whole screen goes blank because of outdoor, users read it as a broken unit.

Where the data comes from. Our server pulls KWeather Air365 readings in advance and serves them from our own store. The device never calls KWeather, so there is no key to embed, and swapping providers will not touch the firmware.

Test token

A demo token works without an account or a registered unit. It returns fixed sample values without touching the database, so you can build the display, the parser, and the error handling before any hardware is enrolled.

k value · development only
demo

Pick the screen state with serial - no need to wait for a real outage or an unregistered unit.

serialState returnedScreen to check
(anything)NET 1 · LASTMIN 1 · LINKED 1Normal - shows a reward
DEMO-OFFLINENET 0 · LASTMIN 37Server not receiving
DEMO-EMPTYLINKED 0 · rewards 0 · LASTMIN -1Not registered
Testing the disconnected screen
curl "https://api.wellbianlabs.io/v1/device?serial=DEMO-OFFLINE&k=demo&format=text"

Outdoor takes the same demo token.

Outdoor · demo
curl "https://api.wellbianlabs.io/v1/outdoor?serial=X&k=demo&format=text"

Demo responses carry a DEMO 1 line. Real tokens never return it. Do not ship demo in production firmware; use that line to detect development mode if you need to.

To exercise error handling, put any string in k - you get BAD_TOKEN.

Example · ESP32 / Arduino

C++
// Read from settings (NVS) - written over BLE at provisioning, not compiled in
String serial = "IARAW2600126";   // written via BLE SERIAL command
String token  = "...";             // written via BLE TOKEN command

struct Status {
  bool   ok       = false;
  int    net      = 0;    // 1 = receiving
  int    linked   = 0;    // 0 = no owner yet (not redeemed)
  String server;          // "Connected" / "Disconnected"
  double withdrawable = 0;
  int    lastMin  = -1;
  char   err[24]  = "";
};

bool fetchStatus(Status &st) {
  HTTPClient http;
  String url = "https://api.wellbianlabs.io/v1/device?format=text&serial=" + serial
             + "&k=" + token;

  http.begin(url);                          // see Security below for cert handling
  http.setTimeout(8000);

  int code = http.GET();
  if (code != 200 && code != 401 && code != 404) { http.end(); return false; }

  String body = http.getString();
  http.end();

  // One "KEY VALUE" per line. Order is not guaranteed - match by key.
  int pos = 0;
  while (pos < body.length()) {
    int nl = body.indexOf('\n', pos);
    if (nl < 0) nl = body.length();
    String line = body.substring(pos, nl);
    pos = nl + 1;

    int sp = line.indexOf(' ');
    if (sp < 0) continue;
    String k = line.substring(0, sp);
    String v = line.substring(sp + 1);

    if      (k == "OK")           st.ok = (v.toInt() == 1);
    else if (k == "NET")          st.net = v.toInt();
    else if (k == "LINKED")       st.linked = v.toInt();
    else if (k == "SERVER")       st.server = v;
    else if (k == "WITHDRAWABLE") st.withdrawable = v.toDouble();
    else if (k == "LASTMIN")      st.lastMin = v.toInt();
    else if (k == "ERROR")        v.toCharArray(st.err, sizeof(st.err));
    // unknown keys are ignored on purpose - new ones may appear
  }
  return true;
}

Implementation notes

Polling

Every 5 minutes is plenty for reward status; rewards only change once a day. Outdoor refreshes on a 10-minute cycle, so 5–10 minutes there too. Faster polling returns the same values and only burns power.

Handling failures

On a network failure, keep showing the last good values and mark them stale rather than blanking the screen. BAD_TOKEN will not fix itself - stop retrying and show a settings error. For 5xx, back off and retry.

Security

Units provisioned via BLE already carry a unique per-unit token - the factory tool fetches it from the server at setup, so no two units share one. Someone who knows a unit's token and serial can see that unit's reward amount (but not the wallet), which is why the token belongs in settings, never hardcoded or logged. Only legacy v6.04 units still share the old vendor-host token; the URL shape is identical either way, only k differs.

For the end user

There is nothing for them to do. Once the unit is redeemed and WiFi is connected, the reward appears on screen.

  1. Register at wellbian.io/redeem with the enclosed redeem code and the unit serial
  2. Accept the licence NFT in the wallet (signature)
  3. Connect the unit to WiFi → readings start accruing → the reward shows on the device

Before redemption the API returns LINKED 0, so show a "Not registered" prompt to nudge the user through registration.