API & webhooks
ScannerScope turns police, fire and EMS radio into structured incidents: AI-written titles, types, urgency, roads, units, a map point, the transcript and the audio. Your tools can pull incidents whenever they like, and get a signed webhook as soon as a matching call has been processed.
/api/v1/changes or /api/v1/events?incident=… for updates as more radio traffic is processed.Freshness: incidents normally become available a few minutes after the radio traffic. If the live recorder is interrupted, ScannerScope falls back to Broadcastify's archives, which run 10 to 60 minutes behind, and fills the gap. Every response says how far processing has got (processed_through, lag_minutes).
Authentication
Send your key as a bearer token. Keys are issued by the ScannerScope admin, and each one can be limited to certain feeds and a request rate.
curl -H "Authorization: Bearer ss_your_key" \
"https://scannerscope.com/api/v1/events?feed=pompton-lakes-riverdale-nj-police-fire-ems&minutes=60" HTTP Basic also works, with the key as the password (curl -u api:ss_your_key), as do X-API-Key: ss_… and ?key=ss_….
Authenticated responses are the master record: full addresses, complete summaries, unedited transcripts and exact map points, including anything the public pages leave out. Every incident carries sensitive (true or false), sensitive_reasons (such as medical, juvenile, home_address) and restricted_details (such as name, plate). Filtering what your readers see is your application's job; sensitive=only or sensitive=exclude narrows /events and /changes. near=41.13,-74.36,2 on /events returns located incidents within 2 km of a point.
Errors come back as {"ok": false, "error": "…"} with 401 (bad or missing key), 404, 429 (rate limit; see Retry-After) or 400.
Endpoints
| GET | Returns |
|---|---|
/api/v1/feeds | The feeds your key can read, live Broadcastify links, available dates and freshness. |
/api/v1/events | Radio events (one dispatch or exchange each) for a time window, filtered. |
/api/v1/events/{feed}/{id} | One event with its full line-by-line transcript and the other updates on the same incident. |
/api/v1/live | The “right now” summary for each feed and its latest activity. |
/api/v1/changes | Everything created or changed since your last call, for tools that poll instead of receiving webhooks. |
/api/v1/stats | Counts over any period, across all history, grouped however you need. |
/api/v1/reports | Written hourly, daily and weekly updates with counts and top incidents. |
Time windows for /events
| Parameter | Meaning |
|---|---|
minutes=N | The last N minutes. |
since=T | From T until now. T is unix seconds, an ISO datetime, or YYYY-MM-DD. |
date=YYYY-MM-DD | One local day in the feed's timezone. |
from=T&to=T | Any period of up to 31 days. A bare date in to includes that whole day. |
| (none) | The last 24 hours. |
Filters & shape
feed=a,b | One or more feed slugs (default: every feed your key can read). |
type=fire,medical | Incident types: fire, fire_alarm, medical, accident, pursuit, traffic_stop, welfare_check, disturbance, suspicious, burglary, hazard, animal, assist, routine, other. |
urgency=urgent | Minimum urgency: routine, elevated, urgent, emergency. |
min_score=6 | Minimum public-interest score (1–10). 4+ is notable, 6+ significant, 8+ major. |
q=smoke+entrapment | Every word must appear in the title, summary, transcript, locations or units. |
road=Kanouse Road | Mentions a road; tolerant of “Rd”, “Rt 23” and similar. |
status=open | Events still receiving radio traffic (open) or finished (closed). |
incident=ID | Every update on one incident. |
group=incident | Return incidents, each with its events, instead of a flat list. |
include=lines · compact=1 | Add timestamped transcript lines · drop transcripts. |
order=asc · limit=100 | Sort by start time (default newest first) · up to 1000. |
An event
{
"id": "2026-10-30-142317",
"feed": "pompton-lakes-riverdale-nj-police-fire-ems",
"incident_id": "2026-10-30-141902",
"status": "closed",
"title": "Gas spill on Route 23 near Echo Lake Road",
"incident_type": "hazard",
"urgency": "urgent",
"score": 7,
"summary": "Units report a fuel spill across both lanes…",
"units": ["Engine 5", "Car 12"],
"locations": ["Route 23 & Echo Lake Road"],
"start": "2026-10-30T14:23:17-04:00", "end": "2026-10-30T14:26:02-04:00",
"start_ts": 1793384597, "end_ts": 1793384762,
"geo": {"lat": 41.0912, "lon": -74.3712, "precision": "intersection", "label": "NJ 23 & Echo Lake Road"},
"transcript": "…",
"links": {
"permalink": "https://scannerscope.com/feed/…/2026-10-30/2026-10-30-142317",
"audio": "https://scannerscope.com/feeds/…/audio/2026-10-30-142317.mp3",
"card": "https://scannerscope.com/feeds/…/og/2026-10-30-142317.png",
"api": "https://scannerscope.com/api/v1/events/…/2026-10-30-142317",
"live": {"listen": "https://www.broadcastify.com/listen/feed/18393", "player": "…", "archives": "…"}
}
} Medical, welfare-check and disturbance calls are only placed to within about 250 m, and the label names the road, not a house number.
Statistics
Ask how many, when and where. There's no 31-day limit here: /api/v1/stats covers everything since March 2026 and usually answers in well under a second.
# How many crashes on Route 23 in the last 90 days, by hour of day?
/api/v1/stats?road=Route%2023&type=accident&days=90&group_by=hour_of_day
# Last week by category, compared with the week before
/api/v1/stats?days=7&group_by=category&compare=previous
# Every month this year, split by category
/api/v1/stats?from=2026-03-01&to=2026-12-31&group_by=month,category
# Busiest roads this month
/api/v1/stats?days=30&group_by=road&limit=10 Windows work like /events, plus days=N (default 30). Filters: feed, type, category (fire, medical, roads, police, other), urgency, min_score, road, unit. group_by takes one or two of hour, day, week, month, hour_of_day, weekday, type, category, urgency, road, unit, feed. Add metric=updates to count radio updates instead of incidents.
Read notes in the response. Days before October 30, 2026 came from an older pipeline that classified incidents differently, so urgency and importance filters leave those days out, and long-range type comparisons should be treated with care.
Hourly, daily and weekly updates
ScannerScope writes an update once each period's audio is fully processed: a headline, a summary, counts by category and urgency, the change from the previous period, and the top incidents. Quiet hours say so. The daily update reuses the daily briefing.
/api/v1/reports?period=hourly&latest=1
/api/v1/reports?period=daily&limit=7
/api/v1/reports?period=weekly&feed=pompton-lakes-riverdale-nj-police-fire-ems The same updates can be pushed to a webhook or Telegram on a schedule. Webhooks receive report.hourly, report.daily or report.weekly with a report object in place of event.
Polling for changes
If your project can't receive webhooks, poll /api/v1/changes every minute or two and send back the cursor from the last response. An event appears again when it grows or is corrected, so key your records on id and overwrite.
GET /api/v1/changes?since=1793843000&min_score=4
{"ok": true, "cursor": 1793843561, "count": 2, "changes": [ … ]}
OpenAPI
A machine-readable description lives at /api/v1/openapi.json, for code generators and AI agents.
Webhooks
Webhooks are configured as destinations in the ScannerScope admin and tied to rules. A rule can match on feed, incident type, score or urgency, keywords (with exclusions), roads, a radius around a point, and days and times. Each incident is sent once per destination. Later radio traffic on the same incident arrives as alert.update, and if the destination opts in, a growing event is re-sent as alert.revised.
POST https://your-agent.example.com/hooks/scanner
Content-Type: application/json
X-ScannerScope-Event: alert.new
X-ScannerScope-Delivery: dlv_3f9a1c…
X-ScannerScope-Signature: t=1793385012,v1=5e1c…
{
"type": "alert.new", // alert.update | alert.revised | test
"delivery_id": "dlv_3f9a1c…",
"sent_at": "2026-10-30T15:10:12-04:00",
"age_minutes": 44.2, // since the radio traffic ended
"rules": [{"id": "rule_…", "name": "Fires", "reasons": ["fire", "score 7"]}],
"feed": {"slug": "…", "name": "…", "live": {"listen": "https://www.broadcastify.com/listen/feed/18393"}},
"event": { …same shape as /api/v1/events… }
} Verifying the signature
The signature is HMAC-SHA256 over "{t}.{raw body}", using the destination's signing secret. Reject requests whose timestamp is more than five minutes old.
import hashlib, hmac, time
def verify(secret: str, header: str, body: bytes, tolerance: int = 300) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
t, sig = int(parts["t"]), parts["v1"]
mac = hmac.new(secret.encode(), f"{t}.".encode() + body, hashlib.sha256).hexdigest()
return hmac.compare_digest(mac, sig) and abs(time.time() - t) < tolerance // Node.js
const crypto = require("crypto");
function verify(secret, header, rawBody) {
const { t, v1 } = Object.fromEntries(header.split(",").map(p => p.split("=")));
const mac = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
return crypto.timingSafeEqual(Buffer.from(mac), Buffer.from(v1)) && Math.abs(Date.now() / 1000 - t) < 300;
} Reply with any 2xx status within 10 seconds. Failed deliveries are retried after 1, 5, 15, 30 and 60 minutes. A 4xx response (other than 429) stops the retries.
Telegram
The ScannerScope bot posts the same alerts to channels and chats: a headline, urgency, location, summary, an expandable transcript, the share card, and buttons to play the audio or open the live feed. Message it with /now, /latest, /search kanouse or /live.
Fair use
ScannerScope summaries are machine-generated from public-safety radio and can be wrong or incomplete. Don't use them to identify or contact private individuals, and don't treat them as an official source. Live audio is provided by Broadcastify; listening requires a Broadcastify account.