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:tsdakikanın başı (saniye 0).steps0–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:tsyatış,endkalkış (en fazla 24 saat).minutesevre dakikaları,efficiencyyüzde,stages[yatıştan dakika, evre, süre]üçlüleri (deep,light,rem,awake).hr_min,spo2_avg,spo2_minölçülmediysenull;hrv_rmssdyalnız RR varsa. Gece, uyandığın güne aittir.score:ts,dategününün yerel gece yarısı.sleepverecovery0–100,strain0–21, hepsinullolabilir.confidenceher skor içinhigh,medium,lowya danone.contributorskatkı satırları (keygerekli;score,weight,value,unit,subscore,baseline,noteisteğe bağlı).algoalgoritmanı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/jsonX-Freeband-Event:sync.completed,sync.partialya dapingX-Freeband-Delivery: her gönderim için rastgele bir UUIDX-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.