
Pikud Haoref Alerts
- 80 installs
- 2 repo stars
- Updated March 7, 2026
- yaniv-golan/pikud-haoref-alerts
Wire Israel Home Front Command alert data into your app via documented Tzofar relay endpoints when official geo-blocked APIs are unavailable.
About
Pikud Haoref Alerts is a narrow integration skill for solo builders building dashboards, bots, or safety tooling around Israeli rocket alert feeds. It explains how Tzofar’s API relays alert history without the geographic restrictions of the official oref.org.il surface, including concrete GET paths for recent groups and single-group lookup by id. Implementers learn that Python urllib’s default user agent is blocked with 403 while requests or a Mozilla User-Agent succeeds, and that bulk history requires walking ids backward because no bulk export exists. Rate-limit guidance is explicit: burst traffic trips 429 after roughly a dozen quick calls, whereas spaced requests can fetch hundreds of groups in minutes. The skill is inappropriate for general notification infrastructure or builders outside this civil-defense data niche—it is procedural API knowledge for agents coding fetchers, caches, or analyzers.
- Documents Tzofar (tzevaadom.co.il) endpoints usable worldwide without oref.org.il geo blocks
- Requires browser-like User-Agent—default Python urllib gets HTTP 403
- Recent history endpoint returns the last ~50 alert groups without pagination
- Historical backfill via per-id GET iteration with 0.3–0.5s delays to avoid HTTP 429 (~13 rapid requests)
- Contrasts official oref paths with third-party relay behavior for agent implementers
Pikud Haoref Alerts by the numbers
- 80 all-time installs (skills.sh)
- +5 installs in the week ending Jul 27, 2026 (Skillselion tracking)
- Ranked #3,052 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Jul 27, 2026 (Skillselion catalog sync)
npx skills add https://github.com/yaniv-golan/pikud-haoref-alerts --skill pikud-haoref-alertsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 80 |
|---|---|
| repo stars | ★ 2 |
| Last updated | March 7, 2026 |
| Repository | yaniv-golan/pikud-haoref-alerts ↗ |
What it does
Wire Israel Home Front Command alert data into your app via documented Tzofar relay endpoints when official geo-blocked APIs are unavailable.
Files
Pikud HaOref Alert APIs
This skill covers everything you need to know to work with the Pikud HaOref (Israel Home Front Command) alert system — from raw official endpoints to community wrapper libraries and deployment strategies.
For detailed reference material, see the references/ directory:
references/community-libraries.md— Node.js, Python, C#, Docker wrapper librariesreferences/mcp-and-homeassistant.md— MCP server for AI integration, Home Assistant setupsreferences/common-patterns.md— Polling loops, notification bots, dashboards, maps, historical archiving, multi-location monitoring, smart home, accessibilityreferences/alternative-data-sources.md— Tzofar (tzevaadom.co.il) API, oref-to-Tzofar category mapping, community archive projects
Critical constraint: geo-blocking
The official oref.org.il API may block non-Israeli IP addresses. This is the most common source of unexpected 403 errors. The blocking appears to be CDN/Akamai-based and may not apply consistently to all non-Israeli IPs — try your endpoint first before setting up proxy infrastructure. Tzofar (tzevaadom.co.il) endpoints are not geo-blocked and work from any IP.
Workaround if blocked: Deploy on a GCP me-west1 (Tel Aviv) VM. Note: e2-micro is not free-tier eligible in me-west1 (free tier is limited to select US regions), but it is the cheapest option for an Israeli IP.
gcloud compute instances create pikud-haoref \
--zone=me-west1-a \
--machine-type=e2-micro \
--image-family=debian-12 \
--image-project=debian-cloud \
--tags=http-server
gcloud compute firewall-rules create allow-pikud-haoref \
--allow=tcp:8000-8002 --target-tags=http-server---
The official endpoints
Pikud HaOref doesn't publish formal API documentation. The endpoints below are reverse-engineered from the official website and mobile app, and have been stable for years. They return JSON (sometimes with a UTF-8 BOM that needs stripping).
Real-time alerts
GET https://www.oref.org.il/warningMessages/alert/alerts.jsonReturns the currently active alert, or an empty response when no alert is active. Must be polled — typically every 1–2 seconds for real-time responsiveness.
Critical: this is a snapshot, not a status check. Alerts appear on this endpoint only briefly (typically seconds to a minute) while the siren is actively sounding. An empty response does NOT mean "all clear" — it only means no siren is sounding right now. To determine if a situation is still active, you must also check the history endpoint for recent alerts without a matching category 13 "event concluded" message (see Determining if a situation is still active below).
Response when alert is active:
{
"id": "134168709720000000",
"cat": "1",
"title": "ירי רקטות וטילים",
"data": ["תל אביב - מרכז העיר", "רמת גן - מערב"],
"desc": "היכנסו למרחב המוגן ושהו בו 10 דקות"
}Fields:
id— Unique alert identifier (numeric string). Useful for deduplication.cat— Category number (see alert categories below).title— Human-readable alert type in Hebrew.data— Array of affected location names in Hebrew.desc— Protective instructions in Hebrew.
Response when no alert: Empty body (possibly with BOM only).
Recommended headers: The endpoint works without special headers, but sending browser-like headers reduces the chance of being blocked:
Referer: https://www.oref.org.il/
X-Requested-With: XMLHttpRequestAlert history (JSON file — unreliable under load)
GET https://www.oref.org.il/warningMessages/alert/History/AlertsHistory.jsonReturns the most recent alerts, hard-capped at 3,000 records with no pagination. During low-activity periods this may cover weeks; during high-intensity conflicts a single day can exceed 3,000 records (e.g., 1,147 pre-alerts + 483 missile alerts + 22 aircraft + 1,348 concluded = 3,000 exactly). Useful for catching alerts you may have missed between polls, but not reliable for historical analysis during escalation periods.
Degradation under load: During high-volume periods (active conflict), AlertsHistory.json can return HTTP 200 with an empty body (~2 bytes) while GetAlarmsHistory.aspx continues to return full data (600KB+). Prefer `GetAlarmsHistory.aspx` as the primary history source and treat AlertsHistory.json as a fallback.
Important: Paths without the /alert/ segment (e.g., /warningMessages/History/AlertsHistory.json) return 403. The path casing itself is not sensitive — both /WarningMessages/ and /warningMessages/ work — but the /alert/ segment is required.
Response format:
[
{
"alertDate": "2024-10-15 14:32:00",
"title": "ירי רקטות וטילים",
"data": "אשדוד - א,ב,ד,ה",
"category": 1
}
]Note that data here is a string (not an array like the real-time endpoint), and category is a number (not a string).
Alert history (recommended)
GET https://alerts-history.oref.org.il/Shared/Ajax/GetAlarmsHistory.aspx?lang=he&mode=1An ASP.NET endpoint on a dedicated subdomain that returns richer history data including matrix_id, rid, and category_desc fields. Useful as a fallback or when you need the extra metadata. Supports lang=he (Hebrew) and lang=en (English).
Same 3,000-record cap applies. The mode parameter accepts values 1–3 (modes 4–5 return empty), but all modes return the same 3,000 most recent records. No date-range query parameters are supported. This is not a pagination mechanism.
Date format differences between history endpoints:
AlertsHistory.json:"alertDate": "2026-03-06 19:33:53"(space-separated, with seconds)GetAlarmsHistory.aspx:"alertDate": "2026-03-06T19:35:00"(ISO 8601 T-separator, seconds always:00)
Alert categories
GET https://www.oref.org.il/alerts/alertCategories.jsonReturns category metadata as objects with id, category (string slug), matrix_id, priority, and queue fields. Note: the response does not directly map category numbers to Hebrew names — the table below is derived from observing live alert data, not from this endpoint alone. Known categories:
| Category | Type | Hebrew |
|---|---|---|
| 1 | Missiles / Rockets | ירי רקטות וטילים |
| 2 | Hostile aircraft intrusion | חדירת כלי טיס עוין |
| 3 | Earthquake | רעידת אדמה |
| 4 | Tsunami | צונאמי |
| 5 | Radiological event | אירוע רדיולוגי |
| 6 | Hazardous materials | חומרים מסוכנים |
| 7 | Terrorist infiltration | חדירת מחבלים |
| 13 | Event conclusion | האירוע הסתיים |
| 14 | Pre-alert / incoming alerts warning | בדקות הקרובות צפויות להתקבל התרעות באזורך |
Categories 101–107 are the drill equivalents of 1–7.
Category 13: event conclusion
When Pikud HaOref determines the threat to a location has passed, it sends a category 13 alert with the title "האירוע הסתיים" (the event has ended) or "ניתן לצאת מהמרחב המוגן" (you may exit the protected space). This is the authoritative signal that a specific alert event is over. It appears per-location in the history endpoint.
Category 14: pre-alert (incoming alerts warning)
A category 14 alert with the title "בדקות הקרובות צפויות להתקבל התרעות באזורך" warns that alerts are expected in the coming minutes for a given area. This is an early warning that a barrage is incoming — valuable for preparation time beyond the standard time-to-shelter countdown. If a pre-alert doesn't escalate to an actual alert (cat 1–7) within ~20 minutes, it can be considered expired.
Matching pre-alerts to actual alerts
A common analysis task is determining which pre-alerts (cat 14) escalated to actual alerts (cat 1–7). The challenge: location strings don't always match cleanly between categories, and multiple alerts may follow a single pre-alert.
from datetime import timedelta
def match_prealerts_to_alerts(history, source="oref_history", window_minutes=20):
"""Match cat 14 pre-alerts to subsequent cat 1-7 alerts by location overlap.
Uses normalize_alert_time() for proper datetime comparison.
Returns list of (prealert, [matching_alerts]) tuples.
"""
prealerts = [e for e in history if e.get("category") == 14]
alerts = [e for e in history if e.get("category") in (1, 2, 3, 4, 5, 6, 7)]
window = timedelta(minutes=window_minutes)
matches = []
for pa in prealerts:
pa_time = normalize_alert_time(pa["alertDate"], source)
pa_locations = pa.get("data", "")
matched = []
for a in alerts:
a_time = normalize_alert_time(a["alertDate"], source)
if a_time <= pa_time or a_time > pa_time + window:
continue
a_locations = a.get("data", "")
# Token overlap: handles partial location name matches
pa_words = set(pa_locations.replace(",", " ").split())
a_words = set(a_locations.replace(",", " ").split())
if pa_words & a_words:
matched.append(a)
matches.append((pa, matched))
return matchesNotes: Location strings between cat 14 and cat 1 don't always match exactly — cat 14 often uses zone/area names while cat 1 uses specific city names. Substring or token overlap matching works better than exact matching. The 20-minute window reflects the typical pre-alert-to-alert escalation time; adjust based on observed patterns.
Determining if a situation is still active
Do not rely solely on `alerts.json` being empty. The real-time endpoint is a narrow snapshot — alerts disappear within seconds. The correct approach is a dual-check:
1. Check `alerts.json` for the instant snapshot (is a siren sounding right now?) 2. Check the history endpoint for recent cat 1–7 alerts for the location 3. Check if a matching cat 13 "event concluded" has followed the alert for that location 4. If no cat 13 has been received yet → the situation is still active
def is_situation_active(location_hebrew, history):
"""Check if a location still has an active alert (no cat 13 received yet)."""
last_alert_time = None
last_concluded_time = None
for entry in history:
if location_hebrew not in entry.get("data", ""):
continue
cat = entry.get("category", 0)
alert_time = entry.get("alertDate", "")
if cat in (1, 2, 3, 4, 5, 6, 7):
if last_alert_time is None or alert_time > last_alert_time:
last_alert_time = alert_time
elif cat == 13:
if last_concluded_time is None or alert_time > last_concluded_time:
last_concluded_time = alert_time
if last_alert_time is None:
return False # no recent alert
if last_concluded_time is None:
return True # alert with no conclusion yet
return last_alert_time > last_concluded_time # alert after last conclusionHistorical data strategy
The 3,000-record cap on both official history endpoints means you need different sources depending on your time horizon:
| Need | Source | Notes |
|---|---|---|
| Last few minutes | alerts.json (real-time) | Poll every 1–2 seconds |
| Last ~3,000 records (all categories) | AlertsHistory.json or GetAlarmsHistory.aspx | Includes pre-alerts + concluded; hours to weeks depending on activity |
| Last 50 alert groups (no pre-alerts) | api.tzevaadom.co.il/alerts-history | No geo-blocking |
| Months/years (no pre-alerts) | Iterate Tzofar alert group IDs backwards | No geo-blocking; watch rate limits (see references/alternative-data-sources.md) |
| Complete historical with pre-alerts | Your own continuous poller | No retroactive source exists — must be running before the period you need |
Pre-alert historical gap (critical limitation): There is no retroactive source for pre-alert data (cat 14) or event-concluded messages (cat 13). Tzofar excludes them entirely. The official oref history endpoints include them but are capped at 3,000 records. During active conflict, those 3,000 records may cover only hours. If you need historical pre-alert data, you MUST set up your own continuous poller before the period you want to analyze. There is no way to recover this data after the fact. Community archives (hasadna, Meir017) also lack pre-alerts unless their specific scraper captures them.
Combining oref + Tzofar data: For multi-day conflict analysis, you'll typically need both sources. Key alignment points: (1) normalize timestamps — oref uses Israel local time strings, Tzofar uses Unix timestamps (UTC); (2) use the category mapping table in references/alternative-data-sources.md to align threat types; (3) deduplicate by matching on timestamp + city name, since both sources report the same underlying alerts.
from datetime import datetime, timezone
from zoneinfo import ZoneInfo # stdlib in Python 3.9+
IST = ZoneInfo("Asia/Jerusalem") # handles DST automatically
def normalize_alert_time(raw, source):
"""Normalize any alert timestamp to a Python datetime in Israel time.
source: 'oref_history' | 'oref_aspx' | 'tzofar'
"""
if source == "oref_history":
# "2026-03-07 19:33:53" (space-separated, Israel local time)
return datetime.strptime(raw, "%Y-%m-%d %H:%M:%S").replace(tzinfo=IST)
elif source == "oref_aspx":
# "2026-03-07T19:35:00" (ISO T-separator, Israel local time)
return datetime.strptime(raw, "%Y-%m-%dT%H:%M:%S").replace(tzinfo=IST)
elif source == "tzofar":
# Unix timestamp (UTC)
return datetime.fromtimestamp(raw, tz=timezone.utc).astimezone(IST)Oref category to Tzofar threat mapping: When working with both data sources, note that the numeric IDs differ. See the full mapping table in references/alternative-data-sources.md.
---
When to use raw endpoints vs. libraries
Default to raw endpoints. The official JSON files are simple, stable, and well-understood. For most use cases — polling for alerts, checking history, building a notification service — direct HTTP requests are all you need.
Reach for a library when it provides data you'd otherwise have to maintain yourself. The most common reason is the cities.json database — it contains ~1,500 locations with Hebrew/English/Russian/Arabic names, GPS coordinates, zone mappings, and time-to-shelter values. See references/community-libraries.md for details on each library.
---
Location-specific queries
A very common use case is checking whether a specific city or area has an active alert. The challenge is that the official API returns location names in Hebrew, but users often ask in English or transliterated Hebrew.
The cities database
The pikud-haoref-api Node.js library ships a comprehensive cities.json file (available at https://github.com/eladnava/pikud-haoref-api) that maps every alertable location. Each entry has:
{
"id": 511,
"name": "אבו גוש",
"name_en": "Abu Ghosh",
"name_ru": "Абу Гош",
"name_ar": "أبو غوش",
"zone": "שפלת יהודה",
"zone_en": "Judean Lowlands",
"lat": 31.80686,
"lng": 35.11038,
"countdown": 90,
"value": "אבו גוש"
}Key fields: name/name_en for matching, zone/zone_en for regional queries, countdown for time-to-shelter in seconds, lat/lng for proximity and maps, value for matching against alert data array.
Matching strategies
Alert location names in the data array don't always exactly match the city name. A location might appear as "תל אביב - מרכז העיר" rather than just "תל אביב". Three strategies:
1. Substring match — "תל אביב" in "תל אביב - מרכז העיר" → True. Simple, works for most cases. 2. Fuzzy match — Use fuzzywuzzy or similar with threshold ~60-70. Handles transliteration variations. 3. Zone-based match — Match against zone/zone_en for broader regional queries.
English-to-Hebrew name resolution
When a user asks about a city by English name, resolve it to the Hebrew value:
- cities.json lookup — Search
name_en. Most reliable. - Fuzzy English search — "Gan Hayim", "Gan Chaim", "Gan Hayyim" all → גן חיים.
- Multi-language —
name_ruandname_aralso available.
Proximity-based lookups
Using lat/lng coordinates from the cities database:
from math import radians, cos, sin, asin, sqrt
def haversine(lat1, lon1, lat2, lon2):
"""Distance in km between two GPS coordinates."""
lat1, lon1, lat2, lon2 = map(radians, [lat1, lon1, lat2, lon2])
dlat, dlon = lat2 - lat1, lon2 - lon1
a = sin(dlat/2)**2 + cos(lat1) * cos(lat2) * sin(dlon/2)**2
return 2 * 6371 * asin(sqrt(a))
def cities_near(lat, lon, radius_km, cities_db):
return [c for c in cities_db
if haversine(lat, lon, c["lat"], c["lng"]) <= radius_km]Time-to-shelter
Every location has a countdown value (seconds). Ranges from 0 seconds (border communities near Gaza) to 90 seconds. Always include this when reporting location-specific alerts — it's life-safety critical.
---
Examples
Example 1: "Build me a Telegram bot for rocket alerts"
- Explain geo-blocking constraint upfront
- Use the polling loop pattern from
references/common-patterns.md - Add Telegram
sendMessagein theon_new_alertcallback - Suggest GCP me-west1 deployment
- Result: Working bot that sends alerts to a Telegram chat
Example 2: "Is there an alert in Ashkelon right now?"
- Load cities.json — note Ashkelon has subareas: "Ashkelon - North" (
אשקלון - צפון) and "Ashkelon - South" (אשקלון - דרום), each with 30-second countdown - Dual-check: poll
alerts.jsonAND check history for recent cat 1–7 alerts without a matching cat 13 - Substring-match "אשקלון" against the alert
datafields to catch all subareas - Report result including time-to-shelter (30 seconds for Ashkelon)
- If alerts.json is empty but history shows a recent alert with no cat 13 → situation is still active
Example 3: "Set up a live alert map dashboard"
- Server-side: polling loop → fan out via SSE or WebSocket (see
references/common-patterns.md) - Client-side: Leaflet.js + OpenStreetMap, markers from cities.json coordinates
- Enrich markers with zone and countdown data
- Don't have browsers poll oref.org.il directly
Example 4: "Integrate alerts into Home Assistant"
- Point to oref_alert HACS integration (see
references/mcp-and-homeassistant.md) - Auto-configures from HA's home location
- Provides
sensor.oref_alertand_time_to_shelterentities - Common automations: flash lights, lock doors, pause media, announce via speakers
Example 5: "Analyze alert patterns over the last week of conflict"
- First question: do you need pre-alerts (cat 14)? If yes, you need your own poller archive — no retroactive source exists
- For actual threat alerts only: iterate Tzofar alert group IDs backwards with conservative pacing (1–2s delays)
- For the most recent data (if within 3,000 records): use
GetAlarmsHistory.aspxwhich includes all categories - Normalize Tzofar and oref data into a common schema using the category mapping in
references/alternative-data-sources.md - Enrich with cities.json for coordinates and zone data for geographic analysis
---
Gotchas and troubleshooting
1. UTF-8 BOM — The official endpoint sometimes returns a BOM (\ufeff) before the JSON. Always strip it: text.lstrip('\ufeff'). 2. Empty vs. no alert — An empty response body means no active alert. Don't parse it as JSON. 3. History corruption — The history endpoint occasionally returns malformed JSON. Retry after a few seconds. 4. Rate limiting — Poll every 1–2 seconds for real-time. Going faster doesn't help and may get you blocked. 5. Hebrew encoding — All location names and descriptions are in Hebrew. Use UTF-8 throughout your stack. 6. Multiple simultaneous alerts — During heavy barrages, multiple alert types can be active simultaneously. Your code should handle arrays, not assume single alerts. 7. Drill alerts — Categories 101–107 are drills. Filter them unless you specifically want them. 8. 3,000-record history cap — Both AlertsHistory.json and GetAlarmsHistory.aspx are hard-capped at 3,000 records with no pagination or date-range filtering. During the March 2026 conflict, 3,000 records were exhausted in ~97 minutes (854 pre-alerts + 480 missiles + 38 aircraft + 1,628 concluded). The mode=1,2,3 parameter on GetAlarmsHistory.aspx does NOT provide pagination — all modes return the same 3,000 most recent records (mode=4,5 return empty). For deeper history, use Tzofar's archive or a community poller (see references/alternative-data-sources.md). 9. Israel timezone — All oref timestamps are in Israel local time (Asia/Jerusalem — use zoneinfo.ZoneInfo("Asia/Jerusalem") for automatic DST handling). Tzofar uses Unix timestamps (UTC). When combining sources or grouping by day, normalize to a consistent timezone first. 10. 403 Forbidden — Two common causes: (a) geo-blocking — deploy from an Israeli IP (GCP me-west1) or use a proxy, though blocking is not consistent for all non-Israeli IPs; (b) missing /alert/ path segment — e.g., /warningMessages/History/AlertsHistory.json returns 403 while /warningMessages/alert/History/AlertsHistory.json works. Path casing is not sensitive.
Alternative Data Sources
Tzofar (tzevaadom.co.il)
Tzofar is a popular third-party alert relay service. Unlike the official oref.org.il API, Tzofar endpoints are not geo-blocked — they work from any IP worldwide.
Endpoints
Required headers: Tzofar blocks Python's default User-Agent: Python-urllib/X.Y with HTTP 403. Set a browser-like User-Agent:
# Works
requests.get(url) # requests uses its own UA, not blocked
urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
# Fails with 403
urllib.request.urlopen(url) # sends Python-urllib/3.xRecent alert groups (last 50):
GET https://api.tzevaadom.co.il/alerts-historyReturns the last 50 alert groups. No pagination support found.
Single alert group by ID:
GET https://api.tzevaadom.co.il/alerts-history/id/{id}Historical data via ID iteration:
There is no bulk download endpoint. To build historical data, iterate alert group IDs backwards from the latest known ID:
GET https://api.tzevaadom.co.il/alerts-history/id/5913
GET https://api.tzevaadom.co.il/alerts-history/id/5912
GET https://api.tzevaadom.co.il/alerts-history/id/5911
...Rate limiting: Tzofar rate-limits burst traffic — rapid-fire requests with no delay trigger HTTP 429 after ~13 requests. Adding short delays (0.3–0.5 seconds between requests) avoids 429s entirely and allows fetching hundreds of groups in a few minutes.
The /alerts-history endpoint returns the most recent ~50 groups — use the lowest id from that response as your starting point for backward iteration. IDs are mostly sequential but gaps exist — e.g., IDs 5599–5663 all return 404 while surrounding IDs work. When iterating, skip 404s and stop after a tolerance limit (e.g., 100 consecutive 404s). Optimization for large gaps: if you hit 10+ consecutive 404s, try jumping back by 50 IDs to skip past the gap faster, then backfill if needed.
Ready-to-use iteration function (stdlib only):
import urllib.request, json, time
def fetch_tzofar_history(start_id, min_date_unix=0, max_gap=100, delay=0.35):
"""Iterate Tzofar alert group IDs backwards.
Args:
start_id: Highest ID to start from (get from /alerts-history).
min_date_unix: Stop when alerts are older than this Unix timestamp.
max_gap: Stop after this many consecutive 404s.
delay: Seconds between requests (0.3-0.5 avoids 429s).
Returns: List of alert group dicts, newest first.
"""
results = []
consecutive_404s = 0
current_id = start_id
while consecutive_404s < max_gap and current_id > 0:
req = urllib.request.Request(
f"https://api.tzevaadom.co.il/alerts-history/id/{current_id}",
headers={"User-Agent": "Mozilla/5.0"}
)
try:
with urllib.request.urlopen(req, timeout=10) as resp:
group = json.loads(resp.read())
consecutive_404s = 0
# Check if oldest alert in group is before our cutoff
times = [a["time"] for a in group.get("alerts", [])]
if times and min(times) < min_date_unix:
results.append(group)
break
results.append(group)
except urllib.error.HTTPError as e:
if e.code == 404:
consecutive_404s += 1
elif e.code == 429:
time.sleep(2) # back off on rate limit
continue # retry same ID
else:
raise
current_id -= 1
time.sleep(delay)
return resultsExpected throughput: ~200 groups in ~70 seconds at 0.35s pacing. Plan for 2+ minutes when fetching 300+ groups (e.g., a full week of conflict). If running inside an LLM agent with bash timeouts, consider splitting into batches with intermediate saves.
Data model
Tzofar groups alerts into "alert groups." Each group represents an incident (typically a single barrage/volley of incoming rockets). Sub-alerts within a group are waves — successive bursts within the same incident, often seconds apart. The cities array in each sub-alert lists all locations affected by that wave. A group with 3 sub-alerts means 3 waves hit during one incident, potentially affecting different locations each time.
`/alerts-history` response (array of alert groups):
[
{
"id": 5913,
"description": null,
"alerts": [
{
"time": 1772857423,
"cities": ["תל אביב - מרכז העיר", "רמת גן - מערב"],
"threat": 0,
"isDrill": false
}
]
}
]`/alerts-history/id/{id}` response (single alert group, same structure as one array element above).
Key fields:
id— Sequential alert group ID. Can be iterated backwards for historical data.alerts[].time— Unix timestamp of the alert.alerts[].cities— Array of affected location names in Hebrew.alerts[].threat— Threat type number (see mapping table below).alerts[].isDrill— Boolean indicating if this is a drill.description— Usually null; occasionally contains a text description.
Threat type mapping
Tzofar uses its own numeric threat types (different from oref categories):
| Threat | Type | Color |
|---|---|---|
| 0 | Red Alert (Rockets/Missiles) | #FF0000 |
| 1 | Hazardous Materials | #9335ee |
| 2 | Terrorist Infiltration | #FFD500 |
| 3 | Earthquake | #00FF55 |
| 4 | Tsunami | #0080FF |
| 5 | Hostile Aircraft (UAV) | #FF8000 |
| 6 | Non-conventional Missile | #ee35a8 |
| 7 | Radiological | #ee35a8 |
| 8 | General Alert | #FF0000 |
Critical differences from oref
- No pre-alerts — Tzofar does NOT include oref category 14 (pre-alert / incoming alerts warning)
- No event-concluded messages — Tzofar does NOT include oref category 13 (event concluded)
- Only actual threat alerts — You cannot distinguish pre-alerts from alerts or determine when a situation has ended using Tzofar data alone
Oref category to Tzofar threat mapping
| Oref Category | Oref Name | Tzofar Threat | Notes |
|---|---|---|---|
| 1 | Missiles/Rockets | 0 | Core alert |
| 2 | Hostile aircraft | 5 | Numbering differs |
| 3 | Earthquake | 3 | Same |
| 4 | Tsunami | 4 | Same |
| 5 | Radiological | 7 | Numbering differs |
| 6 | Hazardous materials | 1 | Numbering differs |
| 7 | Terrorist infiltration | 2 | Numbering differs |
| 13 | Event concluded | — | Not in Tzofar |
| 14 | Pre-alert | — | Not in Tzofar |
Tzofar location and polygon data
Tzofar tracks city and polygon data versions via https://api.tzevaadom.co.il/lists-versions (returns e.g. {"cities": 10, "polygons": 5}). However, the static download URLs (/static/cities.json, /static/polygons.json) are no longer available (404 as of March 2026). Use the eladnava/pikud-haoref-api cities.json instead.
---
Community archive projects
These projects solve the historical data problem by continuously polling and archiving alerts:
- [hasadna/oref-alarms-history](https://github.com/hasadna/oref-alarms-history) — Scraper scripts by The Public Knowledge Workshop (Israel's civic data org). No downloadable dataset — you run the scraper yourself to collect data.
- [Meir017/oref-data](https://github.com/Meir017/oref-data) — Git-based alert aggregator with a
data.jsonfile (~390KB) containing aggregated alerts. - [Kaggle dataset](https://www.kaggle.com/datasets/sab30226/rocket-alerts-in-israel-made-by-tzeva-adom) — Downloadable dataset of historical alerts
- [idodov/RedAlert](https://github.com/idodov/RedAlert) — Home Assistant integration with file-based archiving
Common Patterns by Use Case
Polling loop — the foundation of everything
Almost every integration starts with this. Poll the real-time endpoint, deduplicate by alert ID, and do something when a new alert arrives. This is the building block for notification bots, dashboards, and monitoring tools.
import requests, time, json
ENDPOINT = "https://www.oref.org.il/warningMessages/alert/alerts.json"
HEADERS = {
"Referer": "https://www.oref.org.il/",
"X-Requested-With": "XMLHttpRequest",
"User-Agent": "Mozilla/5.0"
}
seen_ids = set()
def on_new_alert(alert):
"""Replace this with your notification/dashboard/logging logic."""
print(f"ALERT: {alert['title']} in {', '.join(alert['data'])}")
while True:
try:
resp = requests.get(ENDPOINT, headers=HEADERS, timeout=5)
text = resp.text.lstrip('\ufeff').strip() # strip BOM
if text:
alert = json.loads(text)
if alert.get("id") and alert["id"] not in seen_ids:
seen_ids.add(alert["id"])
on_new_alert(alert)
except Exception as e:
print(f"Poll error: {e}")
time.sleep(2)Notification bots (Telegram, Slack, Discord)
The most common use case. The pattern is: polling loop + message send on alert.
Telegram:
import requests
BOT_TOKEN = "your-bot-token"
CHAT_ID = "your-chat-id"
def on_new_alert(alert):
cities = ", ".join(alert["data"])
text = f"🚨 {alert['title']}\n📍 {cities}\n⚠️ {alert['desc']}"
requests.post(
f"https://api.telegram.org/bot{BOT_TOKEN}/sendMessage",
json={"chat_id": CHAT_ID, "text": text}
)Slack (incoming webhook):
SLACK_WEBHOOK = "https://hooks.slack.com/services/T.../B.../..."
def on_new_alert(alert):
requests.post(SLACK_WEBHOOK, json={
"text": f"🚨 *{alert['title']}*\n{', '.join(alert['data'])}"
})Discord (webhook):
DISCORD_WEBHOOK = "https://discord.com/api/webhooks/..."
def on_new_alert(alert):
requests.post(DISCORD_WEBHOOK, json={
"content": f"🚨 **{alert['title']}**\n{', '.join(alert['data'])}"
})For location-filtered notifications (e.g., "only alert me about rockets near Netanya"), combine with the cities.json lookup and zone matching from the location-specific queries section in SKILL.md.
Real-time dashboards and maps
For a live map showing active alerts:
Architecture: Polling loop on the server → push updates to browser clients via WebSocket or SSE. Don't have each browser client poll the oref.org.il endpoint directly — that's wasteful and will get blocked.
Frontend options:
- Leaflet.js with OpenStreetMap tiles — lightweight, free, good for simple alert markers on a map. Use the
lat/lngfrom cities.json to place markers. - Mapbox GL — smoother rendering, better for heatmaps and polygon overlays. Use the
polygons.jsonfrom the pikud-haoref-api repo for zone boundaries. - Google Maps — works fine, but costs money at scale.
Enrichment with cities.json: When an alert comes in with data: ["אשדוד - א,ב,ד,ה"], look up the city in cities.json to get coordinates, zone, and countdown time. Display the marker at those coordinates with a tooltip showing time-to-shelter.
WebSocket relay for multi-client broadcast: The red-alert-websocket project polls in the background and exposes a WebSocket that multiple browser clients can subscribe to, so you don't hammer the official endpoint.
Historical data analysis
The history endpoint (AlertsHistory.json) is hard-capped at 3,000 records — during low-activity periods this may cover weeks, but during heavy conflicts it can be exhausted in hours. For longer-term analysis, consider iterating Tzofar alert group IDs (see references/alternative-data-sources.md) or build your own archive by continuously polling and storing alerts.
Storage approach:
import sqlite3, json, datetime
def store_alert(alert, db_path="alerts.db"):
conn = sqlite3.connect(db_path)
conn.execute("""CREATE TABLE IF NOT EXISTS alerts (
id TEXT PRIMARY KEY,
timestamp TEXT,
category TEXT,
title TEXT,
locations TEXT,
description TEXT
)""")
conn.execute(
"INSERT OR IGNORE INTO alerts VALUES (?, ?, ?, ?, ?, ?)",
(alert["id"], datetime.datetime.now().isoformat(),
alert["cat"], alert["title"],
json.dumps(alert["data"], ensure_ascii=False), alert["desc"])
)
conn.commit()
conn.close()Analysis patterns people commonly want:
- Alert frequency by city/zone over time (heatmap)
- Peak hours / days of week for alerts
- Average number of simultaneous locations per alert
- Time between successive alerts (escalation detection)
- Category breakdown (rockets vs. drones vs. other)
All of these are straightforward SQL queries once you have the data in a database. Enrich with cities.json to get zone/coordinates for geographic analysis.
Multi-location monitoring
For organizations with offices or personnel in several cities — monitor all locations and route alerts to the right people.
Pattern: Load a config of watched locations (with Hebrew names from cities.json), and when an alert comes in, check which watched locations are affected:
WATCHED = {
"Tel Aviv HQ": ["תל אביב"],
"Haifa R&D": ["חיפה"],
"Beer Sheva warehouse": ["באר שבע"],
}
def on_new_alert(alert):
affected = alert.get("data", [])
for office, hebrew_names in WATCHED.items():
for name in hebrew_names:
if any(name in loc for loc in affected):
notify_team(office, alert)
breakPush vs. poll: SSE and WebSockets
The official API is poll-only. But if you're building a system where multiple consumers need alerts, polling once and fanning out is better than each consumer polling independently.
Options for fan-out:
- SSE (Server-Sent Events): The pikud-a-oref-mcp FastAPI middleware (port 8000) already does this — poll once, publish to
/api/alerts-stream. Clients connect withEventSourcein the browser orhttpx/aiohttpin Python. - WebSocket: The red-alert-websocket project does this. Good for bidirectional communication if you need it (e.g., clients acknowledging receipt).
- Message queue (Redis pub/sub, MQTT): For larger systems where consumers are microservices. Poll → publish to a topic → each service subscribes.
Smart home automation
Beyond the Home Assistant integrations (oref_alert, RedAlert AppDaemon) described in references/mcp-and-homeassistant.md, common automations include:
- Flash smart lights red when alert is active in your area
- Lock smart locks and close smart shutters
- Pause media playback and announce alert via smart speakers
- Send push notification to family members' phones
- Activate security cameras
These all follow the same pattern: polling loop (or HA sensor trigger) → condition check (is my area affected?) → action.
Accessibility
The RedAlerts-For-Hearing-Impaired project uses a Raspberry Pi with vibrating motors and LEDs for physical notification — useful reference for any hardware alert system.
For software accessibility: the alert data is text-based, so it works naturally with screen readers. The main challenge is real-time delivery — a desktop notification or system tray app that announces alerts via text-to-speech is straightforward to build on top of the polling loop.
Multi-language support is important here — many Israeli residents are more comfortable in Russian or Arabic. The cities.json name_ru/name_ar fields and the C# RedAlert library's multi-language support help with this.
Third-party data sources
Besides the official oref.org.il endpoints, there are alternative channels. See references/alternative-data-sources.md for full details including API endpoints, data models, and category mappings.
- Tzofar (tzevaadom.co.il) — A third-party alert relay service with no geo-blocking. Provides a recent alerts API and single-alert lookup by ID (iterate backwards for historical data). Critical caveat: does NOT include pre-alerts (oref cat 14) or event-concluded messages (oref cat 13). The oref_alert Home Assistant integration uses it as one of its data channels.
- Community archive projects — hasadna/oref-alarms-history, Meir017/oref-data, and others continuously poll and archive alerts, solving the 3,000-record cap problem. See
references/alternative-data-sources.mdfor links.
The official oref.org.il endpoint should be treated as the authoritative source for real-time alerting. Tzofar and community archives are valuable for historical analysis and as redundant/fallback sources.
Community Wrapper Libraries
These are useful primarily for their bundled data files and convenience methods. You don't always need to install the full library — sometimes just grabbing cities.json or polygons.json from the repo is enough.
Node.js: pikud-haoref-api
GitHub: https://github.com/eladnava/pikud-haoref-api Install: npm install pikud-haoref-api --save
When it adds value: Its cities.json and polygons.json are the most comprehensive location datasets available — even if you're not using Node.js, you can grab these JSON files directly from the repo. The library itself provides a clean polling interface with proxy support for running outside Israel.
const pikudHaoref = require('pikud-haoref-api');
pikudHaoref.getActiveAlert(function(err, alert) {
if (alert.type === 'missiles') {
console.log('Alert in:', alert.cities.join(', '));
}
});Alert type constants: none, missiles, radiologicalEvent, earthQuake, tsunami, hostileAircraftIntrusion, hazardousMaterials, terroristInfiltration, newsFlash, unknown, plus drill variants like missilesDrill.
Note: earlyWarning was renamed to newsFlash because Pikud HaOref started using the same category for "safe to leave shelter" notifications.
Python: python-red-alert
GitHub: https://github.com/Zontex/python-red-alert Install: pip install requests (it's a single-file module)
When it adds value: Geolocation enrichment — it resolves alert codes to coordinates, city names, shelter times, and area codes. Also useful for generating random coordinates within affected cities for map visualizations. For simple "poll and notify" scripts, raw endpoint access is sufficient and this library is overkill.
Key capabilities:
- Real-time alert retrieval
- Location data from alert codes (lat/lon, city name, time to shelter)
- Random coordinate generation within affected cities (useful for map visualizations)
Response enrichment — each city in an alert includes:
label— Location nameareaid,areaname— Regional identifiersmigun_time— Time to reach shelter (seconds)city_data— Nested object with coordinates and administrative info
C#: RedAlert
GitHub: https://github.com/PwnTheStack/RedAlert
When it adds value: If you're in the .NET ecosystem and need an event-driven interface with continuous background sync. Also has built-in multi-language support (Hebrew, Arabic, Russian, English) and mapping capabilities. If you just need to poll and check alerts, a simple HttpClient call to the raw endpoint is easier.
Docker: orefAlerts
GitHub: https://github.com/dmatik/orefAlerts Docker Hub: dmatik/oref-alerts
When it adds value: When you need a self-hosted proxy that solves the Israeli IP problem — deploy this container on a GCP me-west1 VM, then your app anywhere in the world queries it via simple REST (/current, /last_day). If you're already running from an Israeli IP, you probably don't need this layer. Note: deprecated in favor of oref-alerts-proxy-ms (Java Spring Boot).
MCP Server and Home Assistant Integrations
MCP Server: pikud-a-oref-mcp
GitHub: https://github.com/LeonMelamud/pikud-a-oref-mcp
A full middleware + MCP server stack for AI assistant integration. Uses a publish-subscribe architecture:
1. FastAPI middleware (port 8000) — Polls oref.org.il every 2 seconds, publishes via SSE 2. MCP server (port 8001) — Subscribes to SSE stream, exposes tools for Claude/LLMs 3. SSE gateway (port 8002) — Relays alerts to external clients
MCP Tools
| Tool | Purpose |
|---|---|
check_current_alerts | Active alerts from SSE stream |
get_alert_history | Recent alerts with city/limit filtering |
get_connection_status | System health check |
City filtering supports exact substring matching, fuzzy matching (threshold 60), and multi-city queries. Use Hebrew city names.
FastAPI endpoints
| Endpoint | Auth | Purpose |
|---|---|---|
GET /api/alerts-stream | X-API-Key | SSE stream |
GET /api/alerts/current | Optional | Current alert |
GET /api/alerts/history?city=...&limit=... | Optional | Historical query |
GET /api/alerts/city/{city_name} | Optional | City-specific |
GET /api/alerts/stats | Optional | Aggregate stats |
GET /health | None | Health check |
Deployment (Docker)
git clone https://github.com/LeonMelamud/pikud-a-oref-mcp.git
cd pikud-a-oref-mcp
cp .env.example .env # set API_KEY
make deploy # build + start + health checkMCP client config (Claude Desktop / VSCode / Cursor)
{
"servers": {
"pikud-haoref": {
"type": "http",
"url": "http://localhost:8001/mcp"
}
}
}---
Home Assistant Integrations
Two mature options for smart home alert integration:
oref_alert (HACS)
GitHub: https://github.com/amitfin/oref_alert
Monitors emergency messages via the sensor.oref_alert entity, auto-configured from HA's home location. Also provides _time_to_shelter sensors.
Data channels: website-history, website (real-time), mobile (app notifications), tzevaadom (third-party), synthetic (custom actions). When the same alert arrives on multiple channels, only the first is used.
RedAlert (AppDaemon)
GitHub: https://github.com/idodov/RedAlert
AppDaemon app that monitors multiple hazard types: missiles, unauthorized aircraft, earthquakes, tsunamis, terrorist incursions, chemical emergencies.