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.
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.
| Purpose | Address |
|---|---|
| Measurement upload | https://api.wellbianlabs.io/v1/c?<base64> |
| Reward & link status | https://api.wellbianlabs.io/v1/device?serial=<SERIAL>&k=<TOKEN>&format=text |
| Outdoor air quality | https://api.wellbianlabs.io/v1/outdoor?serial=<SERIAL>&format=text (no token) |
| Firmware update | fota.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.
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.
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.
curl "https://api.wellbianlabs.io/v1/device?serial=IARAW2600126&k=<TOKEN>&format=text"
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.
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.
| Name | Required | Description |
|---|---|---|
| serial | yes | The unit's own serial |
| k | yes | Upload token, written to device settings via BLE provisioning at setup (never compiled into firmware). May also be sent as the X-Device-Token header |
| format | no | text 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.
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.
| Key | Type | Meaning |
|---|---|---|
| OK | 0/1 | 1 = success. On 0, the next line is ERROR |
| SERVER | string | For display. Connected / Disconnected - print as-is |
| WITHDRAWABLE | decimal | For display. Amount withdrawable now. Same value as CLAIMABLE, named separately so there is no doubt what to show |
| NET | 0/1 | Whether the server is receiving this unit's data. 1 if seen within 5 minutes |
| LASTMIN | int | Minutes since last reception. -1 if never |
| TODAY | int | Readings received today (KST) |
| CLAIMABLE | decimal | Withdrawable now, for this unit |
| PENDING | decimal | Accrued but still inside the hold window |
| CLAIMED | decimal | Withdrawn to date |
| TOTAL | decimal | Sum of the three above - lifetime earnings of this unit |
| UNIT | string | Reward unit. Currently WLBN |
| LINKED | 0/1 | Whether an owner is linked. 0 means not yet redeemed, so rewards are all 0 |
| ACCRUING | 0/1 | Whether it is accruing right now - linked and receiving data today |
| NEXTSET | int | Minutes until the next settlement. Settlement runs once a day at KST midnight |
| HOLDH | int | Hold 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:
// 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.
Omit format to get JSON. Use it on a roomier MCU or in an app.
{
"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.
| Code | HTTP | Cause · what the device should do |
|---|---|---|
| BAD_TOKEN | 401 | Wrong upload token - check firmware settings. Retrying will not help. Does not apply to outdoor queries |
| NO_SERIAL | 400 | Serial parameter missing |
| UNKNOWN_DEVICE | 404 | Serial not registered on the server - unit was never provisioned |
| RATE_LIMITED | 429 | Too many requests - retry shortly |
With format=text errors look like this:
OK 0 ERROR BAD_TOKEN
Returns the outdoor air for the unit's neighbourhood. Shown next to the indoor readings, it lets the user decide whether to ventilate.
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 "https://api.wellbianlabs.io/v1/outdoor?serial=IARAW2600126&format=text"
OK 1 PM10 40.3 PM25 28.7 GRADE 2 TEMP 29.5 HUMI 68 AREA Guro 3-dong AGE 8
| Key | Type | Meaning |
|---|---|---|
| PM10 | decimal | Coarse particulate ㎍/㎥. -1 if unavailable |
| PM25 | decimal | Fine particulate ㎍/㎥. -1 if unavailable |
| GRADE | 1–4 | 1 Good 2 Moderate 3 Unhealthy 4 Very unhealthy. Follows the worse of PM10 and PM2.5 |
| TEMP | decimal | Outdoor temperature ℃. -999 if unavailable - not -1, which would collide with sub-zero readings |
| HUMI | int | Outdoor humidity %. -1 if unavailable |
| AREA | string | Neighbourhood name |
| AGE | int | Minutes 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.
| Code | HTTP | Cause · what the device should do |
|---|---|---|
| DISABLED | 503 | Outdoor delivery is off or unconfigured - leave the outdoor area blank |
| NO_LOCATION | 503 | Server has no location for this unit - resolves once it is registered |
| NO_DATA | 502 | No 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.
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.
demo
Pick the screen state with serial - no need to wait for a real outage or an unregistered unit.
| serial | State returned | Screen to check |
|---|---|---|
| (anything) | NET 1 · LASTMIN 1 · LINKED 1 | Normal - shows a reward |
| DEMO-OFFLINE | NET 0 · LASTMIN 37 | Server not receiving |
| DEMO-EMPTY | LINKED 0 · rewards 0 · LASTMIN -1 | Not registered |
curl "https://api.wellbianlabs.io/v1/device?serial=DEMO-OFFLINE&k=demo&format=text"
Outdoor takes the same demo token.
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.
// 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; }
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.
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.
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.
There is nothing for them to do. Once the unit is redeemed and WiFi is connected, the reward appears on screen.
Before redemption the API returns LINKED 0, so show a "Not registered" prompt to nudge the user through registration.