{"openapi":"3.1.0","info":{"title":"CityAlert API","version":"1.0.0","description":"CityAlert aggregates live public-safety incidents — police open-data feeds, earthquake agencies, wildfire services, weather warnings, disaster alerts, geocoded media reports and community submissions — into one normalized GeoJSON stream. All endpoints on this surface are public and read-only unless noted; community writes (reports, votes, dashboards) are rate-limited by IP. Always attribute CityAlert and the underlying source when citing incidents; defer to official authorities for emergencies.","license":{"name":"CC BY 4.0","url":"https://creativecommons.org/licenses/by/4.0/"},"contact":{"url":"https://cityalert.live/contact"}},"servers":[{"url":"https://cityalert.live"}],"tags":[{"name":"incidents","description":"Normalized live incident stream"},{"name":"meta","description":"Region and source metadata"},{"name":"history","description":"Historical aggregates and year archives"},{"name":"news","description":"Regional safety news"},{"name":"community","description":"User-submitted reports, votes and dashboards"},{"name":"feeds","description":"Syndication feeds"},{"name":"agents","description":"Machine-consumable interfaces"}],"paths":{"/api/incidents":{"get":{"tags":["incidents"],"operationId":"listIncidents","summary":"Live incidents as a merged GeoJSON FeatureCollection","description":"Returns the merged, deduplicated incident stream across all feeds. Default response is GeoJSON; `format=json` returns a plain incident array. `meta.feeds` reports per-source fetch status and counts.","parameters":[{"name":"region","in":"query","description":"Focus-region key (see GET /api/regions)","schema":{"type":"string","enum":["vancouver","vernon","cabo","manila","seattle","sf","la","chicago","austin","nyc","neworleans","cincinnati","kansascity","batonrouge","detroit","charlotte","tampa","rochester","indianapolis","minneapolis","boise","tucson","tempe","henderson","atlanta","lasvegas","dallas","houston","denver","portland","baltimore","nashville","memphis","philadelphia","sandiego","columbus","cleveland","okc","sacramento","slc","dc","georgetown","riverside","irvine","fremont","redwoodcity","delraybeach","gainesville","palmcoast","worcester","provo","richmond","albuquerque","franklintn","longview","lawrenceks","bridgeport","sanantonio","fortworth","phoenix","stlouis","omaha","oakland","toronto","raleigh","halifax","montreal","ottawa","york","peel","edmonton","regina","victoria","abbotsford","winnipeg","calgary","quebeccity","niagara","waterloo","guelph","southsimcoe","stratford","brantford","quebec","saskatoon","surrey","portcoquitlam","coquitlam","langleybc","reddeer","grandeprairie","fortmcmurray","saopaulo","rio","buenosaires","bogota","lima","santiago","medellin","cdmx","guadalajara","monterrey","tijuana","cancun","sydney","melbourne","brisbane","perth","auckland","wellington","tokyo","istanbul","jakarta","israel","cairo","nairobi","capetown","dubai","riyadh","lagos","hongkong","singapore","darwin","adelaide","kualalumpur","manchester","birmingham","leeds","liverpool","bristol","oslo","lisbon","reykjavik","berlin","london","stockholm","amsterdam","munich","prague","ostrava","plzen","olomouc","johannesburg","delhi","karachi","dhaka"]}},{"name":"category","in":"query","schema":{"type":"string","enum":["crime","fire","disaster","weather","unrest","health","infra","report"]}},{"name":"source","in":"query","description":"Feed key, e.g. 'usgs', 'vpd', 'zwaailicht'","schema":{"type":"string"}},{"name":"bbox","in":"query","description":"Bounding box as minLon,minLat,maxLon,maxLat","schema":{"type":"string","pattern":"^-?\\d+(\\.\\d+)?(,-?\\d+(\\.\\d+)?){3}$"}},{"name":"days","in":"query","description":"Only incidents within the last N days (max 365)","schema":{"type":"integer","minimum":1,"maximum":365}},{"name":"id","in":"query","description":"Fetch a single incident by id","schema":{"type":"string"}},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":10000,"default":10000}},{"name":"format","in":"query","description":"'json' returns {incidents, meta}; default is GeoJSON","schema":{"type":"string","enum":["json","geojson"]}},{"name":"refresh","in":"query","description":"'1' forces a synchronous re-fetch of upstream feeds","schema":{"type":"string","enum":["1"]}}],"responses":{"200":{"description":"GeoJSON FeatureCollection (or {incidents, meta} when format=json)","content":{"application/json":{"schema":{"type":"object","properties":{"type":{"type":"string","enum":["FeatureCollection"]},"features":{"type":"array","items":{"type":"object","properties":{"type":{"type":"string","enum":["Feature"]},"geometry":{"type":"object","properties":{"type":{"type":"string","enum":["Point"]},"coordinates":{"type":"array","items":{"type":"number"},"minItems":2,"maxItems":2}}},"properties":{"type":"object","required":["id","source","category","type","title","lat","lon","time","severity"],"properties":{"id":{"type":"string","description":"Stable incident identifier (source-prefixed)"},"source":{"type":"string","description":"Feed key that produced the incident"},"category":{"type":"string","enum":["crime","fire","disaster","weather","unrest","health","infra","report"]},"type":{"type":"string","description":"Source-specific incident type, e.g. 'earthquake' or 'Break and Enter'"},"title":{"type":"string"},"description":{"type":"string"},"lat":{"type":"number"},"lon":{"type":"number"},"time":{"type":"string","format":"date-time","description":"Reported time, ISO 8601"},"severity":{"type":"string","enum":["low","medium","high","critical"]},"url":{"type":"string","format":"uri"},"region":{"type":"string","description":"Focus-region key when the point falls inside one"},"nhood":{"type":"string","description":"Neighbourhood/area label when the feed provides one"},"status":{"type":"string","enum":["pending","active","resolved","hidden"],"description":"Community-report verification state (manual reports only)"}}}}}},"meta":{"type":"object","properties":{"total":{"type":"integer"},"returned":{"type":"integer"},"fetchedAt":{"type":"string","format":"date-time"},"feeds":{"type":"array","items":{"type":"object","properties":{"source":{"type":"string"},"ok":{"type":"boolean"},"count":{"type":"integer"},"error":{"type":"string"},"stale":{"type":"boolean"}}}}}}}}}}},"400":{"description":"Invalid bbox","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"days>7 requires an API key","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Key plan too low","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"429":{"description":"Daily plan limit reached","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/archive":{"get":{"tags":["history"],"operationId":"queryArchive","summary":"Historical incident archive (paid)","description":"Every incident every feed has ever returned, queryable backwards in time — the monetizable surface. Requires `x-api-key` on a pro or business plan; per-plan daily limits apply (pro 5,000/day, business 50,000/day).","security":[{"apiKey":[]}],"parameters":[{"name":"source","in":"query","description":"Feed key, e.g. 'usgs', 'vpd', 'zwaailicht'","schema":{"type":"string"}},{"name":"region","in":"query","description":"Focus-region key (see GET /api/regions)","schema":{"type":"string"}},{"name":"category","in":"query","schema":{"type":"string","enum":["crime","fire","disaster","weather","unrest","health","infra","report"]}},{"name":"bbox","in":"query","description":"Bounding box as minLon,minLat,maxLon,maxLat","schema":{"type":"string","pattern":"^-?\\d+(\\.\\d+)?(,-?\\d+(\\.\\d+)?){3}$"}},{"name":"from","in":"query","description":"ISO 8601 lower bound on the incident's occurred time","schema":{"type":"string","format":"date-time"}},{"name":"to","in":"query","description":"ISO 8601 upper bound on the incident's occurred time","schema":{"type":"string","format":"date-time"}},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":5000,"default":500}}],"responses":{"200":{"description":"Archive rows, newest first","content":{"application/json":{"schema":{"type":"object","properties":{"incidents":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string","description":"Stable incident identifier (source-prefixed)"},"source":{"type":"string","description":"Feed key that produced the incident"},"category":{"type":"string","enum":["crime","fire","disaster","weather","unrest","health","infra","report"]},"type":{"type":"string","description":"Source-specific incident type, e.g. 'earthquake' or 'Break and Enter'"},"title":{"type":"string"},"description":{"type":"string"},"lat":{"type":"number"},"lon":{"type":"number"},"time":{"type":"string","format":"date-time","description":"Reported time, ISO 8601"},"severity":{"type":"string","enum":["low","medium","high","critical"]},"url":{"type":"string","format":"uri"},"region":{"type":"string","description":"Focus-region key when the point falls inside one"},"nhood":{"type":"string","description":"Neighbourhood/area label when the feed provides one"},"status":{"type":"string","enum":["pending","active","resolved","hidden"],"description":"Community-report verification state (manual reports only)"},"occurred":{"type":"string","format":"date-time","description":"When the incident occurred (ISO 8601)"},"first_seen":{"type":"string","format":"date-time"},"last_seen":{"type":"string","format":"date-time"},"times_seen":{"type":"integer"}}}},"count":{"type":"integer"},"from":{"type":["string","null"],"format":"date-time"},"to":{"type":["string","null"],"format":"date-time"}}}}}},"400":{"description":"Invalid bbox or date range","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"401":{"description":"Missing or invalid x-api-key","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Key plan too low (pro/business required)","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"429":{"description":"Daily plan limit reached","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/regions":{"get":{"tags":["meta"],"operationId":"listRegions","summary":"Focus-region list (key, label, center, zoom, bbox)","responses":{"200":{"description":"Region list","content":{"application/json":{"schema":{"type":"object","properties":{"regions":{"type":"array","items":{"type":"object","required":["key","label","center","zoom","bbox"],"properties":{"key":{"type":"string"},"label":{"type":"string"},"center":{"type":"array","items":{"type":"number"},"minItems":2,"maxItems":2},"zoom":{"type":"number"},"bbox":{"type":"array","items":{"type":"number"},"minItems":4,"maxItems":4},"quakeStats":{"type":"boolean"}}}}}}}}}}}},"/api/trends":{"get":{"tags":["history"],"operationId":"getTrends","summary":"Historical aggregates — crime YoY, quakes by region, GDACS by year","responses":{"200":{"description":"Aggregate trend payload","content":{"application/json":{"schema":{"type":"object","properties":{"generatedAt":{"type":"string","format":"date-time"},"crime":{"type":["object","null"]},"history":{"type":"object"},"quakes":{"type":"array","items":{"type":"object"}},"gdacs":{"type":"array","items":{"type":"object"}},"reports":{"type":"object"},"snapshot":{"type":"object"},"errors":{"type":"array","items":{"type":"string"}}}}}}}}}},"/api/crime-year":{"get":{"tags":["history"],"operationId":"getCrimeYear","summary":"Full-year incident GeoJSON + neighbourhood ranking","description":"Returns a full year of incident-level data for regions with history (Vancouver VPD back to 2003; other cities via police portals or the media-scrape log).","parameters":[{"name":"year","in":"query","required":true,"schema":{"type":"integer","minimum":2003}},{"name":"region","in":"query","schema":{"type":"string","default":"vancouver"}}],"responses":{"200":{"description":"Year archive","content":{"application/json":{"schema":{"type":"object","properties":{"year":{"type":"integer"},"region":{"type":"string"},"label":{"type":"string"},"caveat":{"type":"string"},"byNeighbourhood":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string"},"count":{"type":"integer"},"lat":{"type":"number"},"lon":{"type":"number"}}}},"geojson":{"type":"object","properties":{"type":{"type":"string","enum":["FeatureCollection"]},"features":{"type":"array","items":{"type":"object","properties":{"type":{"type":"string","enum":["Feature"]},"geometry":{"type":"object","properties":{"type":{"type":"string","enum":["Point"]},"coordinates":{"type":"array","items":{"type":"number"},"minItems":2,"maxItems":2}}},"properties":{"type":"object","required":["id","source","category","type","title","lat","lon","time","severity"],"properties":{"id":{"type":"string","description":"Stable incident identifier (source-prefixed)"},"source":{"type":"string","description":"Feed key that produced the incident"},"category":{"type":"string","enum":["crime","fire","disaster","weather","unrest","health","infra","report"]},"type":{"type":"string","description":"Source-specific incident type, e.g. 'earthquake' or 'Break and Enter'"},"title":{"type":"string"},"description":{"type":"string"},"lat":{"type":"number"},"lon":{"type":"number"},"time":{"type":"string","format":"date-time","description":"Reported time, ISO 8601"},"severity":{"type":"string","enum":["low","medium","high","critical"]},"url":{"type":"string","format":"uri"},"region":{"type":"string","description":"Focus-region key when the point falls inside one"},"nhood":{"type":"string","description":"Neighbourhood/area label when the feed provides one"},"status":{"type":"string","enum":["pending","active","resolved","hidden"],"description":"Community-report verification state (manual reports only)"}}}}}}}}}}}}},"400":{"description":"Invalid year or region without history","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"404":{"description":"No data for that year","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/news":{"get":{"tags":["news"],"operationId":"listNews","summary":"Regional safety news items","parameters":[{"name":"place","in":"query","description":"Ad-hoc place name for news lookup outside focus regions","schema":{"type":"string"}},{"name":"refresh","in":"query","schema":{"type":"string","enum":["1"]}}],"responses":{"200":{"description":"News items","content":{"application/json":{"schema":{"type":"object","properties":{"items":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string"},"link":{"type":"string","format":"uri"},"source":{"type":"string"},"published":{"type":"string","format":"date-time"},"region":{"type":"string"}}}},"fetchedAt":{"type":"string","format":"date-time"}}}}}}}}},"/api/reports":{"get":{"tags":["community"],"operationId":"listReports","summary":"Community-submitted incident reports","responses":{"200":{"description":"Community reports","content":{"application/json":{"schema":{"type":"object","properties":{"reports":{"type":"array","items":{"type":"object","required":["id","source","category","type","title","lat","lon","time","severity"],"properties":{"id":{"type":"string","description":"Stable incident identifier (source-prefixed)"},"source":{"type":"string","description":"Feed key that produced the incident"},"category":{"type":"string","enum":["crime","fire","disaster","weather","unrest","health","infra","report"]},"type":{"type":"string","description":"Source-specific incident type, e.g. 'earthquake' or 'Break and Enter'"},"title":{"type":"string"},"description":{"type":"string"},"lat":{"type":"number"},"lon":{"type":"number"},"time":{"type":"string","format":"date-time","description":"Reported time, ISO 8601"},"severity":{"type":"string","enum":["low","medium","high","critical"]},"url":{"type":"string","format":"uri"},"region":{"type":"string","description":"Focus-region key when the point falls inside one"},"nhood":{"type":"string","description":"Neighbourhood/area label when the feed provides one"},"status":{"type":"string","enum":["pending","active","resolved","hidden"],"description":"Community-report verification state (manual reports only)"}}}}}}}}}}},"post":{"tags":["community"],"operationId":"createReport","summary":"Submit a community incident report","description":"Rate-limited to 5 reports/hour per IP. New reports start as 'pending' and are confirmed by community votes before appearing as active.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["title","lat","lon"],"properties":{"title":{"type":"string","maxLength":200},"lat":{"type":"number","minimum":-90,"maximum":90},"lon":{"type":"number","minimum":-180,"maximum":180},"category":{"type":"string","enum":["crime","fire","disaster","weather","unrest","health","infra","report"],"default":"report"},"severity":{"type":"string","enum":["low","medium","high","critical"],"default":"medium"},"description":{"type":"string","maxLength":2000}}}}}},"responses":{"201":{"description":"Report created (status 'pending')","content":{"application/json":{"schema":{"type":"object","properties":{"report":{"type":"object","required":["id","source","category","type","title","lat","lon","time","severity"],"properties":{"id":{"type":"string","description":"Stable incident identifier (source-prefixed)"},"source":{"type":"string","description":"Feed key that produced the incident"},"category":{"type":"string","enum":["crime","fire","disaster","weather","unrest","health","infra","report"]},"type":{"type":"string","description":"Source-specific incident type, e.g. 'earthquake' or 'Break and Enter'"},"title":{"type":"string"},"description":{"type":"string"},"lat":{"type":"number"},"lon":{"type":"number"},"time":{"type":"string","format":"date-time","description":"Reported time, ISO 8601"},"severity":{"type":"string","enum":["low","medium","high","critical"]},"url":{"type":"string","format":"uri"},"region":{"type":"string","description":"Focus-region key when the point falls inside one"},"nhood":{"type":"string","description":"Neighbourhood/area label when the feed provides one"},"status":{"type":"string","enum":["pending","active","resolved","hidden"],"description":"Community-report verification state (manual reports only)"}}}}}}}},"400":{"description":"Validation error","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Blocked","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/reports/{id}/vote":{"post":{"tags":["community"],"operationId":"voteReport","summary":"Vote on a community report (confirm / resolve / flag)","description":"Community verification: 3 confirms activates a report, 2 resolves marks it resolved, 3 flags hides it. Rate-limited to 30 votes/hour per IP.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["kind"],"properties":{"kind":{"type":"string","enum":["confirm","resolve","flag"]}}}}}},"responses":{"200":{"description":"Vote recorded","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"Invalid kind or report state","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Blocked","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/dashboards":{"get":{"tags":["community"],"operationId":"getDashboard","summary":"Fetch a shared dashboard config by id","parameters":[{"name":"id","in":"query","required":true,"schema":{"type":"string"}}],"responses":{"200":{"description":"Dashboard config","content":{"application/json":{"schema":{"type":"object"}}}},"404":{"description":"Not found","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"post":{"tags":["community"],"operationId":"createDashboard","summary":"Create a shareable dashboard","description":"Returns {id, token}; the token is the write credential — share the URL, keep the token private.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["config"],"properties":{"name":{"type":"string","maxLength":100},"config":{"type":"object"}}}}}},"responses":{"200":{"description":"Created dashboard","content":{"application/json":{"schema":{"type":"object","properties":{"id":{"type":"string"},"token":{"type":"string"}}}}}},"400":{"description":"Invalid config","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}},"put":{"tags":["community"],"operationId":"updateDashboard","summary":"Update a dashboard (requires its token)","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["id","token","config"],"properties":{"id":{"type":"string"},"token":{"type":"string"},"name":{"type":"string","maxLength":100},"config":{"type":"object"}}}}}},"responses":{"200":{"description":"Updated","content":{"application/json":{"schema":{"type":"object"}}}},"400":{"description":"Missing fields","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Bad token","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/mcp":{"post":{"tags":["agents"],"operationId":"mcpRpc","summary":"MCP (Model Context Protocol) endpoint","description":"JSON-RPC 2.0 MCP server exposing the incident data as tools for AI assistants (initialize, tools/list, tools/call). Connect with any MCP client. Historical tools (get_trends, get_crime_year) require a business-plan x-api-key.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object"}}}},"responses":{"200":{"description":"JSON-RPC response (may be SSE depending on client)"},"401":{"description":"Historical tools require an API key","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"403":{"description":"Historical tools require a business plan","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"429":{"description":"Daily plan limit reached","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/cities/{slug}/feed.xml":{"get":{"tags":["feeds"],"operationId":"cityFeed","summary":"Atom feed of a city's latest incidents","description":"20 most recent incidents for a city landing page, as an Atom feed. Slugs are listed in the sitemap under /cities/.","parameters":[{"name":"slug","in":"path","required":true,"description":"City page slug, e.g. 'vancouver-bc'","schema":{"type":"string"}}],"responses":{"200":{"description":"Atom 1.0 feed","content":{"application/atom+xml":{}}},"404":{"description":"Unknown city slug","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}}}}},"/api/openapi.json":{"get":{"tags":["agents"],"operationId":"openapiSpec","summary":"This OpenAPI document","responses":{"200":{"description":"OpenAPI 3.1 JSON"}}}}},"components":{"securitySchemes":{"apiKey":{"type":"apiKey","in":"header","name":"x-api-key","description":"Paid API key (ca_…) — see /developers for plan tiers"}}}}