Skip to content
Quick start BASE · api.hailsentinel.com/v1

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, raised by agreement for high-volume workspaces. Workspaces 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.

Request
CURL · GET
# 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..."
Response
200 OK
{
  "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 }
}
Authentication BEARER · HS_LIVE_* · HS_TEST_*

Secure API access.

Every request is authenticated with a Bearer token tied to your account. Separate keys for development and production.

Live keys
Production

Prefixed with hs_live_. Use in production environments. Requests count against your billing quota and return real data.

Test keys
Health check

Prefixed with hs_test_. Validate your authentication and integration wiring against the /v1/health endpoint, free of charge. Data endpoints require a live key.

Bearer token authentication
CURL · HEADER
# 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..."
256-bit encryption
GCP secured infrastructure
99.5% uptime target
Core endpoints REFERENCE · 46 ENDPOINTS

API reference.

46 endpoints across 12 groups — real-time detection, alerting, historical data, climatological risk, and account usage.

Hail detection

GROUP · 11 ENDPOINTS
GET /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

ParameterTypeRequiredDescription
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 }
    }
  ]
}
POST /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

ParameterTypeRequiredDescription
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
}
GET /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

ParameterTypeRequiredDescription
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
}
GET /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

ParameterTypeRequiredDescription
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"
}
GET /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

ParameterTypeRequiredDescription
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"
}
GET /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

ParameterTypeRequiredDescription
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"
}
GET /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

ParameterTypeRequiredDescription
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 }
}
GET /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

ParameterTypeRequiredDescription
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" }
  ]
}
GET /v1/coverage 5 credits

A physics-geometry radar detection-quality prior for a location. 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

ParameterTypeRequiredDescription
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."
}
GET /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

ParameterTypeRequiredDescription
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..."
}
GET /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

ParameterTypeRequiredDescription
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

GROUP · 2 ENDPOINTS
GET /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

ParameterTypeRequiredDescription
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."
}
GET /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

ParameterTypeRequiredDescription
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

GROUP · 3 ENDPOINTS
POST /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

ParameterTypeRequiredDescription
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"
}
POST /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

ParameterTypeRequiredDescription
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\"."
}
GET /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

ParameterTypeRequiredDescription
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

GROUP · 2 ENDPOINTS
POST /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 workspaces with export access (ask with your invite) — others 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

ParameterTypeRequiredDescription
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\"."
}
GET /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

ParameterTypeRequiredDescription
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

GROUP · 5 ENDPOINTS
GET /v1/alerts 1 credit

Lists all alert subscriptions for your business, cursor-paginated.

Query parameters

ParameterTypeRequiredDescription
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
}
POST /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

ParameterTypeRequiredDescription
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"
}
GET /v1/alerts/{id} 1 credit

Retrieves details of an alert subscription, including its configuration and a list of recently triggered alerts.

Path parameters

ParameterTypeRequiredDescription
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"
}
PUT /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)

ParameterTypeRequiredDescription
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"
}
DELETE /v1/alerts/{id} 1 credit

Cancels an active alert subscription. Webhooks will no longer be dispatched for this subscription after cancellation.

Path parameters

ParameterTypeRequiredDescription
id string Yes Subscription ID to cancel

Response example

{
  "success": true
}

Portfolio

GROUP · 4 ENDPOINTS
POST /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

ParameterTypeRequiredDescription
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 }
}
GET /v1/portfolio/locations 1 credit

Lists point locations for the business, ordered by name, cursor paginated.

Query parameters

ParameterTypeRequiredDescription
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"
}
DELETE /v1/portfolio/locations 5 credits

Deletes point locations by ID (max 2,000 per request) and decrements the workspace 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

ParameterTypeRequiredDescription
ids string[] Yes Location IDs to delete

Response example

{
  "deleted_count": 2,
  "not_found_count": 1,
  "deleted": [ "EW1IxPagDKJTqzH17MSC", "04TVnfBvPk64ZhcczAOW" ],
  "not_found": [ "totally-fake-id" ]
}
GET /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

ParameterTypeRequiredDescription
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

GROUP · 6 ENDPOINTS
POST /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-workspace 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

ParameterTypeRequiredDescription
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 workspace’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"
}
GET /v1/areas 1 credit

Lists monitored areas for the business, newest-updated first, plus the usage counters against the workspace quota.

Query parameters

ParameterTypeRequiredDescription
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 }
}
GET /v1/areas/{id} 1 credit

Returns area metadata (not the full polygon geometry — see the geometry endpoint below).

Path parameters

ParameterTypeRequiredDescription
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"
}
GET /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

ParameterTypeRequiredDescription
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"
}
PUT /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

ParameterTypeRequiredDescription
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"
}
DELETE /v1/areas/{id} 2 credits

Deletes the area, its geometry sub-doc, its alert-pipeline projection, and decrements workspace usage.

Path parameters

ParameterTypeRequiredDescription
id string Yes Area ID

Response example

{
  "success": true,
  "id": "k3Jm8QpXvR2wLtZbNc7Y"
}

Webhooks

GROUP · 8 ENDPOINTS
POST /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

ParameterTypeRequiredDescription
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"
}
GET /v1/webhooks 1 credit

Lists registered webhooks for the business. No query parameters — returns the full list (max 10 per business).

Parameters

ParameterTypeRequiredDescription

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
}
GET /v1/webhooks/{id} 1 credit

Returns a single webhook (same shape as the list entries).

Path parameters

ParameterTypeRequiredDescription
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
}
PUT /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

ParameterTypeRequiredDescription
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" ]
}
DELETE /v1/webhooks/{id} 1 credit

Deletes the webhook registration.

Path parameters

ParameterTypeRequiredDescription
id string Yes Webhook ID

Response example

{
  "success": true
}
POST /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

ParameterTypeRequiredDescription
id string Yes Webhook ID

Response example

{
  "success": true,
  "status_code": 200,
  "delivery_id": "test_4a3b2c1d0e9f8a7b"
}
GET /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

ParameterTypeRequiredDescription
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
}
POST /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

ParameterTypeRequiredDescription
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

GROUP · 1 ENDPOINT
GET /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 verified storm history back to 2020 (backfill ongoing).

Query parameters

ParameterTypeRequiredDescription
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

GROUP · 1 ENDPOINT
GET /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

ParameterTypeRequiredDescription
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

GROUP · 1 ENDPOINT
GET /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

ParameterTypeRequiredDescription
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

GROUP · 2 ENDPOINTS
GET /v1/usage Free

Your current billing-period credit usage — workspace 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

ParameterTypeRequiredDescription
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"
}
GET /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

ParameterTypeRequiredDescription
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
Webhooks & SDKs EVENTS · HMAC-SHA256 · SDKS

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.

Webhook consumer guide
5 EVENTS

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.created A new alert has been generated based on your subscription criteria.
  • alert.updated Reserved — a legal subscription value, but no dispatch path emits it yet (only alert.created/alert.resolved are actually triggered today).
  • alert.resolved The 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_summary Reserved — a legal subscription value for portfolio customers, but the scheduled evaluator that would trigger it isn't wired up yet.
  • webhook.disabled Fires 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.

Example payload
POST · JSON
{
  "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

Python
# 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_...",
)
JavaScript
// 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 (Maven)
// Java SDK — coming soon
// Until then, call the REST API with any HTTP client.
Rate limits & credits
60/MIN · CREDIT METERED
Dimension How it's set
Requests / min 60 per key, flat — raised by agreement for high-volume workspaces
Credits / month Set per workspace at invite time, scaled to call volume
Webhooks Included on workspaces with real-time delivery access

Usage is metered in credits (different endpoints cost different amounts), not a fixed query count. Your credit allowance and overage handling are agreed when your workspace is invited in.

Error code reference
21 CODES

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 access the workspace doesn't have.
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?

An API key comes with a workspace invite. Request one for real-time hail detection, alerts, forecasts, and climatological risk in your own systems.

View the API changelog & deprecation policy
Get started FREE TIER · LIVE NOW

Ready to protect what matters?

Hail alerts for your exact address in the free app — and an invite-only console for the teams who work the aftermath.

Free tier available · Live on iOS and Android