API documentation.
Integrate real-time hail detection, alerts, forecasts, and climatological risk into your applications.
Up and running in minutes.
Authenticate with your API key and start querying real-time hail data immediately.
Base URL
https://api.hailsentinel.com/v1 Authentication
All requests require a Bearer token in the Authorization header. Obtain your API key from the developer dashboard after signing up.
Rotating a key
Rotate a key from the console (Settings > API Keys > Rotate) at any time — you'll get a brand-new key with the same scopes, shown once. The old key isn't cut off immediately: it keeps working for a 30-day grace period so you have time to deploy the new one, then stops authenticating on its own.
Rate limits
Every API key is capped at 60 requests/minute. Plans differ by monthly query allowance and webhook access, not request rate. Rate limit headers are included in every response: X-RateLimit-Remaining, X-RateLimit-Reset. Repeated invalid-key attempts are locked out separately (by source IP and by the key being guessed) and return 429 too_many_failed_attempts.
Response format
All responses are JSON, returned as the resource directly (e.g. a cells array for detections, an hourly array for forecasts) — no envelope wrapper. Errors include a machine-readable error code and a human-readable message.
Idempotent retries
Pass an Idempotency-Key header (any string, up to 255 characters) on POST /v1/alerts/subscribe, /v1/webhooks, /v1/verify, /v1/exports, or /v1/portfolio/locations and a retry after a dropped connection is safe: the same key replayed within 24 hours returns the exact original response — Idempotent-Replay: true header included — instead of creating a second resource or billing you twice. A second request with the same key while the first is still in flight gets 409 idempotency_key_in_progress; briefly retry. Failed (5xx) attempts are never cached, so a retry after an infra hiccup runs fresh rather than replaying the failure for 24 hours.
Freshness & caching
Every market-data GET (hail/current, hail/forecast, hail/risk, hail/nowcast, outlooks/spc, warnings, hail/outlook, hail/swaths, storms, coverage, tiles, hail/history, events) returns a real data_timestamp (or an equivalently-named field where the source has its own freshness concept — data_as_of, fetched_at, model.run_time) and an ETag header. Send it back as If-None-Match on your next request and get 304 Not Modified with an empty body instead of re-downloading bytes you already have — a 304 is billed the same as the 200 it mirrors, so it saves bandwidth, not credits. Verified freshness: radar-fusion-derived endpoints (current conditions, history, swaths, storms, events, warnings, nowcast) refresh on the storm-scanner's real ~2-minute scan cadence, typically current within 5 minutes; hail/forecast and outlooks/spc run hourly (short-range-model-driven); hail/risk's climatology component refreshes weekly; hail/outlook (Day 2-7) refreshes daily. coverage is a static radar-geometry prior with no scan cadence, so it has no data_timestamp — not an oversight.
# Get current hail activity near Denver, CO
curl "https://api.hailsentinel.com/v1/hail/current\
?lat=39.7392&lon=-104.9903&radius_km=25" \
-H "Authorization: Bearer hs_live_abc123..." {
"active": true,
"detail": "standard",
"summary": {
"total_cells": 3,
"max_size_mm": 32,
"closest_cell_km": 4.2,
"max_size_category": "quarter"
},
"cells": [
{
"geohash5": "9xj64",
"lat": 39.7412,
"lon": -104.9876,
"distance_km": 4.2,
"age_seconds": 95,
"last_detected": "2026-02-28T18:42:00Z",
"scan_count": 4,
"hail": {
"estimated_size_mm": 32,
"ml_predicted_size_mm": 28,
"size_category": "quarter",
"confidence": 0.87
}
}
],
"query": { "lat": 39.7392, "lon": -104.9903, "radius_km": 25, "lookback_minutes": 30 }
} Secure API access.
Every request is authenticated with a Bearer token tied to your account. Separate keys for development and production.
Prefixed with hs_live_. Use in production environments. Requests count against your billing quota and return real data.
Prefixed with hs_test_. Validate your authentication and integration wiring against the /v1/health endpoint, free of charge. Data endpoints require a live key.
# Include your API key in the Authorization header
curl "https://api.hailsentinel.com/v1/hail/current?lat=39.7392&lon=-104.9903" \
-H "Authorization: Bearer hs_live_a1b2c3d4e5f6..." API reference.
46 endpoints across 12 groups — real-time detection, alerting, historical data, climatological risk, and account usage.
Hail detection
/v1/hail/current 15 credits (25 with detail=advanced) Returns active hail cells within a given radius of a location (last 30 minutes). Data is sourced from multi-source radar composites processed through our detection pipeline.
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
lat | number | Yes | Latitude (-90 to 90) |
lon | number | Yes | Longitude (-180 to 180) |
radius_km | number | No | Search radius in kilometers. Default: 50, max: 150 |
detail | string | No | standard (default) or advanced for full radar, storm-structure, and impact fields |
Response example
{
"active": true,
"detail": "standard",
"summary": { "total_cells": 3, "max_size_mm": 32, "closest_cell_km": 4.2 },
"cells": [
{
"geohash5": "9xj64",
"lat": 39.7412,
"lon": -104.9876,
"distance_km": 4.2,
"hail": { "estimated_size_mm": 32, "confidence": 0.87 }
}
]
} /v1/hail/current/batch 8 credits per location (min 8) The same current-conditions check as GET /v1/hail/current, but for up to 50 locations in a single request — one BigQuery UNION ALL query, not 50 round trips. Pass an id per location to correlate results back to your own records; a ?estimate=true query param returns the credit cost for the batch without running it.
Request body
| Parameter | Type | Required | Description |
|---|---|---|---|
locations | array | Yes | Up to 50 objects: { lat, lon, radius_km?, id? } |
Response example
{
"results": [
{ "id": "loc_denver", "lat": 39.74, "lon": -104.99, "active": true, "max_size_mm": 41, "max_size_category": "golf_ball", "max_confidence": 0.87, "cell_count": 2, "closest_km": 4.2 },
{ "id": "loc_boulder", "lat": 40.01, "lon": -105.27, "active": false }
],
"count": 2,
"active_count": 1
} /v1/hail/forecast 5 credits Returns an hourly hail probability forecast for a location, up to 48 hours out. Combines our short-range hail model with our ML post-processing layer.
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
lat | number | Yes | Latitude (-90 to 90) |
lon | number | Yes | Longitude (-180 to 180) |
hours | number | No | Forecast window in hours. Range: 1–48, default: 24 |
Response example
{
"location": { "lat": 39.7392, "lon": -104.9903, "geohash4": "9xj6" },
"model": { "name": "hail-sentinel-shortrange", "run_time": "2026-02-28T18:00:00Z", "source": "hail-sentinel" },
"peak": {
"valid_time": "2026-02-28T20:00:00Z",
"hail_probability": 0.72,
"estimated_size_mm": 28,
"risk_category": "elevated"
},
"hourly": [
{
"valid_time": "2026-02-28T19:00:00Z",
"hail_probability": 0.45,
"estimated_size_mm": 19,
"risk_category": "moderate",
"risk_score": 58
}
],
"count": 12
} /v1/hail/history 15/25/40/60 credits — tiered by lookback window (≤30/90/180/365 days); 25/35/55/80 with detail=advanced Retrieves historical hail events near a location over a lookback window. Useful for insurance risk assessment, claims validation, and climatological analysis.
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
lat | number | Yes | Latitude (-90 to 90) |
lon | number | Yes | Longitude (-180 to 180) |
radius_km | number | No | Search radius in kilometers. Default: 25, max: 100 |
days | number | No | Lookback window in days. Default: 30, max: 365 |
min_size_mm | number | No | Minimum hail size to include, in mm |
limit | number | No | Max events to return. Default: 50, max: 200 |
Response example
{
"events": [
{
"storm_date": "2025-06-14",
"location": { "lat": 39.74, "lon": -104.98, "geohash5": "9xj64" },
"distance_km": 3.1,
"duration_minutes": 18,
"scan_count": 9,
"hail": { "estimated_size_mm": 44, "size_category": "golf ball", "confidence": 0.91 }
}
],
"count": 1,
"detail": "standard"
} /v1/hail/nowcast 10 credits Returns a short-term hail probability nowcast (T+15/30/45 minutes) for the nearest actively-tracked storm within range. One lead-conditioned ML model scored at three horizons — T+15/T+30 are production-quality; T+45 is flagged "experimental" in the response since it did not clear our internal accuracy gate.
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
lat | number | Yes | Latitude (-90 to 90) |
lon | number | Yes | Longitude (-180 to 180) |
radius_km | number | No | Search radius in kilometers. Default: 15, max: 50 |
Response example
{
"active": true,
"location": { "lat": 39.7392, "lon": -104.9903 },
"nearest_storm": {
"track_id": "c12df7b9-d249-43",
"distance_km": 1.4,
"current_probability": 0.64,
"data_timestamp": "2026-06-01T18:26:41Z",
"motion": { "speed_kmh": 35, "direction_deg": 220 }
},
"nowcast": {
"t15": { "minutes": 15, "probability": 0.23, "quality": "production", "note": null },
"t30": { "minutes": 30, "probability": 0.13, "quality": "production", "note": null },
"t45": { "minutes": 45, "probability": 0.06, "quality": "experimental", "note": "Did not clear the internal per-lead accuracy gate..." }
},
"model_version": "anvil-nowcast-2"
} /v1/hail/outlook 15 credits Returns the same 7-day blended hail probability outlook the mobile app's Forecast tab shows for a location — a daily blend of our convective outlook product, our short-range hail model, and climatology, with an hourly strip for Day 1. Coverage is on-demand, not full-CONUS; the nearest currently-monitored cell within range is used, and covered: false is returned honestly when nothing is in range.
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
lat | number | Yes | Latitude (-90 to 90) |
lon | number | Yes | Longitude (-180 to 180) |
radius_km | number | No | Search radius for the nearest covered cell. Default: 50, max: 200 |
Response example
{
"location": { "lat": 39.7392, "lon": -104.9903 },
"covered": true,
"cell": { "geohash4": "9xj3", "distance_km": 11.9 },
"outlook": [
{
"day": 1,
"forecast_date": "2026-06-01",
"hail_probability": 2,
"confidence": { "score": 0.5, "state": null },
"primary_source": "hrrr",
"components": { "spc_pct": null, "hrrr_pct": 1, "climo_pct": 2 },
"hourly": [ 0.8, 0.3, 0.3, null ]
},
{ "day": 2, "...": "5 more days, no hourly field" }
],
"data_as_of": "2026-06-01T23:49:43Z"
} /v1/storms 15 credits (25 with detail=advanced) Returns currently-active storm cells within a given radius — one entry per tracked cell (track_id), with position, motion, ML probability, hail size band, and trend. The same object-tracking pipeline that drives mobile alerts. detail=advanced adds a raw radar-signal block.
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
lat | number | Yes | Latitude (-90 to 90) |
lon | number | Yes | Longitude (-180 to 180) |
radius_km | number | No | Search radius in kilometers. Default: 50, max: 150 |
detail | string | No | standard (default) or advanced for raw radar fields |
Response example
{
"active": true,
"detail": "standard",
"data_as_of": "2026-07-03T00:50:41Z",
"summary": { "total_cells": 11, "closest_cell_km": 2.1, "max_size_category": "golf_ball" },
"cells": [
{
"track_id": "cbefb72c-ee34-42",
"lat": 37.4716, "lon": -101.7027,
"distance_km": 2.1,
"scan_time": "2026-07-03T00:50:41Z",
"motion": { "speed_kmh": 68.1, "direction_deg": 253.3 },
"hail": { "estimated_size_mm": 28.7, "size_category": "quarter", "ml_probability": 0.56 },
"trend": "rapidly_intensifying",
"growth": { "size_growth_rate_mm_per_min": 1.38 },
"track": { "duration_min": 7.98, "peak_estimated_size_mm": 35.2 }
},
{ "...": "10 more cells, nearest first" }
],
"query": { "lat": 37.49, "lon": -101.7, "radius_km": 50, "lookback_minutes": 30 }
} /v1/storms/{track_id} 10 credits (18 with detail=advanced) Full point-by-point history for one tracked storm cell, scoped to a single UTC day. track_id comes from GET /v1/storms; pass date for a track that occurred on a day other than today.
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
track_id | string | Yes | Path parameter — the track_id from GET /v1/storms |
date | string | No | UTC date the track occurred on, YYYY-MM-DD. Default: today |
detail | string | No | standard (default) or advanced for raw radar fields |
Response example
{
"track_id": "cbefb72c-ee34-42",
"date": "2026-07-03",
"detail": "standard",
"data_as_of": "2026-07-03T00:50:41Z",
"summary": {
"scan_count": 5,
"first_detected": "2026-07-03T00:42:42Z",
"last_detected": "2026-07-03T00:50:41Z",
"duration_min": 7.98,
"peak_estimated_size_mm": 35.2,
"peak_size_category": "quarter",
"peak_ml_probability": 0.61,
"size_km2": 418.1
},
"points": [
{ "scan_time": "2026-07-03T00:42:42Z", "lat": 37.5092, "lon": -101.5902, "...": "motion, hail, trend, growth" },
{ "...": "4 more points, ordered oldest to newest" }
]
} /v1/coverage 5 credits A physics-geometry radar detection-quality prior for a location — a transparency differentiator no competitor publishes. Whether radar beam geometry can sample the hail-growth layer here (sample coverage, beam blockage, dual-pol availability), NOT an empirically-measured detection rate. Same hdqi score and Excellent/Good/Fair/Limited band the mobile app shows for the same cell. The same detection_quality block is also available inline on /v1/hail/current and /v1/hail/history in detail=advanced mode — use this endpoint to check coverage BEFORE querying hail data somewhere.
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
lat | number | Yes | Latitude (-90 to 90) |
lon | number | Yes | Longitude (-180 to 180) |
Response example
{
"location": { "lat": 39.7392, "lon": -104.9903 },
"covered": true,
"month": 7,
"detection_quality": {
"hdqi": 0.993,
"detection_word": "excellent",
"resolution": "cell",
"components": {
"sample_coverage": 1,
"beam_blockage": 1,
"dual_pol_availability": 1
},
"nearest_radar": { "id": "FTG", "distance_km": 39.7 },
"beam_height_m": 2136,
"freezing_level_m": 4846
},
"note": "A physics-geometry prior — not an empirically-measured detection rate."
} /v1/hail/swaths 30 credits The flagship endpoint: real GeoJSON MultiPolygon footprints for a storm day, built from per-pixel detections (not point+radius). Buffered and unioned separately per size tier — nickel/quarter/golf_ball/tennis_ball/baseball/softball — the same bands used across every other endpoint. A tier with no coverage that day is simply absent, never a fake empty polygon. Pass format=geojson for a standard FeatureCollection instead of the default envelope.
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
date | string | Yes | Storm day, YYYY-MM-DD |
bbox | string | No | minLon,minLat,maxLon,maxLat. Mutually exclusive with state. Default: whole CONUS |
state | string | No | 2-letter USPS state code. Mutually exclusive with bbox |
min_size_mm | number | No | Floor tier to compute/return. Default: 19 (nickel) |
simplify_m | number | No | Simplification tolerance in meters, clamped to [0, 5000]. Default: 500 |
format | string | No | geojson for a raw FeatureCollection instead of the structured envelope |
Response example
{
"date": "2026-07-02",
"query": { "bbox": { "minLat": 36.5, "maxLat": 38.5, "minLon": -102.5, "maxLon": -100.5 }, "state": null, "min_size_mm": 19, "simplify_m": 500 },
"active": true,
"n_detections": 18031,
"max_size_mm": 65.7,
"data_as_of": "2026-07-02",
"tiers": [
{ "threshold_mm": 19, "category": "nickel", "area_km2": 3287.1, "geometry": { "type": "MultiPolygon", "...": "..." } },
{ "threshold_mm": 25, "category": "quarter", "area_km2": 1938.9, "geometry": "..." },
{ "threshold_mm": 38, "category": "golf_ball", "area_km2": 647.7, "geometry": "..." },
{ "...": "tennis_ball, baseball — softball absent (max was 65.7mm)" }
],
"geometry_validity_note": "BigQuery GEOGRAPHY values are guaranteed non-self-intersecting by construction..."
} /v1/events 15 credits One summary row per storm day in a date range that had any hail activity in the region — cell_count (distinct tracked storm objects), max_size_mm, n_detections, affected_area_km2, and centroid. A coarse per-day rollup for browsing event history over a season or year, not a per-point join — range capped at 365 days. affected_area_km2 uses a convex-hull area, not the tiered buffered-union footprint /v1/hail/swaths computes (measured 12x cheaper); each event-day includes a swath_url to fetch the precise footprint for that one day on demand.
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
date_start | string | Yes | Start of the range, YYYY-MM-DD |
date_end | string | Yes | End of the range, YYYY-MM-DD (inclusive). Range capped at 365 days |
bbox | string | No | minLon,minLat,maxLon,maxLat. Mutually exclusive with state. Default: whole CONUS |
state | string | No | 2-letter USPS state code. Mutually exclusive with bbox |
Response example
{
"date_range": { "start": "2026-07-01", "end": "2026-07-02" },
"area_method": "convex_hull",
"region": { "bbox": "-102.5,36.5,-100.5,38.5" },
"events": [
{ "event_date": "2026-07-01", "cell_count": 28, "max_size_mm": 56.4, "n_detections": 16131, "affected_area_km2": 4318.3, "centroid": { "lat": 38.0216, "lon": -101.5592 }, "swath_url": "/v1/hail/swaths?date=2026-07-01&bbox=-102.5,36.5,-100.5,38.5" },
{ "event_date": "2026-07-02", "cell_count": 49, "max_size_mm": 65.7, "n_detections": 88188, "affected_area_km2": 9552.5, "centroid": { "lat": 37.6881, "lon": -101.4277 }, "swath_url": "/v1/hail/swaths?date=2026-07-02&bbox=-102.5,36.5,-100.5,38.5" }
],
"event_count": 2,
"data_as_of": "2026-07-02"
} Radar tiles
/v1/tiles/scan 3 credits Discovers the PMTiles radar tile archives for a storm day — the same pipeline output the mobile app's live radar view renders from. Each frame is one scan's reflectivity+hail composite (~2-4 min cadence during active weather). The underlying bucket is publicly readable — this endpoint's value is authenticated, metered discovery of which scan_id/date is current, not access control. Fetch url directly with any PMTiles-compatible map client (e.g. maplibre-gl + @maplibre/pmtiles).
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
date | string | No | Storm day, YYYY-MM-DD. Default: the latest available day |
Response example
{
"layer": "scan",
"date": "2026-07-02",
"available": true,
"data_as_of": "2026-07-03T03:08:40Z",
"frame_count": 48,
"frames": [
{
"scan_id": "20260703_013039",
"scan_time": "2026-07-03T01:30:39Z",
"url": "https://storage.googleapis.com/hailsentinel-tiles/mrms/scans/2026-07-02/20260703_013039.pmtiles",
"content_type": "application/vnd.pmtiles",
"cache_control": "public, max-age=31536000, immutable",
"max_prob": 0,
"max_size_mm": 43.9,
"storms": [ { "lat": 40.28, "lon": -101.16, "mm": 43.9 } ]
},
{ "...": "47 more frames" }
],
"note": "Public, immutable, cache indefinitely — fetch url directly with a PMTiles client."
} /v1/tiles/daily 2 credits Discovers the single daily-peak PMTiles archive for a date (each pixel is the day's peak MESH value) — the fallback layer when no live scans are available. Same public-bucket, no-proxy model as GET /v1/tiles/scan.
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
date | string | No | Date, YYYY-MM-DD. Default: today (UTC) |
Response example
{
"layer": "daily",
"date": "2026-06-11",
"available": true,
"url": "https://storage.googleapis.com/hailsentinel-tiles/mrms/daily/2026-06-11.pmtiles",
"content_type": "application/vnd.pmtiles",
"cache_control": "public, max-age=120",
"size_bytes": 1468464,
"note": "Public, no proxy — unlike scan frames, can be updated intraday; respect cache_control."
} Verification
/v1/verify 50 credits Date-of-loss verification — the claims workflow endpoint. Returns one verdict per storm day in the window that had any signal within 5 miles: hit (a radar detection corroborated by a ground report, or a strong uncorroborated detection), marginal (a weak or uncorroborated detection, or a ground report with no radar corroboration — a real, honest gap), or miss (neither signal). Every response embeds the same detection_quality block GET /v1/coverage returns, so a verdict is always readable alongside its radar-geometry context — a report with no radar corroboration in a low-HDQI area reads very differently than the same gap somewhere with excellent coverage. Deterministic and reproducible: identical inputs return an identical verdict, stamped with an explicit methodology.version.
Request body
| Parameter | Type | Required | Description |
|---|---|---|---|
lat | number | Yes | Latitude (-90 to 90) |
lon | number | Yes | Longitude (-180 to 180) |
date_start | string | Yes | Start of the investigation window, YYYY-MM-DD |
date_end | string | Yes | End of the investigation window, YYYY-MM-DD (inclusive). Range capped at 90 days |
Response example
{
"location": { "lat": 37.52, "lon": -101.89 },
"date_range": { "start": "2026-07-02", "end": "2026-07-02" },
"radius_mi": 5,
"methodology": { "version": "verify-v1", "description": "hit = radar detection corroborated...", "bands_mi": "[...]" },
"detection_quality": { "hdqi": 0.437, "detection_word": "fair", "...": "..." },
"events": [
{
"event_date": "2026-07-02",
"verdict": "hit",
"band": "at_location",
"max_size_mm": 52.8,
"ml_confidence": 0.639,
"n_detections": 5296,
"ground_truth": {
"corroborated": true,
"nearest_reports": [ { "source": "iem_lsr", "max_size_mm": 25.4, "distance_m": 0 } ]
}
}
],
"event_count": 1,
"data_as_of": "2026-07-02"
} /v1/verify/report 100 credits Starts generation of a branded PDF version of the verdict above: verdict table, a schematic detection-location snippet, a strictly factual summary paragraph (Gemini-authored restatement of the computed facts only — never invented specifics), the full methodology, and a verification_hash so the PDF can be checked against a re-run of the API later. This is an async job — the call returns 202 immediately with a report_id and poll_url; poll GET /v1/verify/report/{reportId} until status is done or failed. Reports are retained 400 days per business, then deleted (the underlying verdict is deterministic and reproducible — the PDF is a convenience artifact, not the system of record).
Request body
| Parameter | Type | Required | Description |
|---|---|---|---|
lat | number | Yes | Latitude (-90 to 90) |
lon | number | Yes | Longitude (-180 to 180) |
date_start | string | Yes | Start of the investigation window, YYYY-MM-DD |
date_end | string | Yes | End of the investigation window, YYYY-MM-DD (inclusive). Range capped at 90 days |
Response example
{
"report_id": "816c756f-49aa-4ae2-a8a2-75c874e4d958",
"status": "pending",
"poll_url": "/v1/verify/report/816c756f-49aa-4ae2-a8a2-75c874e4d958",
"message": "Report generation queued. Poll poll_url until status is \"done\" or \"failed\"."
} /v1/verify/report/{reportId} Free Polls a report job. done includes a pdf_url — a v4 signed GCS URL valid for 15 minutes; poll again after it expires to mint a fresh one (no re-generation). failed includes an error. Free — the generation cost is charged once, on the initiating POST. Scoped to your business; another business's report_id returns 404, never a cross-tenant leak.
Path parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
reportId | string | Yes | The report_id returned by POST /v1/verify/report |
Response example
{
"report_id": "816c756f-49aa-4ae2-a8a2-75c874e4d958",
"status": "done",
"params": { "lat": 37.52, "lon": -101.89, "date_start": "2026-07-02", "date_end": "2026-07-02" },
"result_summary": { "event_count": 1, "verdicts": [ { "event_date": "2026-07-02", "verdict": "hit" } ] },
"verification_hash": "33d4ad9b5cf86af52b7c8e58f70bbba441b4ca0d116f028cec46a90d3c8010fa",
"generated_at": "2026-07-03T16:34:50.942Z",
"data_as_of": "2026-07-02",
"pdf_url": "https://storage.googleapis.com/hailsentinel-verification-reports/reports/.../816c756f....pdf?X-Goog-Signature=...",
"pdf_url_expires_in_seconds": 900
} Bulk export
/v1/exports 40 credits Exports raw rows for detections, swaths, or reports over a date range and region, in csv, geojson, or parquet format. Available on the Portfolio plan and above (self-serve) or any Enterprise tier — other plans get 403 feature_not_available. This is an async job — the call returns 202 immediately with a job_id and poll_url; poll GET /v1/exports/{jobId} until status is done or failed. Date range capped at 31 days; detections rows capped at 250,000 and reports at 50,000 per job (truncated: true if the cap was hit — never silent). swaths returns one row per storm-day/MESH-tier convex-hull footprint, the same fast method GET /v1/events uses, since a multi-day export must stay well inside Cloud Run's 30s request timeout.
Request body
| Parameter | Type | Required | Description |
|---|---|---|---|
dataset | string | Yes | detections, swaths, or reports |
date_start | string | Yes | Start of the range, YYYY-MM-DD |
date_end | string | Yes | End of the range, YYYY-MM-DD (inclusive). Range capped at 31 days |
bbox | string | No | minLon,minLat,maxLon,maxLat. Mutually exclusive with state. Defaults to whole-CONUS if neither is given |
state | string | No | Two-letter US state code. Mutually exclusive with bbox |
format | string | Yes | csv, geojson, or parquet |
Response example
{
"job_id": "9fe3ae22-0cfe-4d25-8d31-a82c410d376a",
"status": "pending",
"poll_url": "/v1/exports/9fe3ae22-0cfe-4d25-8d31-a82c410d376a",
"message": "Export queued. Poll poll_url until status is \"done\" or \"failed\"."
} /v1/exports/{jobId} Free Polls an export job. done includes a download_url — a v4 signed GCS URL valid for 15 minutes; poll again after it expires to mint a fresh one (no re-generation). failed includes an error. Free — the generation cost is charged once, on the initiating POST. Scoped to your business; another business's job_id returns 404, never a cross-tenant leak.
Path parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
jobId | string | Yes | The job_id returned by POST /v1/exports |
Response example
{
"job_id": "9fe3ae22-0cfe-4d25-8d31-a82c410d376a",
"status": "done",
"params": { "dataset": "detections", "date_start": "2026-07-01", "date_end": "2026-07-02", "bbox": "-102.5,36.5,-100.5,38.5", "format": "csv" },
"row_count": 104319,
"truncated": false,
"generated_at": "2026-07-03T18:12:04.221Z",
"download_url": "https://storage.googleapis.com/hailsentinel-verification-reports/exports/.../9fe3ae22....csv?X-Goog-Signature=...",
"download_url_expires_in_seconds": 900
} Alerts
/v1/alerts 1 credit Lists all alert subscriptions for your business, cursor-paginated.
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
limit | integer | No | Page size. Default 25, max 100 |
cursor | string | No | A subscription ID from a previous page's next_cursor |
Response example
{
"subscriptions": [
{
"id": "sub_8f7e6d5c4b3a",
"name": "Denver warehouse",
"lat": 39.7392,
"lon": -104.9903,
"radius_km": 25,
"min_size_mm": 25,
"webhook_url": "https://example.com/webhooks/hail",
"status": "active",
"trigger_count": 3,
"created_at": "2026-05-01T12:00:00.000Z",
"last_triggered_at": "2026-07-01T22:14:03.000Z"
}
],
"count": 1
} /v1/alerts/subscribe 2 credits Creates a new alert subscription for a location. When hail exceeding your threshold is detected, a webhook is dispatched to your specified URL. The response includes a one-time webhook_secret for HMAC verification — store it.
Request body
| Parameter | Type | Required | Description |
|---|---|---|---|
lat | number | Yes | Latitude (-90 to 90) |
lon | number | Yes | Longitude (-180 to 180) |
webhook_url | string | Yes | HTTPS URL to receive webhook POST requests |
radius_km | number | No | Alert radius in kilometers. Default: 25, max: 100 |
min_mesh_mm | number | No | Minimum hail size in mm to trigger the alert. Default: 25 |
name | string | No | Human-readable label for the subscription |
Response example
{
"id": "sub_8f7e6d5c4b3a",
"name": "Denver warehouse",
"lat": 39.7392,
"lon": -104.9903,
"radius_km": 25,
"min_size_mm": 25,
"webhook_url": "https://example.com/webhooks/hail",
"webhook_secret": "whsec_3a1f...(store this)",
"status": "active"
} /v1/alerts/{id} 1 credit Retrieves details of an alert subscription, including its configuration and a list of recently triggered alerts.
Path parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Subscription ID (e.g., sub_8f7e6d5c4b3a) |
Response example
{
"id": "sub_8f7e6d5c4b3a",
"name": "Denver warehouse",
"lat": 39.7392,
"lon": -104.9903,
"radius_km": 25,
"min_size_mm": 25,
"webhook_url": "https://example.com/webhooks/hail",
"status": "active",
"trigger_count": 3,
"created_at": "2026-02-28T14:30:00Z",
"last_triggered_at": "2026-02-27T21:15:00Z"
} /v1/alerts/{id} 2 credits Partial update — only fields present in the body are changed. webhook_secret and trigger history (trigger_count, last_triggered_at) are never touched, regardless of which fields you update. webhook_url is re-validated (HTTPS + a DNS/private-IP check) the same way PUT /v1/webhooks/{id} re-validates. Set status to paused to stop deliveries without deleting the subscription, or back to active to resume.
Request body (all optional — only sent fields change)
| Parameter | Type | Required | Description |
|---|---|---|---|
name | string | No | Non-empty, truncated to 100 characters |
radius_km | number | No | 1-100 |
min_size_mm | number | No | Minimum hail size in mm (>= 10). min_mesh_mm accepted as a legacy alias |
webhook_url | string | No | HTTPS URL, re-validated |
status | string | No | active or paused |
Response example
{
"id": "sub_8f7e6d5c4b3a",
"name": "Denver warehouse",
"lat": 39.7392,
"lon": -104.9903,
"radius_km": 25,
"min_size_mm": 25,
"webhook_url": "https://example.com/webhooks/hail",
"status": "active",
"trigger_count": 3,
"created_at": "2026-02-28T14:30:00Z",
"last_triggered_at": "2026-02-27T21:15:00Z"
} /v1/alerts/{id} 1 credit Cancels an active alert subscription. Webhooks will no longer be dispatched for this subscription after cancellation.
Path parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Subscription ID to cancel |
Response example
{
"success": true
} Portfolio
/v1/portfolio/locations 10 credits Bulk-creates or updates point locations for enterprise portfolios — a property/HOA portfolio, a dealership fleet, a facilities list. Accepts either format: "json" (an array of row objects) or format: "csv" (raw CSV text with a header row; a tags cell uses | as its sub-separator since , is the field separator). Every row is validated and reported individually — rejected rows (bad coordinates, missing name, outside CONUS, quota exceeded) are itemized in rejected, never silently dropped. Include a row’s existing id to update it in place (no additional quota consumed); omit id to create a new location. Writes land in the same monitoredAreas collection the console’s Territories page manages.
Request body
| Parameter | Type | Required | Description |
|---|---|---|---|
format | string | Yes | json or csv |
data | array | string | Yes | An array of row objects (format=json) or a raw CSV string (format=csv). Row fields: id (optional, for update), name, lat, lon, address, tags |
Response example
{
"total_rows": 1000,
"accepted_count": 998,
"rejected_count": 2,
"accepted": [
{ "row_index": 0, "id": "hZM80QPMudqmLaedK79B", "name": "Property 0", "action": "created" },
{ "...": "..." }
],
"rejected": [
{ "row_index": 417, "name": "Bad Row", "reason": "[999, 999] is outside CONUS bounds (24..50 lat, -125..-66 lon)" }
],
"quota": { "point_locations_before": 0, "point_locations_after": 998, "limit": 1000 }
} /v1/portfolio/locations 1 credit Lists point locations for the business, ordered by name, cursor paginated.
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
limit | integer | No | Page size. Default 50, max 200 |
cursor | string | No | A location ID from a previous page's response to continue from |
Response example
{
"locations": [
{ "id": "04TVnfBvPk64ZhcczAOW", "name": "Property 0", "lat": 30, "lon": -110, "address": "0 Main St", "tags": [ "priority" ], "created_at": "2026-07-03T17:26:32.277Z", "updated_at": "2026-07-03T17:26:32.277Z" }
],
"count": 1,
"next_cursor": "xIdSRP8m3LCHSaehv8yk"
} /v1/portfolio/locations 5 credits Deletes point locations by ID (max 2,000 per request) and decrements the plan quota accordingly. IDs that don’t exist, or that belong to a polygon (canvassing territory) rather than a point location, are reported in not_found rather than failing the whole call.
Request body
| Parameter | Type | Required | Description |
|---|---|---|---|
ids | string[] | Yes | Location IDs to delete |
Response example
{
"deleted_count": 2,
"not_found_count": 1,
"deleted": [ "EW1IxPagDKJTqzH17MSC", "04TVnfBvPk64ZhcczAOW" ],
"not_found": [ "totally-fake-id" ]
} /v1/portfolio/impacts 20 credits Checks every point location in your portfolio against radar detections for one storm day, in a SINGLE BigQuery spatial join (the full asset list is inlined as one query parameter and joined via ST_DWITHIN — not one query per asset). Only assets with a detection within 1 mile appear in impacts; a calm day for a given asset simply omits it, never a fake zero-confidence row. assets_checked (portfolio size) and impacted_count (assets that actually appear) are returned separately so the two are never confused.
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
date | string | Yes | Storm day to check, YYYY-MM-DD |
limit | integer | No | Page size over the impacts result. Default 50, max 500 |
cursor | string | No | An asset_id from a previous page's response to continue from |
Response example
{
"date": "2026-07-02",
"radius_mi": 1,
"assets_checked": 3,
"impacted_count": 2,
"impacts": [
{ "asset_id": "UnILcP9AVXfwYluTwdTN", "name": "Nearby Property", "nearest_detection_m": 0, "max_size_mm": 25.5, "ml_confidence": 0.577, "n_detections": 151 },
{ "asset_id": "d1M4Zqg6AVDsdZKrldmc", "name": "Real HIT Property", "nearest_detection_m": 709.6, "max_size_mm": 25.5, "ml_confidence": 0.615, "n_detections": 129 }
],
"data_as_of": "2026-07-02"
} Areas
/v1/areas 3 credits Creates a monitored area — a polygon (canvassing/monitoring territory: arbitrary GeoJSON, validated for topology, simplified under 500 vertices, billed by true area) or a point (a fixed asset, billed at a nominal flat rate). Maps directly onto the same businesses/{id}/monitoredAreas collection the B2B console’s Territories page manages — same doc shape, same per-plan mi² quota. The created area is automatically projected into the alert pipeline’s location index, so it is evaluated by the exact same hail-alert matching code path as a console-created area.
Request body
| Parameter | Type | Required | Description |
|---|---|---|---|
kind | string | Yes | polygon or point |
name | string | Yes | Max 100 characters |
geometry | object | kind=polygon | GeoJSON Polygon, CONUS bounds, outer ring only |
point | object | kind=point | { lat, lng } |
real_time | boolean | No | Polygon-kind only — counts against the plan’s real-time-polygon limit |
purpose | string | No | Polygon-kind only — monitoring, canvassing, or both |
group_id | string | No | Point-kind only — groups sub-zones under a shared property (max 8/group) |
Response example
{
"id": "k3Jm8QpXvR2wLtZbNc7Y",
"kind": "polygon",
"name": "North Denver Canvass Zone",
"description": null,
"tags": [ "q3-campaign" ],
"priority": "standard",
"group_id": null,
"real_time": true,
"purpose": "canvassing",
"bbox": [ -104.99, 39.73, -104.98, 39.74 ],
"centroid_geohash4": "9xj6",
"area_sq_mi": 0.3671,
"version": 1,
"point": null,
"created_at": "2026-07-03T18:12:04.221Z",
"updated_at": "2026-07-03T18:12:04.221Z"
} /v1/areas 1 credit Lists monitored areas for the business, newest-updated first, plus the usage counters against the plan quota.
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
kind | string | No | Filter to polygon or point |
group_id | string | No | Filter to a sub-zone group |
limit | integer | No | Page size. Default 50, max 200 |
cursor | string | No | An area ID from a previous page's next_cursor |
Response example
{
"areas": [
{ "id": "k3Jm8QpXvR2wLtZbNc7Y", "kind": "polygon", "name": "North Denver Canvass Zone", "area_sq_mi": 0.3671, "version": 1 }
],
"count": 1,
"usage": { "polygon_count": 1, "point_count": 0, "real_time_polygon_count": 1, "total_area_sq_mi": 0.3671 }
} /v1/areas/{id} 1 credit Returns area metadata (not the full polygon geometry — see the geometry endpoint below).
Path parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Area ID |
Response example
{
"id": "k3Jm8QpXvR2wLtZbNc7Y",
"kind": "polygon",
"name": "North Denver Canvass Zone",
"description": null,
"tags": [ "q3-campaign" ],
"priority": "standard",
"group_id": null,
"real_time": true,
"purpose": "canvassing",
"bbox": [ -104.99, 39.73, -104.98, 39.74 ],
"centroid_geohash4": "9xj6",
"area_sq_mi": 0.3671,
"version": 1,
"point": null,
"created_at": "2026-07-03T18:12:04.221Z",
"updated_at": "2026-07-03T18:12:04.221Z"
} /v1/areas/{id}/geometry 1 credit Lazily loads the full outer-ring coordinates for a polygon-kind area (omitted from list/get responses to keep those payloads small). Returns { "geometry": null } for point-kind areas.
Path parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Area ID |
Response example
{
"geometry": {
"type": "Polygon",
"coordinates": [ [ [ -104.99, 39.73 ], [ -104.98, 39.73 ], [ -104.98, 39.74 ], [ -104.99, 39.74 ], [ -104.99, 39.73 ] ] ]
},
"updated_at": "2026-07-03T18:12:04.221Z"
} /v1/areas/{id} 3 credits Partial update — only fields present in the body are changed. Requires expected_version (optimistic locking) — a stale version returns 409. Replacing geometry re-runs the same validation/simplification as create and re-bills the mi² delta.
Request body
| Parameter | Type | Required | Description |
|---|---|---|---|
expected_version | integer | Yes | Current version from a prior GET/create response |
name | string | No | |
geometry | object | No | Polygon-kind only — full replacement |
point | object | No | Point-kind only — full replacement |
Response example
{
"id": "k3Jm8QpXvR2wLtZbNc7Y",
"kind": "polygon",
"name": "North Denver Canvass Zone",
"description": null,
"tags": [ "q3-campaign" ],
"priority": "standard",
"group_id": null,
"real_time": true,
"purpose": "canvassing",
"bbox": [ -104.99, 39.73, -104.98, 39.74 ],
"centroid_geohash4": "9xj6",
"area_sq_mi": 0.3671,
"version": 1,
"point": null,
"created_at": "2026-07-03T18:12:04.221Z",
"updated_at": "2026-07-03T18:12:04.221Z"
} /v1/areas/{id} 2 credits Deletes the area, its geometry sub-doc, its alert-pipeline projection, and decrements plan usage.
Path parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Area ID |
Response example
{
"success": true,
"id": "k3Jm8QpXvR2wLtZbNc7Y"
} Webhooks
/v1/webhooks 2 credits Registers a webhook endpoint. The signing secret (whsec_...) is returned ONCE in the creation response — store it; it is masked on every later read. Max 10 webhooks per business.
Request body
| Parameter | Type | Required | Description |
|---|---|---|---|
url | string | Yes | HTTPS URL to receive events |
events | string[] | No | Event types to subscribe to. Defaults to all valid events |
Response example
{
"webhook_id": "wh_9c8b7a6d5e4f",
"secret": "whsec_3f8e2a1b9c7d6e5f4a3b2c1d0e9f8a7b",
"url": "https://example.com/hooks/hail",
"events": [ "alert.created", "alert.resolved" ],
"status": "active"
} /v1/webhooks 1 credit Lists registered webhooks for the business. No query parameters — returns the full list (max 10 per business).
Parameters
| Parameter | Type | Required | Description |
|---|
Response example
{
"webhooks": [
{ "id": "wh_9c8b7a6d5e4f", "url": "https://example.com/hooks/hail", "secret": "whsec_3f8e...8a7b", "events": [ "alert.created" ], "status": "active", "success_count": 142, "failure_count": 0, "consecutive_failures": 0 }
],
"count": 1
} /v1/webhooks/{id} 1 credit Returns a single webhook (same shape as the list entries).
Path parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Webhook ID |
Response example
{
"webhooks": [
{ "id": "wh_9c8b7a6d5e4f", "url": "https://example.com/hooks/hail", "secret": "whsec_3f8e...8a7b", "events": [ "alert.created" ], "status": "active", "success_count": 142, "failure_count": 0, "consecutive_failures": 0 }
],
"count": 1
} /v1/webhooks/{id} 2 credits Partial update — only fields present in the body are changed. Editing url re-validates it synchronously (HTTPS + a DNS/private-IP check). Setting status: "active" resets the consecutive-failure counter, effectively re-enabling a disabled webhook.
Request body
| Parameter | Type | Required | Description |
|---|---|---|---|
url | string | No | HTTPS URL, re-validated (SSRF check) |
events | string[] | No | Replaces the subscribed event list |
status | string | No | active, paused, or disabled |
Response example
{
"success": true,
"updated": [ "status" ]
} /v1/webhooks/{id} 1 credit Deletes the webhook registration.
Path parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Webhook ID |
Response example
{
"success": true
} /v1/webhooks/{id}/test 3 credits Sends a synthetic test event to the webhook endpoint for verification. Not written to the persisted delivery history below — it is a synchronous, on-demand ping, not a real alert delivery.
Path parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Webhook ID |
Response example
{
"success": true,
"status_code": 200,
"delivery_id": "test_4a3b2c1d0e9f8a7b"
} /v1/webhooks/{id}/deliveries 1 credit Returns the rolling last-100 real delivery attempts for this webhook (newest first) — successes, failures, and webhook.disabled fan-out notices. Each record's id is the exact value sent as the X-HailSentinel-Delivery-Id header.
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
limit | integer | No | Page size. Default 25, max 100 |
cursor | string | No | A delivery ID from a previous page's next_cursor |
Response example
{
"deliveries": [
{ "id": "whd_1a2b3c4d5e6f7a8b", "event": "alert.created", "status_code": 200, "success": true, "error": null, "redelivered_from": null, "created_at": "2026-07-03T18:12:04.221Z" },
{ "id": "whd_9f8e7d6c5b4a3210", "event": "alert.created", "status_code": 503, "success": false, "error": null, "redelivered_from": null, "created_at": "2026-07-03T18:07:51.009Z" }
],
"count": 2
} /v1/webhooks/{id}/deliveries/{deliveryId}/redeliver 3 credits Re-sends a past delivery's event + data as a brand-new delivery attempt with its own fresh delivery ID (so your idempotency-key dedup doesn't silently no-op it as a repeat). Returns 409 if the webhook is currently paused or disabled — re-enable it first.
Path parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Webhook ID |
deliveryId | string | Yes | A delivery ID from GET .../deliveries |
Response example
{
"success": true,
"status_code": 200,
"delivery_id": "redeliver_2c1d0e9f8a7b3c4d",
"redelivered_from": "whd_9f8e7d6c5b4a3210"
} Risk & climatology
/v1/hail/risk 5 credits Returns a climatological hail risk assessment for a location — annual and monthly probabilities (any / severe / significant / giant), a composite risk score, and a risk tier. Built from 5+ years of verified storm history.
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
lat | number | Yes | Latitude (-90 to 90) |
lon | number | Yes | Longitude (-180 to 180) |
Response example
{
"location": {
"lat": 39.7392,
"lon": -104.9903,
"geohash4": "9xj6"
},
"annual": {
"any": 0.41,
"severe": 0.12,
"significant": 0.03,
"score": 73,
"risk_tier": "high",
"peak_month": 6,
"years_of_data": 5
},
"monthly": [
{ "month": 6, "probability": { "any": 0.18, "severe": 0.06 }, "score": 88 }
]
} Convective outlooks
/v1/outlooks/spc 5 credits Returns our categorical convective outlook for a location on a given day (1-8) — our severe-weather outlook product. Day 1-3 include per-hazard hail/tornado/wind probabilities; Day 4-8 carry a combined severe probability. format=geojson returns the containing grid cell as a polygon.
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
lat | number | Yes | Latitude (-90 to 90) |
lon | number | Yes | Longitude (-180 to 180) |
day | number | No | Outlook day, 1-8. Default: 1 |
format | string | No | json (default) or geojson |
Response example
{
"location": { "lat": 39.8145, "lon": -102.832, "geohash4": "9xnf" },
"day": 1,
"outlook": {
"category": "MRGL",
"category_label": "Marginal",
"hail_probability_pct": 5,
"hail_significant": false,
"tornado_probability_pct": null,
"wind_probability_pct": null,
"severe_probability_pct": null
},
"issue_time": "2026-06-01T20:00:00Z",
"valid_time": "2026-06-01T20:00:00Z",
"expire_time": "2026-06-02T12:00:00Z",
"has_extended_risk": false
} Warnings & watches
/v1/warnings 8 credits Returns active Severe Thunderstorm and Tornado Warnings (polygon-precise) together with active Severe Thunderstorm/Tornado Watches and Mesoscale Discussions for a location. active=false&days=N switches to a historical lookback instead of the default currently-in-effect view.
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
lat | number | Yes | Latitude (-90 to 90) |
lon | number | Yes | Longitude (-180 to 180) |
radius_km | number | No | Search radius in kilometers. Default: 50, max: 200 |
active | string | No | true (default) or false for historical mode |
days | number | No | Lookback in days when active=false. Default: 1, max: 30 |
Response example
{
"location": { "lat": 44.87, "lon": -97.23 },
"active_only": true,
"warnings": [
{
"id": "urn:oid:2.49.0.1.840...002.1",
"event": "Tornado Warning",
"severity": "Extreme",
"issued_at": "2026-06-01T23:42:00Z",
"expires_at": "2026-06-02T00:00:00Z",
"tornado_detection": "RADAR INDICATED",
"headline": "Tornado Warning issued...",
"geometry": { "type": "Polygon", "coordinates": [ "..." ] }
}
],
"watches": [
{
"id": "WW_0437",
"type": "watch",
"watch_number": 437,
"issued_at": "2026-06-01T23:16:00Z",
"expires_at": "2026-06-02T02:00:00Z",
"headline": "Severe Thunderstorm Watch #437",
"geometry": null,
"bbox": { "min_lat": 41.49, "max_lat": 43.55, "min_lon": -95.09, "max_lon": -90.14 }
}
],
"data_as_of": "2026-06-01T23:47:02Z"
} Account & usage
/v1/usage Free Your current billing-period credit usage — plan allowance, remaining balance, spend cap, a per-operation breakdown (cursor-paginated), and a per-day timeseries. Free (0 credits) so you can always check your balance without it costing anything.
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
period | string | No | Billing period, YYYY-MM. Default: current month |
operations_limit | integer | No | Page size for the per-operation breakdown. Default 20, max 100 |
operations_cursor | string | No | An operation name from a previous page's next_cursor |
Response example
{
"period": "2026-07",
"plan": "regional",
"credits": { "limit": 150000, "used": 42130, "remaining": 107870, "spend_cap": 300000, "until_cap": 257870 },
"overage": { "credits": 0, "estimated_cost": "$0.00", "rate": "$0.02/credit" },
"overage_policy": { "policy": "allow", "spend_cap": 300000, "per_key_overrides": false },
"operations": {
"items": [
{ "operation": "hail.current", "count": 2104, "credits": 31560, "cost_per_call": 15 },
{ "operation": "webhooks.create", "count": 3, "credits": 6, "cost_per_call": 2 }
],
"total_count": 2,
"next_cursor": null
},
"daily": [ { "date": "2026-07-03", "credits": 1420 }, { "date": "2026-07-04", "credits": 1105 } ],
"keys": { "total_requests": 2107, "count": 1, "items": [ { "id": "key_abc123", "name": "Production", "status": "active", "requests": 2107, "scopes": ["*"] } ] },
"last_updated": "2026-07-04T06:18:06.873Z"
} /v1/usage/export Free CSV export of the complete (unpaginated) per-operation breakdown for a billing period — a reformat of the same data GET /v1/usage shows, not a new data product, so it's free too.
Query parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
period | string | No | Billing period, YYYY-MM. Default: current month |
Response example
# GET /v1/usage/export -> Content-Type: text/csv
# Content-Disposition: attachment; filename="usage-2026-07.csv"
operation,count,credits,cost_per_call
hail.current,2104,31560,15
webhooks.create,3,6,2 Event-driven integration.
Receive real-time push notifications via signed webhooks. Native SDKs for Python and Node.js are available now (Java is on the roadmap) — any HTTP client also works directly against the REST API.
Subscribe to real-time events. Each webhook POST includes an X-HailSentinel-Signature header for HMAC-SHA256 verification (see the SDK examples for verifying it) — plus X-HailSentinel-Event and X-HailSentinel-Timestamp (reject anything older than ~5 minutes to stop replay). What to build against: verify the signature before trusting the body, dedup on delivery ID (retries/idempotency below), and drive a stow/resume workflow off alert.created/alert.resolved's all-clear fields (workflow below).
-
alert.createdA new alert has been generated based on your subscription criteria. -
alert.updatedReserved — a legal subscription value, but no dispatch path emits it yet (only alert.created/alert.resolved are actually triggered today). -
alert.resolvedThe threat has cleared — explicit all-clear semantics (threat_cleared_at, max_size_during_event, duration_seconds), enough to drive a stow/resume workflow without polling. -
portfolio.impact_summaryReserved — a legal subscription value for portfolio customers, but the scheduled evaluator that would trigger it isn't wired up yet. -
webhook.disabledFires to your OTHER active webhooks when one of your webhooks auto-disables after 10 consecutive delivery failures.
Retry policy & idempotency
A failed delivery (any non-2xx response, timeout, or connection error) is retried up to 5 times with exponential backoff — 10 seconds initially, doubling up to a 600-second ceiling, capped at a 1-hour total retry window — then the attempt is dropped (every attempt, success or failure, is still recorded in GET /v1/webhooks/{id}/deliveries). All payloads are signed with HMAC-SHA256 using your webhook secret. Your endpoint should be idempotent: dedup on X-HailSentinel-Delivery-Id (same value as the body's id) so a retried delivery of an event you already processed is a safe no-op, not a double-charge or double-notify. After 10 consecutive failed delivery attempts (across distinct events — not retries of the same one), the webhook auto-disables — your other active webhooks subscribed to webhook.disabled get notified, and your account's Admin/Manager team members get an email. Re-enable it any time via PUT /v1/webhooks/{id} (status: "active"), then use GET /v1/webhooks/{id}/deliveries + POST .../redeliver to catch up on anything missed while it was down.
Schema versioning & delivery IDs
Every payload carries a top-level schema_version (an integer, currently 1 for every event type). It only increments on a breaking change to the payload shape — new fields are added freely without bumping it, so don't gate on it defensively for additive changes. The X-HailSentinel-Delivery-Id header (and matching GET /v1/webhooks/{id}/deliveries record id) is stable per delivery attempt — treat it as an idempotency key and dedup any retried delivery you've already processed. A manual POST .../redeliver mints a fresh delivery ID on purpose, since it's a deliberate re-send, not a retry of the original attempt.
Stow/resume workflow
alert.created and alert.resolved are enough to drive a stow/resume workflow (secure equipment or pause outdoor work when a threat is detected, resume normal operations when it clears) without polling — the only other vendor offering this shape today is Xweather Protect.
{
"id": "evt_8f7e6d5c4b3a91d2",
"schema_version": 1,
"event": "alert.created",
"created_at": "2026-02-28T18:42:00.000Z",
"data": {
"subscription_id": "sub_8f7e6d5c4b3a",
"estimated_size_mm": 32,
"confidence": 0.87,
"location": {
"lat": 39.7412,
"lon": -104.9876
}
}
} alert.resolved — all-clear semantics
{
"id": "evt_a1b2c3d4e5f60718",
"schema_version": 1,
"event": "alert.resolved",
"created_at": "2026-02-28T19:26:00.000Z",
"data": {
"alert_id": "e29b9a7e-...-4a5c",
"location_id": "loc_8f7e6d5c4b3a",
"location_name": "Denver Warehouse",
"threat_level": "CLEAR",
"threat_cleared_at": "2026-02-28T19:26:00.000Z",
"max_size_during_event": 32,
"duration_seconds": 2640
}
} SDKs
# pip install hailsentinel-api
from hailsentinel_api import HailSentinelClient, verify_webhook_signature
client = HailSentinelClient(api_key="hs_live_...")
current = client.get_hail_current(lat=39.74, lon=-104.99, radius_km=50)
# Verifying an inbound webhook delivery
valid = verify_webhook_signature(
payload=raw_body, # the raw request body, not re-serialized JSON
signature=headers["X-HailSentinel-Signature"],
timestamp=headers["X-HailSentinel-Timestamp"],
secret="whsec_...",
) // npm install @hailsentinel/api-client
const { HailSentinelClient, verifyWebhookSignature } = require("@hailsentinel/api-client");
const client = new HailSentinelClient({ apiKey: "hs_live_..." });
const current = await client.getHailCurrent({ lat: 39.74, lon: -104.99, radius_km: 50 });
// Verifying an inbound webhook delivery
const valid = verifyWebhookSignature({
payload: rawBody, // the raw request body, not re-serialized JSON
signature: req.get("X-HailSentinel-Signature"),
timestamp: req.get("X-HailSentinel-Timestamp"),
secret: "whsec_...",
}); // Java SDK — coming soon
// Until then, call the REST API with any HTTP client. | Plan | Requests / min | Credits / month | Webhooks |
|---|---|---|---|
| Field | 60 | 7,500 | ✓ |
| Team | 60 | 37,500 | ✓ |
| Regional | 60 | 150,000 | ✓ |
| Portfolio | 60 | 750,000 | ✓ |
| Verify | 60 | 10,000 | — |
| Partner | 600 (2,400 burst) | 500k call pool | ✓ |
Usage is metered in credits (different endpoints cost different amounts), not a fixed query count. Overage is billed per credit; see pricing.
Every error response is shaped { error, message, request_id } — error is the stable machine-readable code below (branch your code on this), message is human-readable text that may change wording at any time (never parse it), request_id correlates a report with our server logs. A handful of long-running async jobs (GET /v1/exports/{id}, GET /v1/verify/report/{id}) report a failed job the same way inside a 200 poll response — it's the underlying job that failed, not the poll itself. See the API changelog for the versioning policy this registry follows.
| Code | HTTP status | Meaning |
|---|---|---|
bad_request | 400 | Request validation failed (missing/invalid field, bad format, out-of-range value). |
invalid_parameter | 400 | A query/path parameter failed validation (distinct from a request-body validation failure — see bad_request). |
invalid_idempotency_key | 400 | The Idempotency-Key header was malformed (empty or over 255 chars). |
unauthorized | 401 | Missing or invalid API key / OIDC token. |
forbidden | 403 | Authenticated, but not permitted for a reason other than scope (e.g. a disabled test key, an invalid Cloud Tasks caller). |
insufficient_scope | 403 | Authenticated, but the API key lacks the specific scope this operation requires. |
feature_not_available | 403 | The endpoint requires a plan tier the business hasn't purchased. |
not_found | 404 | The requested resource id does not exist (or doesn't belong to this business). |
version_conflict | 409 | An optimistic-concurrency write lost a race (e.g. a stale ETag/version on update). |
webhook_not_active | 409 | A webhook-scoped mutation was rejected because the webhook is paused/disabled. |
idempotency_key_in_progress | 409 | A request with the same Idempotency-Key is still being processed by another in-flight request. |
rate_limited | 429 | Per-minute request rate limit exceeded. |
resource_exhausted | 429 | A resource-COUNT limit was hit (e.g. max webhooks/subscriptions per business), distinct from request-rate limiting. |
spend_cap_exceeded | 429 | The business's configured monthly spend cap would be exceeded by this call. |
too_many_failed_attempts | 429 | Too many consecutive failed-auth attempts on this key; temporarily locked out. |
internal_error | 500 | Unhandled server error — see server logs by request_id. |
upstream_write_failed | 502 | A downstream write (e.g. to a platform data store shared with the consumer app) failed. |
metering_unavailable | 503 | The credit-metering backend (Firestore) could not be reached to authorize this request. |
export_generation_failed | 200 | An async bulk-export job finished in a failed state (inside a 200 poll response — see the note below). |
report_generation_failed | 200 | An async verification-report job finished in a failed state (inside a 200 poll response — see the note below). |
webhook_unreachable | 200 | A webhook test-delivery attempt could not reach the target URL (the route call itself always 200s — check the response body). |
Ready to build?
Get started with our API today. Request an API key to access real-time hail detection, alerts, forecasts, and climatological risk.
View the API changelog & deprecation policy