SvitloPulse

For developers

The public API is open and keyless. You need a key only for higher quotas or webhooks instead of polling.

Status: This describes capabilities and is not a commercial offer. There is no SLA or availability guarantee at the moment. Keys are issued by the service operator by hand; there is no self-service or billing in the interface.

Plans

Free

no key
  • The whole public API: /api/v1
  • 600 reads per minute per IP (burst 150)
  • For websites, bots and hobby projects

Pro

1 000 000 / dayindicative
  • A key, up to 50 requests per second
  • Quotas count per key, not per IP
  • Webhooks instead of polling

Business

10 000 000 / dayindicative
  • A key, up to 200 requests per second
  • The same as Pro with higher quotas
  • Terms are agreed individually

The Pro and Business figures are indicative and may change before a key is issued. There are no prices: the service has no invoicing or payment system.

Using a key

Send the key in the X-API-Key header or as Authorization: Bearer. The endpoints are the same as in the API documentation.

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
  • The key is a secret. Keep it on a server only; do not embed it in page JavaScript or a mobile app, where it is easy to extract.
  • Requests from visitors’ browsers need no key: use the keyless API or widgets.
  • The operator can revoke a compromised key: write to us and we will issue a new one. A revoked key gets 401.

Quotas and headers

Responses with a key carry the headers X-RateLimit-Limit, X-RateLimit-Remaining (the daily quota) and X-RateLimit-Reset (the unix time the quota resets).

HTTPcodeMeaning
401invalid_key, revoked_keyThe key is unknown or revoked.
429rate_limitedThe per-second limit was exceeded. Retry-After says how long to wait.
429quota_exceededThe daily quota is used up until X-RateLimit-Reset.
  • Do not exceed quotas: on 429 wait for Retry-After and back off.
  • For real-time updates use the SSE /api/v1/stream or webhooks, and ETag: a request with If-None-Match returns 304.

Webhooks

Instead of polling we send a POST to your HTTPS URL when a queue or region changes state. Webhooks are for key holders only: you register them yourself with the same key.

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"]}
  • Scope: one queue (operator_id + group) or one region (oblast, ISO or slug).
  • The secret whsec_… is returned once if you did not supply your own (16–128 characters). Save it.
  • HTTPS URLs on public hosts only: private, loopback and link-local addresses are rejected. The number of webhooks per key is limited. List and delete: GET /api/v1/webhooks, DELETE /api/v1/webhooks/{id}.
EventWhen
outage_startedA queue switched to “power off”.
power_restoredA queue switched to “power on”.
schedule_changedThe operator changed a queue’s schedule.

Every request carries a signature header and also X-SvitloPulse-Event, X-SvitloPulse-Event-Id (identical on every retry, an idempotency key) and X-SvitloPulse-Attempt.

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

Here t is the unix send time and v1 is the HMAC-SHA256 of the string {t}.{тіло запиту} with your webhook secret, in hex. The exact request body is signed, byte for byte.

The values in the header example illustrate the format and are not a real signature.

The body is JSON: id, type, created_at, scope (group or oblast), operator_id, group, oblast, oblast_slug, data (the same state as /api/v1/status plus previous_state). Events are derived from public data, so a webhook never shows more than the API.

Verifying the signature

  • Compute the HMAC over the raw body, before parsing JSON.
  • Compare in constant time and reject requests where t differs from the current time by more than 5 minutes: this guards against replay.
  • Answer 2xx within 5 seconds; redirects are not followed; move heavy work to a queue.
  • A failed delivery is retried after 1, 5 and 30 minutes (four attempts in all), and a webhook that keeps failing is disabled. Handle events idempotently by 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", ""))

How to request a key

Write to contact@svitlopulse.example and tell us:

  • what you are building and whether the project is public (a link);
  • which regions or queues you need;
  • the expected load: requests per second and per day;
  • what you need webhooks for (you register them yourself once you have the key).

Placeholder address. The domain svitlopulse.example does not exist: the service operator must replace this address with a working one before launch.

Honest caveats

  • The primary data belongs to distribution system operators. Before keys are issued for commercial use, licensing of that upstream data has to be agreed: until then we issue keys only for non-commercial and research uses.
  • There is no SLA and no guarantee of uptime or response time. Data may be stale, partial or missing (stale, unknown).
  • This is not an official source. Do not build automatic decisions on it where people’s safety depends on it.
  • Privacy is unchanged: data is aggregated to queue and region, there are no individual houses or sensors, and a key does not unlock them.
  • The format may gain new fields; do not rely on unknown fields being absent.