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. SendIf-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
Originheader) and set an anonymous HttpOnly cookiesp_dev.
Codes and states
- oblast iso
- ISO 3166-2, for example
UA-46. Each oblast also has aslug. - group
- An operator queue, key
<operator_id>:<group>, for examplelvivoblenergo: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
| Method | Path | What it returns |
|---|---|---|
| GET | /summary | Summary: queues and regions with outages, operator freshness. demo: true means synthetic data. |
| GET | /oblasts | All 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 | /operators | Operators: 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 | /stream | SSE: 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
| Method | Path | What 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&to | State 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&to | The same for one queue. |
| GET | /history/daily/{iso}?days=14 | Per-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
| Method | Path | What it returns |
|---|---|---|
| GET | /posts?scope=&before=&limit= | Discussions. scope: global, oblast:UA-46, place:<slug>, group:<operator>:<queue>. |
| GET | /posts/{id}/replies | Replies to a post. |
| POST | /posts | Create 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}/like | Like toggle. Responds {likes}. |
| POST | /posts/{id}/report | Complaint {reason}. After 3 complaints a post is hidden. |
| POST | /reports | Report {operator_id, group, state: "on"|"off"}. 202. A signal becomes public only after quorum and delay, never on its own. |
| POST | /subscribe-link | A 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”.