Conventions
- Units are in the name.
_mmmillimetres,_km2square kilometres,_mimiles,_mmetres,_mphmiles per hour,_inchesinches. Coordinates are decimal degrees (WGS 84); times are UTC, ISO 8601. - Dates are storm days (12:00 to 12:00 UTC, named for the day they start), written
YYYY-MM-DD. nullversus omitted. A field isnullwhen it belongs to the response but has no value now; it is omitted when it does not apply to this response at all. A field that can benullis marked "nullable" below.- Model probability (
ml_probability,ml_confidence) is 0–1 on the scale of the model generation that scored the row; compare it only with that generation's bar (ml_provenance,methodology.ml_confidence_bars). - Radar size (
mesh_mm, swaththreshold_mm) is radar's uncalibrated estimate of the largest hail aloft.size_categoryis a calibrated class and is empty below the detection threshold: "no estimate", neverpea. - Counts of detections count each radar grid cell once per scan.
- Data version: every response carries
X-Data-Version; see the data changelog.
Outcomes and coverage verdicts
outcome (GET /v1/hail/history)
Values: signal, report_signal, possible_signal, no_signal, insufficient_coverage.
The window-level answer of GET /v1/hail/history (both scopes). READ THIS, NOT count. signal: at least one event: a detection that passes min_confidence (the ML confidence bar) and min_size_mm. report_signal: no event, but a vetted ground hail report (SPC storm reports, NWS Local Storm Reports, CoCoRaHS; see ground_reports) meeting min_size_mm lies inside the radius on a storm day of the window. The evidence is the report, not radar: the radar-only answer is in radar_outcome (typically no_signal or possible_signal). It takes precedence over possible_signal and over any coverage state. Radar can miss hail a person saw: storms far from the nearest radar, where the beam passes kilometres above the hail; storms the hail-detection model scores weak; and reports whose filed time or place is off from the storm. possible_signal: no event, but the radius holds detections below the confidence bar that the hail-detection model still flagged at its own detection threshold (daily_peaks[].below_bar_detection_count > 0). It means "radar saw hail-like echoes the model rates as weak here": worth a look, not a confirmed hail signal, and not a statement of how likely hail was or how large. Measured on a sample of populated point-days over 2022-07..2026-10, a possible_signal window held a ground hail report in the radius about 7% of the time at 5 mi and 6% at 15 mi (small samples: 43 and 66 windows), against 17% and 36% for signal and about 1% for a random point-day. Detections the model did not flag at its detection threshold never count, so no_signal still means nothing the model calls hail was seen. no_signal: neither, AND every day of the window is on record (temporal_coverage.covered: full). insufficient_coverage: neither, but part of the window is not on record; never "no hail". Setting min_confidence at or below the detection threshold leaves no room between the two, so possible_signal cannot occur. signal, possible_signal, no_signal and insufficient_coverage describe the radar record only; ground reports change the answer only to report_signal, never to signal.
outcome (verify, exports)
Values: signal, no_signal, insufficient_coverage.
The window-level answer. READ THIS, NOT events.length. signal: at least one event day. no_signal: nothing found AND every day of the window is on record (temporal_coverage.covered: full): the only state that may be worded as "no hail signal". insufficient_coverage: nothing found, but part of the window is not on record; say which days are missing (temporal_coverage.uncovered_ranges), never "no hail". A window the radar record holds nothing for (temporal_coverage.searchable: false) is answered by the verify grains from ground reports alone when the ground-report archive holds every day of it (temporal_coverage.radar_searched: false, ground_reports_searched: true): signal when a ground report was found (charged), otherwise insufficient_coverage; never no_signal, because radar was not consulted; and that answer is free (X-Credits-Cost: 0). Only a window NEITHER record can be searched for is a 422.
possible_signal versus signal
signal means at least one detection passed the confidence bar and the size floor in the radius. possible_signal means none did, but the model flagged detections in the radius below the bar: radar saw hail-like echoes the model rates as weak. It is a reason to look, not a confirmed hail signal and not a statement of size or likelihood.
Coverage verdicts
temporal_coverage.covered is full, partial or none for the requested window; uncovered_ranges names each missing span and its reason. An empty answer on a window that is not fully covered is insufficient_coverage, never "no hail". The full block is under Temporal coverage.
Export file columns
Bulk exports (POST /v1/exports) are CSV, GeoJSON or Parquet. CSV files open with # comment lines (any CoCoRaHS credit, ml_provenance, data_version) before the header row; GeoJSON and Parquet carry the same blocks as a top-level member and as key-value metadata. An empty CSV cell is a null.
detections
| Column | Type | Unit | Meaning |
|---|---|---|---|
scan_time | string | — | UTC time of the radar scan (ISO 8601). |
storm_date | string | — | Storm date (12:00 to 12:00 UTC, named for the day it starts). |
lat | double | degrees (WGS 84) | Pixel centre latitude, decimal degrees (WGS 84). |
lon | double | degrees (WGS 84) | Pixel centre longitude, decimal degrees (WGS 84). |
geohash5 | string | — | Geohash of the pixel, 5 characters (about 4.9 km cells). |
mesh_mm | double | mm | Radar estimate of the largest hail aloft at the pixel, millimetres (uncalibrated; leans high against ground reports). |
ml_probability | double | — | Hail-detection model probability, 0–1, on the scale of the row's own model generation. Never null in an export. |
model_version | string | — | Public id of the model generation that scored the row (e.g. anvil-detect-6); empty when that generation has no public id. |
detection_strength | string | — | Display tier of ml_probability on the row's own model generation: marginal, moderate or strong. Prefer it over the raw probability, which reads low even for real hail. Empty when the generation cannot be resolved. |
size_category | string | — | Calibrated, model-gated size class (pea … softball). Empty when the row is below the detection threshold: no size estimate, never pea, and never to be back-filled from mesh_mm. |
reports
| Column | Type | Unit | Meaning |
|---|---|---|---|
event_timestamp | string | — | UTC time of the report (ISO 8601). |
storm_date | string | — | Storm date (12:00 to 12:00 UTC). |
lat | double | degrees (WGS 84) | Report latitude, decimal degrees (WGS 84). |
lon | double | degrees (WGS 84) | Report longitude, decimal degrees (WGS 84). |
max_size_inches | double | in | Largest hail reported, inches. |
size_mm | double | mm | max_size_inches in millimetres, one decimal. |
source | string | — | Report source (NWS/SPC storm reports, IEM Local Storm Reports, CoCoRaHS). CoCoRaHS rows carry a CC BY 3.0 credit in the file. |
reporter_source | string | — | Who filed the report, as published (e.g. trained spotter, CO-OP observer). |
measurement_method | string | — | measured or estimated, as published. |
swaths
| Column | Type | Unit | Meaning |
|---|---|---|---|
storm_date | string | — | Storm date (12:00 to 12:00 UTC). |
threshold_mm | double | mm | Tier floor: radar size estimate at least this many millimetres. |
category | string | — | Tier label (same vocabulary as size classes, but a radar-size threshold, not a calibrated size). |
area_km2 | double | km² | Footprint area of the tier, square kilometres, one decimal. |
geometry_geojson | string | — | Tier footprint as a GeoJSON polygon string (CSV/Parquet); the GeoJSON format carries it as the feature geometry. |
Endpoint fields
Every field of each licensed endpoint's success response, as the API documentation defines it. Open an endpoint to see its fields.
GET /v1/hail/swaths Hail event-day footprint polygons, banded by size tier 39 fields
| Field | Type | Unit | Meaning |
|---|---|---|---|
date | string | — | Storm date the answer is for (YYYY-MM-DD; a storm day runs 12:00 to 12:00 UTC). |
query | object | — | The request parameters as the server read them. |
query.bbox | object | — | Bounding box as minLon,minLat,maxLon,maxLat, or null. |
query.state | string | — | Two-letter US state code, or null. |
query.min_size_mm | number | mm | Smallest swath tier requested, millimetres. |
query.simplify_m | number | m | Polygon simplification tolerance applied, metres. |
active | boolean | — | True while the storm day is still open (it closes at 12:00 UTC the next day); counts can still grow. |
outcome | string | — | The window-level answer. READ THIS, NOT events.length. signal: at least one event day. no_signal: nothing found AND every day of the window is on record (temporal_coverage.covered: full): the only state that may be worded as "no hail signal". insufficient_coverage: nothing found, but part of the window is not on record; say which days are missing (temporal_coverage.uncovered_ranges), never "no hail". A window the radar record holds nothing for (temporal_coverage.searchable: false) is answered by the verify grains from ground reports alone when the ground-report archive holds every day of it (temporal_coverage.radar_searched: false, ground_reports_searched: true): signal when a ground report was found (charged), otherwise insufficient_coverage; never no_signal, because radar was not consulted; and that answer is free (X-Credits-Cost: 0). Only a window NEITHER record can be searched for is a 422. One of: signal, no_signal, insufficient_coverage. |
temporal_coverage | object | — | See Temporal coverage. |
ml_provenance | object · nullable | — | See Model provenance. |
n_detections | integer | — | Radar hail detections counted once per scan and pixel location. |
max_size_mm | number | mm | Radar-derived MESH (Maximum Estimated Size of Hail), in millimetres; a size inferred from reflectivity ALOFT, in the storm above the ground, not a measurement of a stone that landed. Radar and ground figures can legitimately differ for the same storm. The response also carries size_basis, which states this machine-readably. |
data_as_of | string | — | The radar hail record's watermark: the newest provably complete day (temporal_coverage.data_through). Never the requested date echoed back. |
tiers | array | — | One polygon per size tier that has any area. |
tiers[].threshold_mm | number | mm | Tier floor: the radar size estimate (day maximum within 2.5 km) is at least this many millimetres. |
tiers[].category | string | — | Size class name for the tier (nickel 19 mm, quarter 25, golf_ball 38, tennis_ball 51, baseball 64, softball 76). One of: nickel, quarter, golf_ball, tennis_ball, baseball, softball. |
tiers[].area_km2 | number | km² | Area, square kilometres. |
tiers[].geometry | object | — | GeoJSON MultiPolygon. |
geometry_validity_note | string | — | How polygon validity was ensured (simplification and repair applied). |
size_basis | object | — | What a GET /v1/hail/swaths tier is: a threshold on the uncalibrated radar size field, with the measured agreement against ground reports. |
size_basis.basis | string | — | Which size field the tiers threshold: the uncalibrated radar estimate, not the calibrated size class. |
size_basis.quantity | string | — | The quantity the tiers threshold: the uncalibrated radar hail-size estimate. |
size_basis.units | string | — | Units of quantity (mm). One of: mm. |
size_basis.calibrated | boolean | — | Always false for swath tiers. |
size_basis.tier_semantics | string | — | What a tier means, as a sentence you can print beside it. |
size_basis.compare_with | string | — | How the tiers relate to size_category on the other hail endpoints. |
size_basis.tier_agreement | object | — | Swath tier at each deduplicated SPC/IEM ground report (>= 0.75 in) compared with the reported size class, over window. Fractions of n_reports; mean_bias_mm is radar minus report. |
size_basis.tier_agreement.method | string | — | How agreement with ground reports was measured. |
size_basis.tier_agreement.window | object | — | Storm dates the agreement was measured over. |
size_basis.tier_agreement.window.start | string | — | First day of the range (YYYY-MM-DD, inclusive). |
size_basis.tier_agreement.window.end | string | — | Last day of the range (YYYY-MM-DD, inclusive). |
size_basis.tier_agreement.n_reports | integer | — | De-duplicated ground hail reports (≥0.75 in) scored. |
size_basis.tier_agreement.exact | number | — | Fraction of n_reports whose swath tier equals the reported size class. |
size_basis.tier_agreement.over | number | — | Fraction of n_reports whose swath tier is above the reported size class. |
size_basis.tier_agreement.under | number | — | Fraction of n_reports whose swath tier is below the reported size class. |
size_basis.tier_agreement.within_one_tier | number | — | Fraction of n_reports within one size class of the report. |
size_basis.tier_agreement.mean_bias_mm | number | mm | Mean of radar minus reported size, millimetres (positive = radar higher). |
coverage | object | — | See Service-area coverage. |
spatial_clip | object | — | See Spatial clip. |
GET /v1/hail/history Radar-detection hail history (short record) 76 fields
| Field | Type | Unit | Meaning |
|---|---|---|---|
coverage | object | — | See Service-area coverage. |
events | array | — | Qualifying hail events in the window: detections passing min_confidence and min_size_mm. Empty is not "no hail": read outcome. |
events[].hail | object | — | The event's hail detail: size, confidence and model generation. |
events[].hail.estimated_size_mm | number | mm | Peak model size estimate (mm) for the cell on that storm day. |
events[].hail.size_category | string · nullable | — | **Calibrated and ML-gated, not raw MESH**, and NOT a bucket of estimated_size_mm: see GET /v1/hail/current's max_size_category for the full semantics. null when the cell is below the hail-detection confidence threshold. Endpoint-specific note: this endpoint reads back up to 365 days, spanning several detection-model generations whose probability scales differ. The size gate is applied PER GENERATION (each cell is gated at the detection threshold the model that scored it actually served under) so the question "did this system assert hail here?" is the same one across the whole window. model_version on this object names the generation. One of: pea, nickel, quarter, golf_ball, tennis_ball, baseball, softball. |
events[].hail.size_method | string · nullable | — | Which method set size_category for this cell: model (the trained size model's median) or radar_band (the empirical radar-to-report size bands). null exactly when size_category is. The same size_category value can come from either method depending on the storm day, so read this before comparing classes across eras. See the *Size method* note in this API's description for the measured era boundary (storm days from 2026-07-01). One of: model, radar_band. |
events[].hail.detection_strength | string · nullable | — | Display tier of confidence, tiered on the model generation that scored it: marginal (cleared the hail-detection threshold), moderate (cleared the default history filter), strong (cleared the bar at which radar alone can confirm hail). null below the detection threshold or when no model probability is available. Prefer this word over the raw probability for display: the probability is a calibrated chance that a 0.75 in+ hail report is filed within 15 km, so it reads low (often 10 to 40%) even for real hail. The bars move with each model promotion; the meaning of each word does not. One of: marginal, moderate, strong. |
events[].hail.detection_strength_rank | integer · nullable | — | detection_strength as 1 (marginal), 2 (moderate) or 3 (strong), for sorting and filtering. Null with it. |
events[].hail.confidence | number · nullable | — | Peak Anvil hail probability for the cell on that storm day: P(hail >= 0.75" / 19.05 mm within 15 km of the cell), the SAME quantity ml_probability / ml_confidence carry elsewhere in this API. It is NOT a measure of how certain the system is that the event happened; a well-calibrated 0.29 means roughly a 29% chance of hail, not "29% sure". SCALE-BOUND across this endpoint's up-to-365-day window: stored probabilities are never rescored, so a value is only comparable to another value from the SAME generation. model_version on this object is the field that names the scale: compare only within it. |
events[].hail.model_version | string · nullable | — | Public identity of the Anvil detect model that produced this cell's confidence, as anvil-detect-{generation}. The generation counter increments once per promotion, so a change here means the probability is on a new model and is NOT comparable to a value carrying a different version. Because stored rows are never rescored, one response covering a long window can legitimately carry several. null when the scoring model has no published public version. |
count | integer | — | Number of events. Read outcome, not this. |
daily_peaks | array | — | One entry per storm day with any radar detection inside radius_km, newest first: that day's largest radar size estimate over EVERY detection in the window and radius that the hail-detection model flagged at its own detection threshold (ENTER rung); no hail output serves a detection below it, so a day's peak never comes from one. It is computed before min_confidence and min_size_mm are applied and is not limited by limit, so it can be larger than every events[].hail.estimated_size_mm for the same day (a day's largest estimate can sit on a detection whose own confidence is under the filter, or in a cell outside the returned rows). Same source, same size field and same admission as POST /v1/verify events[].max_size_mm: for a storm day wholly inside both windows, with verify's 5-mile search area inside radius_km (radius_km >= 8.05), estimated_size_mm here is never smaller than verify's. A storm day cut by the start or end of the window covers only the scans inside it. An estimate somewhere in the radius, never at the queried point. Always present; empty when the window holds no detection. |
daily_peaks[].storm_date | string | — | Storm date (YYYY-MM-DD; a storm day runs 12:00 to 12:00 UTC and is named for the day it starts). |
daily_peaks[].estimated_size_mm | number | mm | Largest radar size estimate (mm, 0.1 mm) among all detections within radius_km that storm day at or above the detection threshold (ENTER rung). A size inferred by radar aloft, not a measured stone: see size_basis. |
daily_peaks[].distance_km | number · nullable | km | Distance from the queried point to the centre of the ~1 km radar grid cell that carried estimated_size_mm. |
daily_peaks[].location | object | — | Centre of that grid cell. |
daily_peaks[].location.lat | number · nullable | degrees (WGS 84) | Latitude, decimal degrees (WGS 84). |
daily_peaks[].location.lon | number · nullable | degrees (WGS 84) | Longitude, decimal degrees (WGS 84). |
daily_peaks[].detection_strength | string · nullable | — | Display tier of confidence, tiered on the model generation that scored it: marginal (cleared the hail-detection threshold), moderate (cleared the default history filter), strong (cleared the bar at which radar alone can confirm hail). null below the detection threshold or when no model probability is available. Prefer this word over the raw probability for display: the probability is a calibrated chance that a 0.75 in+ hail report is filed within 15 km, so it reads low (often 10 to 40%) even for real hail. The bars move with each model promotion; the meaning of each word does not. One of: marginal, moderate, strong. |
daily_peaks[].detection_strength_rank | integer · nullable | — | detection_strength as 1 (marginal), 2 (moderate) or 3 (strong), for sorting and filtering. Null with it. |
daily_peaks[].confidence | number · nullable | — | Anvil hail probability of the detection that carried the peak: the same scale-bound quantity as events[].hail.confidence, for that one detection (not the day's highest confidence). |
daily_peaks[].model_version | string · nullable | — | Public identity of the model confidence is on. |
daily_peaks[].meets_min_confidence | boolean | — | Whether the peak detection itself passes the min_confidence filter applied to events. false means the day's largest estimate is NOT among events. |
daily_peaks[].filtered_estimated_size_mm | number · nullable | mm | Largest estimate that storm day among detections that pass min_confidence, when it also meets min_size_mm; null when none does. This is the most events can show for the day, and it is independent of limit. |
daily_peaks[].detection_count | integer | — | Detection records (radar grid cell x scan) within radius_km that storm day at or above the detection threshold (ENTER rung), before min_confidence and min_size_mm. |
daily_peaks[].below_bar_estimated_size_mm | number · nullable | mm | Largest radar size estimate that storm day among detections that MISS min_confidence but that the hail-detection model still flagged at its own detection threshold (the generation's detect ENTER rung, the gate alerts use), and that meet min_size_mm. null when there are none. A size estimate, not a hail chance. |
daily_peaks[].below_bar_detection_count | integer | — | Detection records counted the same way (radar grid cell x scan). Non-zero on any day with events empty makes outcome possible_signal. |
daily_peaks_basis | string | — | Plain-language statement of what daily_peaks is and how it differs from events, carried in the response. |
temporal_coverage | object | — | See Temporal coverage. |
outcome | string | — | The window-level answer of GET /v1/hail/history (both scopes). READ THIS, NOT count. signal: at least one event: a detection that passes min_confidence (the ML confidence bar) and min_size_mm. report_signal: no event, but a vetted ground hail report (SPC storm reports, NWS Local Storm Reports, CoCoRaHS; see ground_reports) meeting min_size_mm lies inside the radius on a storm day of the window. The evidence is the report, not radar: the radar-only answer is in radar_outcome (typically no_signal or possible_signal). It takes precedence over possible_signal and over any coverage state. Radar can miss hail a person saw: storms far from the nearest radar, where the beam passes kilometres above the hail; storms the hail-detection model scores weak; and reports whose filed time or place is off from the storm. possible_signal: no event, but the radius holds detections below the confidence bar that the hail-detection model still flagged at its own detection threshold (daily_peaks[].below_bar_detection_count > 0). It means "radar saw hail-like echoes the model rates as weak here": worth a look, not a confirmed hail signal, and not a statement of how likely hail was or how large. Measured on a sample of populated point-days over 2022-07..2026-10, a possible_signal window held a ground hail report in the radius about 7% of the time at 5 mi and 6% at 15 mi (small samples: 43 and 66 windows), against 17% and 36% for signal and about 1% for a random point-day. Detections the model did not flag at its detection threshold never count, so no_signal still means nothing the model calls hail was seen. no_signal: neither, AND every day of the window is on record (temporal_coverage.covered: full). insufficient_coverage: neither, but part of the window is not on record; never "no hail". Setting min_confidence at or below the detection threshold leaves no room between the two, so possible_signal cannot occur. signal, possible_signal, no_signal and insufficient_coverage describe the radar record only; ground reports change the answer only to report_signal, never to signal. One of: signal, report_signal, possible_signal, no_signal, insufficient_coverage. |
radar_outcome | string | — | The radar-only answer: outcome computed without ground reports. Equal to outcome unless outcome is report_signal, when it says what the radar record alone shows for the window. One of: signal, possible_signal, no_signal, insufficient_coverage. |
ground_reports | object | — | Vetted ground hail reports inside radius_km on storm days (12Z-12Z) of the window, from SPC storm reports, NWS Local Storm Reports (via IEM) and CoCoRaHS observers; never user-submitted reports. Only reports with a recorded size of more than 0 that meets min_size_mm, never a "no hail" observation, inside the same US clip as the radar answer. A report is a person's observation: its size is the observer's, its time and place are as filed, and it is never a radar measurement. One National Weather Service report relayed by both the SPC and LSR feeds is counted once (sources lists both). |
ground_reports.outcome | string | — | report_signal when at least one vetted report lies in the radius on a storm day of the window, else no_report. About reports only; the window answer is outcome on the response. One of: report_signal, no_report. |
ground_reports.count | integer | — | Distinct reports over the window, after cross-feed duplicates are counted once. |
ground_reports.filed | integer | — | Raw filings over the window, before duplicates are merged. |
ground_reports.max_size_mm | number · nullable | mm | Largest reported hail size over the window, mm. null with no report. |
ground_reports.truncated | boolean | — | true when more than 2,000 filings matched and the rest were not read: counts are then lower bounds. Narrow the window or the radius. |
ground_reports.basis | string | — | Plain-language statement of what ground_reports is, carried in the response. |
ground_reports.days | array | — | One entry per storm day with a report, newest first. |
ground_reports.days[].storm_date | string | — | Storm date (YYYY-MM-DD; a storm day runs 12:00 to 12:00 UTC and is named for the day it starts). |
ground_reports.days[].report_count | integer | — | Distinct reports that day. |
ground_reports.days[].filed_count | integer | — | Raw filings that day. |
ground_reports.days[].max_size_mm | number · nullable | mm | Largest reported hail size that storm day, mm (the observer's, not radar). |
ground_reports.days[].nearest_distance_km | number · nullable | km | Distance from the queried point to the nearest report that day. |
ground_reports.days[].reports | array | — | The day's distinct reports, nearest first, at most 10. |
ground_reports.days[].reports[].size_mm | number · nullable | mm | Reported hail size, mm (the observer's, not radar). |
ground_reports.days[].reports[].size_in | number · nullable | — | The same size in inches. |
ground_reports.days[].reports[].distance_km | number · nullable | km | Distance from the queried point to the report's filed location, km. |
ground_reports.days[].reports[].event_time | string · nullable | — | The time the report was filed for, as filed. |
ground_reports.days[].reports[].location | object | — | The point the answer is for. |
ground_reports.days[].reports[].location.lat | number | degrees (WGS 84) | Latitude, decimal degrees (WGS 84). |
ground_reports.days[].reports[].location.lon | number | degrees (WGS 84) | Longitude, decimal degrees (WGS 84). |
ground_reports.days[].reports[].source | string | — | Warehouse source of the nearest filing (spc_storm_reports, iem_lsr, cocorahs). |
ground_reports.days[].reports[].source_label | string | — | The source in words. |
ground_reports.days[].reports[].sources | array | — | Every feed that carried this report. |
spatial_clip | object | — | See Spatial clip. |
ml_provenance | object · nullable | — | See Model provenance. |
detail | string | — | Detail level of the response (standard or advanced). One of: standard, advanced. |
query | object | — | The parameters actually applied. days is the window's day count whichever way it was given; start_date/end_date are the storm days (12Z-12Z) of an explicit window. For a trailing days window, they are the storm days holding now minus days and now. |
spans | array | — | Only with scope=address_history. The window cut where the record is, oldest first, covering every day of it exactly once. |
spans[].start | string | — | First day of the range (YYYY-MM-DD, inclusive). |
spans[].end | string | — | Last day of the range (YYYY-MM-DD, inclusive). |
spans[].days | integer | — | Number of days in the range. |
spans[].covered | boolean | — | false = the record does not provably hold these days completely. Searched only when partial_data is true; an empty result there is not a finding of no hail. |
spans[].reason | string | — | Present when covered is false. Same values as VerificationTemporalCoverage.uncovered_ranges[].reason. One of: before_archive, after_watermark, ingest_gap, coverage_exception. |
spans[].partial_data | boolean | — | Present when covered is false. true = the days hold some rows but are not provably complete: the span was searched and carries chunks, events, daily_peaks, max_size_mm and truncated like a covered span. false = the days hold no rows: the span was not queried and carries none of those keys. |
spans[].chunks | integer | — | Present when the span was searched (covered or partial_data true). Queries this span ran. |
spans[].events | array | — | Present when the span was searched (covered or partial_data true); the same items as the top-level events, for this span's days, newest first. |
spans[].daily_peaks | array | — | Present when the span was searched; the same items as the top-level daily_peaks. |
spans[].max_size_mm | number · nullable | mm | Present when the span was searched. Largest daily_peaks estimate in the span; null when it holds none. |
spans[].truncated | boolean | — | Present when the span was searched. A chunk filled limit, so events may be cut short; daily_peaks is not. |
chunks | integer | — | Only with scope=address_history. Queries run across all searched spans (covered and partial_data). |
window | object | — | Only with scope=address_history. The resolved window. |
window.start_date | string | — | First day of the window. |
window.end_date | string | — | Last day of the window. |
window.days | integer | — | Number of days in the range. |
GET /v1/events Regional event-day summaries over a date range 19 fields
| Field | Type | Unit | Meaning |
|---|---|---|---|
date_range | object | — | The date window the answer covers. |
area_method | string | — | How affected area was computed. One of: convex_hull. |
region | object | — | The region filter as the server read it. |
events | array | — | One entry per storm date with radar hail detections in the region. |
events[].event_date | string | — | Storm date of the event (YYYY-MM-DD, 12:00 to 12:00 UTC). |
events[].cell_count | integer | — | Distinct tracked storm objects (distinct track ids) with at least one counted detection that storm day. Not a count of per-scan objects or geohash cells. |
events[].max_size_mm | number | mm | Radar-derived MESH (Maximum Estimated Size of Hail), in millimetres; a size inferred from reflectivity ALOFT, in the storm above the ground, not a measurement of a stone that landed. Radar and ground figures can legitimately differ for the same storm. The response also carries size_basis, which states this machine-readably. |
events[].n_detections | integer | — | Radar hail detections counted once per scan and pixel location. |
events[].affected_area_km2 | number | km² | Footprint area of the day's detections, square kilometres. |
events[].centroid | object | — | Centroid of the day's footprint. |
events[].centroid.lat | number | degrees (WGS 84) | Latitude, decimal degrees (WGS 84). |
events[].centroid.lon | number | degrees (WGS 84) | Longitude, decimal degrees (WGS 84). |
events[].swath_url | string | — | Path to the day's hail swath polygons. |
coverage | object | — | See Service-area coverage. |
spatial_clip | object | — | See Spatial clip. |
event_count | integer | — | Number of entries in events. |
outcome | string | — | The window-level answer. READ THIS, NOT events.length. signal: at least one event day. no_signal: nothing found AND every day of the window is on record (temporal_coverage.covered: full): the only state that may be worded as "no hail signal". insufficient_coverage: nothing found, but part of the window is not on record; say which days are missing (temporal_coverage.uncovered_ranges), never "no hail". A window the radar record holds nothing for (temporal_coverage.searchable: false) is answered by the verify grains from ground reports alone when the ground-report archive holds every day of it (temporal_coverage.radar_searched: false, ground_reports_searched: true): signal when a ground report was found (charged), otherwise insufficient_coverage; never no_signal, because radar was not consulted; and that answer is free (X-Credits-Cost: 0). Only a window NEITHER record can be searched for is a 422. One of: signal, no_signal, insufficient_coverage. |
temporal_coverage | object | — | See Temporal coverage. |
data_as_of | string | — | The radar hail record's watermark: the newest provably complete day (temporal_coverage.data_through). Never the requested date echoed back. |
POST /v1/verify Date-of-loss verification: per-event-day verdicts for a location 121 fields
| Field | Type | Unit | Meaning |
|---|---|---|---|
location | object | — | The point the answer is for. |
date_range | object | — | The date window the answer covers. |
radius_mi | number | mi | Search radius, miles. |
methodology | object | — | How this verdict was computed; versioned with the API. |
methodology.version | string | — | The verdict RULE's version, not the model's. Bumps whenever the rule or its bars change, so a previously-issued verdict stays reproducible under its own stamp. A model promotion that moves a bar therefore bumps this too. |
methodology.description | string | — | Plain-language description of the block. |
methodology.mesh_min_mm | number | mm | The mesh leg of the strong-uncorroborated-detection rule (mm). |
methodology.ml_confidence_bars | array | — | The ML leg, PER MODEL GENERATION: the exact table classifyVerdict cuts against, keyed by public model version. A detection is "strong" when its neighbourhood peak probability reaches the bar for the generation that scored it. Historical bars sit ABOVE the current one: older scales ran hotter, so a single flat bar would over-admit hit on old claims. |
methodology.ml_confidence_bars[].model_version | string | — | Public detect-model identity, as anvil-detect-{generation}. |
methodology.ml_confidence_bars[].min_ml_confidence | number | — | Detection-model probability a detection must reach to count, for that model generation. |
methodology.ml_confidence_bar_default | number | — | Bar applied when a detection's scoring generation cannot be resolved. Deliberately the STRICTEST bar in the table, so an unresolvable row can never be wrong in the direction that admits a hit. |
methodology.at_property_radius_mi | number | mi | The radius the verdict's distance leg cuts at. Evidence with nothing inside it is nearby; inside it, a ground report or a strong detection makes the day hit and any other detection makes it detected. Published because a client that renders one of these as another is making a claim this API did not make. |
methodology.bands_mi | array | mi | Distance bands, miles, used to classify proximity. |
detection_quality | object | — | Same shape as GET /v1/coverage's detection_quality block. Null if the location has no HDQI coverage. |
events | array | — | One verdict per storm day with any signal within 5 miles. Days with no signal are absent, never a zero row. |
events[].event_date | string | — | Storm date of the event (YYYY-MM-DD, 12:00 to 12:00 UTC). |
events[].verdict | string | — | hit: "Confirmed at the property". Hit-grade evidence AND, within methodology.at_property_radius_mi of the queried location, a ground report or a STRONG radar detection (at_property.strong_detection). detected: "Detected at the property, not counted". Added in verify-v5. The SAME hit-grade evidence, and a radar detection within that radius, but neither a strong detection nor a ground report within it: the detection at the location did not meet the standard that confirms a day, and the report or strong detection that documents the storm was farther away. Through verify-v4 these days returned hit. A detected day is not a hit and not confirmed. confirmed_by is null; the day's sentence is in basis_statements. nearby: the SAME hit-grade evidence, but its closest element was farther than that radius. The storm is real and documented; it did not pass over this location. Added in verify-v4: through verify-v3 these days returned hit, because the rule read the 5-mile search disc while consumers worded the answer as "hail at this address". A nearby day is not a hit: that is the exact defect this value exists to prevent. marginal: a weak/uncorroborated detection, OR a ground report the radar did not corroborate. Decided BEFORE distance: proximity never promotes weak evidence. miss: neither signal present. Not reachable in a response; such days are omitted from events entirely. One of: hit, detected, nearby, marginal, miss. |
events[].confirmed_by | string · nullable | — | Only on a hit day (null on every other verdict, detected included): what CONFIRMED the day inside methodology.at_property_radius_mi. radar: a strong radar detection; ground_report: a ground report, with no STRONG detection that close (there may be no radar detection inside the radius at all, so at_property.max_size_mm can be null, or a small one that is not what confirmed the day); both. Wording only: never an input to the verdict. A ground_report day does not mean nothing reached the location. Through verify-v4 radar meant a detection of any size. One of: radar, ground_report, both. |
events[].band | string | — | Null when no detection matched (report-only marginal case). One of: at_location, within_1mi, within_3mi, within_5mi. |
events[].max_size_mm | number | mm | Peak radar-estimated stone size anywhere in the 5-MILE SEARCH DISC: NOT at the queried location. Read it with proximity.peak_detection, which says how far away and in which direction it was measured; for the location itself use at_property.max_size_mm. Publishing this number without its offset is how a stone measured 3.3 miles away was presented as a property's own peak size. |
events[].proximity | object | — | WHERE the evidence sat relative to the queried location. Every other scalar on this event describes the 5-mile disc; this block is what stops a disc fact being read as a fact about the roof. |
events[].proximity.at_property | boolean | — | True when the closest evidence (radar OR ground report) was within at_property_radius_mi. It says WHERE, not that the day is confirmed: it is true on hit and on detected days (and on a marginal day whose weak evidence is close), false on nearby. Read verdict for the finding. |
events[].proximity.at_property_radius_mi | number | mi | Radius, miles, counted as at the property. |
events[].proximity.nearest_detection | object | — | Closest radar detection: lat, lon, distance_mi, bearing_deg and a 16-point direction (the way you would look FROM the location TO the evidence). Bearing and direction are null inside 0.05 mi, where a compass point would be noise. Null when the day had no detection. |
events[].proximity.peak_detection | object | — | Same shape, for the cell carrying max_size_mm. |
events[].proximity.nearest_report | object | — | distance_mi and direction to the closest ground report. Null when none was filed. This and nearest_detection.distance_mi / nearest_detection.direction are the per-day nearest-evidence distances: read them here (the verification report copies them to event_days[].nearest_ground_report_mi etc.). A ground report CONFIRMS a day only when confirmed_by is ground_report or both (it is inside at_property_radius_mi). Otherwise state its distance ("a ground report was also filed 3.06 mi S"): never "corroborated" and never "nearby". Radar distances are to the centre of a ~1 km radar grid cell: a radar distance under about 0.3 mi means the pin lies inside that cell. |
events[].at_property | object | — | The ONLY block describing the location itself: radar cells within at_property_radius_mi. Nulls here are a FINDING, not missing data: a null max_size_mm means nothing qualifying reached the location, which is the honest answer for a storm that passed to one side. |
events[].at_property.max_size_mm | number · nullable | mm | Peak radar-estimated stone size within proximity.at_property_radius_mi of the queried location. Null means nothing qualifying reached it: a finding, not a gap in the record. |
events[].at_property.ml_confidence | number · nullable | — | Peak Anvil hail probability among the detections within the at-property radius: P(hail >= 0.75" / 19.05 mm within 15 km), the SAME quantity ml_probability carries elsewhere in this API, and the same quantity as the event-level ml_confidence above. It is NOT a measure of how certain the system is that the event happened. SCALE-BOUND: comparable only within one detect model generation; the event's model_version names which. Null when no detection reached the property. |
events[].at_property.strong_detection | boolean | — | Added in verify-v5. True when a detection within the at-property radius cleared BOTH the strong size (methodology.mesh_min_mm) and its model generation's confidence bar: on the same detection (one grid cell in one scan). The radar half of what separates hit from detected. Not derivable from max_size_mm and ml_confidence above: those are separate maxima and can come from different detections. |
events[].at_property.n_detections | integer | — | Radar hail detections counted once per scan and pixel location. |
events[].footprint | array | — | The day's swath as drawable cells: one entry per distinct grid cell carrying that cell's largest estimate across every scan (lat, lon, mesh_mm). Capped at limits.footprint_cells_per_event; compare against footprint_cell_count to detect truncation, because a clipped footprint drawn on a map depicts a smaller storm than the one that happened. |
events[].footprint_cell_count | integer | — | Distinct cells the day actually had, before the cap. |
events[].ml_confidence | number | — | Peak Anvil hail probability over the matched detections: P(hail >= 0.75" / 19.05 mm within 15 km), the SAME quantity ml_probability carries elsewhere in this API. It is NOT a measure of how certain the system is that the event happened; a well-calibrated 0.29 means roughly a 29% chance of hail, not "29% sure". Named ml_confidence for historical reasons on this endpoint only. SCALE-BOUND: comparable only within one detect model generation; model_version below names which. |
events[].model_version | string · nullable | — | Public identity of the Anvil detect model that produced this event's ml_confidence, as anvil-detect-{generation}, and therefore the row of methodology.ml_confidence_bars this event's verdict was cut against. Stored rows are never rescored, so a verdict on an old storm day legitimately reports an older version than a recent one. null when the scoring model has no published public version. |
events[].n_detections | integer | — | Detection CELLS: one row per radar grid cell per scan with a qualifying signal in the search area. NOT a count of radar scans: a 42-minute storm can carry over a thousand of these across nine scans. The number of scans is scan_counts[].scans_with_hail. Unchanged and part of the hashed events. |
events[].first_scan_time | string · nullable | — | ⚠️ READ THIS BEFORE USING THE TIMESTAMP. This and its two siblings are radar scan times when a hail signal was present over the search area: here, the 5-mile disc around the location. They are NOT "when hail hit the property". first_scan_time is the valid time of the earliest MRMS scan in which a qualifying signal appeared ANYWHERE in that disc; hail may have reached this particular address later, earlier in a part of the storm we scored differently, or not at all. Null on a report-only day (no radar detection to time). ISO-8601 UTC. |
events[].peak_scan_time | string · nullable | — | Valid time of the scan carrying this day's LARGEST radar-estimated stone within the search area. Same caveat as first_scan_time: a radar scan time over the search area, NOT "when hail hit the property". Ties are broken toward the earlier scan so a day always reports the same instant. |
events[].last_scan_time | string · nullable | — | Valid time of the LAST scan in which a qualifying signal was present in the search area. Same caveat as first_scan_time: a radar scan time over the search area, NOT "when hail hit the property". |
events[].track_ids | array | — | Stable storm-track ids whose detections fell inside the search area on this day. Two event days sharing an id were produced by the same tracked storm system: see storm_groups, and read its warning before using this for anything contractual. Empty when the day's rows carry no track lineage (the column is nullable and older rows predate it): absence is unknown linkage, never evidence of a separate storm. Capped at limits.track_ids_per_event per day. |
events[].wind | object | — | 🔴 WIND CONTEXT: WHAT THE NATIONAL WEATHER SERVICE WARNED, AND NOTHING ELSE. Whether an NWS Severe Thunderstorm or Tornado Warning carrying a wind-threat tag covered this search area on this convective day. The source is the issued warning itself: a public, citable government act you can look up by warning_id, the same warnings GET /v1/warnings serves. It is NEVER a model-derived wind probability; this API does not publish one. Wind is EVIDENCE ATTACHED TO A VERDICT ALREADY REACHED. It is never an input to the hail verdict, and adding it did not move methodology.version. Three things this block does not say. (1) The absence of a warning is NOT the absence of wind: the NWS warns on forecast severity, not on observed damage, and damaging gusts are frequently never warned. (2) It makes no statement about what caused any damage: whether damage is hail damage or wind damage is not a determination this API makes, and nothing here ranks hail against wind as a cause. (3) It makes no statement about what a policy covers. |
events[].wind.checked | boolean | — | Whether we looked at all. Read this BEFORE warned. false means "we could not check", NOT "no wind threat was warned" and NOT "there was no wind": see reason. |
events[].wind.warned | boolean · nullable | — | True when at least one NWS warning carrying a wind-threat tag covered this search area on this convective day. False means we looked and found none: which is NOT the same as "there was no wind", because the absence of a warning is NOT the absence of wind. Always null when checked is false, so "we did not look" can never be read as "we looked and found nothing". |
events[].wind.reason | string | — | Present ONLY when checked is false. before_wind_data_era: this date precedes wind_context.data_era_start, the first convective day the NWS warning archive behind this API covers completely. wind_lookup_failed: the warning query failed; the hail verdict on this day is unaffected, because wind is never an input to it. One of: before_wind_data_era, wind_lookup_failed. |
events[].wind.warning_count | integer | — | Distinct covering WARNINGS: each warning's CAP messages (its issuance and every update/cancellation) count once, grouped by the warning's P-VTEC identity (office, phenomenon, event number, year) or, for messages archived before 2026-10-25 without VTEC, by issuing office + expiry + overlapping polygon. Absent when checked is false: a count of zero must never stand in for "we did not look". May exceed the length of warnings, which is capped at limits.wind_warnings_per_event. |
events[].wind.threat_tags | array | — | The NWS impact-based wind-threat tags carried by the covering warnings, VERBATIM and de-duplicated: in practice RADAR INDICATED or OBSERVED. There is deliberately NO gust-speed field: this is a tag column, not a measurement, and deriving a wind speed from it would publish precision the source does not carry. |
events[].wind.warnings | array | — | The covering warnings, oldest first, capped at limits.wind_warnings_per_event. Every entry is independently checkable against the National Weather Service by its warning_id. One entry per warning, described by its FIRST CAP message. |
events[].wind.warnings[].warning_id | string | — | The CAP @id of the warning's first (issuing) message. |
events[].wind.warnings[].event | string | — | Warning or watch type. One of: Severe Thunderstorm Warning, Tornado Warning. |
events[].wind.warnings[].wind_threat | string | — | The NWS impact-based wind-threat tag, verbatim. |
events[].wind.warnings[].onset | string | — | Warning onset / effective time, ISO-8601 UTC. |
events[].wind.warnings[].expires | string | — | Warning expiration time, ISO-8601 UTC. |
events[].wind.note | string | — | The honesty statement for this day's specific state, written to be rendered beside the value. Render it: it is what stops warned: false being read as "it was not windy", and what keeps checked: false from being read as a finding. |
events[].impact | object | — | HOW the stone was falling in the cell carrying the day's largest estimate: angle from vertical, and the bearing it came in from. Derived from RADAR CELL TRACKING (the parent storm's tracked motion and the stone's terminal velocity): never from a wind model, and never from a forecast field. NULL on a day with no radar geometry; a report-only day has no detection to take an angle from, and a zeroed shape would read as "it fell straight down". EVIDENCE ONLY: no verdict is decided by it and methodology.version does not move for it. |
events[].impact.angle_deg | number | degrees | Impact angle from vertical, degrees. 0 = straight down, 90 = fully horizontal. Computed at the PARENT STORM OBJECT and denormalised to its pixels, so it describes the cell over the search area rather than a measurement at any one structure. |
events[].impact.storm_moving_toward_deg | number | degrees | Azimuth the tracked cell was travelling TOWARD, degrees, 0 = north. This is a heading, NOT the meteorological "direction it comes from" convention used for wind. |
events[].impact.storm_moving_toward | string | — | 16-point compass form of storm_moving_toward_deg. |
events[].impact.arrives_from_deg | number | degrees | The bearing hail came IN FROM: storm_moving_toward_deg flipped 180 degrees, because the stone is carried with the cell and therefore strikes surfaces facing the opposite way. Published rather than left for the caller to derive: reading this backwards points an inspection at the wrong side of a building. |
events[].impact.arrives_from | string | — | 16-point compass form of arrives_from_deg. |
events[].impact.exposed_elevations | array | — | The building faces the storm's motion pointed hail at: the windward face and its two neighbours, since an oblique fall loads the corner as well as the flat side. An INSPECTION PRIORITY, never a finding that those elevations were damaged. EMPTY when the fall was near-vertical or the cell's motion was never tracked; no elevation is guessed. |
events[].impact.near_vertical | boolean | — | True when the fall was too close to vertical for any elevation to be preferentially exposed. Null when no angle was recorded. |
events[].impact.note | string | — | The sentence that must render wherever the geometry does. States that an exposed elevation is an inspection priority and not a damage finding, and that this describes the cell rather than the structure. |
events[].ground_truth | object | — | Ground hail reports matched to the event. |
events[].ground_truth.corroborated | boolean | — | ⚠️ NAMED FOR HISTORY, NOT FOR WHAT IT PROVES. True when at least one ground report was filed ANYWHERE in the search area on this day: up to radius_mi away. It does NOT mean a report corroborates hail at the location: on a hit day only confirmed_by says whether a report was inside the at-property radius. The distance to the nearest report is proximity.nearest_report. (The name cannot change: events is covered by the verification hash.) |
events[].ground_truth.nearest_reports | array | — | Closest ground reports first, capped at limits.reports_per_event. Each carries source, max_size_mm, lat, lon, distance_m, distance_mi, a 16-point direction, and event_time. Positions since verify-v4: a report list with distances but no direction cannot be drawn, and cannot answer the question customers actually ask; which way did it go? ⚠️ event_time is the ONLY timestamp on this response that is not a radar scan time: it is when the OBSERVER says the hail fell, at the report's own position. Null when the warehouse has no time for that report: a report you cannot place in time must not be given one. source LABELS (one map, lib/groundReportSources.js; the verification report PDF prints these, mirror them exactly): spc_storm_reports / spc → "SPC storm report"; iem_lsr → "Local storm report"; cocorahs → "CoCoRaHS volunteer observer"; user / app_user → "User-submitted report"; any other value → "Ground report (<source>)"; no source → "Ground report". Displaying a cocorahs report owes the CC BY 3.0 credit ("Data provided by the Community Collaborative Rain, Hail and Snow Network (CoCoRaHS), used under CC BY 3.0."). |
temporal_coverage | object | — | See Temporal coverage. |
outcome | string | — | The window-level answer. READ THIS, NOT events.length. signal: at least one event day. no_signal: nothing found AND every day of the window is on record (temporal_coverage.covered: full): the only state that may be worded as "no hail signal". insufficient_coverage: nothing found, but part of the window is not on record; say which days are missing (temporal_coverage.uncovered_ranges), never "no hail". A window the radar record holds nothing for (temporal_coverage.searchable: false) is answered by the verify grains from ground reports alone when the ground-report archive holds every day of it (temporal_coverage.radar_searched: false, ground_reports_searched: true): signal when a ground report was found (charged), otherwise insufficient_coverage; never no_signal, because radar was not consulted; and that answer is free (X-Credits-Cost: 0). Only a window NEITHER record can be searched for is a 422. One of: signal, no_signal, insufficient_coverage. |
ml_provenance | object · nullable | — | See Model provenance. |
event_count | integer | — | Number of entries in events. |
reported_sizes | array | — | The hail size the GROUND REPORTS themselves recorded, one entry per event day in events order. A report-only day has no radar size (events[].max_size_mm and events[].at_property.max_size_mm are null), so this is that day's only size: show it as a reported size ("reported 0.87 in, ground report"), never as a radar estimate and never as a size at the address. A SIBLING of events, deliberately not a key inside it: the verification report's verification_hash is sha256({params, events, methodology_version}) and is recomputable from this response's events. |
reported_sizes[].event_date | string | — | Storm date of the event (YYYY-MM-DD, 12:00 to 12:00 UTC). |
reported_sizes[].reported_max_size_mm | number · nullable | mm | Largest size, mm (1 decimal), among EVERY ground report filed within radius_mi on that day: not only the ground_truth.nearest_reports listed, which is capped at limits.reports_per_event. Null when no report on the day recorded a size, and on a day with no report. A person's measurement at the report's own position. |
ground_report_counts | array | — | How many ground reports each event day really has, one entry per event day in events order. Use this for any count: never the length of ground_truth.nearest_reports, which is capped at limits.reports_per_event. observations.hail_reports carries one National Weather Service storm report twice (SPC's storm-report feed and the Local Storm Report feed both relay it), so filed over-counts observers. distinct counts a sized report carried by BOTH of those feeds, same coordinate to 2 decimals (about 1 km), within 10 minutes, same size, once. Nothing else is merged: CoCoRaHS and user-submitted reports are independent observers, and a report with no recorded size is never merged. Every raw filing is still listed in ground_truth.nearest_reports with its own source. Word it "N ground reports (M with a size); largest reported X" with N = distinct, M = distinct_with_size, X = reported_sizes[].reported_max_size_mm. A SIBLING of events, like reported_sizes: not part of the verification_hash. Point grain only. |
ground_report_counts[].event_date | string | — | Storm date of the event (YYYY-MM-DD, 12:00 to 12:00 UTC). |
ground_report_counts[].filed | integer | — | Every ground report filed within radius_mi that day (a true count, not the listed length). |
ground_report_counts[].filed_with_size | integer | — | How many of filed recorded a hail size. |
ground_report_counts[].distinct | integer | — | filed with each report carried by both NWS relay feeds counted once. |
ground_report_counts[].distinct_with_size | integer | — | How many of distinct recorded a hail size. |
ground_report_counts[].duplicates_merged | integer | — | filed minus distinct. |
ground_report_counts[].complete | boolean | — | False only when more reports were filed than were examined for duplicates (250 per day); the unexamined ones are each counted, so distinct is then an upper bound: say "up to N". |
scan_counts | array | — | Radar SCANS versus detection CELLS, one entry per event day in events order. events[].n_detections counts cells (grid cell x scan rows); the number of radar scans with a hail signal in the search area is scans_with_hail. For example "Radar scans with a hail signal: 9" beside "detection cells: 1,141"; n_detections is not a scan count. A SIBLING of events (hashed), like reported_sizes. |
scan_counts[].event_date | string | — | Storm date of the event (YYYY-MM-DD, 12:00 to 12:00 UTC). |
scan_counts[].scans_with_hail | integer · nullable | — | Distinct radar scan times carrying a qualifying hail detection inside the search area that day. Null on a report-only day (no radar rows): not zero. |
scan_counts[].detection_cells | integer | — | The day's events[].n_detections, repeated so both counts can be shown under their correct names. |
basis_statements | array | — | Added in verify-v5. Each event day's customer-facing label and the sentence its verdict rests on, one entry per event day in events order. Server-authored so a client never words a basis itself: print verdict_label, and where basis_statement is present print it beside the label. Point grain only. A SIBLING of events (not hashed). |
basis_statements[].event_date | string | — | Storm date of the event (YYYY-MM-DD, 12:00 to 12:00 UTC). |
basis_statements[].verdict_label | string | — | One of "Confirmed at the property", "Confirmed at the property: by ground report", "Detected at the property, not counted", "Nearby, not overhead", "Weak signal", "Reported, not radar-confirmed", "Reported, radar data not available". |
basis_statements[].basis_statement | string · nullable | — | On a hit day: "confirmed by ...": which evidence was inside the at-property radius with its distance, anything outside it as a separate sentence, and the at-address radar estimate. On a detected day: "radar detected hail 0.09 mi NW. That detection is inside the 0.5 mi at-property radius. Radar's largest estimate there was 0.33" (8.5 mm). That is below 25 mm, not strong on its own. No ground report was filed inside that radius. The nearest was 3.1 mi S. Distances are measured from the pin placed for this address.": never the word "confirmed". The size sentence names the true shortfall, decided on the size as printed: "That is below 25 mm, not strong on its own." (under the strong size), "It lacked the model confidence to be strong." (the size reached it), or "That is within rounding of 25 mm. It fell short on size or model confidence." (a printed 25.0; the unrounded size may be either side). Null on every other verdict. Starts lower-case so it can follow "Basis: ". Distances are measured from "the pin placed for this address" unless subject_precision is rooftop, which says "the address". The same string a report filed for this response stores in event_days[].basis_statement. |
evidence_kinds | array | — | What each event day's CLOSEST evidence was, one entry per event day in events order, for every verdict (not only confirmed days: events[].confirmed_by is null elsewhere). Derived from events[].proximity; the nearer of the closest radar detection and the closest ground report, a tie going to radar. Use this, never ground_truth.corroborated (true for a report filed ANYWHERE in the search area), to say what a day's evidence basis was. Point grain only. A SIBLING of events. |
evidence_kinds[].event_date | string | — | Storm date of the event (YYYY-MM-DD, 12:00 to 12:00 UTC). |
evidence_kinds[].closest_kind | string · nullable | — | Kind of the closest evidence; null when the day has neither. One of: radar, ground_report. |
evidence_kinds[].has_radar | boolean | — | A radar detection of any size was inside the search area that day. |
evidence_kinds[].has_ground_report | boolean | — | A ground report was filed inside the search area that day. |
charged | boolean | — | Whether this answer was billed. true on every answered (2xx) live request, INCLUDING one whose outcome is insufficient_coverage on a window the radar record was searched for: a partly-covered window that was searched and returned nothing is an answer and costs the same as any other. The refusals (422, 503) carry charged: false. false on a 2xx for a caller that is not metered, for the sandbox fixture, and for an insufficient_coverage answer on a window the radar record holds nothing for (temporal_coverage.radar_searched: false: only ground reports were searched and none was found); that answer is free (X-Credits-Cost: 0); one that found a ground report (signal) is charged. An Idempotent-Replay: true response repeats the original body and is not charged again. |
credits | integer | — | Credits charged for this answer: the same number as the X-Credits-Cost response header. 0 when charged is false. |
storm_groups | array | — | 🔴 A METEOROLOGICAL FACT, NOT AN INSURANCE DETERMINATION. Two event dates sharing a track id were produced by the same tracked storm system. This is a meteorological fact, NOT an occurrence determination under any insurance policy: occurrence is a contractual test this API does not and cannot apply, because its definition (72-hour clauses and the like) lives in the policy, not in the weather. Grouping is by CONNECTED COMPONENT: two dates land in one group when they share a track directly, or transitively through another date in the same group. Do not read a group as proof that every pair inside it shares a track. Event days whose detections carry no track lineage are absent entirely. |
storm_groups[].track_ids | array | — | Every distinct track id appearing on the group's dates. |
storm_groups[].event_dates | array | — | The event dates this group links, ascending. |
wind_context | object | — | What the per-event-day wind blocks are, what geometry they were scoped to, and (most importantly) what they do not say. Present on every verification response, including one with no event days. |
wind_context.source | string | — | Always NWS warned threat. |
wind_context.source_detail | string | — | The exact provenance: impact-based wind-threat tags on National Weather Service Severe Thunderstorm and Tornado Warnings. |
wind_context.scope | string | — | The search geometry the warnings were matched against, in words. It is the SAME geometry the hail verdict searched at this grain, never widened to make more warnings match. |
wind_context.convective_day | string | — | How a warning is dated. A warning is filed under the convective day (12Z-12Z UTC) of its onset: the same convention event_date uses. |
wind_context.data_era_start | string | — | The first convective day the NWS warning archive behind this API covers completely. An event date before this returns checked: false with before_wind_data_era: we did not look, which is NOT a finding that no wind threat was warned. |
wind_context.honesty | string | — | The statement of what this feature refuses to say: warned: false does NOT mean there was no wind, checked: false is a different state again, and nothing here is a determination of what caused any damage, of whether damage is hail damage or wind damage, or of what a policy covers. |
impact_context | object | — | What events[].impact is, and what it is not. Read before using it. |
impact_context.source | string | — | Always "radar storm tracking". |
impact_context.source_detail | string | — | Names the derivation and states explicitly that no wind model is involved. |
impact_context.bearing_convention | string | — | The toward-vs-from distinction, spelled out, because inverting it inverts the answer. |
impact_context.honesty | string | — | The refusal: this is the geometry of the fall in the cell over the search area, NOT a measurement at the structure and NOT a statement that any elevation was damaged. |
limits | object | — | Published caps on the bounded evidence arrays. |
limits.track_ids_per_event | integer | — | Maximum distinct track ids returned per event day. |
limits.wind_warnings_per_event | integer | — | Maximum warnings listed in events[].wind.warnings per day. wind.warning_count is the true total and is not capped. |
coverage | object | — | Whether this point is inside the CONUS radar domain at all. READ THIS BEFORE READING events. Days with no signal are omitted from events, so an empty array is ambiguous on its own: it means "no hail signal here" when in_service_area: true, and "we cannot observe this point" when in_service_area: false. An empty events is not a clean bill of health until this field is checked: an out-of-domain address has no verdict, not a miss. |
coverage.in_service_area | boolean | — | True when any part of the request footprint is inside the contiguous US. |
coverage.message | string | — | Present only when in_service_area is false: states what the empty result does and does not mean. |
coverage.see_also | string | — | Where to read more about the service area. |
coverage.quality | string | — | HOW WELL we can observe here, where in_service_area only says WHETHER. The HDQI band for this cell (radar sample coverage, beam blockage, dual-pol availability, mosaic overlap): the same band GET /v1/coverage and detection_quality.detection_word report. unknown means no HDQI cell resolved for this location, not that coverage is good. One of: excellent, good, fair, limited, unknown. |
coverage.quality_hdqi | number · nullable | — | The 0-1 HDQI score behind quality, rounded to 3 decimal places. Null when no cell resolved. DISPLAY PRECISION: 3 decimals, exactly as returned (0.995 prints "0.995"); never rounded or truncated to 2 (the verification report PDF prints the same 3-decimal value as detection_quality.hdqi). |
coverage.quality_caveat | string · nullable | — | Non-null when quality is fair, limited or unknown. ONE-SIDED ON PURPOSE: degraded radar geometry can leave real hail unobserved, so it makes a NEGATIVE result (a miss, or a day absent from events) weaker evidence here than the same result in a well-covered cell. It does NOT make a POSITIVE result wrong: hail detected here was detected. Null when coverage is good or excellent. |
data_as_of | string | — | The hail record's real watermark: the newest convective day that is provably complete (temporal_coverage.data_through). NOT an echo of the request's date_end: a window ending today reports an earlier date here. |
POST /v1/verify/area Territory-grain verification: per-event-day verdicts for a monitored area 70 fields
| Field | Type | Unit | Meaning |
|---|---|---|---|
area | object | — | The territory as stored: id, name, kind, purpose, area_sq_mi, centroid, bbox. |
grain | string | — | "area" for a polygon (boundary cut, no radius). "point" for a point-kind area, whose answer is the 5-mile-disc answer from POST /v1/verify: read it as a point result, not a boundary one. One of: area, point. |
date_range | object | — | The date window the answer covers. |
coverage | object | — | Read this before events. in_service_area is false when no part of the territory is inside CONUS radar coverage; fully_covered is false when only part of it is. |
coverage.in_service_area | boolean | — | True when any part of the request footprint is inside the contiguous US. |
coverage.fully_covered | boolean | — | False when only part of the territory is inside contiguous-US radar coverage. |
coverage.message | string | — | Human-readable text. Wording may change; never parse it. |
coverage.see_also | string | — | Where to read more about the service area. |
coverage.quality | string | — | HOW WELL we can observe, where in_service_area / fully_covered only say WHETHER. Measured at the territory CENTROID (the same point detection_quality describes) so a large or elongated boundary can contain cells graded differently from this one. One of: excellent, good, fair, limited, unknown. |
coverage.quality_hdqi | number · nullable | — | The 0-1 HDQI score behind quality, rounded to 3 decimal places. Null when no cell resolved. DISPLAY PRECISION: 3 decimals, exactly as returned (0.995 prints "0.995"); never rounded or truncated to 2 (the verification report PDF prints the same 3-decimal value as detection_quality.hdqi). |
coverage.quality_caveat | string · nullable | — | Non-null when quality is fair, limited or unknown. ONE-SIDED: degraded radar geometry can leave real hail unobserved, so a NEGATIVE result is weaker evidence here than in a well-covered cell. It does NOT make a POSITIVE result wrong. |
methodology | object | — | Shared with POST /v1/verify: same version, same per-generation ML bars, plus grain=area. |
detection_quality | object | — | Radar geometry at the territory centroid. Null if the centroid has no HDQI coverage. |
honesty | string | — | States the areal claim this endpoint refuses to make, and why. |
events | array | — | One verdict per storm day with a signal inside the territory. Days with no signal are absent. |
events[].event_date | string | — | Storm date of the event (YYYY-MM-DD, 12:00 to 12:00 UTC). |
events[].verdict | string | — | For grain: area (a polygon). A boundary has no at-property radius, so hit-grade evidence inside it is hit; nearby and detected are never returned and verify-v5 changed nothing at this grain. For grain: point (a point-kind area) the events are the point grain's own: verdict is one of hit, detected, nearby, marginal, and the response also carries basis_statements; see POST /v1/verify. One of: hit, marginal, miss. |
events[].max_size_mm | number | mm | Peak radar-estimated stone size inside the boundary that day. |
events[].detection_strength | string · nullable | — | Display tier of ml_confidence, tiered on the model generation that scored it: marginal (cleared the hail-detection threshold), moderate (cleared the default history filter), strong (cleared the bar at which radar alone can confirm hail). null below the detection threshold or when no model probability is available. Prefer this word over the raw probability for display: the probability is a calibrated chance that a 0.75 in+ hail report is filed within 15 km, so it reads low (often 10 to 40%) even for real hail. The bars move with each model promotion; the meaning of each word does not. One of: marginal, moderate, strong. |
events[].detection_strength_rank | integer · nullable | — | detection_strength as 1 (marginal), 2 (moderate) or 3 (strong), for sorting and filtering. Null with it. |
events[].ml_confidence | number · nullable | — | Peak Anvil hail probability over the detections INSIDE the boundary: P(hail >= 0.75" / 19.05 mm within 15 km), the SAME quantity ml_probability carries elsewhere in this API. It is NOT a measure of how certain the system is that the event happened; a well-calibrated 0.29 means roughly a 29% chance of hail, not "29% sure". Named ml_confidence to match the point-grain endpoint. SCALE-BOUND: comparable only within one detect model generation; model_version below names which. |
events[].model_version | string · nullable | — | Public identity of the Anvil detect model that produced this event's ml_confidence, as anvil-detect-{generation}, and therefore the row of methodology.ml_confidence_bars that number must be read against. Never an internal run ID; null when the generation has no registered public identity. |
events[].n_detections | integer | — | Detection CELLS (grid cell x scan rows) inside the boundary. NOT radar scans (see scan_counts) and NOT an area measurement. |
events[].peak_at | object | — | Where the day's largest estimate fell, so a crew can be sent to the right corner. |
events[].first_scan_time | string · nullable | — | ⚠️ READ THIS BEFORE USING THE TIMESTAMP. This and its two siblings are radar scan times when a hail signal was present over the search area: here, anywhere inside the territory boundary. They are NOT "when hail hit the property". On a large polygon the instant recorded here can belong to a corner miles from any given parcel. Null on a report-only day. ISO-8601 UTC. |
events[].peak_scan_time | string · nullable | — | Valid time of the scan carrying the day's LARGEST estimate inside the boundary: the same scan peak_at locates. Same caveat: a radar scan time over the search area, NOT "when hail hit the property". |
events[].last_scan_time | string · nullable | — | Valid time of the LAST scan with a qualifying signal inside the boundary. Same caveat: a radar scan time over the search area, NOT "when hail hit the property". |
events[].track_ids | array | — | Stable storm-track ids whose detections fell inside the boundary this day. See storm_groups: and read its warning before using it for anything contractual. Empty when the rows carry no track lineage. Capped at limits.track_ids_per_event. |
events[].points | array | — | WHERE inside the boundary the signal fell: individual detection grid samples for this day, largest first, so a client can render which blocks of a large site were hit and which were clean instead of reading one peak pin for the whole polygon. Bounded to limits.points_per_event per day; points_truncated says whether that cap bit. THESE ARE GRID SAMPLES, NOT A COVERAGE POLYGON. No percentage-of-area-affected figure is derivable from them and none is published: see honesty. |
events[].points[].lat | number | degrees (WGS 84) | Latitude, decimal degrees (WGS 84). |
events[].points[].lon | number | degrees (WGS 84) | Longitude, decimal degrees (WGS 84). |
events[].points[].size_mm | number | mm | Radar-estimated stone size at this grid sample (mm). |
events[].points_truncated | boolean | — | True when this day had more detections than limits.points_per_event and points is a clipped sample of the largest. A clipped sample must never be read as the complete set. |
events[].wind | object | — | 🔴 WIND CONTEXT: WHAT THE NATIONAL WEATHER SERVICE WARNED, AND NOTHING ELSE. Whether an NWS Severe Thunderstorm or Tornado Warning carrying a wind-threat tag covered this search area on this convective day. The source is the issued warning itself: a public, citable government act you can look up by warning_id, the same warnings GET /v1/warnings serves. It is NEVER a model-derived wind probability; this API does not publish one. Wind is EVIDENCE ATTACHED TO A VERDICT ALREADY REACHED. It is never an input to the hail verdict, and adding it did not move methodology.version. Three things this block does not say. (1) The absence of a warning is NOT the absence of wind: the NWS warns on forecast severity, not on observed damage, and damaging gusts are frequently never warned. (2) It makes no statement about what caused any damage: whether damage is hail damage or wind damage is not a determination this API makes, and nothing here ranks hail against wind as a cause. (3) It makes no statement about what a policy covers. |
events[].wind.checked | boolean | — | Whether we looked at all. Read this BEFORE warned. false means "we could not check", NOT "no wind threat was warned" and NOT "there was no wind": see reason. |
events[].wind.warned | boolean · nullable | — | True when at least one NWS warning carrying a wind-threat tag covered this search area on this convective day. False means we looked and found none: which is NOT the same as "there was no wind", because the absence of a warning is NOT the absence of wind. Always null when checked is false, so "we did not look" can never be read as "we looked and found nothing". |
events[].wind.reason | string | — | Present ONLY when checked is false. before_wind_data_era: this date precedes wind_context.data_era_start, the first convective day the NWS warning archive behind this API covers completely. wind_lookup_failed: the warning query failed; the hail verdict on this day is unaffected, because wind is never an input to it. One of: before_wind_data_era, wind_lookup_failed. |
events[].wind.warning_count | integer | — | Distinct covering WARNINGS: each warning's CAP messages (its issuance and every update/cancellation) count once, grouped by the warning's P-VTEC identity (office, phenomenon, event number, year) or, for messages archived before 2026-10-25 without VTEC, by issuing office + expiry + overlapping polygon. Absent when checked is false: a count of zero must never stand in for "we did not look". May exceed the length of warnings, which is capped at limits.wind_warnings_per_event. |
events[].wind.threat_tags | array | — | The NWS impact-based wind-threat tags carried by the covering warnings, VERBATIM and de-duplicated: in practice RADAR INDICATED or OBSERVED. There is deliberately NO gust-speed field: this is a tag column, not a measurement, and deriving a wind speed from it would publish precision the source does not carry. |
events[].wind.warnings | array | — | The covering warnings, oldest first, capped at limits.wind_warnings_per_event. Every entry is independently checkable against the National Weather Service by its warning_id. One entry per warning, described by its FIRST CAP message. |
events[].wind.warnings[].warning_id | string | — | The CAP @id of the warning's first (issuing) message. |
events[].wind.warnings[].event | string | — | Warning or watch type. One of: Severe Thunderstorm Warning, Tornado Warning. |
events[].wind.warnings[].wind_threat | string | — | The NWS impact-based wind-threat tag, verbatim. |
events[].wind.warnings[].onset | string | — | Warning onset / effective time, ISO-8601 UTC. |
events[].wind.warnings[].expires | string | — | Warning expiration time, ISO-8601 UTC. |
events[].wind.note | string | — | The honesty statement for this day's specific state, written to be rendered beside the value. Render it: it is what stops warned: false being read as "it was not windy", and what keeps checked: false from being read as a finding. |
events[].ground_truth | object | — | Ground hail reports matched to the event. |
temporal_coverage | object | — | See Temporal coverage. |
outcome | string | — | The window-level answer. READ THIS, NOT events.length. signal: at least one event day. no_signal: nothing found AND every day of the window is on record (temporal_coverage.covered: full): the only state that may be worded as "no hail signal". insufficient_coverage: nothing found, but part of the window is not on record; say which days are missing (temporal_coverage.uncovered_ranges), never "no hail". A window the radar record holds nothing for (temporal_coverage.searchable: false) is answered by the verify grains from ground reports alone when the ground-report archive holds every day of it (temporal_coverage.radar_searched: false, ground_reports_searched: true): signal when a ground report was found (charged), otherwise insufficient_coverage; never no_signal, because radar was not consulted; and that answer is free (X-Credits-Cost: 0). Only a window NEITHER record can be searched for is a 422. One of: signal, no_signal, insufficient_coverage. |
ml_provenance | object · nullable | — | See Model provenance. |
event_count | integer | — | Number of entries in events. |
scan_counts | array | — | Radar scans versus detection cells per event day, in events order, same shape and rule as on POST /v1/verify: scans_with_hail is the distinct scan times with a signal inside the boundary; detection_cells is events[].n_detections. Beside the hashed events, never inside it. A point-kind area is answered by the point engine and carries the point-grain block (plus evidence_kinds) instead. |
scan_counts[].event_date | string | — | Storm date of the event (YYYY-MM-DD, 12:00 to 12:00 UTC). |
scan_counts[].scans_with_hail | integer · nullable | — | Distinct radar scan times with a hail signal inside the boundary that day. |
scan_counts[].detection_cells | integer | — | Detections that day, counted once per scan and pixel (events[].n_detections). |
storm_groups | array | — | 🔴 A METEOROLOGICAL FACT, NOT AN INSURANCE DETERMINATION. Two event dates sharing a track id were produced by the same tracked storm system. This is a meteorological fact, NOT an occurrence determination under any insurance policy: occurrence is a contractual test this API does not and cannot apply. Grouped by CONNECTED COMPONENT: dates land together when they share a track directly, or transitively through another date in the group. Days with no track lineage are absent. |
storm_groups[].track_ids | array | — | Storm track ids that link the dates in this group. |
storm_groups[].event_dates | array | — | Event dates produced by the same tracked storm system (a meteorological fact, not an occurrence determination). |
wind_context | object | — | What the per-event-day wind blocks are, what geometry they were scoped to, and (most importantly) what they do not say. Present on every verification response, including one with no event days. |
wind_context.source | string | — | Always NWS warned threat. |
wind_context.source_detail | string | — | The exact provenance: impact-based wind-threat tags on National Weather Service Severe Thunderstorm and Tornado Warnings. |
wind_context.scope | string | — | The search geometry the warnings were matched against, in words. It is the SAME geometry the hail verdict searched at this grain, never widened to make more warnings match. |
wind_context.convective_day | string | — | How a warning is dated. A warning is filed under the convective day (12Z-12Z UTC) of its onset: the same convention event_date uses. |
wind_context.data_era_start | string | — | The first convective day the NWS warning archive behind this API covers completely. An event date before this returns checked: false with before_wind_data_era: we did not look, which is NOT a finding that no wind threat was warned. |
wind_context.honesty | string | — | The statement of what this feature refuses to say: warned: false does NOT mean there was no wind, checked: false is a different state again, and nothing here is a determination of what caused any damage, of whether damage is hail damage or wind damage, or of what a policy covers. |
limits | object | — | Published caps on the bounded evidence arrays. |
limits.track_ids_per_event | integer | — | Most track ids listed per event. |
limits.points_per_event | integer | — | Maximum detection points returned per event day. |
limits.wind_warnings_per_event | integer | — | Maximum warnings listed in events[].wind.warnings per day. wind.warning_count is the true total and is not capped. |
data_as_of | string | — | The hail record's real watermark: the newest convective day that is provably complete (temporal_coverage.data_through). NOT an echo of the request's date_end: a window ending today reports an earlier date here. |
POST /v1/verify/portfolio Post-storm triage: a verdict for every monitored property 114 fields
| Field | Type | Unit | Meaning |
|---|---|---|---|
grain | string | — | Verdict grain (portfolio). One of: portfolio. |
date_range | object | — | The date window the answer covers. |
radius_mi | number | mi | The sweep radius (1 mile): a day is listed for a property when something was recorded within this distance of it. A listed day's evidence is read over methodology.evidence_radius_mi. |
assets_checked | integer | — | Assets checked: those whose impact radius reaches the contiguous US. |
truncated | boolean | — | True when the portfolio exceeded limits.max_assets. The list is INCOMPLETE: the properties not returned were not checked. |
limits | object | — | Published caps on the request and on the bounded evidence arrays. |
limits.max_assets | integer | — | Most assets accepted in one request. |
limits.max_range_days | integer | — | Longest date window accepted, days. |
limits.track_ids_per_event | integer | — | Maximum distinct track ids returned per property-day. |
limits.wind_warnings_per_event | integer | — | Maximum warnings listed in properties[].days[].wind.warnings per property-day. wind.warning_count is the true total and is not capped. |
wind_context | object | — | What the per-event-day wind blocks are, what geometry they were scoped to, and (most importantly) what they do not say. Present on every verification response, including one with no event days. |
wind_context.source | string | — | Always NWS warned threat. |
wind_context.source_detail | string | — | The exact provenance: impact-based wind-threat tags on National Weather Service Severe Thunderstorm and Tornado Warnings. |
wind_context.scope | string | — | The search geometry the warnings were matched against, in words. It is the SAME geometry the hail verdict searched at this grain, never widened to make more warnings match. |
wind_context.convective_day | string | — | How a warning is dated. A warning is filed under the convective day (12Z-12Z UTC) of its onset: the same convention event_date uses. |
wind_context.data_era_start | string | — | The first convective day the NWS warning archive behind this API covers completely. An event date before this returns checked: false with before_wind_data_era: we did not look, which is NOT a finding that no wind threat was warned. |
wind_context.honesty | string | — | The statement of what this feature refuses to say: warned: false does NOT mean there was no wind, checked: false is a different state again, and nothing here is a determination of what caused any damage, of whether damage is hail damage or wind damage, or of what a policy covers. |
coverage | object | — | Read this before the list. Always present on a 200 (a business with no point assets is refused 422 no_portfolio_assets). ONE PROBE, AT THE PORTFOLIO CENTROID. getDetectionQuality is a warehouse lookup; running it per property would make the cost scale with portfolio size for a field that is context, not a verdict. So it is fetched once, at the arithmetic mean of your asset positions: and for a portfolio spread across several states that centroid is a point no property occupies. quality_basis says so on every response. For true per-property radar geometry call POST /v1/verify (or GET /v1/coverage) for that address. |
coverage.in_service_area | boolean | — | Whether the portfolio centroid is inside the CONUS radar domain. |
coverage.message | string | — | Human-readable text. Wording may change; never parse it. |
coverage.see_also | string | — | Where to read more about the service area. |
coverage.centroid | object | — | The point quality was measured at: mean of the asset positions. |
coverage.quality_basis | string | — | Always present, even when quality_caveat is null, so a centroid grade is never mistaken for a per-property one. One of: portfolio_centroid. |
coverage.quality | string | — | HDQI band at the portfolio centroid. See quality_basis. One of: excellent, good, fair, limited, unknown. |
coverage.quality_hdqi | number · nullable | — | The 0-1 HDQI score behind quality, rounded to 3 decimal places. Null when no cell resolved. DISPLAY PRECISION: 3 decimals, exactly as returned (0.995 prints "0.995"); never rounded or truncated to 2 (the verification report PDF prints the same 3-decimal value as detection_quality.hdqi). |
coverage.quality_caveat | string · nullable | — | Non-null when quality is fair, limited or unknown. ONE-SIDED: degraded radar geometry can leave real hail unobserved, so a NEGATIVE result (a miss, or a day absent from a property's days) is weaker evidence than the same result in a well-covered cell. It does NOT make a POSITIVE result wrong. On this endpoint the text additionally notes that each property is searched within 1 mile (tighter than the 5-mile single-address check) so degraded coverage removes proportionally more of the signal a wider search would still have caught. |
temporal_coverage | object | — | See Temporal coverage. |
outcome | string | — | The window-level answer. READ THIS, NOT events.length. signal: at least one event day. no_signal: nothing found AND every day of the window is on record (temporal_coverage.covered: full): the only state that may be worded as "no hail signal". insufficient_coverage: nothing found, but part of the window is not on record; say which days are missing (temporal_coverage.uncovered_ranges), never "no hail". A window the radar record holds nothing for (temporal_coverage.searchable: false) is answered by the verify grains from ground reports alone when the ground-report archive holds every day of it (temporal_coverage.radar_searched: false, ground_reports_searched: true): signal when a ground report was found (charged), otherwise insufficient_coverage; never no_signal, because radar was not consulted; and that answer is free (X-Credits-Cost: 0). Only a window NEITHER record can be searched for is a 422. One of: signal, no_signal, insufficient_coverage. |
ml_provenance | object · nullable | — | See Model provenance. |
summary | object | — | One count per PROPERTY verdict across the returned properties (a property's verdict is its strongest day). The six counts add up to the list. miss is counted, not derived: a quiet property on a window that is not fully on record is counted under insufficient_coverage, never miss. |
summary.hit | integer | — | Properties with at least one day "Confirmed at the property". |
summary.detected | integer | — | Properties whose strongest day is "Detected at the property, not counted". Not confirmed: never add this to hit. |
summary.nearby | integer | — | Properties whose strongest day is "Nearby, not overhead": the storm is documented, something was recorded within 1 mile of the property and nothing within 0.5 mile of it. Added in verify-v5. |
summary.marginal | integer | — | Properties whose strongest day is marginal. |
summary.miss | integer | — | Properties with no signal on a window fully on record. |
summary.insufficient_coverage | integer | — | Quiet properties on a window not fully on record: never counted as miss. |
size_fields | object | — | What the two radar size fields on every property and day measure, as sentences a client can print beside the number. The two measure different things. |
size_fields.at_property_size_mm | string | mm | Sentence stating what at_property_size_mm measures (within 0.5 mile of the property). |
size_fields.sweep_peak_size_mm | string | mm | Sentence stating what sweep_peak_size_mm measures (the storm's peak across the search radius). |
methodology | object | — | version (verify-v5), grain: portfolio, description (the rule above in plain words, generated from the constants the rule uses), the per-generation confidence thresholds (ml_confidence_bars, ml_confidence_bar_default), mesh_min_mm, radius_mi and at_property_radius_mi. |
methodology.version | string | — | Verdict-rule version (e.g. verify-v5); changes when the rule changes. |
methodology.grain | string | — | Verdict grain (portfolio). One of: portfolio. |
methodology.description | string | — | Plain-language description of the block. |
methodology.radius_mi | number | mi | The sweep radius around each property (1): which days are listed. |
methodology.evidence_radius_mi | number | mi | The area a listed day's evidence is read over (5): the POST /v1/verify search area, so "is this storm documented" has one answer on both endpoints. |
methodology.at_property_radius_mi | number | mi | The radius that separates hit / detected from nearby (0.5): the same one POST /v1/verify publishes. |
methodology.mesh_min_mm | number | mm | Smallest radar size estimate a detection needs to count, millimetres. |
properties | array | — | One entry per asset checked, with its strongest-day verdict and per-day detail. |
properties[].asset_id | string | — | Your asset id, as uploaded. |
properties[].name | string | — | Your asset name, as uploaded. |
properties[].verdict | string | — | The property's STRONGEST day in the window: hit > detected > nearby > marginal > miss. A property with no storm day is miss ONLY when every day of the window is on record (temporal_coverage.covered: full); otherwise it is insufficient_coverage; we did not see every day, so "no hail" is not a finding for this roof. insufficient_coverage is not no signal, and detected or nearby is not a hit. The list is ordered hit, detected, nearby, marginal, miss, insufficient_coverage; then observable properties before ones outside radar coverage; then at_property_size_mm, then sweep_peak_size_mm (largest first); then name. One of: hit, detected, nearby, marginal, miss, insufficient_coverage. |
properties[].verdict_label | string | — | The verdict in the words a customer reads: "Confirmed at the property" (or "Confirmed at the property; by ground report" when every confirmed day was confirmed by a ground report alone), "Detected at the property, not counted", "Nearby, not overhead", "Weak signal", "Reported, not radar-confirmed", "No signal", or "Dates not on record" (insufficient_coverage). Print as returned. |
properties[].coverage | object | — | See Service-area coverage. |
properties[].storm_days | integer | — | How many days of the window have a row in days (any verdict). NOT the number of confirmed days: that is confirmed_days. |
properties[].confirmed_days | integer | — | Days whose verdict is hit ("Confirmed at the property"). |
properties[].detected_days | integer | — | Days whose verdict is detected. Not confirmed days: never add them to confirmed_days. |
properties[].nearby_days | integer | — | Days whose verdict is nearby. |
properties[].marginal_days | integer | — | Days whose verdict is marginal. |
properties[].corroborated_days | integer | — | Days with a ground report within 1 mile of the property: not necessarily within 0.5 mile, so not the number of days a ground report confirmed. |
properties[].at_property_size_mm | number · nullable | mm | Largest radar size estimate within 0.5 mile of the property, over the window. Null when radar made no estimate within 0.5 mile on any day. |
properties[].at_property_size_date | string · nullable | — | The day at_property_size_mm was estimated. |
properties[].sweep_peak_size_mm | number · nullable | mm | Largest radar size estimate within 1 mile of the property, over the window. It may have been up to a mile from the property: it is not the size at the property. |
properties[].sweep_peak_date | string · nullable | — | The day sweep_peak_size_mm was estimated. |
properties[].peak_size_mm | number · nullable | mm | Same value as sweep_peak_size_mm (within 1 mile of the property), under its pre-verify-v5 name. Prefer sweep_peak_size_mm. |
properties[].peak_date | string · nullable | — | Same value as sweep_peak_date. |
properties[].closest_distance_mi | number · nullable | mi | Distance from the property to the closest evidence on any day of the window: a radar detection or a ground report, whichever was closer. Miles, 2 decimals. Radar distances are to the centre of a ~1 km grid cell. Null for a property with no storm day. |
properties[].closest_distance_date | string · nullable | — | The day closest_distance_mi was measured on. |
properties[].days | array | — | Per-day detail, newest first. |
properties[].days[].event_date | string | — | Storm date of the event (YYYY-MM-DD, 12:00 to 12:00 UTC). |
properties[].days[].verdict | string | — | The day's verdict: the same one POST /v1/verify returns for this property and day. A day with no detection and no ground report within 1 mile has no row, so miss never appears here. One of: hit, detected, nearby, marginal. |
properties[].days[].verdict_label | string | — | The day's verdict as a customer reads it (see properties[].verdict_label). Print as returned. |
properties[].days[].basis_statement | string · nullable | — | WHAT the day's verdict rests on, in words, on a hit day ("confirmed by ...") and a detected day ("radar detected hail ..."); null on nearby and marginal. Starts lower-case so it can follow "Basis: ". Distances are measured from "the pin placed for this address" (a saved property records no rooftop-level match). "The search area" in this sentence is the 5-mile evidence area. The same sentence basis_statements carries on POST /v1/verify for this pin and day: a detected day never says "confirmed". |
properties[].days[].confirmed_by | string · nullable | — | What confirmed a hit day, inside 0.5 mile: a strong radar detection, a ground report, or both. Null on every other verdict. One of: radar, ground_report, both. |
properties[].days[].at_property_size_mm | number · nullable | mm | Largest radar size estimate within 0.5 mile of the property that day. Null when none. |
properties[].days[].sweep_peak_size_mm | number · nullable | mm | Largest radar size estimate within 1 mile of the property that day. |
properties[].days[].closest_distance_mi | number · nullable | mi | Distance to the closest evidence that day (radar or ground report). Miles, 2 decimals. |
properties[].days[].proximity | object | — | WHERE the day's evidence was, relative to the property. |
properties[].days[].proximity.at_property | boolean | — | The closest evidence is within 0.5 mile. True on hit AND detected (and on a weak day whose evidence is close): it says where, not that the day is confirmed. |
properties[].days[].proximity.at_property_radius_mi | number | mi | Radius, miles, counted as at the property. |
properties[].days[].proximity.nearest_detection | object · nullable | — | lat, lon, distance_mi, bearing_deg, direction of the closest radar detection within 5 miles (it may be outside the 1-mile sweep). Null when there was none. |
properties[].days[].proximity.nearest_report | object · nullable | — | distance_mi, direction of the closest ground report within 5 miles (it may be outside the 1-mile sweep). Null when none was filed there. |
properties[].days[].at_property | object | — | What radar recorded within 0.5 mile of the property that day. |
properties[].days[].at_property.max_size_mm | number · nullable | mm | Largest size estimate within 0.5 mile of the property that day, millimetres (see size_fields). |
properties[].days[].at_property.ml_confidence | number · nullable | — | Peak Anvil hail probability over the detections within 0.5 mile on this day: the same quantity ml_probability carries elsewhere in this API, not a measure of certainty that the event happened. |
properties[].days[].at_property.n_detections | integer | — | Radar hail detections counted once per scan and pixel location. |
properties[].days[].at_property.strong_detection | boolean | — | One detection within 0.5 mile was at least 25 mm AND met its confidence threshold. The radar half of what separates hit from detected. |
properties[].days[].max_size_mm | number | mm | Same value as sweep_peak_size_mm: the largest estimate within 1 mile of the property that day, NOT the size at the property (at_property_size_mm). Radar-derived MESH (Maximum Estimated Size of Hail), in millimetres: a size inferred from reflectivity ALOFT, in the storm above the ground, not a measurement of a stone that landed. Radar and ground figures can legitimately differ for the same storm. The response also carries size_basis, which states this machine-readably. |
properties[].days[].detection_strength | string · nullable | — | Display tier of ml_confidence, tiered on the model generation that scored it: marginal (cleared the hail-detection threshold), moderate (cleared the default history filter), strong (cleared the bar at which radar alone can confirm hail). null below the detection threshold or when no model probability is available. Prefer this word over the raw probability for display: the probability is a calibrated chance that a 0.75 in+ hail report is filed within 15 km, so it reads low (often 10 to 40%) even for real hail. The bars move with each model promotion; the meaning of each word does not. One of: marginal, moderate, strong. |
properties[].days[].detection_strength_rank | integer · nullable | — | detection_strength as 1 (marginal), 2 (moderate) or 3 (strong), for sorting and filtering. Null with it. |
properties[].days[].ml_confidence | number · nullable | — | Peak Anvil hail probability over the detections within this asset's radius on this day: P(hail >= 0.75" / 19.05 mm within 15 km), the SAME quantity ml_probability carries elsewhere in this API. It is NOT a measure of how certain the system is that the event happened; a well-calibrated 0.29 means roughly a 29% chance of hail, not "29% sure". Named ml_confidence to match the point-grain endpoint. SCALE-BOUND: comparable only within one detect model generation; model_version below names which. |
properties[].days[].model_version | string · nullable | — | Public id of the detection-model generation the day's ml_confidence is on. |
properties[].days[].n_detections | integer | — | Radar hail detections counted once per scan and pixel location. |
properties[].days[].first_scan_time | string · nullable | — | ⚠️ READ THIS BEFORE USING THE TIMESTAMP. This and its two siblings are radar scan times when a hail signal was present over the search area: here, the 1-mile disc around this property. They are NOT "when hail hit the property": the signal may have been a mile away from the structure. Null on a report-only day. ISO-8601 UTC. |
properties[].days[].peak_scan_time | string · nullable | — | Valid time of the scan carrying this property-day's LARGEST estimate. Same caveat: a radar scan time over the search area, NOT "when hail hit the property". |
properties[].days[].last_scan_time | string · nullable | — | Valid time of the LAST scan with a qualifying signal in this property's disc. Same caveat: a radar scan time over the search area, NOT "when hail hit the property". |
properties[].days[].track_ids | array | — | Stable storm-track ids whose detections fell in this property's disc that day. See storm_groups: and read its warning before using it for anything contractual. Capped at limits.track_ids_per_event. |
properties[].days[].wind | object | — | 🔴 WIND CONTEXT: WHAT THE NATIONAL WEATHER SERVICE WARNED, AND NOTHING ELSE. Whether an NWS Severe Thunderstorm or Tornado Warning carrying a wind-threat tag covered this search area on this convective day. The source is the issued warning itself: a public, citable government act you can look up by warning_id, the same warnings GET /v1/warnings serves. It is NEVER a model-derived wind probability; this API does not publish one. Wind is EVIDENCE ATTACHED TO A VERDICT ALREADY REACHED. It is never an input to the hail verdict, and adding it did not move methodology.version. Three things this block does not say. (1) The absence of a warning is NOT the absence of wind: the NWS warns on forecast severity, not on observed damage, and damaging gusts are frequently never warned. (2) It makes no statement about what caused any damage: whether damage is hail damage or wind damage is not a determination this API makes, and nothing here ranks hail against wind as a cause. (3) It makes no statement about what a policy covers. |
properties[].days[].wind.checked | boolean | — | Whether we looked at all. Read this BEFORE warned. false means "we could not check", NOT "no wind threat was warned" and NOT "there was no wind": see reason. |
properties[].days[].wind.warned | boolean · nullable | — | True when at least one NWS warning carrying a wind-threat tag covered this search area on this convective day. False means we looked and found none: which is NOT the same as "there was no wind", because the absence of a warning is NOT the absence of wind. Always null when checked is false, so "we did not look" can never be read as "we looked and found nothing". |
properties[].days[].wind.reason | string | — | Present ONLY when checked is false. before_wind_data_era: this date precedes wind_context.data_era_start, the first convective day the NWS warning archive behind this API covers completely. wind_lookup_failed: the warning query failed; the hail verdict on this day is unaffected, because wind is never an input to it. One of: before_wind_data_era, wind_lookup_failed. |
properties[].days[].wind.warning_count | integer | — | Distinct covering WARNINGS: each warning's CAP messages (its issuance and every update/cancellation) count once, grouped by the warning's P-VTEC identity (office, phenomenon, event number, year) or, for messages archived before 2026-10-25 without VTEC, by issuing office + expiry + overlapping polygon. Absent when checked is false: a count of zero must never stand in for "we did not look". May exceed the length of warnings, which is capped at limits.wind_warnings_per_event. |
properties[].days[].wind.threat_tags | array | — | The NWS impact-based wind-threat tags carried by the covering warnings, VERBATIM and de-duplicated: in practice RADAR INDICATED or OBSERVED. There is deliberately NO gust-speed field: this is a tag column, not a measurement, and deriving a wind speed from it would publish precision the source does not carry. |
properties[].days[].wind.warnings | array | — | The covering warnings, oldest first, capped at limits.wind_warnings_per_event. Every entry is independently checkable against the National Weather Service by its warning_id. One entry per warning, described by its FIRST CAP message. |
properties[].days[].wind.warnings[].warning_id | string | — | The CAP @id of the warning's first (issuing) message. |
properties[].days[].wind.warnings[].event | string | — | Warning or watch type. One of: Severe Thunderstorm Warning, Tornado Warning. |
properties[].days[].wind.warnings[].wind_threat | string | — | The NWS impact-based wind-threat tag, verbatim. |
properties[].days[].wind.warnings[].onset | string | — | Warning onset / effective time, ISO-8601 UTC. |
properties[].days[].wind.warnings[].expires | string | — | Warning expiration time, ISO-8601 UTC. |
properties[].days[].wind.note | string | — | The honesty statement for this day's specific state, written to be rendered beside the value. Render it: it is what stops warned: false being read as "it was not windy", and what keeps checked: false from being read as a finding. |
properties[].days[].ground_truth | object | — | Ground hail reports matched to the event. |
storm_groups | array | — | 🔴 A METEOROLOGICAL FACT, NOT AN INSURANCE DETERMINATION. Two event dates sharing a track id were produced by the same tracked storm system. This is a meteorological fact, NOT an occurrence determination under any insurance policy: occurrence is a contractual test this API does not and cannot apply. Computed across the WHOLE portfolio, not per property: two dates land in one group when any property links them, directly or transitively through another date in the group. Days with no track lineage are absent. |
storm_groups[].track_ids | array | — | Storm track ids that link the dates in this group. |
storm_groups[].event_dates | array | — | Event dates produced by the same tracked storm system (a meteorological fact, not an occurrence determination). |
data_as_of | string · nullable | — | The hail record's real watermark: the newest convective day that is provably complete (temporal_coverage.data_through). NOT an echo of the request's date_end: a window ending today reports an earlier date here. |
GET /v1/verify/coverage The hail record's coverage bounds (free) 25 fields
| Field | Type | Unit | Meaning |
|---|---|---|---|
data_version | object | — | See Data version. |
record | string | — | Which record the block describes (radar_detections). One of: radar_detections. |
archive_start | string | — | First fully covered storm day of the record. |
data_through | string | — | Newest storm day provably complete. |
latest_scan_time | string · nullable | — | Newest radar scan in the record (UTC). |
spans | array | — | The covered spans, as read from the record. A span breaks at every day listed in the dark-day ledger (see the API-level note *Days withheld from coverage*), at a day holding no rows in a month that borders a hole in the record, at every day listed in coverage_exceptions, and (from the first full day of the scanner heartbeat) at any day the heartbeat does not attest complete. |
spans[].start | string | — | First day of the range (YYYY-MM-DD, inclusive). |
spans[].end | string | — | Last day of the range (YYYY-MM-DD, inclusive). |
uncovered_ranges_recent | array | — | Uncovered ranges inside the last 30 days, same shape as temporal_coverage.uncovered_ranges. |
coverage_exceptions | array | — | Every convective day our hail-record completeness audit recorded as incomplete, ascending ([] when none). Such a day is never counted covered: inside the record's range spans break at it and uncovered_ranges name it coverage_exception; a listed day already outside the record keeps its own reason. The list can only withhold coverage, never grant it. |
as_of | string | — | When the bounds were read. |
window | object | — | The temporal_coverage block for start_date..end_date. Present only when a window was requested. |
outcome_if_empty | string | — | Present only when a window was requested. One of: no_signal, insufficient_coverage. |
reports | object · nullable | — | The caller's monthly report allowance on the server's billing period (POST /v1/verify/report), read for free. issued is the count the allowance is enforced on. null when the allowance could not be read (never a zero). |
reports.period | string | — | Billing period key, YYYY-MM (UTC calendar month). |
reports.period_starts_at | string | — | Start of the current billing period (UTC). |
reports.period_ends_at | string | — | Exclusive: the first instant of the next period, when the allowance resets. |
reports.issued | integer | — | Counted reports issued this period. Duplicates and free re-issues are not counted. |
reports.included | integer · nullable | — | The plan's published monthly allowance. null on a plan with no published count (Enterprise, kind: fair_use). |
reports.kind | string | — | Allowance type: included (a published monthly count) or fair_use. One of: included, fair_use. |
reports.today | object | — | The per-UTC-day report ceiling. |
reports.today.issued | integer | — | Reports issued so far in the period (or today). |
reports.today.ceiling | integer | — | Daily report ceiling. |
reports.today.applies | boolean | — | False during the account's onboarding grace. |
request_id | string | — | Correlates this response with server logs; quote it in a support request. |
GET /v1/portfolio/impacts Which portfolio assets were impacted on a given date 32 fields
| Field | Type | Unit | Meaning |
|---|---|---|---|
date | string | — | Storm date the answer is for (YYYY-MM-DD; a storm day runs 12:00 to 12:00 UTC). |
radius_mi | number | mi | Search radius, miles. |
assets_checked | integer | — | Point assets checked against the day's detections. Excludes assets whose whole impact radius lies outside the contiguous-US boundary (never checked, never "unaffected"). |
truncated | boolean | — | True when the portfolio holds more point assets than limits.max_assets; only the first max_assets were checked. |
limits | object | — | Published caps on the request. |
limits.max_assets | integer | — | Most point assets one call checks. |
impacted_count | integer | — | Assets listed in impacts. |
outcome | string | — | The window-level answer. READ THIS, NOT events.length. signal: at least one event day. no_signal: nothing found AND every day of the window is on record (temporal_coverage.covered: full): the only state that may be worded as "no hail signal". insufficient_coverage: nothing found, but part of the window is not on record; say which days are missing (temporal_coverage.uncovered_ranges), never "no hail". A window the radar record holds nothing for (temporal_coverage.searchable: false) is answered by the verify grains from ground reports alone when the ground-report archive holds every day of it (temporal_coverage.radar_searched: false, ground_reports_searched: true): signal when a ground report was found (charged), otherwise insufficient_coverage; never no_signal, because radar was not consulted; and that answer is free (X-Credits-Cost: 0). Only a window NEITHER record can be searched for is a 422. One of: signal, no_signal, insufficient_coverage. |
temporal_coverage | object | — | See Temporal coverage. |
impacts | array | — | Portfolio assets with hail on the date. |
impacts[].asset_id | string | — | Your asset id, as uploaded. |
impacts[].name | string | — | Your asset name, as uploaded. |
impacts[].nearest_detection_m | number | m | Distance from the asset to the nearest qualifying detection, metres. |
impacts[].max_size_mm | number · nullable | mm | The largest CALIBRATED, ML-gated hail size (mm) across the geohash5 cells matched within the impact radius; a calibrated estimate of REPORTED surface size (empirical radar-to-report bands below the large-hail crossover, the trained quantile size model at and above it), still inferred from the storm aloft, not a stone that landed. The same per-cell figure GET /v1/hail/current answers as summary.max_size_mm and the portfolio.impact_summary webhook row carries for the same cells. NOT radar MESH (changed BL-271: it used to be the raw radar maximum within the radius). null when no matched cell clears the hail-detection confidence threshold: never 0, and no raw radar figure is substituted. The top-level max_size_basis states its basis machine-readably. Sizes are gated on the current hail-detection model's entry threshold for every date, including dates scored by earlier model generations; on those older dates a cell the scoring model would not itself have sized can carry a size, so treat a historical size here as a looser screen than GET /v1/hail/history and POST /v1/verify, which apply each generation's own threshold. |
impacts[].max_size_category | string · nullable | — | The largest per-cell size class across the matched cells, classed from the same calibrated per-cell estimate as max_size_mm (so the two cannot disagree); null when no matched cell clears the hail-detection confidence threshold. Sizes are gated on the current hail-detection model's entry threshold for every date, including dates scored by earlier model generations; on those older dates a cell the scoring model would not itself have sized can carry a size, so treat a historical size here as a looser screen than GET /v1/hail/history and POST /v1/verify, which apply each generation's own threshold. One of: pea, nickel, quarter, golf_ball, tennis_ball, baseball, softball. |
impacts[].max_size_method | string · nullable | — | Which method produced max_size_mm / max_size_category, for the cell that sets the maximum: model (the trained size model's median) or radar_band (the empirical radar-to-report size bands). null when max_size_mm is. Storm days before 2026-07-01 mostly carry radar_band at and above about 40 mm; see the *Size method* note in this API's description for the measured boundary. One of: model, radar_band. |
impacts[].ml_confidence | number · nullable | — | The maximum hail-detection model probability over the asset's matched cells, 0 to 1, rounded to 3 decimals: the peak Anvil probability of P(hail >= 0.75" / 19.05 mm within 15 km), the SAME quantity ml_probability carries elsewhere in this API. It is NOT a measure of how certain the system is that the event happened; a well-calibrated 0.29 means roughly a 29% chance of hail, not "29% sure". Named ml_confidence for historical reasons on this endpoint only. null when no matched detection carried a scored probability. SCALE-BOUND: stored probabilities are never rescored, so a value is comparable only to one from the SAME detect model generation; this row does not name that generation. |
impacts[].n_detections | integer | — | Radar hail detections counted once per scan and pixel location. |
max_size_basis | object | — | What every impacts[].max_size_mm is, machine-readably: the calibrated-size basis, identical to GET /v1/hail/current's summary.max_size_basis and the portfolio.impact_summary webhook's data.max_size_basis. Never describes raw radar MESH. |
max_size_basis.basis | string | — | Which size field impacts[].max_size_mm is: the calibrated, model-gated size. |
max_size_basis.quantity | string | — | What impacts[].max_size_mm measures: the calibrated, model-gated size estimate (never the raw radar field). |
max_size_basis.units | string | — | Units of impacts[].max_size_mm (mm). One of: mm. |
data_as_of | string | — | The radar hail record's watermark: the newest provably complete day (temporal_coverage.data_through). Never the requested date echoed back. |
service_area | object | — | Always present. Assets excluded because their whole 1-mile impact radius lies outside the contiguous-US boundary every detection is clipped to (a Windsor, Ontario, an Anchorage or a San Juan lot). They were NOT checked and are not counted in assets_checked: never read their absence from impacts as unaffected. excluded_count: 0 for an all-US portfolio. The same block the portfolio.impact_summary webhook sends as data.service_area. |
service_area.clipped_to | string | — | The boundary results were clipped to. One of: us_boundary. |
service_area.excluded_count | integer | — | Always complete. |
service_area.excluded_assets | array | — | The excluded assets, capped at 500 (see excluded_assets_truncated). |
service_area.excluded_assets[].asset_id | string | — | Your id for an asset that was NOT checked (outside the contiguous US). |
service_area.excluded_assets[].name | string · nullable | — | Your name for an asset that was NOT checked. |
service_area.excluded_assets_truncated | boolean | — | True when excluded_assets is cut at its cap; excluded_count is the full number. |
service_area.partially_covered_count | integer | — | Checked assets whose impact radius crosses the US border: checked on the contiguous-US side only. |
service_area.note | string | — | Fixed disclosure text. Says excluded assets lie outside the contiguous United States (Canada, Mexico, Alaska, Hawaii, US territories) and that their absence from impacts is not a finding of no hail. |
GET /v1/wind/swaths Storm-day wind swaths: URMA analysis and/or convective (reports + radar) layers 58 fields
| Field | Type | Unit | Meaning |
|---|---|---|---|
date | string | — | Storm date the answer is for (YYYY-MM-DD; a storm day runs 12:00 to 12:00 UTC). |
query | object | — | The resolved request. |
active | boolean | — | True when at least one band has area in the query area. |
outcome | string | — | READ THIS, not active. no_signal only for a complete day. One of: signal, no_signal, insufficient_coverage. |
max_gust_mph | number | mph | Analysis: highest storm-day max gust (mph) of any returned polygon. Convective/all: highest measured or estimated REPORT gust in the area (null when only damage, radar or analysis bands are present). |
bands | array | — | Convective/all: the blended max per band; each item adds bases (the layers present in that band). |
bands[].band_mph | integer | mph | Every point inside had a storm-day max analysed gust >= band_mph. One of: 40, 50, 58, 70, 80. |
bands[].label | string | — | Display label for the band. One of: gusty, damaging, severe, significant, destructive. |
bands[].area_km2 | number | km² | Geodesic area of the band inside the query area. |
bands[].max_gust_mph | number | mph | Analysis layer: highest storm-day max analysed gust within the band's polygons. Convective/all: highest measured or estimated report gust in the band; null for damage_report, radar_indicated and analysis areas. |
bands[].n_polygons | integer | — | Stored polygons unioned into this band. |
bands[].bases | array | — | Convective/all blended bands only: the layers with area in this band. One of: gust_report, damage_report, radar_indicated, analysis. |
bands[].geometry | object | — | GeoJSON Polygon or MultiPolygon (WGS84). |
layers | array | — | Convective/all only: the attributed polygons, one entry per basis. |
layers[].basis | string | — | What set this layer's bands: a measured or estimated gust report, a damage report, radar indication, or the wind analysis. One of: gust_report, damage_report, radar_indicated, analysis. |
layers[].speed_claimed | boolean | — | True only for gust_report; never read a speed off another basis. |
layers[].description | string | — | Plain-language description of the block. |
layers[].bands | array | — | Wind-speed bands present, strongest last. |
layers[].bands[].band_mph | integer | mph | Every point inside had a storm-day max analysed gust >= band_mph. One of: 40, 50, 58, 70, 80. |
layers[].bands[].label | string | — | Display label for the band. One of: gusty, damaging, severe, significant, destructive. |
layers[].bands[].area_km2 | number | km² | Geodesic area of the band inside the query area. |
layers[].bands[].max_gust_mph | number | mph | Analysis layer: highest storm-day max analysed gust within the band's polygons. Convective/all: highest measured or estimated report gust in the band; null for damage_report, radar_indicated and analysis areas. |
layers[].bands[].n_polygons | integer | — | Stored polygons unioned into this band. |
layers[].bands[].bases | array | — | Convective/all blended bands only: the layers with area in this band. One of: gust_report, damage_report, radar_indicated, analysis. |
layers[].bands[].geometry | object | — | GeoJSON Polygon or MultiPolygon (WGS84). |
at_point | object | — | Radius queries only (null otherwise). |
at_point.lat | number | degrees (WGS 84) | Latitude, decimal degrees (WGS 84). |
at_point.lon | number | degrees (WGS 84) | Longitude, decimal degrees (WGS 84). |
at_point.max_band_mph | integer | mph | Highest band whose polygon covers the point; null if none. |
at_point.basis | string | — | Convective/all: the layer that set max_band_mph. One of: gust_report, damage_report, radar_indicated, analysis. |
at_point.speed_claimed | boolean | — | Convective/all: true only when a gust report set the point's band. |
at_point.band_by_basis | object | — | Convective/all: the point's band in each requested layer (null if none). |
at_point.max_band_in_radius_mph | integer | mph | Highest band anywhere in the circle. |
at_point.evidence | object | — | Convective/all only: facts to set your own confidence bar. Built from the storm day's convective wind reports, de-duplicated as in GET /v1/wind/reports. Strict reading: basis = gust_report and nearest_gust_report.distance_miles small. Loose reading: any basis. Validation (2025-2026): about 68% of measured severe thunderstorm gusts fall in the ≥58 band at about 40% over-call. |
at_point.evidence.nearest_gust_report | object · nullable | — | Nearest measured or estimated gust report within search_radius_km of the point; null when none. |
at_point.evidence.nearest_gust_report.distance_km | number | km | Great-circle distance from the query point to the report, in kilometers (1 decimal). |
at_point.evidence.nearest_gust_report.distance_miles | number | mi | The same distance in statute miles (1 decimal). |
at_point.evidence.nearest_gust_report.gust_mph | number | mph | The report's gust speed in mph (the highest of its de-duplicated SPC/IEM copies). |
at_point.evidence.nearest_gust_report.measurement | string | — | measured | estimated | unknown (as filed) |
at_point.evidence.nearest_gust_report.event_time | string | — | When the gust was observed (UTC), as filed in the report. |
at_point.evidence.reports_in_radius | object | — | De-duplicated convective reports inside the request radius. |
at_point.evidence.reports_in_radius.gust_report | integer | — | Reports with a gust speed (any value). |
at_point.evidence.reports_in_radius.damage_report | integer | — | Damage-only reports (no speed). |
at_point.evidence.reports_in_radius.severe | integer | — | Gust ≥ 58 mph or damage-only. |
at_point.evidence.radius_miles | number | mi | The request radius in statute miles that reports_in_radius counts within. |
at_point.evidence.search_radius_km | integer | km | How far the nearest-gust search reaches (100). |
coverage | object | — | How much of the storm day's input the layer had. Where the radar sub-layer lacks a full day, the answer is insufficient_coverage, never "no wind". |
coverage.hours_used | integer | — | Hourly URMA analyses folded into the day max. |
coverage.hours_expected | integer | — | Analysis hours expected for the storm day. |
coverage.complete | boolean | — | At least 22 of 24 hourly analyses present. |
coverage.processed_at | string | — | When the day was last processed (UTC). |
coverage.source | string | — | Human-readable description of the source layer. |
coverage.source_key | string | — | The source this response was read from. One of: urma. |
coverage.archive_start | string | — | First fully covered storm day of the record. |
coverage.layers | array | — | Convective/all only: per basis: complete, hours_covered (convective: hours with radar storm objects; analysis: URMA hours), hours_expected, processed_at. |
data_as_of | string | — | When this storm day was processed. |
spatial_clip | object | — | See Spatial clip. |
analysis_note | string | — | The caveat for this source (see the endpoint description). Preserve it when displaying bands. |
GET /v1/wind/reports Ground-truth wind reports within a radius, de-duplicated, plus long-record aggregate 44 fields
| Field | Type | Unit | Meaning |
|---|---|---|---|
query | object | — | The resolved request (radius in both units, storm-date window, types, filters). |
reports | array | — | Wind reports in the radius. |
reports[].event_time | string | — | Report event time (UTC). |
reports[].storm_date | string | — | SPC storm day (1200-1200 UTC). |
reports[].lat | number | degrees (WGS 84) | Latitude, decimal degrees (WGS 84). |
reports[].lon | number | degrees (WGS 84) | Longitude, decimal degrees (WGS 84). |
reports[].distance_miles | number | mi | Distance from the search center, statute miles. |
reports[].distance_km | number | km | Distance from the search center, km. |
reports[].report_type | string | — | Report kind: measured gust, estimated gust, or damage. One of: tstm_wind_gust, tstm_wind_damage, non_tstm_wind_gust, non_tstm_wind_damage, marine_tstm_wind. |
reports[].convective | boolean | — | Thunderstorm-produced (false for gradient/synoptic wind). |
reports[].gust_mph | number | mph | Highest gust in the cluster, mph. Null for damage reports without a speed. |
reports[].gust_kt | number | — | gust_mph in knots. |
reports[].measurement | string | — | How the speed was obtained (see the endpoint description). One of: measured, estimated, unknown, damage_only. |
reports[].severe | boolean | — | gust_mph >= 58 (NWS severe criterion). False when no speed. |
reports[].location | string | — | NWS location text, e.g. "4 ESE Tryon". |
reports[].county | string | — | County named in the report. |
reports[].state | string | — | Two-letter US state code, or null. |
reports[].wfo | string | — | Issuing NWS forecast office. |
reports[].reporter | string | — | Report source (Mesonet, ASOS, Trained Spotter, Public, ...), when known. |
reports[].remarks | string | — | NWS free-text remarks (truncated to 500 characters). |
reports[].nws_product_id | string | — | NWS LSR text product id, when known. |
reports[].sources | array | — | Every feed that carried this report. One of: iem_lsr, spc_storm_reports. |
count | integer | — | Length of the capped reports list. |
aggregate | object | — | Cross-source de-duplicated summary of the long wind record within the search radius, rolled up per storm day. |
aggregate.report_count | integer | — | De-duplicated reports (gust + damage). |
aggregate.gust_report_count | integer | — | Gust reports in the window. |
aggregate.damage_report_count | integer | — | De-duplicated reports without a speed. |
aggregate.raw_report_count | integer | — | Pre-dedup row count (reveals the SPC/IEM duplication factor). |
aggregate.max_gust_mph | number | mph | Strongest gust reported, mph. |
aggregate.max_measured_gust_mph | number | mph | Highest gust whose NWS qualifier is "measured". |
aggregate.event_days | integer | — | Distinct storm days with at least one report. |
aggregate.severe_event_days | integer | — | Days with a thunderstorm gust >= 58 mph or a thunderstorm wind damage report. |
aggregate.pct_severe_days | number | — | Share of storm days in the window with a severe gust report (≥58 mph), percent. |
aggregate.severe_gust_threshold_mph | number | mph | Gust speed counted as severe, mph (58). |
aggregate.window_years | integer | — | Years in the climatology window. |
aggregate.window_start | string | — | First storm day the aggregate covers (never before the 2022-07-21 archive floor). |
aggregate.min_year | integer | — | First year in the climatology window. |
aggregate.max_year | integer | — | Last year in the climatology window. |
data_as_of | string · nullable | — | The newest ingested wind report time (UTC): nothing after it has been ingested. Null only when no report was ingested in the last 60 days AND the window ends before those 60 days. |
window_partially_ingested | boolean | — | True when the window extends past data_as_of (its storm days end at 12Z the next day), so reports for the later part may not be ingested yet. coverage.ingest_note says so. |
coverage | object | — | Archive bounds and the coverage caveat for this product, plus the ingest note on a partly ingested window. |
coverage.archive_start | string | — | First storm day in the archive (2022-07-21); continuous from here. |
coverage.note | string | — | Plain-language note on the block. |
coverage.ingest_note | string | — | Present only when window_partially_ingested is true. |
POST /v1/exports Bulk data export (async job): CSV, GeoJSON, or Parquet 9 fields
| Field | Type | Unit | Meaning |
|---|---|---|---|
job_id | string | — | Export job id; poll GET /v1/exports/{jobId}. |
status | string | — | Job state. One of: pending. |
poll_url | string | — | Path to poll for the job's status. |
data_version | object | — | See Data version. |
snapshot | object · nullable | — | See Export snapshot. |
temporal_coverage | object · nullable | — | See Temporal coverage. |
coverage | object | — | See Service-area coverage. |
spatial_clip | object | — | See Spatial clip. |
message | string | — | Human-readable text. Wording may change; never parse it. |
GET /v1/exports/{jobId} Poll an export job / fetch the finished file 26 fields
| Field | Type | Unit | Meaning |
|---|---|---|---|
job_id | string | — | Export job id; poll GET /v1/exports/{jobId}. |
status | string | — | Job state. One of: pending, processing, done, failed. |
params | object | — | The request as sent (data_version only when it was named). |
data_version | object · nullable | — | See Data version. |
snapshot | object · nullable | — | See Export snapshot. |
temporal_coverage | object · nullable | — | See Temporal coverage. |
coverage | object · nullable | — | See Service-area coverage. |
spatial_clip | object · nullable | — | See Spatial clip. |
outcome | string · nullable | — | Present when status is done (see VerificationOutcome): READ this, not row_count; no_signal only for a window fully on record. null when there is no radar coverage verdict (dataset: reports). One of: signal, no_signal, insufficient_coverage. |
row_count | integer | — | Present when status is done. |
truncated | boolean | — | Present when status is done. True if the dataset's row cap was hit. |
attribution | object · nullable | — | Present when status is done. CC BY 3.0 attribution for the CoCoRaHS share of a dataset: reports file. Non-null whenever any exported row has source containing "cocorahs"; null for every other file (SPC and IEM Local Storm Reports are U.S. government public domain and owe none) and for a job finished before this was recorded. CC BY 3.0 §4(a) requires the licence URI to accompany every copy you distribute, so preserve it if you redistribute the file. The same credit is inside the file when this is non-null: CSV opens with it as a # comment line (before the header row; skip # lines when parsing), GeoJSON carries a top-level attribution member, Parquet carries attribution and attribution_license_url key-value metadata. |
attribution.notice | string | — | Human-readable summary of what is being attributed. |
attribution.text | string | — | The required CoCoRaHS credit line. Preserve verbatim on redistribution. |
attribution.license_url | string | — | The CC BY 3.0 licence text. |
ml_provenance | object · nullable | — | See Model provenance. |
content_sha256 | string · nullable | — | Present when status is done. Lowercase hex SHA-256 of the delivered file; every later download is that same file. null for a job finished before it was recorded. |
retained_until | string · nullable | — | Present when status is done. The UTC date the stored copy of the file is deleted (400 days after generation); until then a poll re-signs the same file. null for a job finished before it was recorded. |
generated_at | string | — | When the file was generated (UTC). |
download_url | string | — | Signed download URL, valid for download_url_expires_in_seconds. |
download_url_expires_in_seconds | integer | s | Lifetime of download_url, seconds. |
error | string | — | Present when status is failed. One of: export_generation_failed. |
message | string | — | Present when status is failed. Human text; do not parse. |
failure_reason | string | — | Present when status is failed. generation_interrupted: the generating instance died (the job sat in processing past the 5-minute staleness threshold). One of: generation_failed, generation_interrupted. |
retryable | boolean | — | Present when status is failed. True while free generation attempts remain. |
retry_url | string | — | Present when retryable is true. Poll it to re-run generation free of charge. |