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 -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).
| HTTP | code | Meaning |
|---|---|---|
| 401 | invalid_key, revoked_key | The key is unknown or revoked. |
| 429 | rate_limited | The per-second limit was exceeded. Retry-After says how long to wait. |
| 429 | quota_exceeded | The daily quota is used up until X-RateLimit-Reset. |
- Do not exceed quotas: on 429 wait for
Retry-Afterand back off. - For real-time updates use the SSE
/api/v1/streamor webhooks, and ETag: a request withIf-None-Matchreturns 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 -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}.
| Event | When |
|---|---|
outage_started | A queue switched to “power off”. |
power_restored | A queue switched to “power on”. |
schedule_changed | The 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.
X-SvitloPulse-Signature: t=1730000000,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bdHere 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
tdiffers 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.
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
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'));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.