API v1.4.1 · Enterprise

Crime data API

Crime incidents for 41 US and Canadian cities in a single, uniform payload — pick a city code, pick a date range, page through the results — plus a crime score for any address or coordinate and 311 service requests for the 27 cities that publish them. Every city returns the same field names with the same meanings, so you integrate once.

Overview

Base URLhttps://www.seecrime.com/api/v1
ProtocolHTTPS only. Requests over plain HTTP are redirected and should be treated as a leaked credential.
AuthAPI key in a request header. Server-to-server only.
MethodsGET only — the API is strictly read-only.
FormatJSON (UTF-8). gzip supported via Accept-Encoding.
Contract version1.4.1 — semver over the response contract, returned on every endpoint as contract_version. The MAJOR always matches the path version: v11.4.1. A minor bump is additive — new endpoint or new field, nothing you built against changed shape.
Payload revision2026-09-03 — the date that contract version shipped. Cite it if a field looks different from what you built against; a support thread usually starts from “it worked last Tuesday”. Latest (2026-08-26, additive): permitted_use on /risk-score, request_id on every error body, and a distinct org_rate_limit_exceeded code.
VersioningThe version is in the path. A breaking change ships as /api/v2; v1 keeps its contract. New fields and new cities can appear in v1 at any time — treat unknown fields as additive and do not pin to field order.

One narrow exception, and we would rather state it than let you discover it: if a field is found to be reporting a value it never measured, we correct it in place rather than carrying the wrong value forward in v1 until a v2 exists. That has happened once — on 2026-08-10 an unscored /risk-score response stopped zero-filling incidents.* and measures.*, which had included reporting trend: "stable" for a point we could not score at all. Those are null now. Payload revision above is how you detect such a change: it moves, and this note says what moved. We will not use this to reshape working fields.

Authentication

Getting a key: API access is part of an Enterprise plan. Create an account or talk to us, and an owner or admin of your organization can then mint keys from this page.

Send your key in a header. Both forms are accepted:

# preferred
Authorization: Bearer sck_xxxxxxxxxxxx_yyyyyyyy…

# equivalent
X-API-Key: sck_xxxxxxxxxxxx_yyyyyyyy…

The key is never accepted in the query string. A ?api_key= parameter would be copied verbatim into access logs, browser history, and every proxy in between — which is how API keys leak in practice. Requests without a header credential get 401 missing_api_key.

Quick start

curl -s "https://www.seecrime.com/api/v1/incidents?city=CHI&start=2026-07-01&end=2026-07-31&limit=5" \
  -H "Authorization: Bearer $SEECRIME_API_KEY"

Response:

{
  "api_version": "v1",
  "meta": {
    "city_key": "CHI",
    "city_name": "Chicago",
    "city_country": "US",
    "start": "2026-07-01",
    "end": "2026-07-31",
    "date_basis": "occurred",
    "timezone": "America/Chicago",
    "limit": 5,
    "returned": 5,
    "has_more": true,
    "next_cursor": "eyJkIjoiMjAyNi0wNy0zMVQxOTowMDowMCIsImkiOiIxMzMzNzExNiJ9",
    "multiple_records_per_incident": false,
    "data_earliest": "2021-01-01",
    "data_latest": "2026-07-30"
  },
  "incidents": [
    {
      "city_key": "CHI",
      "city_name": "Chicago",
      "city_state": "IL",
      "city_timezone": "America/Chicago",
      "record_id": "CHI-13337116",
      "incident_code": "JH123456",
      "incident_date": "2026-07-12T21:00:00",
      "incident_date_basis": "occurred",
      "incident_time_known": true,
      "incident_offense": "Motor Vehicle Theft",
      "incident_offense_code": "240",
      "incident_offense_description": "Motor Vehicle Theft",
      "incident_offense_detail_description": "Motor Vehicle Theft at 050XX N LAKE SHORE DR SB",
      "incident_offense_crime_against": "Property",
      "incident_offense_action": null,
      "incident_offense_severity": 5,
      "incident_offense_is_violent": false,
      "incident_source_name": "Chicago_Police_Department",
      "incident_source_original_type": "MOTOR VEHICLE THEFT - AUTOMOBILE",
      "incident_source_original_code": "0910",
      "incident_latitude": 41.975178,
      "incident_longitude": -87.649961,
      "incident_address": "050XX N LAKE SHORE DR SB",
      "incident_arrest": false,
      "incident_domestic": false,
      "incident_source_fields": {
        "description": "AUTOMOBILE",
        "location_description": "STREET",
        "beat": "2033",
        "district": "020",
        "ward": "48",
        "community_area": "3"
      }
    }
  ]
}

GET /incidents

One page of incidents for one city. city is the only required parameter.

ParameterTypeDefaultNotes
city requiredstringCity code, e.g. CHI. Case-insensitive.
startdateend − 30dYYYY-MM-DD, inclusive.
enddatetoday (UTC)YYYY-MM-DD, inclusive of the whole day.
offensestring listallComma-separated canonical offenses, e.g. Robbery,Burglary. Case-sensitive — copy the value from the catalog verbatim; robbery returns 400 unknown_offense.
crime_againststring listallPersons, Property, Society, Other. Case-insensitive, unlike offense.
violentbooleantrue restricts to violent offenses, false excludes them.
exclude_non_crimebooleanfalseDrop administrative records — see caveats.
bboxstringminLon,minLat,maxLon,maxLat (GeoJSON order).
lat, lon, radius_kmnumbersRadius search; all three required together. Max radius 50 km.
incident_codestringExact match on the agency's incident/case number. Up to 128 characters. Returns 400 unsupported_filter for a city that publishes no case number.
limitinteger1001–1000. Values above 1000 are clamped, not rejected.
cursorstringOpaque. Pass meta.next_cursor verbatim.

Filters combine with AND, and offense, crime_against and violent intersect with each other. A combination with no possible matches returns an empty incidents array — not an error.

GET /risk-score

A crime score for a single point — pass coordinates, or a US street address and we geocode it. Same scorer that powers the risk card in the app, so the two never disagree.

Address lookup is US-only. The geocoder behind address= covers US street addresses; for a covered city outside the US, pass lat and lon directly — the scorer itself works anywhere we hold data. An address we cannot locate returns 422 address_not_found, which is deliberately distinct from 503 geocoder_unavailable.

ParameterTypeNotes
lat + lonnumbersWGS-84. lng is accepted as an alias for lon. Fastest path — no geocoding.
addressstringUp to 200 characters. A full street address with city and state matches best. Ignored when lat/lon are supplied.

Supply either coordinates or an address. The search radius is a fixed ¼ mile (0.4 km) and is not configurable — two addresses are only comparable when the circle is the same size.

curl -s "https://www.seecrime.com/api/v1/risk-score?address=233+S+Wacker+Dr,+Chicago,+IL" \
  -H "Authorization: Bearer $SEECRIME_API_KEY"
{
  "api_version": "v1",
  "query": {
    "lat": 41.878876, "lon": -87.635915,
    "address": "233 S Wacker Dr, Chicago, IL",
    "matched_address": "233 S WACKER DR, CHICAGO, IL, 60606",
    "geocoder": "census",
    "radius_km": 0.4
  },
  "city_key": "CHI", "city_name": "Chicago", "city_state": "IL",
  "scored": true,
  "score": 54, "grade": "C", "label": "Moderate Risk",
  "incidents": {
    "radius_km": 0.4, "total": 128, "violent": 31, "property": 74,
    "last_30_days": 39, "last_90_days": 128,
    "top_offenses": [{"offense": "Larceny/Theft", "count": 58}]
  },
  "measures": {
    "vs_city_average_pct": 18.4, "trend": "stable",
    "night_share_pct": 34, "night_share_coverage": 1.0, "has_night_data": true,
    "arrest_rate_pct": 11, "has_arrest_data": true,
    "has_foot_traffic_data": true
  },
  "coverage": {
    "limitation": null, "area_coverage_pct": 1.0,
    "nearby_incidents": 4412, "nearby_radius_km": 5.0,
    "city_has_recent_data": true, "outside_data_footprint": false
  }
}

Read scored before you read score

score and grade are null whenever scored is false — never 0, and never a grade. A zero would be a number you could average, chart or compare to a threshold, and it would read as safest possible when the truth is we don't know. A point we cannot score still returns HTTP 200: the request was fine, the answer is "no data here".

It is not only score. When scored is false, every measured field is null and top_offenses is [] — nothing under incidents or measures carries information, including trend, which would otherwise have read "stable" for a point in the middle of the ocean. What stays meaningful is coverage (facts about what we know — a nearby_incidents of 0 is the evidence for the verdict, not a fabricated count), label, query.radius_km, the has_*_data capability booleans, and the city_* fields. Branch on scored before you read anything else.

Response fields

FieldTypeDescription
scoredbooleanWhether there was enough data to produce a verdict. Read this first.
score / gradeinteger | null
string | null
0–100 and a letter. null when scored is false.
labelstringThe plain-language verdict that matches the grade — e.g. Very Low Risk, Moderate Risk. Set to No Coverage on an unscored point, so it is meaningful either way. Treat it as display text, not as an enum to branch on: use grade or coverage.limitation for logic.
incidents.radius_kmnumberThe scored circle — an echo of the fixed 0.4 km, not a measurement, so it survives an unscored response.
incidents.totalinteger | nullIncidents in the circle over the trailing 90 days. Incidents, not offense rows — de-duplicated for the cities that file one row per offense, so this agrees with what /incidents returns for the same area.
incidents.violent / .propertyinteger | nullA partition of total, and they sum to it. Violent follows the FBI definition, so robbery counts as violent — see incident_offense_is_violent. property is therefore everything not violent, which is wider than the NIBRS Property group.
incidents.last_30_days / .last_90_daysinteger | nullThe same count over two windows. The 90-day figure is the whole scored window, so it equals total.
incidents.top_offensesarray[{"offense", "count"}], most common first. [] when unscored.
measures.vs_city_average_pctnumber | nullPercentage above (positive) or below (negative) this city's own density baseline, to one decimal. Comparable within a city, not across cities.
measures.trendstring | nullExactly one of rising, falling, stable — recent volume against the earlier part of the window. This is the machine value; do not parse a direction out of any display string.
measures.night_share_pctinteger | nullPercentage of the nearby incidents that occurred overnight (22:00–04:59 local). Measured over the incidents whose hour we could read, not over all of them — which is what the next field tells you. null where no nearby record carried a readable hour.
measures.night_share_coveragenumber | nullRead this alongside night_share_pct. The fraction of the scored incidents the percentage was actually measured over — 1.0 means every one carried a timestamp, 0.08 means eight per cent did. Without it, a feed that timestamps a twelfth of its records produces a night share that looks exactly like a complete one.
measures.has_night_databooleanfalse when there is no time-of-day signal at all. Display a dash, not a zero.
measures.arrest_rate_pctinteger | nullPercentage of the nearby incidents carrying an arrest disposition. Only meaningful where has_arrest_data is true; elsewhere treat it as absent, not as a low clearance rate.
measures.has_arrest_databooleanWhether this city publishes a disposition we are willing to count. true for 4 cities. This is a wider set than incident_arrest's and the two are not interchangeable: a per-record boolean needs a column that means "an arrest was made on this record", while a rate only needs a disposition we can count. Do not infer one from the other.
measures.has_foot_traffic_databooleanThis one affects score itself. The score includes a foot-traffic normalisation worth up to 15 points, derived from open POI and census-population data for the surrounding quarter mile. Those are third-party services we query under a strict outbound budget, so occasionally we cannot get a reading — and when we cannot, we skip the adjustment rather than substitute an average. false therefore means this score omits an input that other scores include, and the difference has been measured at a full letter grade. If you are ranking or thresholding addresses, either filter these out or re-request them later; do not treat a false here as "average foot traffic". We do not publish the index itself — it is an internal heuristic you could not recompute.
coverage.limitationstring | nullThe one field to branch on. See the table below.
coverage.area_coverage_pctnumberGeometric, and explicitly NOT evidence of data coverage. The fraction of the search circle that falls inside the city we matched, so an address on a boundary is not divided by a full circle's worth of expected incidents. A point can sit in the middle of a city — 1.0 here — with no data published anywhere near it. nearby_incidents is what answers that, by counting rows.
coverage.nearby_incidentsintegerHow many incidents exist within the wider nearby_radius_km ring. This is the measurement that separates "a quiet block" from "our data does not reach here".
coverage.nearby_radius_kmnumberThe ring that count was taken over — 5 km.
coverage.city_has_recent_databooleanfalse when we cover this city but its feed has stopped publishing recent records.
coverage.outside_data_footprintbooleantrue when nothing at all was published within the ring.
queryobjectWhat we resolved your request to: lat, lon, radius_km, and for an address lookup address, matched_address and geocoder. Log matched_address — it is how you tell a good score for the wrong building from a good score for the right one.

coverage.limitation

One field to branch on. null means score it as read.

ValuescoredMeaning
outside_covered_citiesfalseThe point is not in any city we cover. Check /cities.
city_feed_not_currentfalseWe cover this city but its source feed has stopped publishing recent data. Not a low-crime finding.
outside_data_footprintfalseInside the city's boundary, but nothing published within 5 km. The city's data does not reach here.
partial_area_coveragetrueScored, but under half the search circle lies inside the city we matched — an edge or boundary address. Treat as indicative.

Zero incidents does not by itself mean no coverage. A genuinely quiet residential block also returns zero, so we additionally count what exists within nearby_radius_km: something nearby and nothing in the circle means quiet, and it still gets a grade. Nothing nearby either means the data does not reach the address, and that is the case we refuse to grade.

Errors specific to this endpoint

StatusCodeMeaning
400invalid_parameterNeither an address nor a complete coordinate pair, or a coordinate out of range.
422address_not_foundWe asked the geocoder and it does not know that address. Fix the address, or send coordinates.
503geocoder_unavailableWe could not reach the geocoder. Your address may be perfectly fine — retry, or send coordinates. Deliberately distinct from address_not_found.
500score_failedThe score could not be computed. Nothing is wrong with your request — retry shortly. Distinct from an unscored 200, which is a real answer meaning "we hold no data here".

Address lookups add roughly 0.2–2 s on a first request and are cached afterwards. If you are scoring the same locations repeatedly, geocode once on your side and pass lat/lon.

The score is calibrated for comparison between addresses, using severity-weighted incident density against the city's own baseline. It is not a prediction, and it is not a statement about any person or property. Do not use it for tenant screening, lending, insurance underwriting, or any other decision about an individual — see the terms.

GET /service-requests

311 service requests — potholes, graffiti, illegal dumping, missed collections — for the 27 of 41 cities that publish them. Same city codes, same pagination, same date semantics as /incidents.

request_type is NOT normalised across cities, and that is the one thing to understand before using this endpoint. Crime has a real cross-city taxonomy behind incident_offense — 311 has no equivalent, so a "Pothole" in one city may be "Street Repair – Pavement" in the next. The field names are uniform; the values are each city's own vocabulary, and meta.request_type_vocabulary says "city_specific" on every response to keep that in front of you. Use /service-request-types to enumerate a city's vocabulary rather than guessing strings. Do not group or sum request_type across cities.

ParameterTypeNotes
city requiredstringCity code. Must be one with 311 coverage.
start / enddateYYYY-MM-DD. Defaults to the last 30 days, on the request's opened date.
typestringExact match on that city's own request_type. Up to 200 characters. Enumerate the vocabulary with /service-request-types.
statusstringExact match. Up to 100 characters. Returns 400 unsupported_filter for the 2 cities that publish no status field at all — meta.publishes_status tells you which case you are in before you filter.
open_onlybooleanOnly requests with no close date. Returns 400 unsupported_filter for the 5 cities that publish no close date, rather than silently returning everything — check meta.publishes_closed_date first.
request_codestringExact match on the city's service-request number. Up to 128 characters.
bbox, lat/lon/radius_kmIdentical to /incidents. Max radius 50 km.
limit, cursorIdentical to /incidents. Max 1000.
curl -s "https://www.seecrime.com/api/v1/service-requests?city=CHI&type=Pothole+in+Street&limit=2" \
  -H "Authorization: Bearer $SEECRIME_API_KEY"
{
  "meta": {
    "city_key": "CHI", "start": "2026-07-11", "end": "2026-08-10",
    "limit": 2, "returned": 2, "has_more": true, "next_cursor": "eyJk…",
    "request_type_vocabulary": "city_specific",
    "publishes_closed_date": true,
    "publishes_status": true
  },
  "service_requests": [
    {
      "city_key": "CHI", "city_name": "Chicago", "city_state": "IL",
      "city_timezone": "America/Chicago",
      "record_id": "CHI-SR26-01234567",
      "request_code": "SR26-01234567",
      "request_type": "Pothole in Street",
      "request_subtype": "PHF",
      "request_status": "Completed",
      "request_is_open": false,
      "request_opened_date": "2026-07-28T08:14:00",
      "request_opened_time_known": true,
      "request_closed_date": "2026-08-04T15:02:00",
      "request_channel": "Phone Call",
      "request_latitude": 41.902117, "request_longitude": -87.663443,
      "request_address": "1600 N WESTERN AVE",
      "request_source_fields": {
        "zip_code": "60647", "ward": "1", "community_area": "24",
        "community_area_name": "WEST TOWN", "police_district": "014"
      }
    }
  ]
}

request_is_open is tri-state

5 of the 27 cities publish no close date at all. For those, request_is_open is nullnot true, because "no close date recorded" and "still open" are different claims and we only know the first. meta.publishes_closed_date tells you which case you are in, and open_only returns 400 unsupported_filter rather than silently returning everything.

Field notes

FieldDescription
record_idGlobally unique (<CITY>-<service request number>). Your primary key.
request_opened_dateISO-8601 local civil time, no offset — same contract as incident_date. Combine with city_timezone.
request_channelHow it was reported (phone / web / app). Meaning is uniform; the value vocabulary is city-specific.
request_source_fieldsCity-specific, not comparable. Dallas and Austin put a department in community_area_name; Phoenix puts a patrol grid there. Read a city's own portal for meaning.

Asking for a city we cover for crime but that publishes no 311 returns 404 no_311_coverage — deliberately different from unknown_city, which means the code itself was wrong.

GET /service-request-types

Every distinct request_type for one city, with counts, most common first. This is how the type filter becomes usable — call it once per city and cache it.

curl -s "https://www.seecrime.com/api/v1/service-request-types?city=CHI" \
  -H "Authorization: Bearer $SEECRIME_API_KEY"
{
  "api_version": "v1",
  "city_key": "CHI", "city_name": "Chicago",
  "count": 96,
  "truncated_at": null,
  "request_types": [
    { "request_type": "Pothole in Street", "count": 184203 },
    { "request_type": "Graffiti Removal",  "count": 151874 }
  ]
}

Capped at 500 types; truncated_at is null unless a city exceeded it, in which case it holds the cap. Asking for a city we cover for crime but that publishes no 311 returns 404 no_311_coverage, the same as /service-requests — a code that is not recognised at all is 400 unknown_city.

GET /cities

Every covered city with its code, timezone, publishing agency, available date range, and per-city capabilities. Call this at startup and cache it — it is how a newly added city reaches your integration without a code change on your side. No parameters.

curl -s "https://www.seecrime.com/api/v1/cities" \
  -H "Authorization: Bearer $SEECRIME_API_KEY"
{
  "api_version": "v1",
  "count": 41,
  "cities": [
    {
      "city_key": "CHI",
      "city_name": "Chicago",
      "city_state": "IL",
      "city_country": "US",
      "city_timezone": "America/Chicago",
      "source_name": "Chicago_Police_Department",
      "date_basis": "occurred",
      "data_earliest": "2021-01-01",
      "data_latest": "2026-07-30",
      "multiple_records_per_incident": false,
      "supports": {
        "date_filter": true,
        "offense_filter": true,
        "spatial_filter": true,
        "arrest_field": true,
        "domestic_field": true
      },
      "has_311_data": true
    }
  ]
}
FieldTypeDescription
countintegerNumber of entries in cities. It is the whole roster — this endpoint does not paginate.
city_keystringThe code you pass as city everywhere else. Stable forever: a published code is never reused or re-pointed at a different city, because it is your primary key.
city_name / city_state / city_timezonestringDisplay name, state/province code (USPS for a US city, Canada Post for a Canadian one), and the IANA zone that this city's incident_date values are expressed in.
city_countrystringISO 3166-1 alpha-2 country code (US or CA). city_state is the state/province code within it.
source_namestringThe publishing agency, underscored — who says so. Matches incident_source_name on every record from this city.
date_basisstringoccurred or reported. Which event the city's feed is keyed on, and therefore what start/end filter against. See caveats.
data_earliest / data_lateststring | nullYYYY-MM-DD spanning everything we hold for that city — not the window of any query you have run. data_latest reflects that agency's own publication lag, so it can sit days or weeks behind today while nothing at all is wrong; it is the authoritative answer to "how current is this city?". null when we currently hold nothing.
multiple_records_per_incidentbooleantrue where the agency files one row per offense, so several records can share an incident_code. Count record_id for offenses, DISTINCT incident_code for incidents. 21 of 41 cities are true.
has_311_databooleanWhether you can call /service-requests and /service-request-types for this city. 27 of 41 are true; the rest return 404 no_311_coverage. Branch on this rather than on a list of your own.
supports.date_filterbooleanWhether start/end are honoured. true everywhere today.
supports.offense_filterbooleanWhether offense, crime_against and violent are honoured. true everywhere today.
supports.spatial_filterbooleanWhether bbox and the lat/lon/radius_km trio are honoured. true everywhere today.
supports.arrest_fieldbooleanWhether incident_arrest carries a real value for this city. True for 1 of 41 — check it before building anything that depends on arrests.
supports.domestic_fieldbooleanWhether incident_domestic carries a real value. True for 2 of 41.

GET /offenses

The complete offense vocabulary — every value incident_offense can take, with its NIBRS code, group, severity weight and violent flag. Static apart from taxonomy releases, and the same 25 values rendered in the catalog below. No parameters.

curl -s "https://www.seecrime.com/api/v1/offenses" \
  -H "Authorization: Bearer $SEECRIME_API_KEY"
{
  "api_version": "v1",
  "count": 25,
  "offenses": [
    {
      "offense": "Robbery",
      "offense_code": "120",
      "crime_against": "Property",
      "severity": 8,
      "is_violent": true,
      "is_crime": true
    },
    {
      "offense": "Non-Crime",
      "offense_code": null,
      "crime_against": "Other",
      "severity": 0,
      "is_violent": false,
      "is_crime": false
    }
  ]
}
FieldTypeDescription
offensestringThe value incident_offense takes, and the value you pass to the offense filter. Case-sensitive there — copy it verbatim. This, not offense_code, is the unique key.
offense_codestring | nullNIBRS code for the representative offense in the roll-up. null for Non-Crime, which is not an offense. Not unique — see the note under the catalog.
crime_againststringNIBRS group: Persons, Property, Society, Other. What the crime_against filter matches.
severityinteger0–10 weight, consistent across cities. See the note under the catalog.
is_violentbooleanFBI violent-crime definition — the same flag as incident_offense_is_violent, and what violent=true selects. Robbery is true despite sitting in the NIBRS Property group.
is_crimebooleanfalse only for Non-Crime, the administrative-record bucket. exclude_non_crime=true on /incidents drops exactly these.

Incident fields

Everything at the top level has the same meaning in every city. Anything whose meaning varies by agency is quarantined inside incident_source_fields.

FieldTypeDescription
city_keystringCity code, e.g. CHI.
city_name / city_statestringDisplay name and state/province code (USPS for a US city, Canada Post for a Canadian one).
city_timezonestringIANA zone that incident_date is expressed in.
record_idstringGlobally unique per row (<CITY>-<source key>). Use this as your primary key and for idempotent upserts.
incident_codestringThe agency's incident/case number. Not unique where an agency files one row per offense — see multiple_records_per_incident.
incident_datestringISO-8601 local civil time, no offset2026-07-12T21:00:00, or 2026-07-12 when the feed publishes no time. Combine with city_timezone to get an absolute instant.
incident_date_basisstringoccurred or reported. Read this before comparing cities.
incident_time_knownbooleanfalse when the agency published a date only. Do not infer midnight.
incident_offensestringCanonical offense — the cross-city classification. This is the field to group and filter on.
incident_offense_codestringNIBRS-aligned code for the canonical offense — ours, derived identically for every city, so it stays cross-city (and cross-border) comparable. Not the agency's own code — that is incident_source_original_code. null for Non-Crime.
incident_offense_descriptionstringHuman-readable form of incident_offense.
incident_offense_detail_descriptionstringOffense plus location, ready to display.
incident_offense_crime_againststringNIBRS group: Persons, Property, Society, Other.
incident_offense_actionstring | nullNIBRS attempted/completed. Tri-state: A where the agency's offense text says attempted, C where we can read a feed that would have said so, and null where this feed gives no readable signal either way. Only 6 of 41 cities can assert C; treating null as completed invents an attempted/completed split.
incident_offense_severityinteger0–10 severity weight, consistent across cities. Ours, not NIBRS. 0 is reserved for Non-Crime — an administrative record carries no offense severity — and every real offense is 1–10. A client asserting 1 <= severity <= 10, or normalising by dividing, breaks on it.
incident_offense_is_violentbooleanFBI violent-crime definition. Note that robbery counts as violent even though NIBRS groups it under Property.
incident_source_namestringPublishing agency.
incident_source_original_typestringThe agency's own offense string, verbatim — what our classification was derived from. In the source language (Montreal's is French, e.g. Vols qualifiés).
incident_source_original_codestringThe agency's own offense code, verbatim. Vocabulary differs per city. For a Canadian city this is a Canadian UCR code, not FBI/NIBRS — not comparable to a US city's; see caveats.
incident_latitude / incident_longitudenumberWGS-84, 6 decimal places, as published. Many agencies publish block-level or offset coordinates — see caveats.
incident_addressstringBlock or intersection as published. null where the agency publishes none.
incident_arrestboolean | nullnull means the agency does not publish it, which is not the same as "no arrest". Populated for 1 of the 41 cities — everywhere else it is null on every record. Check supports.arrest_field on /cities.
incident_domesticboolean | nullSame tri-state rule, and populated for 2 of the 41 cities. Only where the source column genuinely means domestic/family violence. Check supports.domestic_field.
incident_source_fieldsobjectCity-specific, not comparable across cities. Verbatim agency values for description, location_description, beat, district, ward, community_area. What each holds depends on the feed — one city's district is a police district, another's is a ZIP.

incident_arrest and incident_domestic are null on almost every record, and that is the answer, not a gap. An arrest disposition we are willing to stand behind is published by 1 of the 41 agencies; a domestic-violence flag by 2. Read supports.arrest_field and supports.domestic_field from /cities before you build a clearance-rate or domestic-violence feature — otherwise you will ship one that silently reports near-zero nationwide.

Several other feeds do carry something in the equivalent slot, but it is not the same thing: a shooting indicator in one city, a firearm-involved flag in another, a case-resolution string in a third. Publishing those as incident_domestic or incident_arrest would be wrong data under a right-sounding name, so they stay verbatim inside incident_source_fields where their meaning is documented as city-specific.

City codes

41 cities27 also publish 311

CodeCityStateCountryTimezoneDate basis311Rows per incidentSource
ATL Atlanta GA US America/New_York occurred no many Atlanta Police Department
AUS Austin TX US America/Chicago occurred yes one Austin Police Department
BAL Baltimore MD US America/New_York occurred yes many Baltimore Police Department
BOI Boise ID US America/Boise occurred no many Boise Police Department
BOS Boston MA US America/New_York occurred yes one Boston Police Department
BTR Baton Rouge LA US America/Chicago reported yes many Baton Rouge Police Department
CHI Chicago IL US America/Chicago occurred yes one Chicago Police Department
CLE Cleveland OH US America/New_York reported yes many Cleveland Division of Police
CLT Charlotte NC US America/New_York occurred yes one Charlotte-Mecklenburg Police Department
COS Colorado Springs CO US America/Denver reported no many Colorado Springs Police Department
DAL Dallas TX US America/Chicago occurred yes one Dallas Police Department
DC Washington DC DC US America/New_York occurred yes one Metropolitan Police Department DC
DEN Denver CO US America/Denver occurred no many Denver Police Department
DET Detroit MI US America/Detroit occurred yes many Detroit Police Department
EVN Evanston IL US America/Chicago occurred no one Evanston Police Department
HOU Houston TX US America/Chicago occurred yes one Houston Police Department
JAX Jacksonville FL US America/New_York reported no many Jacksonville Sheriffs Office
KCMO Kansas City MO US America/Chicago reported yes many Kansas City Missouri Police Department
LA Los Angeles CA US America/Los_Angeles occurred yes one Los Angeles Police Department
LV Las Vegas NV US America/Los_Angeles reported no many Las Vegas Metropolitan Police Department
MEM Memphis TN US America/Chicago occurred yes one Memphis Police Department
MESA Mesa AZ US America/Phoenix reported no one Mesa Police Department
MKE Milwaukee WI US America/Chicago occurred no one Milwaukee Police Department
MSP Minneapolis MN US America/Chicago occurred yes many Minneapolis Police Department
MTL Montreal QC CA America/Toronto reported yes one Service de police de la Ville de Montreal SPVM
NSH Nashville TN US America/Chicago occurred yes many Metro Nashville Police Department
NYC New York NY US America/New_York occurred yes one New York City Police Department
OAK Oakland CA US America/Los_Angeles occurred yes many Oakland Police Department
OMA Omaha NE US America/Chicago occurred yes many Omaha Police Department
ORL Orlando FL US America/New_York reported no one Orlando Police Department
OTT Ottawa ON CA America/Toronto reported no one Ottawa Police Service
PHL Philadelphia PA US America/New_York reported yes one Philadelphia Police Department
PHX Phoenix AZ US America/Phoenix occurred yes one Phoenix Police Department
RAL Raleigh NC US America/New_York reported yes many Raleigh Police Department
SAC Sacramento CA US America/Los_Angeles occurred yes one Sacramento Police Department
SAN San Diego CA US America/Los_Angeles occurred yes one San Diego Police Department
SEA Seattle WA US America/Los_Angeles occurred yes many Seattle Police Department
SF San Francisco CA US America/Los_Angeles occurred yes many San Francisco Police Department
TOR Toronto ON CA America/Toronto reported no many Toronto Police Service
TPA Tampa FL US America/New_York reported no many Tampa Police Department
TUS Tucson AZ US America/Phoenix reported no many Tucson Police Department

“Rows per incident: many” means the agency files one record per offense, so several rows can share an incident_code. Count record_id for offenses; group by incident_code for incidents.

“311: yes” means /service-requests and /service-request-types work for that code. The same fact is on every /cities entry as has_311_data — read it from there rather than copying this column.

Offense catalog

OffenseNIBRSCrime againstSeverityViolentIs a crime
Animal Cruelty 720 Society 5 no yes
Arson 200 Property 8 yes yes
Assault 13A Persons 7 yes yes
Burglary 220 Property 6 no yes
DUI 90D Society 3 no yes
Disorderly Conduct 90C Society 2 no yes
Drug/Narcotic 35A Society 4 no yes
Fraud/Forgery 26A Property 3 no yes
Gambling 39A Society 1 no yes
Homicide 09A Persons 10 yes yes
Kidnapping 100 Persons 9 yes yes
Larceny/Theft 23H Property 4 no yes
Liquor/Public Order 90G Society 2 no yes
Motor Vehicle Theft 240 Property 5 no yes
Non-Crime Other 0 no no
Offense Against Family 90F Persons 6 no yes
Other 90Z Other 2 no yes
Prostitution 40A Society 2 no yes
Protective Order Violation 90Z Persons 6 no yes
Robbery 120 Property 8 yes yes
Sex Offense 11A Persons 9 yes yes
Stolen Property 280 Property 4 no yes
Trespass 90J Property 3 no yes
Vandalism 290 Property 3 no yes
Weapons 520 Society 7 yes yes

Our offenses are deliberate roll-upsAssault spans NIBRS 13A/13B/13C, Sex Offense spans the 11x/36x family — so the NIBRS code shown is the representative offense for the group, not a claim about every row. When you need the exact offense, read incident_source_original_type. Two offenses share 90Z because NIBRS has no dedicated code for a protective-order violation; incident_offense, not the code, is the unique key.

Severity runs 0–10, not 1–10. 0 is reserved for Non-Crime, because an administrative record is not an offense and carries no offense severity; every real offense is 1–10. So do not assert 1 <= severity <= 10, and do not normalise by dividing by a value that can be 0. Filter with exclude_non_crime=true, or check is_crime, before doing severity arithmetic.

Pagination

Cursor-based, newest first. Follow meta.next_cursor until meta.has_more is false:

cursor = None
while True:
    q = {"city": "CHI", "start": "2026-01-01", "end": "2026-06-30", "limit": 1000}
    if cursor: q["cursor"] = cursor
    r = requests.get(BASE + "/incidents", params=q, headers=H, timeout=30).json()
    yield from r["incidents"]
    if not r["meta"]["has_more"]:
        break
    cursor = r["meta"]["next_cursor"]

Caching

How current the data is

Our ingest sweep starts at 09:00 UTC and works through the cities one at a time. That is a start time, not an instant: the sweep is bounded by a wall-clock budget and rotates where it left off, so a large city can land several hours later, or roll over to the following day's run. Polling more often than hourly will not surface anything new.

data_latest on /cities is the authoritative answer to "how current is this city?" — not the clock. On top of our schedule, every agency publishes on its own cadence and with its own lag; some are a day behind, some are weeks, and at least one feed is frozen upstream. An empty recent window is almost always publication lag rather than an absence of crime, and data_latest is what distinguishes the two. Read it per city instead of assuming a single freshness guarantee across the API.

Rate limits

Three limits apply to every request, on different axes. You need to know about all three, because the one that stops you is not always the one you sized for.

LimitCounted perNotes
120/minute (default)API keyConfigurable per key — ask us if you need more for a bulk load. Reported in the X-RateLimit-* headers below.
600/minuteOrganizationEvery key you hold counts against this together. It is why minting more keys does not buy throughput — an organization may hold up to 20 active keys, and they share this ceiling. Exceeding it returns org_rate_limit_exceeded rather than rate_limit_exceeded, so you can tell the two apart. Remaining budget is reported in X-RateLimit-Org-Remaining.
600/minuteSource IP addressA coarse backstop on every endpoint. Not per key — see the two consequences below.

The per-key limit runs in a fixed 60-second window, and every response tells you where you stand against it:

X-RateLimit-Limit: 120
X-RateLimit-Remaining: 118
X-RateLimit-Reset: 1786000260   # unix seconds

Exceeding it returns 429 rate_limit_exceeded with Retry-After. Honour it — retry with exponential backoff rather than a tight loop.

Two consequences of the per-IP limit

A per-IP 429 carries no Retry-After and no X-RateLimit-* headers. It is produced before your credential is even read, so there is no key to report a budget for. The body is still {"error": "rate_limit_exceeded", …}, so branching on the code works either way — but a client that reads Retry-After unconditionally will find it missing. Treat a 429 with no X-RateLimit-Reset as "back off for the remainder of this minute", then resume with normal exponential backoff.

Errors

Every error is JSON with a stable error code and a human message. Branch on the code, not the message.

{ "error": "unknown_city", "message": "The \"city\" parameter is required and must be a city code. See GET /api/v1/cities for the current list." }
StatusCodeMeaning
400unknown_citycity missing or not a recognised code.
400invalid_date / invalid_date_rangeMalformed date, or start after end.
400invalid_parameterA value is the wrong type or out of range.
400invalid_bboxbbox does not have exactly four comma-separated values. Four values that are not valid numbers are invalid_parameter instead.
400invalid_cursorCursor not recognised. Omit it to restart.
400unknown_offenseValue outside the catalog. offense is case-sensitiverobbery is rejected, Robbery is not. Copy the value from /offenses verbatim.
400unknown_crime_againstNot one of Persons, Property, Society, Other. Unlike offense, this one is case-insensitive.
400unsupported_filterThe filter is real, but this city cannot answer it — status or open_only where the city publishes no status or no close date, or incident_code where it publishes no case number. Not a retry: drop that parameter for this city. meta.publishes_status / meta.publishes_closed_date and /cities let you know in advance.
400city_unavailableAlso emitted as a 400, not only 503: a recognised city whose feed configuration is currently incomplete. If you branch on status alone you will mis-handle one of the two — branch on the code.
401missing_api_keyNo credential header.
401invalid_api_keyUnknown, malformed or revoked key. Deliberately the same body for all three, so it cannot be used to probe which key IDs exist.
401api_key_expiredCorrect key, past its expiry.
404no_311_coverageA city we cover for crime that publishes no 311 data. Distinct from unknown_city on purpose: one means stop asking, the other means you mistyped the code. Check has_311_data on /cities.
429rate_limit_exceededSlow down. See Rate limitsRetry-After is present on a per-key 429 and absent on a per-IP one.
500query_failedWe could not complete it. Narrow the range and retry.
500score_failed/risk-score only. The score could not be computed; your request was fine. Retry shortly.
503city_unavailableThat city is temporarily not queryable.
503filter_unavailableWe could not resolve the offense / crime_against / violent filter for this city right now. The request was valid, and we return this rather than quietly dropping the filter and serving you unfiltered rows under a 200. Retry with backoff.
503service_unavailableWe could not reach the credential store to check your key. This is not a problem with your key. Retry with backoff — do not revoke and re-issue. A client that treats every failure on the auth path as a bad credential will eventually revoke a perfectly good key over a momentary database blip.

Data caveats

These are properties of the source data, not of the API. They are documented because getting them wrong produces analysis that looks fine and is wrong.

  1. Timestamps are local, with no offset. Agencies publish local civil time. We do not stamp a Z on it — that would be wrong by five to eight hours on every row. Localise using city_timezone. 3 cities (Mesa, Phoenix, Tucson) do not observe DST — their IANA zone says so, which is why you should localise from city_timezone rather than from a UTC offset you worked out once.
  2. occurred vs reported is not cosmetic. 15 of 41 cities key their feed on when the report was filed. Comparing those to occurred cities day-by-day will mis-date incidents.
  3. Coverage windows differ per city. Always read data_earliest / data_latest from /cities instead of assuming a range. Some feeds also lag their source by days or weeks, and at least one is frozen upstream — an empty recent window is usually publication lag, not zero crime. See how current the data is.
  4. Some agencies publish administrative records — warrant service, lost property, mental-health transports, found vehicles — in the same dataset as offenses. We classify these as Non-Crime so they are visible rather than silently counted; they exceed 30% of records in a handful of cities. Pass exclude_non_crime=true for crime-only analysis.
  5. Some agencies file one row per offense, not one per incident. 21 of the 41 cities do, so several records legitimately share one incident_code — a single arrest with three charges is three records — and counting rows over-counts incidents. Two places tell you which cities: meta.multiple_records_per_incident on every response, and the Rows per incident column in the City codes table. Count record_id for offenses and DISTINCT incident_code for incidents. This is not a filter you can turn off, deliberately: both views are legitimate, and collapsing the rows server-side would throw away the individual charges — which for many consumers are the point.
  6. null is not zero and not false. A null in incident_arrest means the agency publishes no disposition. Counting nulls as "no arrest" invents a clearance rate — and null is what you will get from all but 1 of the 41 cities. The same applies to incident_domestic and to incident_offense_action.
  7. Coordinates are as published. Many agencies deliberately publish block-level or slightly offset points to protect victim privacy. Do not treat a coordinate as a street address, and be careful with very small radii.
  8. Offense counts are not crime rates. Recording practices, classification and what each agency chooses to publish all differ. incident_offense and incident_offense_severity exist to make cross-city comparison possible; they do not make it automatic.
  9. Canadian cities report under Canadian UCR, not FBI/NIBRS. Montreal, Ottawa, Toronto publish under the Canadian Uniform Crime Reporting survey — a different coding system from the FBI's. incident_offense and incident_offense_code are our canonical classification, derived the same way for every city, so they stay comparable across the border. But a Canadian city's incident_source_original_code (TOR) is a raw Canadian UCR code and is not comparable to a US city's raw code. Group and compare on incident_offense; never on raw incident_source_original_code across countries.

Questions, a higher rate limit, or a field you need that is not here — get in touch. If you are integrating something ambitious, tell us and we will sanity-check the approach.