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. 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.

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.9% uptime target
Core endpoints REFERENCE · 7 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: 148, 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 — 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

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 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

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 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

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-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

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

Lists monitored areas for the business, newest-updated first, plus the usage counters against the plan 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 plan 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 5+ years of verified storm history.

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 — 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

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 — the only other vendor offering this shape today is Xweather Protect.

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 by plan
60/MIN · CREDIT METERED
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.

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 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
Get started FREE TIER · LIVE NOW

Ready to protect what matters?

AI-powered hail intelligence to stay ahead of severe weather — for homeowners, businesses, and anyone who needs to know.

Free tier available · Live now on iOS and Android