SvitloPulse

API

An open JSON API: the same data as on the map. No key and no sign-up.

Basics

  • Base: the same origin, path /api/v1. JSON, UTF-8.
  • Times are RFC 3339: Kyiv offset in responses, UTC in history.
  • Reads are served from an immutable per-revision snapshot: strong ETag and gzip, Cache-Control: public, max-age=15..30. Send If-None-Match, to get 304.
  • Errors: {"error": "…", "code": "machine_code"} with status 400, 403, 404, 422, 429 or 503.
  • Limits: 600 reads per minute per IP (burst 150), 30 writes per minute. Writes require the same origin (the Origin header) and set an anonymous HttpOnly cookie sp_dev.

Codes and states

oblast iso
ISO 3166-2, for example UA-46. Each oblast also has a slug.
group
An operator queue, key <operator_id>:<group>, for example lvivoblenergo:3.2.
group.state
on | off | maybe | unknown. “unknown” must never be drawn as “on”.
group.class
planned | emergency | local | unknown | on
oblast.state
on | off | partial | emergency | unknown. off_share — share of queues off (0..1); stale — operator data older than 45 minutes.
restore_at
The PLANNED restoration time (end of the current off slot). next_at / next_state — the next planned change.

Time without data is reported as unknown_min and is excluded from on_share and avg_off_share.

Live data

MethodPathWhat it returns
GET/summarySummary: queues and regions with outages, operator freshness. demo: true means synthetic data.
GET/oblastsAll 27 units (24 oblasts, Crimea, Kyiv, Sevastopol): state, share of queues off, a stale flag.
GET/oblasts/{slug}One oblast: operators, queues, regimes and its 12 largest settlements.
GET/operatorsOperators: names, number of queues, how many are off, freshness.
GET/groups/{operator}/{group}A queue: state, class, probability, confidence, coverage, planned restoration time, schedules for today and tomorrow.
GET/ics/{operator}/{group}A queue’s planned outages as an iCalendar feed (today, tomorrow and anything already known). Subscribe in Google or Apple Calendar. Plans only, no signals.
GET/places?oblast=UA-46&limit=437 settlements with the state of their queues.
GET/places/{slug}A settlement: its queues and nearby places.
GET/search?q=Search for settlements and known addresses. Not cached.
GET/streamSSE: a snapshot event with the revision number after each recomputation (~30 s), a heartbeat comment every 25 s. On an event, refetch /summary and /oblasts.

History

MethodPathWhat it returns
GET/history/map?at=Oblast states at moment T (RFC 3339 or unix), 5-minute step, up to 400 days back.
GET/history/oblast/{iso}?from&toState segments for an oblast and availability (on/off/maybe/unknown minutes, shares). Window up to 31 days, last 24 h by default.
GET/history/group/{operator}/{group}?from&toThe same for one queue.
GET/history/daily/{iso}?days=14Per-day totals in Kyiv time (for a calendar heatmap).
GET/history/plan/{operator}/{group}?asof=&date=The schedule as the operator had published it at asof.

Discussions, reports, subscriptions

MethodPathWhat it returns
GET/posts?scope=&before=&limit=Discussions. scope: global, oblast:UA-46, place:<slug>, group:<operator>:<queue>.
GET/posts/{id}/repliesReplies to a post.
POST/postsCreate a post {scope, body, parent_id?}. 201 {id}. 422 with code restricted_topic, links_not_allowed, phone_not_allowed, coordinates_not_allowed, abuse, too_short, too_long, no_text. 429 after 5 posts in 10 minutes.
POST/posts/{id}/likeLike toggle. Responds {likes}.
POST/posts/{id}/reportComplaint {reason}. After 3 complaints a post is hidden.
POST/reportsReport {operator_id, group, state: "on"|"off"}. 202. A signal becomes public only after quorum and delay, never on its own.
POST/subscribe-linkA single-use bot link {operator_id, group, place_slug?, label?, address_text?, lead_minutes?[5..180]}. Responds {url, token, expires_in_s, bot_enabled}. Valid for 30 minutes.
GET/bot{username, url, enabled}

Examples

curl

curl -s https://svitlopulse.andrewboyko.name/api/v1/summary
curl -s https://svitlopulse.andrewboyko.name/api/v1/oblasts | jq '.oblasts[] | {slug, state, off_share}'
curl -s "https://svitlopulse.andrewboyko.name/api/v1/history/map?at=2026-11-04T10:00:00Z"

JavaScript

const r = await fetch('/api/v1/oblasts');
const { oblasts } = await r.json();
const off = oblasts.filter((o) => o.state === 'off' || o.state === 'emergency');

// live updates: refetch when the server recomputes (~30 s)
const es = new EventSource('/api/v1/stream');
es.addEventListener('snapshot', () => refresh());

A /summary response excerpt

{
  "demo": false,
  "rev": 1280,
  "at": "2026-11-04T22:11:39+02:00",
  "fact_enabled": true,
  "fact_delay_min": 30,
  "summary": { "groups": 324, "groups_off": 52, "oblasts_affected": 20, "emergency": false },
  "operators_total": 25, "operators_fresh": 23, "oblasts_total": 27
}

The numbers above illustrate the format, not real data.

Privacy rules

  • Public data is aggregated to queue and oblast level.
  • Individual signals appear only from at least 5 distinct sources and with a delay of at least 30 minutes (the fact_delay_min field).
  • There are no substation or power line layers.
  • Discussions reject strike and coordinate content.

Honest caveats

  • This is not an official source and no guarantee of supply. The primary data belongs to the operators.
  • The v1 format may gain new fields. Do not rely on unknown fields being absent.
  • Do not build automatic decisions on it where people’s safety depends on it.
  • Respect the limits. For frequent updates use /stream and ETag, not polling.
  • Need higher quotas or webhooks? There are keys (an X-API-Key or Authorization: Bearer header): For developers. Badges, an iframe card and a Home Assistant config are generated on “Widgets and badges”.