Daylo started as a favor for one friend who needed weight tracking with zero willpower required — step on the scale, walk away, let the software care. The scale already ships the number. The vendors just hide it behind different APIs. So we built the boring part once. — why this exists
for developers & their agents
One API for every smart scale.
Withings and Tanita each speak their own dialect — different auth, different date
formats, different ideas of what a number is. Daylo normalizes them behind
one REST API and a JSON-first CLI, so your agent can just run
daylo latest.
open source · runs on your Cloudflare account ·
bunx github:pivop-inc/daylo
$ daylo sync
{ "synced": { "withings": 3 } }
$ daylo latest
{
"id": "withings:1841562847",
"provider": "withings",
"measuredAt": "2026-07-11T06:42:11Z",
"weightKg": 72.4,
"fatRatioPercent": 18.2
}
§ 01the problem
Every vendor speaks its own dialect.
Here is the same 72.4 kg reading from the same morning, as two vendors actually return it. Payloads abridged; the pain is not.
// GET .../measure?action=getmeas
{
"status": 0,
"body": {
"measuregrps": [{
"grpid": 1841562847,
"date": 1783752131,
"measures": [
{ "value": 72400, "unit": -3, "type": 1 },
{ "value": 18200, "unit": -3, "type": 6 }
]
}]
}
}
- weight is
value × 10^unit— 72400 × 10⁻³ - the metric is a magic number:
type1 vs 6 - timestamps are raw epoch seconds
// GET /status/innerscan.json?tag=6021,6022
{
"birth_date": "19900101",
"height": "172",
"sex": "male",
"data": [
{ "date": "20260711154211",
"keydata": "72.40", "tag": "6021" },
{ "date": "20260711154211",
"keydata": "18.20", "tag": "6022" }
]
}
- every number is a string
- dates are JST
yyyyMMddHHmmss— no offset, no hint -
the metric is a tag code:
"6021"weight,"6022"fat %
Same ritual, same number — two schemas, two OAuth dances, two date systems. Your agent shouldn't need a per-vendor adapter to know what you weigh. That adapter layer is exactly the code nobody wants to write twice. So we wrote it once.
§ 02the answer
One shape, whatever the scale.
Every measurement comes back as the same normalized object — vendor quirks stay inside the adapters, nothing leaks past the interface. This type is copied verbatim from the v1 spec, and the API returns exactly this shape.
type ProviderId = "withings" | "tanita";
type WeightMeasurement = {
id: string; // "<provider>:<provider-native measurement id>"
provider: ProviderId;
measuredAt: string; // ISO 8601, UTC (Tanita returns JST — adapters normalize)
weightKg: number;
fatRatioPercent: number | null;
};
- Numbers are numbers. Kilograms are kilograms.
- Timestamps are ISO 8601 UTC — Tanita's JST strings are converted before you ever see them.
idis stable and prefixed by provider, so dedup is trivial.
$ curl -s "$DAYLO_API_URL/api/v1/weight/latest" \
-H "Authorization: Bearer $DAYLO_API_KEY"
{
"latest": {
"id": "withings:1841562847",
"provider": "withings",
"measuredAt": "2026-07-11T06:42:11Z",
"weightKg": 72.4,
"fatRatioPercent": 18.2
}
}
$ curl -s "$DAYLO_API_URL/api/v1/weight/list?days=30&provider=withings" \
-H "x-api-key: $DAYLO_API_KEY"
{ "measurements": [ /* sorted by measuredAt, newest first */ ] }
/api/v1 — the whole surface
auth:Authorization: Bearer <key> or x-api-key ·
every response carries X-Request-Id
/api/health
{ "ok": true } — no auth, for probes
/api/v1/weight/latest
{ "latest": WeightMeasurement | null }
/api/v1/weight/list?days=30&provider=withings
newest first · days default 30, max 365 ·
provider optional
/api/v1/sync
pull new readings from every connected provider →
{ "synced": { "withings": 3, "tanita": 0 } }
/api/v1/providers
connection status per provider
/api/v1/providers/:provider/connect
begins OAuth → { "authorizeUrl": "...", "state": "..." }
/api/v1/providers/:provider
disconnect and delete stored tokens
/api/v1/me/data
delete all your measurements and provider tokens — one call, no ceremony
errors, everywhere
{ "error": { "code": "unauthorized", "message": "..." }, "requestId": "..."
}
§ 03built for agents
Your agent doesn't want a dashboard. It wants stdout.
The daylo CLI is JSON-first: machine-parseable output is the default and
humans are the special case. That inversion is the whole design.
-
stdout is JSON, by default
--prettyis the flag for humans — not the other way around. -
errors are JSON too
On stderr, as
{ "error": { "code", "message" } }— parse failures, not prose. - exit codes mean something 0 success · 1 error · 2 usage. Your retry logic reads three integers, not log lines.
-
config where tools expect it
~/.config/daylo/config.json(mode 600).DAYLO_API_URLor--api-urloverride for local dev.
# current weight, one pipe away
$ daylo latest | jq .weightKg
72.4
# a month of history — `list` prints the array itself
$ daylo list --days 30 --provider withings | jq length
31
# poll for fresh readings on your schedule
$ daylo sync
{ "synced": { "withings": 3, "tanita": 0 } }
# and when a human is watching:
$ daylo latest --pretty
§ 04providers
Honest status, not a logo wall.
Two adapters, one contract test suite that runs identically against both. Here's exactly where each stands today.
supported
Withings
OAuth connect, token refresh with locking, encrypted token storage, sync, normalization — the full path works end to end today.
daylo connect withings
approval pending
Tanita Health Planet
The adapter is implemented against the official API docs and passes the same contract tests with mocked responses. Tanita's API credential approval is still pending — the moment it lands, so does the switch.
daylo connect tanita — soon
Your scale here
A provider is one interface: build the authorize URL, exchange the code, refresh tokens, fetch measurements. If it passes the contract tests, it ships.
request a provider →Why tell you about the pending one? Because you'd find out anyway, and because a status page you can trust is worth more than a longer logo wall.
§ 05quickstart
Zero to daylo latest in four commands.
No install step — the CLI runs straight from the repo with bun.
-
sign in
loginopens your browser (Google or magic link) via the device flow, mints an API key namedcli, and saves config to~/.config/daylo/config.json. -
link your scale
connect withingsopens the vendor's OAuth page in your browser and polls until the scale is linked (5-minute timeout). Then close the tab forever. -
read the number
syncpulls new readings;latestprints normalized JSON. Cron it, pipe it, hand it to your agent — done.
# runs from the repo — nothing to install but bun
$ bunx github:pivop-inc/daylo login
$ bunx github:pivop-inc/daylo connect withings
$ bunx github:pivop-inc/daylo sync
$ bunx github:pivop-inc/daylo latest
The CLI talks to any Daylo deployment — deploy the Worker to your own Cloudflare
account and point DAYLO_API_URL at it (a local
wrangler dev works too). Rather not run infrastructure?
Join the hosted-API waitlist.
Your scale has an API.
Now it's a good one.
Read the spec, run the CLI tonight, wire it into your agent tomorrow morning. If you'd rather we run the backend, say so — the waitlist is one click.
the waitlist is a GitHub issue with a prefilled title — no form, no tracking, no newsletter