Freeband

freeband.v1

API belgesi

Freeband hub'ı verine bir REST API ve imzalı webhook'larla açar. Bu sayfa sözleşmenin (freeband.v1) hub tarafını anlatır.

Tüm zamanlar ISO 8601 UTC'dir, örneğin 2026-10-13T20:59:41Z. Taban adres: https://freeband.cobanov.dev

Kimlik doğrulama

Her istek kişisel anahtarınla gelir: Authorization: Bearer fb_.... Anahtarı uygulama, Web erişimini açtığında oluşturur. Sunucuda anahtarın kendisi değil, yalnız SHA-256 özeti tutulur; anahtarı kaybedersen yenisini oluşturman gerekir.

export FREEBAND_TOKEN=fb_...
curl https://freeband.cobanov.dev/v1/me -H "Authorization: Bearer $FREEBAND_TOKEN"

Hata cevapları hep aynı biçimdedir: {"error": "kod", "message": "açıklama"}. Sık görülen kodlar: 400 invalid_body ya da invalid_query, 401 unauthorized, 403 cross_origin, 404 not_found, 409, 413, 415, 429 rate_limited (Retry-After başlığıyla).

API tarayıcıdan başka sitelerin çağırması için açık değildir (CORS başlığı yok). Kendi sunucundan, betiklerinden ya da komut satırından çağır.

Hesap

Hesap oluştur

Uygulama bunu Web erişimini açtığında kendisi yapar. Anahtar yalnız bu cevapta bir kez döner. IP başına saatte 10 istekle sınırlıdır.

curl -X POST https://freeband.cobanov.dev/v1/accounts \
  -H "Content-Type: application/json" \
  -d '{"device_name": "Xiaomi Smart Band 10 Pro D9B9", "tz": "Europe/Istanbul"}'

{"account_id": "…", "token": "fb_…", "web_url": "https://freeband.cobanov.dev/login"}

Web paneline doğrudan girmek için web_url adresinin sonuna #token= ve anahtarı ekle. Anahtar URL parçasında (#) kaldığı için sunucuya gitmez; sayfa onu oturum çerezine çevirir ve adres çubuğundan siler.

Hesap bilgisi

curl https://freeband.cobanov.dev/v1/me -H "Authorization: Bearer $FREEBAND_TOKEN"

{"account_id": "…", "name": "…", "tz": "Europe/Istanbul", "created_at": "…",
 "samples": {"live": 1520, "battery": 44, "minute": 8640, "sleep": 6, "score": 6, "total": 10216},
 "devices": [{"id": "5B0E2C9A-…", "name": "Xiaomi Smart Band 10 Pro D9B9", "model": "band10pro", "last_seen": "…"}],
 "webhooks": 1}

Anahtarı yenile

Yeni anahtar döner, eskisi hemen geçersiz olur. Diğer web oturumları kapanır. Yeni anahtarı telefonda Freeband'de Ayarlar → Web erişimi → API anahtarını gir alanına yapıştır; o zamana kadar uygulama veri gönderemez.

curl -X POST https://freeband.cobanov.dev/v1/token/rotate -H "Authorization: Bearer $FREEBAND_TOKEN"

Hesabı sil

Hesabı, tüm örnekleri, dakika, uyku ve skor kayıtlarını, bant kayıtlarını, webhook'ları ve oturumları kalıcı olarak siler.

curl -X DELETE https://freeband.cobanov.dev/v1/account -H "Authorization: Bearer $FREEBAND_TOKEN"

Veri gönderme

POST /v1/ingest uygulamanın kullandığı uç noktadır. Gövde en fazla 512 KB ve 5000 örnek olabilir. Her gövde tek bir banda aittir. Her örnek (device.id, type, ts) ile tekildir. live ve battery tekrar gelirse sayılır ama saklanmaz; minute, sleep ve score tekrar gelirse son gelen eskisinin yerine geçer (aşağıda). Hesap başına saatte 120 istek ve günde 200 000 örnek kabul edilir.

curl -X POST https://freeband.cobanov.dev/v1/ingest -H "Authorization: Bearer $FREEBAND_TOKEN" \
  -H "Content-Type: application/json" \
  --data-binary @sync.json

{"accepted": 2, "duplicates": 0}
{
  "schema": "freeband.v1",
  "event": "sync.completed",
  "device": {
    "id": "5B0E2C9A-4C7D-4F57-9A39-2D2B1D9E8A10",
    "name": "Xiaomi Smart Band 10 Pro D9B9",
    "model": "band10pro",
    "app_version": "0.4 (8)",
    "tz": "Europe/Istanbul"
  },
  "sent_at": "2026-10-13T21:00:02Z",
  "summary": { "date": "2026-10-13", "steps": 8421, "calories": 312, "heart_rate_last": 74, "battery": 46 },
  "samples": [
    { "type": "live", "ts": "2026-10-13T20:59:41Z", "steps": 8421, "calories": 312, "heart_rate": 74, "standing_hours": 9 },
    { "type": "battery", "ts": "2026-10-13T20:59:40Z", "level": 46 }
  ]
}

Doğrulanan alanlar: schema freeband.v1, event sync.completed ya da sync.partial, device.id UUID, device.model band10 ya da band10pro, heart_rate 0–250, level 0–100, steps 0–200 000, calories 0–50 000, standing_hours 0–24, ts en fazla 2 gün ileride. Bir hesapta en fazla 10 bant olabilir. device.tz gelirse hesabın saat dilimi ona göre güncellenir.

sync.partial, senkron yarıda kaldığında (ör. 20 dosyadan 12'si okunduğunda) gönderilir. Gövdedeki örnekler geçerlidir ve saklanır; kalanı sonraki senkronda gelir. Webhook'lara aynı olay adıyla iletilir.

Sözleşme yalnız ekleyerek büyür. Hub tanımadığı bir türdeki örneği reddetmez, atlar ve cevapta sayar: {"accepted": 1, "duplicates": 0, "ignored": 2} (ignored yalnız 0'dan büyükse gelir). Tanınan bir türde tek bir geçersiz alan bütün gövdeyi 400 invalid_body ile reddeder; hata mesajı alanın yolunu verir (ör. samples[3].heart_rate).

Sağlık verisi: dakika, uyku, skor

Uygulama bandın aktivite dosyalarını çözünce üç tür daha gönderir. Üçü de üzerine yazılır: aynı (device.id, type, ts) yeniden gelirse son gelen kazanır ve satırın tamamı yenisiyle değişir. Değişmeden gelen satır duplicates'e sayılır ve webhook'ları yeniden tetiklemez. Nabız 30–220, SpO2 70–100, stres 1–100 aralığındadır; ölçülmeyen isteğe bağlı alan gönderilmez ya da null olur.

[
  { "type": "minute", "ts": "2026-10-13T07:42:00Z", "steps": 112, "heart_rate": 96,
    "spo2": 97, "stress": 34, "active_calories": 6, "distance_m": 84 },
  { "type": "sleep", "ts": "2026-10-12T20:41:00Z", "end": "2026-10-13T04:12:00Z", "is_nap": false,
    "minutes": { "deep": 82, "light": 241, "rem": 96, "awake": 32 },
    "wake_count": 3, "efficiency": 93,
    "stages": [[0, "light", 14], [14, "deep", 38], [52, "light", 61], [113, "rem", 22]],
    "hr_min": 48, "spo2_avg": 96.4, "spo2_min": 91, "hrv_rmssd": 42 },
  { "type": "score", "ts": "2026-10-12T21:00:00Z", "date": "2026-10-13",
    "sleep": 82, "recovery": 64, "strain": 11.4, "rhr": 54, "hrv_rmssd": 41.5,
    "confidence": { "sleep": "high", "recovery": "medium", "strain": "low" },
    "contributors": [
      { "score": "sleep", "key": "duration", "weight": 35, "value": 431, "unit": "min", "subscore": 88, "baseline": 418 },
      { "score": "recovery", "key": "hrv", "weight": 40, "value": null, "subscore": null, "note": "RR yok" }
    ],
    "algo": "freeband-score/1" }
]
  • minute: ts dakikanın başı (saniye 0). steps 0–1000; isteğe bağlı heart_rate, spo2, stress, active_calories (0–100 kcal), distance_m (0–1000). Yalnız boş olmayan dakikalar gönderilir; bir gövdede en fazla bir gün önerilir.
  • sleep: ts yatış, end kalkış (en fazla 24 saat). minutes evre dakikaları, efficiency yüzde, stages [yatıştan dakika, evre, süre] üçlüleri (deep, light, rem, awake). hr_min, spo2_avg, spo2_min ölçülmediyse null; hrv_rmssd yalnız RR varsa. Gece, uyandığın güne aittir.
  • score: ts, date gününün yerel gece yarısı. sleep ve recovery 0–100, strain 0–21, hepsi null olabilir. confidence her skor için high, medium, low ya da none. contributors katkı satırları (key gerekli; score, weight, value, unit, subscore, baseline, note isteğe bağlı). algo algoritmanın sürümü.

Veriyi okuma

Ham örnekler

GET /v1/samples canlı ve pil örneklerini zamana göre artan sırada döner. Parametreler: device (bant kimliği; verilmezse tüm bantlar), type (live ya da battery), from, to, limit (varsayılan 1000, en fazla 5000). from ve to ya ISO 8601 zaman (from dahil, to hariç) ya da hesabın saat dilimine göre YYYY-MM-DD gün (iki uç da dahil) olabilir. has_more doğruysa son örneğin zamanını yeni from olarak kullan.

curl "https://freeband.cobanov.dev/v1/samples?type=live&from=2026-10-13&to=2026-10-13" -H "Authorization: Bearer $FREEBAND_TOKEN"

{"samples": [{"device_id": "5B0E2C9A-…", "type": "live", "ts": "2026-10-13T20:59:41Z",
  "steps": 8421, "calories": 312, "heart_rate": 74, "standing_hours": 9}],
 "has_more": false}

Dakikalar

GET /v1/minutes dakikaları zamana göre artan sırada döner. Parametreler: device, from, to (ham örneklerdeki gibi), limit (varsayılan 1440, en fazla 5000) ve after. has_more doğruysa aynı isteği after= ve cevaptaki next değeriyle tekrarla; birden çok bant aynı dakikayı paylaşsa da hiçbir satır atlanmaz.

curl "https://freeband.cobanov.dev/v1/minutes?from=2026-10-13&to=2026-10-13" -H "Authorization: Bearer $FREEBAND_TOKEN"

{"minutes": [{"device_id": "5B0E2C9A-…", "type": "minute", "ts": "2026-10-13T07:42:00Z",
  "steps": 112, "heart_rate": 96, "spo2": 97, "stress": 34, "active_calories": 6, "distance_m": 84}],
 "has_more": true, "next": "…"}

curl "https://freeband.cobanov.dev/v1/minutes?from=2026-10-13&to=2026-10-13&after=…" -H "Authorization: Bearer $FREEBAND_TOKEN"

Uyku

GET /v1/sleep uyku oturumlarını döner. from ve to kalkış zamanına (end) uygulanır: from=2026-10-13&to=2026-10-13 12'sini 13'üne bağlayan geceyi ve 13'ündeki şekerlemeleri verir. limit varsayılan 100, en fazla 1000; sayfalama dakikalardaki gibi.

curl "https://freeband.cobanov.dev/v1/sleep?from=2026-10-13&to=2026-10-13" -H "Authorization: Bearer $FREEBAND_TOKEN"

{"sleep": [{"device_id": "5B0E2C9A-…", "type": "sleep", "ts": "2026-10-12T20:41:00Z", "end": "2026-10-13T04:12:00Z",
  "is_nap": false, "minutes": {"deep": 82, "light": 241, "rem": 96, "awake": 32}, "wake_count": 3, "efficiency": 93,
  "stages": [[0, "light", 14], [14, "deep", 38]], "hr_min": 48, "spo2_avg": 96.4, "spo2_min": 91, "hrv_rmssd": 42}],
 "has_more": false, "next": null}

Skorlar

GET /v1/scores günlük skorları kendi date alanına göre döner. from ve to yalnız YYYY-MM-DD olabilir (iki uç dahil); verilmezse sınır yok. limit varsayılan 366, en fazla 1000; sayfalama dakikalardaki gibi.

curl "https://freeband.cobanov.dev/v1/scores?from=2026-10-01&to=2026-10-13" -H "Authorization: Bearer $FREEBAND_TOKEN"

{"scores": [{"device_id": "5B0E2C9A-…", "type": "score", "ts": "2026-10-12T21:00:00Z", "date": "2026-10-13",
  "sleep": 82, "recovery": 64, "strain": 11.4, "rhr": 54, "hrv_rmssd": 41.5,
  "confidence": {"sleep": "high", "recovery": "medium", "strain": "low"},
  "contributors": [{"score": "sleep", "key": "duration", "weight": 35, "value": 431, "unit": "min", "subscore": 88}],
  "algo": "freeband-score/1"}],
 "has_more": false, "next": null}

Günlük özet

GET /v1/days her gün ve her bant için bir satır döner; günler hesabın saat dilimine göredir. steps, canlı örneklerin birikimli en büyük değeri ile dakika adımlarının toplamından büyük olanıdır. calories canlı örneklerden gelir. Nabız özetleri canlı örnekleri ve dakikaları birlikte kullanır ve yalnız 30–220 bpm arasını sayar. Varsa sleep_minutes (o gün biten gece uykusunun derin + hafif + REM dakikaları), nap_minutes (o gün biten şekerlemeler) ve score (o günün skorları) da gelir. Hiç verisi olmayan günler listede yer almaz. Varsayılan aralık bugün dahil son 14 gün, en fazla 366 gün.

curl "https://freeband.cobanov.dev/v1/days?from=2026-10-01&to=2026-10-13" -H "Authorization: Bearer $FREEBAND_TOKEN"

{"tz": "Europe/Istanbul", "from": "2026-10-01", "to": "2026-10-13", "days": [
  {"date": "2026-10-13", "device_id": "5B0E2C9A-…", "steps": 8421, "calories": 312,
   "heart_rate_min": 52, "heart_rate_max": 131, "heart_rate_avg": 74, "battery_last": 46,
   "sleep_minutes": 419, "nap_minutes": 35,
   "score": {"sleep": 82, "recovery": 64, "strain": 11.4, "rhr": 54, "hrv_rmssd": 41.5,
             "confidence": {"sleep": "high", "recovery": "medium", "strain": "low"}, "algo": "freeband-score/1"}}]}

Dışa aktarma

GET /v1/export.csv ve GET /v1/export.json, /v1/samples ile aynı from, to ve device parametrelerini alır; type live, battery, minute, sleep ya da score olabilir. Parametresiz çağrı tüm veriyi verir. Her satır device_id taşır. Tek seferde toplam en fazla 200 000 satır; daha fazlası için aralığı böl.

JSON her türü içerir: samples (canlı ve pil), minutes, sleep ve scores dizileri, okuma uç noktalarındaki biçimde. CSV düz bir tablo olduğu için her tür kendi dosyasındadır: varsayılan CSV canlı ve pil örnekleridir; type=minute, type=sleep ve type=score kendi sütunlarıyla gelir. Uyku evreleri ve skor katkıları CSV'de tırnaklı JSON hücresidir.

curl -o freeband.csv "https://freeband.cobanov.dev/v1/export.csv?from=2026-10-01" -H "Authorization: Bearer $FREEBAND_TOKEN"

device_id,type,ts,steps,calories,heart_rate,standing_hours,level
5B0E2C9A-…,battery,2026-10-13T20:59:40Z,,,,,46
5B0E2C9A-…,live,2026-10-13T20:59:41Z,8421,312,74,9,

curl -o dakikalar.csv "https://freeband.cobanov.dev/v1/export.csv?type=minute&from=2026-10-01" -H "Authorization: Bearer $FREEBAND_TOKEN"

device_id,type,ts,steps,heart_rate,spo2,stress,active_calories,distance_m
5B0E2C9A-…,minute,2026-10-13T07:42:00Z,112,96,97,34,6,84

curl -o uyku.csv "https://freeband.cobanov.dev/v1/export.csv?type=sleep" -H "Authorization: Bearer $FREEBAND_TOKEN"
curl -o skorlar.csv "https://freeband.cobanov.dev/v1/export.csv?type=score" -H "Authorization: Bearer $FREEBAND_TOKEN"
curl -o freeband.json "https://freeband.cobanov.dev/v1/export.json" -H "Authorization: Bearer $FREEBAND_TOKEN"

Webhook'lar

Hub, her başarılı gönderimden sonra (en az bir yeni ya da değişen satır varsa) doğruladığı gövdeyi, gövdenin olayıyla (sync.completed ya da sync.partial) hesabın webhook'larına POST eder. Hub'ın tanımadığı türler iletilmez. Adres yalnız https:// olabilir, hub'ın kendi alan adı, IP adresleri ve yerel adlar kabul edilmez. Hesap başına en fazla 5 webhook. Yönlendirmeler izlenmez, zaman aşımı 5 saniyedir, şimdilik yeniden deneme yoktur. Aynı örnek iki kez gelebilir; alıcı (device.id, type, ts) ile tekilleştirmeli, minute, sleep ve score için son geleni saklamalı, tanımadığı türleri atlamalı.

curl -X POST https://freeband.cobanov.dev/v1/webhooks -H "Authorization: Bearer $FREEBAND_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"url": "https://ornek.com/freeband"}'

{"id": "…", "url": "https://ornek.com/freeband", "secret": "fbwh_…", "created_at": "…"}

Gizli değer yalnız bu cevapta bir kez döner; sunucuda şifreli tutulur ve bir daha gösterilmez.

curl https://freeband.cobanov.dev/v1/webhooks -H "Authorization: Bearer $FREEBAND_TOKEN"
curl https://freeband.cobanov.dev/v1/webhooks/ID -H "Authorization: Bearer $FREEBAND_TOKEN"
curl -X POST https://freeband.cobanov.dev/v1/webhooks/ID/test -H "Authorization: Bearer $FREEBAND_TOKEN"
curl -X DELETE https://freeband.cobanov.dev/v1/webhooks/ID -H "Authorization: Bearer $FREEBAND_TOKEN"

Test isteği X-Freeband-Event: ping ile şu gövdeyi gönderir: {"schema": "freeband.v1", "event": "ping", "sent_at": "…", "webhook_id": "…"}. Cevap {"delivered", "status", "error"} olur.

İmzayı doğrulama

Her istek şu başlıklarla gelir:

  • Content-Type: application/json
  • X-Freeband-Event: sync.completed, sync.partial ya da ping
  • X-Freeband-Delivery: her gönderim için rastgele bir UUID
  • X-Freeband-Signature: sha256= ve gövdenin HMAC-SHA256 özeti (küçük harf hex). Anahtar, gizli değerin UTF-8 baytlarıdır.

İmzayı gövdeyi ayrıştırmadan önce, ham baytlar üzerinden ve sabit zamanlı karşılaştırmayla doğrula.

Node.js:

import { createHmac, timingSafeEqual } from "node:crypto";
import http from "node:http";

const SECRET = process.env.FREEBAND_WEBHOOK_SECRET; // fbwh_...

http.createServer((req, res) => {
  const chunks = [];
  req.on("data", (c) => chunks.push(c));
  req.on("end", () => {
    const raw = Buffer.concat(chunks); // imzayı ham gövde üzerinden doğrula
    const expected = Buffer.from(
      "sha256=" + createHmac("sha256", SECRET).update(raw).digest("hex"),
    );
    const got = Buffer.from(req.headers["x-freeband-signature"] ?? "");
    if (got.length !== expected.length || !timingSafeEqual(got, expected)) {
      res.writeHead(401).end();
      return;
    }
    const body = JSON.parse(raw.toString("utf8"));
    if (body.event === "sync.completed" || body.event === "sync.partial") {
      // Tanımadığın türleri atla; minute/sleep/score için son geleni sakla.
      const known = body.samples.filter((s) => ["live", "battery", "minute", "sleep", "score"].includes(s.type));
      console.log(body.device.id, body.event, known.length);
    }
    res.writeHead(204).end();
  });
}).listen(8080);

Python:

import hashlib, hmac, json, os
from http.server import BaseHTTPRequestHandler, HTTPServer

SECRET = os.environ["FREEBAND_WEBHOOK_SECRET"].encode()  # fbwh_...

class Hook(BaseHTTPRequestHandler):
    def do_POST(self):
        raw = self.rfile.read(int(self.headers.get("Content-Length", 0)))
        expected = "sha256=" + hmac.new(SECRET, raw, hashlib.sha256).hexdigest()
        got = self.headers.get("X-Freeband-Signature", "")
        if not hmac.compare_digest(got, expected):
            self.send_response(401)
            self.end_headers()
            return
        body = json.loads(raw)
        if body["event"] in ("sync.completed", "sync.partial"):
            # Tanımadığın türleri atla; minute/sleep/score için son geleni sakla.
            known = [s for s in body["samples"] if s["type"] in ("live", "battery", "minute", "sleep", "score")]
            print(body["device"]["id"], body["event"], len(known))
        self.send_response(204)
        self.end_headers()

HTTPServer(("", 8080), Hook).serve_forever()

Web paneli oturumu

POST /v1/session gövdesi {"token": "fb_…"} olan bir istekle 30 gün geçerli bir oturum çerezi verir (HttpOnly; Secure; SameSite=Strict). DELETE /v1/session çıkış yapar. Bu iki uç nokta yalnız panel sayfalarından çağrılabilir; çerezle yapılan veri değiştiren istekler de aynı kaynaktan gelmek zorundadır.