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

~ — your agent's morning
$ 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.

Withings · Measure API
// 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: type 1 vs 6
  • timestamps are raw epoch seconds
Tanita · Health Planet
// 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.

docs/spec.md · the core type
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.
  • id is stable and prefixed by provider, so dedup is trivial.
curl · latest reading
$ 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 · 30-day history, one provider
$ 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
GET /api/health { "ok": true } — no auth, for probes
GET /api/v1/weight/latest { "latest": WeightMeasurement | null }
GET /api/v1/weight/list?days=30&provider=withings newest first · days default 30, max 365 · provider optional
POST /api/v1/sync pull new readings from every connected provider → { "synced": { "withings": 3, "tanita": 0 } }
GET /api/v1/providers connection status per provider
POST /api/v1/providers/:provider/connect begins OAuth → { "authorizeUrl": "...", "state": "..." }
DELETE /api/v1/providers/:provider disconnect and delete stored tokens
DELETE /api/v1/me/data delete all your measurements and provider tokens — one call, no ceremony
4xx/5xx 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 --pretty is 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_URL or --api-url override for local dev.
the entire integration surface of your agent
# 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.

Abstract amber line-work portrait of a Withings smart scale supported

Withings

OAuth connect, token refresh with locking, encrypted token storage, sync, normalization — the full path works end to end today.

daylo connect withings
Abstract amber line-work portrait of a Tanita smart scale 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
yours?

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.

  1. sign in login opens your browser (Google or magic link) via the device flow, mints an API key named cli, and saves config to ~/.config/daylo/config.json.
  2. link your scale connect withings opens the vendor's OAuth page in your browser and polls until the scale is linked (5-minute timeout). Then close the tab forever.
  3. read the number sync pulls new readings; latest prints normalized JSON. Cron it, pipe it, hand it to your agent — done.
quickstart — copy, paste, weigh
# 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.

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

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