SvitloPulse

Для розробників

Публічне API відкрите й без ключа. Ключ потрібен, якщо вам потрібні вищі квоти чи вебхуки замість опитування.

Статус: це опис можливостей, а не комерційна пропозиція. Жодного SLA чи гарантії доступності зараз немає. Ключі видає оператор сервісу вручну; самообслуговування й оплати в інтерфейсі немає.

Плани

Free

без ключа
  • Усе публічне API: /api/v1
  • 600 читань на хвилину з IP (сплеск 150)
  • Для сайтів, ботів і домашніх проєктів

Pro

1 000 000 / добуорієнтовно
  • Ключ, до 50 запитів на секунду
  • Квоти рахуються на ключ, а не на IP
  • Вебхуки замість опитування

Business

10 000 000 / добуорієнтовно
  • Ключ, до 200 запитів на секунду
  • Те саме, що Pro, із вищими квотами
  • Умови погоджуються індивідуально

Цифри для Pro і Business орієнтовні й можуть змінитися до видачі ключа. Цін немає: рахунків і платіжної системи в сервісі не існує.

Як користуватися ключем

Передайте ключ заголовком X-API-Key або Authorization: Bearer. Усі ендпоїнти ті самі, що в документації API.

curl
curl -s -H "X-API-Key: $SVITLOPULSE_KEY" https://svitlopulse.andrewboyko.name/api/v1/oblasts
curl -s -H "Authorization: Bearer $SVITLOPULSE_KEY" https://svitlopulse.andrewboyko.name/api/v1/groups/lvivoblenergo/3.2
  • Ключ — секрет. Тримайте його лише на сервері, не вшивайте в JavaScript сторінки чи мобільний застосунок: його легко витягти.
  • Для запитів із браузера відвідувачів ключ не потрібен: користуйтеся безключовим API чи віджетами.
  • Скомпрометований ключ оператор може відкликати: напишіть нам, і ми видамо новий. Відкликаний ключ отримує 401.

Квоти й заголовки

Відповіді з ключем мають заголовки X-RateLimit-Limit, X-RateLimit-Remaining (денна квота) і X-RateLimit-Reset (unix-час, коли квота оновиться).

HTTPcodeЩо означає
401invalid_key, revoked_keyКлюч невідомий або відкликаний.
429rate_limitedПеревищено ліміт запитів на секунду. Заголовок Retry-After каже, скільки чекати.
429quota_exceededВичерпано добову квоту до X-RateLimit-Reset.
  • Не перевищуйте квоти: на 429 чекайте Retry-After і збільшуйте паузу.
  • Для оновлень у реальному часі використовуйте SSE /api/v1/stream або вебхуки, а також ETag: запит із If-None-Match повертає 304.

Вебхуки

Замість опитування ми надсилаємо POST на ваш HTTPS-адрес, коли стан черги чи області змінюється. Вебхуки доступні лише власникам ключа: ви реєструєте їх самі тим самим ключем.

curl · register
curl -s -X POST https://svitlopulse.andrewboyko.name/api/v1/webhooks \
  -H "X-API-Key: $SVITLOPULSE_KEY" -H "Content-Type: application/json" \
  -d '{"url":"https://example.com/hooks/svitlo","operator_id":"lvivoblenergo","group":"3.2"}'
# or a whole region: {"url":"…","oblast":"lviv-oblast"}
# 201 -> {"id":…,"secret":"whsec_…","events":["outage_started","power_restored","schedule_changed"]}
  • Область дії: одна черга (operator_id + group) або одна область (oblast, ISO чи slug).
  • Секрет whsec_… повертається один раз, якщо ви не передали власний (16–128 символів). Збережіть його.
  • Лише HTTPS-адреси, що ведуть на публічні хости: приватні, loopback і link-local адреси відхиляються. Кількість вебхуків на ключ обмежена. Список і видалення: GET /api/v1/webhooks, DELETE /api/v1/webhooks/{id}.
ПодіяКоли
outage_startedЧерга перейшла в стан «світла немає».
power_restoredЧерга перейшла в стан «світло є».
schedule_changedОператор змінив графік черги.

Кожен запит має заголовок підпису, а також X-SvitloPulse-Event, X-SvitloPulse-Event-Id (однаковий в усіх повторах, це ключ ідемпотентності) і X-SvitloPulse-Attempt.

HTTP header
X-SvitloPulse-Signature: t=1730000000,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd

Тут t — unix-час відправлення, а v1 — HMAC-SHA256 від рядка {t}.{тіло запиту} із вашим секретом вебхука, у hex. Підписується точне тіло запиту, байт у байт.

Значення в прикладі заголовка — ілюстрація формату, а не справжній підпис.

Тіло — JSON: id, type, created_at, scope (group або oblast), operator_id, group, oblast, oblast_slug, data (стан такий самий, як у /api/v1/status, плюс previous_state). Події виводяться з публічних даних, тож вебхук не показує більше, ніж API.

Перевірка підпису

  • Рахуйте HMAC із сирого тіла, до розбору JSON.
  • Порівнюйте у постійний час і відхиляйте запити, де t відрізняється від поточного часу більш ніж на 5 хвилин: це захищає від повтору.
  • Відповідайте 2xx протягом 5 секунд, редиректи не відстежуються; важку роботу виносьте в чергу.
  • Невдалу доставку повторюємо через 1, 5 і 30 хвилин (разом чотири спроби), а вебхук, що стабільно не відповідає, вимикається. Обробляйте події ідемпотентно за X-SvitloPulse-Event-Id.
Node.js
import crypto from 'node:crypto';

export function verify(rawBody, header, secret, toleranceSec = 300) {
  const parts = Object.fromEntries(header.split(',').map((p) => p.trim().split('=')));
  const t = Number(parts.t);
  if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
  const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest('hex');
  const got = Buffer.from(parts.v1 || '', 'hex');
  const want = Buffer.from(expected, 'hex');
  return got.length === want.length && crypto.timingSafeEqual(got, want);
}
PHP
<?php
function verify_svitlopulse(string $rawBody, string $header, string $secret, int $tolerance = 300): bool {
    $parts = [];
    foreach (explode(',', $header) as $p) { [$k, $v] = array_pad(explode('=', trim($p), 2), 2, ''); $parts[$k] = $v; }
    $t = (int)($parts['t'] ?? 0);
    if (!$t || abs(time() - $t) > $tolerance) return false;
    $expected = hash_hmac('sha256', $t . '.' . $rawBody, $secret);
    return hash_equals($expected, $parts['v1'] ?? '');
}
// $raw = file_get_contents('php://input');
// verify_svitlopulse($raw, $_SERVER['HTTP_X_SVITLOPULSE_SIGNATURE'] ?? '', getenv('SVITLOPULSE_WEBHOOK_SECRET'));
Python
import hashlib, hmac, time

def verify(raw_body: bytes, header: str, secret: str, tolerance: int = 300) -> bool:
    parts = dict(p.strip().split("=", 1) for p in header.split(",") if "=" in p)
    try:
        t = int(parts.get("t", "0"))
    except ValueError:
        return False
    if not t or abs(time.time() - t) > tolerance:
        return False
    expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts.get("v1", ""))

Як отримати ключ

Напишіть на contact@svitlopulse.example і розкажіть:

  • що ви будуєте і чи публічний проєкт (посилання);
  • для яких областей чи черг потрібні дані;
  • орієнтовне навантаження: запитів на секунду й на добу;
  • для чого потрібні вебхуки (їх ви реєструєте самі після отримання ключа).

Адреса-заглушка. Домен svitlopulse.example не існує: оператор сервісу має замінити цю адресу на робочу перед запуском.

Чесні застереження

  • Первинні дані належать операторам системи розподілу. Перш ніж видавати ключі для комерційного використання, потрібно узгодити ліцензування цих даних: без цього ключі видаємо лише для некомерційних і дослідницьких задач.
  • Жодного SLA, гарантії безперервності чи часу відповіді немає. Дані можуть бути застарілими, неповними чи відсутніми (stale, unknown).
  • Це не офіційне джерело. Не будуйте на ньому автоматичних рішень, де від цього залежить безпека людей.
  • Приватність незмінна: дані агреговані до черги й області, окремих будинків і датчиків немає, і ключ цього не відкриває.
  • Формат може розширюватися новими полями; не покладайтеся на відсутність невідомих полів.