Lightning API · Documentation

API Reference

One endpoint. Pass your API key and get back a JSON array of lightning flashes. Filter by time window and geographic bounding box. No SDK required.

Authentication

All requests must include your API key in the X-API-Key header. Keys are generated from your dashboard and can be rotated at any time. Never expose your key in client-side code, proxy requests through your own backend.

Heads up: api.lightningapi.dev now also works as an API hostname, alongside api.warpulse.com. Nothing is required from you right now, both work identically and api.warpulse.com is not going away today. We plan to retire it in favor of api.lightningapi.dev in early 2027, and we will give plenty of advance notice before that happens.

curl "https://api.lightningapi.dev/v1/flashes?since_minutes=15" \
  -H "X-API-Key: YOUR_API_KEY"

Endpoint

GEThttps://api.lightningapi.dev/v1/flashes

Returns an array of lightning flash events matching the given filters.

Query Parameters

All parameters are optional. Without a bounding box, the response covers the full Americas, Atlantic, and Pacific region (including Hawaii and Alaska).

Parameter

Type

Default

Description

since_minutes

integer

15

Return flashes from the last N minutes. Minimum: 1. Maximum depends on your plan, see the table below.

min_lat

float

None

Minimum latitude of bounding box (-90 to 90). Must be provided with max_lat, min_lon, max_lon.

max_lat

float

None

Maximum latitude of bounding box (-90 to 90).

min_lon

float

None

Minimum longitude of bounding box (-180 to 180).

max_lon

float

None

Maximum longitude of bounding box (-180 to 180).

limit

integer

2000

Maximum number of flashes to return. Results are ordered newest first, so when more flashes match than the limit allows, the most recent are returned. Maximum depends on your plan, see the table below.

Geographic Filtering

Narrow results to any rectangular region by passing all four bounding box parameters together. All four must be present, a partial set returns a 400 error. Coordinates use decimal degrees (WGS84).

Bounding box format

# Florida
curl "https://api.lightningapi.dev/v1/flashes" \
  -H "X-API-Key: YOUR_API_KEY" \
  -G \
  -d since_minutes=15 \
  -d min_lat=24.52 \
  -d max_lat=31.00 \
  -d min_lon=-87.63 \
  -d max_lon=-80.03

# Custom box, NYC metro area
curl "https://api.lightningapi.dev/v1/flashes" \
  -H "X-API-Key: YOUR_API_KEY" \
  -G \
  -d since_minutes=30 \
  -d min_lat=40.45 \
  -d max_lat=41.15 \
  -d min_lon=-74.30 \
  -d max_lon=-73.65

Response Format

Successful responses return HTTP 200 with a JSON object containing count, the number of flashes returned; since_utc, the start of the requested window; and a flashes array, ordered newest first. Each flash has five fields.

{
  "count": 1284,
  "since_utc": "2026-06-25 21:44:00.000000",
  "flashes": [
    {
      "flash_id": 58097,
      "lat": -5.91299,
      "lon": -77.38203,
      "flash_timestamp_utc": "2026-06-25 21:58:59.348063",
      "type": "CG"
    },
    ...
  ]
}

Field

Type

Description

flash_id

integer

Unique identifier for this flash event.

lat

float

Latitude of the flash centroid in decimal degrees (WGS84).

lon

float

Longitude of the flash centroid in decimal degrees (WGS84).

flash_timestamp_utc

string

UTC timestamp of the flash in ISO 8601 format.

type

string | null

Cloud-to-ground (CG) vs. intra-cloud (IC) classification. "unknown" in rare circumstances where a flash couldn't be confidently classified. Available for Pro plans and above.

Plan Limits

Every numeric cap referenced above (since_minutes and limit on /v1/flashes) depends on your plan, shown below. Exceeding the rate limit returns HTTP 429; exceeding the history window or per-call limit with an explicitly-passed value returns HTTP 400.

PAYG

Monthly calls

per account

5,000 free calls, then $0.003/call (100,000 cap)

History window

per account

15 minutes

Limit / call

per account

500

Rate limit

per key

Fair usage policy

Max zones

per account

1

Starter

Monthly calls

per account

150,000

History window

per account

15 minutes

Limit / call

per account

2,000

Rate limit

per key

Fair usage policy

Max zones

per account

5

Pro

Monthly calls

per account

5,000,000

History window

per account

6 hours

Limit / call

per account

20,000

Rate limit

per key

Fair usage policy

Max zones

per account

25

Ultimate

Monthly calls

per account

25,000,000

History window

per account

24 hours

Limit / call

per account

50,000

Rate limit

per key

Fair usage policy

Max zones

per account

100

Supercell

Monthly calls

per account

Unlimited

History window

per account

7 days

Limit / call

per account

200,000

Rate limit

per key

300 req/sec

Max zones

per account

1,000

Enterprise

Monthly calls

per account

Negotiated

History window

per account

Negotiated

Limit / call

per account

Negotiated

Rate limit

per key

Negotiated

Max zones

per account

Negotiated

Commercial use is permitted on every current plan. Non-profit organisations on Pay As You Go can request a raised free allowance: see Licensing.

Monthly calls, history window, and limit per call all apply to your account as a whole, so extra API keys don't add quota. Rate limit is the one that's tracked separately per key, so each key gets its own allowance.

"History window" is the maximum value allowed for since_minutes. "Limit / call" is the maximum value allowed for limit (default 2,000, itself capped to your plan's value if you don't pass one explicitly). Both silently clamp down to your plan's cap if you never touch the parameter at all; passing a value that explicitly exceeds your plan's cap returns a 400 with a message naming the exact limit.

Monthly quotas reset on your billing anniversary each month, not the calendar month. Usage is visible on your dashboard.

Quota is consumed based on how many flashes a request returns, not per request: every 100 flashes returned costs 1 unit of your monthly quota, with a minimum of 1 unit per call. A request returning 50 flashes still costs 1 unit; a request returning 5,000 flashes costs 50. The X-Quota-Cost response header on every /v1/flashes call tells you exactly what that call cost.

Retrieving a full history window

Each request returns up to your plan's per-call limit: 500 flashes on Pay As You Go, 2,000 on Starter, 20,000 on Pro, 50,000 on Ultimate, and 200,000 on Supercell, with Enterprise limits set by agreement. Results are ordered newest first. If more flashes match your window and bounding box than a single request can return, you receive the most recent ones, count equals the limit you passed, and the older part of the window is not included in that response.

To build a complete history over a long window or a wide area:

  • Keep the results of frequent, short requests. Polling every minute or two with a matching since_minutes keeps each response well under the limit, and storing what comes back gives you the full window without large requests.
  • Split large areas into smaller bounding boxes. Each request has its own limit, so dividing an area with heavy activity into several boxes returns more of the window.
  • Treat a response where count equals limit as incomplete, and split that area further before requesting it again.
  • On Ultimate and above, live streaming delivers flashes as they are detected instead of in requests.

The type field (CG/IC classification) is available for Pro plans and above.

Radar Tiles

Beta. Live and included on the plans below. The shape of this API may still move before it settles, and anything that changes will be announced first.

Live radar as PNG map tiles, addressed as /{frame}/{z}/{x}/{y}.png. Coverage is the continental United States.

Tile requests use a short-lived token instead of your API key. Request one first:

curl -X POST "https://api.lightningapi.dev/v1/radar-tiles/token" \
  -H "X-API-Key: YOUR_API_KEY"

The response includes the token, a url_template and window_seconds. Tokens last one hour; request a new one on a 401.

Frame ids are YYYYMMDDHHMM, with a new frame about every two minutes. List the frames your plan can reach:

curl "https://tiles.lightningapi.dev/v1/frames?t=TOKEN"

# -> {"frames": ["202609030040", "202609030045"], "window_seconds": 86400}

Then point a tile layer at the template. Frames outside your plan's window return 403. Tiles outside coverage return a transparent PNG.

https://tiles.lightningapi.dev/v1/radar/{frame}/{z}/{x}/{y}.png?t=TOKEN

Optional palette parameter: reflectivity (default),amber, amber-violet or thermal. Zoom 0 to 12.

To read the reflectivity at a point, for example to show a value under the cursor, request the frame's value with the same token:

curl "https://tiles.lightningapi.dev/v1/radar/202609030045/value?lat=35.47&lon=-97.52&t=TOKEN"

# -> {"frame": "202609030045", "lat": 35.47, "lon": -97.52, "dbz": 42.5}

dbz is null where there is no return. Each lookup counts as one tile against your allowance, so request a value when the cursor comes to rest rather than on every movement.

To follow a point through a loop, request its history instead. It returns the value at that point for every frame your plan can reach, oldest first, in one request that counts as one tile:

curl "https://tiles.lightningapi.dev/v1/radar/history?lat=35.47&lon=-97.52&t=TOKEN"

# -> {"lat": 35.47, "lon": -97.52, "points": [{"frame": "202609030040", "dbz": 38.0}, ...]}

On Pro and above, a page in the browser can read these responses directly from the tile host. Below Pro, call them from your own server. Tiles themselves display on every plan either way.

Loops

On Ultimate and Supercell, a whole loop for one tile comes back in a single request: every frame stacked top to bottom, oldest first, as one PNG 256 pixels wide. count sets how many of the most recent frames to include (default 60, up to 120), end optionally sets the last frame, and the X-Radar-Frames response header lists the frame ids in order. Each frame in the strip counts as one tile.

https://tiles.lightningapi.dev/v1/radar/loop/{z}/{x}/{y}.png?count=60&t=TOKEN

Tiles have their own monthly allowance, counted separately from your API calls: 5,000 a month on Pay As You Go, 25,000 on Starter, 250,000 on Pro, 5,000,000 on Ultimate and no ceiling on Supercell. Past the allowance, tiles return 429; on Pay As You Go they keep serving and bill $0.0010 each against your spending cap. window_seconds follows your plan's history window, from 15 minutes to 7 days.

Error Codes

400

Bad Request

Invalid parameter value or combination (e.g. min_lat without max_lat, limit exceeds plan cap).

401

Unauthorized

Missing or invalid X-API-Key header.

429

Too Many Requests

Rate limit exceeded for your plan. Back off and retry.

500

Server Error

Unexpected server error. Contact support if the issue persists.

Interactive Query Builder

Pick a US state or region, draw a custom bounding box, or enter coordinates manually. The query updates live in your chosen language.

Pay As You Go & Starter cap at 15 min. Pro caps at 6 hours, Ultimate at 24 hours, Supercell at 7 days, and Enterprise has no history limit.

Pay As You Go caps at 500/call. Starter caps at 2,000/call. Pro caps at 20,000/call. Ultimate caps at 50,000/call. Supercell caps at 200,000/call. Enterprise limits are negotiated.

Bounding box (or use the map below)

Regions

US States

Generated query

curl "https://api.lightningapi.dev/v1/flashes" \
  -H "X-API-Key: YOUR_API_KEY" \
  -G \
  -d since_minutes=15

Storm Proximity Alerts (Zones & Webhooks)

Define a zone (a center point and a radius) and get a signed webhook POST when a strike lands inside it. Delivery is near real-time and doesn't count against your monthly call quota. Manage zones via the API below, or from the dashboard, using the same X-API-Key header as /v1/flashes for API access. Zone count is capped by plan; see Max zones in the Plan Limits table above.

Endpoints

POSThttps://api.lightningapi.dev/developer/zonesCreate a zone. Returns webhook_secret once; store it, it is never shown again.
GEThttps://api.lightningapi.dev/developer/zonesList your zones. Never includes webhook_secret.
DELETEhttps://api.lightningapi.dev/developer/zones/{id}Delete a zone. Its alert history is deleted with it.
GEThttps://api.lightningapi.dev/developer/zones/{id}/alertsRecent firings for a zone, with delivery_status. Useful for debugging a webhook endpoint that isn't receiving deliveries.

Request body (POST /developer/zones)

Parameter

Type

Default

Description

center_latrequired

float

None

Latitude of the zone center (-90 to 90).

center_lonrequired

float

None

Longitude of the zone center (-180 to 180).

radius_kmrequired

float

None

Zone radius in kilometers. Must be greater than 0 and at most 20,000.

webhook_urlrequired

string

None

http:// or https:// URL to receive the signed webhook POST.

name

string

None

Optional label for the zone.

cooldown_minutes

integer

5

Minimum minutes between repeat firings for this zone.

cg_only

boolean

None

Only fire for cloud-to-ground strikes, skipping intra-cloud activity. Pro plans and above.

min_strikes

integer

None

Only fire once at least this many strikes land nearby within rate_window_minutes. Must be set together with rate_window_minutes. Pro plans and above.

rate_window_minutes

integer

None

Trailing window in minutes min_strikes is measured over. Must be set together with min_strikes. Pro plans and above.

predictive

boolean

None

Fire ahead of a tracked storm cell's projected arrival, based on its heading and speed, instead of only after a strike lands inside the zone. Ultimate and above.

predictive_horizon_minutes

integer

30

How far ahead to project a storm cell's path when predictive is enabled.

min_dbz

float

None

Fire when a tracked radar cell whose peak reflectivity reaches this many dBZ overlaps the zone, independent of lightning activity. 0 to 100. Pro plans and above.

Create a zone

curl -X POST https://api.lightningapi.dev/developer/zones \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Construction Site A",
    "center_lat": 33.41,
    "center_lon": -94.02,
    "radius_km": 10,
    "cooldown_minutes": 5,
    "webhook_url": "https://your-server.example.com/lightning-webhook"
  }'

Debounce & cooldown

The first strike inside a zone fires immediately. Further strikes in that same zone are suppressed for cooldown_minutes (default 5) before it can fire again.

Webhook payload & signature verification

Every delivery includes an X-Lightning-Signature header: sha256=<hex hmac>, computed over the raw request body using your zone's webhook_secret as the HMAC key. Verify it before trusting the payload.

The payload shape depends on event_type. A plain zone fires flash_proximity the moment a strike lands inside it:

{
  "delivery_id": 4821,
  "zone_id": 123,
  "zone_name": "Construction Site A",
  "event_type": "flash_proximity",
  "flash": {
    "lat": 33.41,
    "lon": -94.02,
    "timestamp_utc": "2026-08-04T18:22:10",
    "type": "CG",
    "energy_fj": 142.35
  },
  "distance_km": 3.2
}

A predictive zone instead fires storm_approaching ahead of a tracked storm cell's projected arrival, with no single triggering flash:

{
  "delivery_id": 4822,
  "zone_id": 123,
  "zone_name": "Construction Site A",
  "event_type": "storm_approaching",
  "storm_cell": { "id": 16935, "heading_deg": 27.3, "speed_kmh": 4.9 },
  "eta_minutes": 18.5
}

A zone with min_dbz set fires radar_threshold when a tracked radar cell at or above that reflectivity overlaps it, whether or not the storm is producing lightning:

{
  "delivery_id": 4823,
  "zone_id": 123,
  "zone_name": "Construction Site A",
  "event_type": "radar_threshold",
  "radar_cell": { "id": 48211 },
  "observed_max_dbz": 52.5
}

type and energy_fj on flash are only present when known (Pro plans and above for type).

import hashlib, hmac

def verify(secret: str, body: bytes, header: str) -> bool:
    expected = "sha256=" + hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, header)

Retries

1stImmediate
2nd+30s
3rd+2min
4th+10min, then marked failed

Retried on any non-2xx response or timeout. Your endpoint must respond within 5 seconds; anything slower is treated as a timeout and retried on the same schedule. Use delivery_id for idempotency if you receive the same delivery more than once during a retry window.

Delivery history

GET /developer/zones/{id}/alerts returns each firing's delivery_status, one of pending (queued, not yet delivered), delivered (2xx received), or failed (all 4 attempts exhausted). Useful for confirming whether your endpoint is actually receiving deliveries.

Storm Cell Tracking

Every active storm is clustered into a tracked cell with its own position, heading, and speed, followed across its full lifecycle including merges and splits with neighboring cells. Available on Ultimate and above.

GEThttps://api.lightningapi.dev/v1/storm-cells

Query Parameters

Parameter

Type

Default

Description

since_minutes

integer

15

Return cells last seen within the last N minutes (includes recently-dissipated cells still in this window).

min_lat

float

None

Minimum latitude of bounding box. Must be provided with max_lat, min_lon, max_lon.

max_lat

float

None

Maximum latitude of bounding box.

min_lon

float

None

Minimum longitude of bounding box.

max_lon

float

None

Maximum longitude of bounding box.

limit

integer

None

Maximum number of cells to return.

include_track

boolean

false

Include each cell's full position history as a track array.

include_lineage

boolean

false

Include parent_cell_id and merged_into_cell_id, tracking splits and merges between cells.

Response

curl "https://api.lightningapi.dev/v1/storm-cells?since_minutes=30" \
  -H "X-API-Key: YOUR_API_KEY"

{
  "count": 3,
  "since_utc": "2026-08-22 17:30:52.953017",
  "storm_cells": [
    {
      "id": 16935,
      "centroid_lat": 28.789954,
      "centroid_lon": -84.947727,
      "radius_km": 20.27,
      "flash_count_window": 25,
      "heading_deg": 27.28,
      "speed_kmh": 4.89,
      "status": "active",
      "first_seen_at": "2026-08-22 18:00:13.614927",
      "last_seen_at": "2026-08-22 18:30:13.576041"
    }
  ]
}

status is active or dissipated. With include_lineage=true, a dissipated cell that merged into another carries merged_into_cell_id; a cell created when a storm split off an existing one carries parent_cell_id. With include_track=true, each cell also includes a track array of { observed_at, lat, lon, flash_count } points, oldest first.

Reconnecting Without Gaps

A WebSocket connection ends sometimes. We deploy, a network path changes, your own process restarts. When that happens the stream can pick up exactly where it stopped, so nothing is missed in between.

Keep the flash_id of the last flash you processed. On reconnect, pass it as resume_from_flash_id. Everything that arrived while you were away is replayed first, then the live feed continues from that point with no gap and no duplicates.

last_id = None

while True:
    url = "wss://api.lightningapi.dev/v1/stream"
    if last_id is not None:
        url += f"?resume_from_flash_id={last_id}"

    try:
        async with websockets.connect(
            url, additional_headers={"X-API-Key": "YOUR_API_KEY"}
        ) as ws:
            async for raw in ws:
                msg = json.loads(raw)
                if msg["type"] == "flashes":
                    for flash in msg["flashes"]:
                        handle(flash)
                        last_id = flash["flash_id"]
    except websockets.ConnectionClosed:
        await asyncio.sleep(1)   # then reconnect, resuming from last_id

Replayed batches carry "replay": true so you can tell catch-up from live data. The catch-up is bracketed by resume_started, which reports how many flashes were missed, and resume_complete, which reports how many were sent.

Before a planned restart the server sends a notice and closes with code 1012, the standard signal for a service restart. Treat that as your cue to reconnect with resume_from_flash_id set. Recovery is typically a few seconds, and no flashes are lost across it.

Two limits worth knowing. A catch-up covers at most 200,000 flashes; beyond that the server replies resume_skipped rather than delivering part of the gap, because a partial replay you cannot identify is worse than none. Backfill with /v1/flashes in that case. And replayed flashes count toward your quota at exactly the same rate as live ones, because they are the same data delivered the same way.

Quota

Storm cells are billed at one unit per 10 cells returned, the same rate as /v1/radar-cells and ten times denser than the flash endpoints, with the usual minimum of one unit per response. Your plan's limit and since_minutes caps apply here as they do on /v1/flashes.

Predictive zones (see predictive above) are built directly on this same tracking data: a zone projects each nearby cell's heading and speed forward and fires storm_approaching once the projected path enters the zone within predictive_horizon_minutes.

Radar Storm Cell Tracking

Storm cells detected directly from radar reflectivity rather than from lightning density, so they cover heavy rain, hail, and squall lines that are not producing much lightning yet. Each cell carries its position, size, peak reflectivity, intensity category, and movement, refreshed every 5 minutes. Available on Pro plans and above.

This is a separate endpoint from /v1/storm-cells, not a replacement for it. The two answer different questions: lightning-derived cells track electrical activity, radar-derived cells track precipitation intensity. Many storms appear in both.

GEThttps://api.lightningapi.dev/v1/radar-cells

Query Parameters

Parameter

Type

Default

Description

since_minutes

integer

15

Return cells last seen within the last N minutes (includes recently-dissipated cells still in this window).

min_lat

float

Minimum latitude of bounding box. Must be provided with max_lat, min_lon, max_lon.

max_lat

float

Maximum latitude of bounding box.

min_lon

float

Minimum longitude of bounding box.

max_lon

float

Maximum longitude of bounding box.

limit

integer

Maximum number of cells to return.

Response

curl "https://api.lightningapi.dev/v1/radar-cells?since_minutes=30" \
  -H "X-API-Key: YOUR_API_KEY"

{
  "count": 2,
  "since_utc": "2026-08-24 23:40:12.118304",
  "radar_cells": [
    {
      "id": 48211,
      "centroid_lat": 32.41883,
      "centroid_lon": -95.22014,
      "radius_km": 18.6,
      "max_dbz": 52.5,
      "category": "intense",
      "heading_deg": 71.4,
      "speed_kmh": 38.2,
      "status": "active",
      "first_seen_at": "2026-08-24 22:55:03.221844",
      "last_seen_at": "2026-08-24 23:55:04.907712"
    }
  ]
}

status is active or dissipated. max_dbz is the cell's peak reflectivity, and category is the band that value falls into:

light

30 to 40 dBZ

moderate

40 to 45 dBZ

heavy

45 to 50 dBZ

intense

50 to 55 dBZ

extreme

55 dBZ and above

Cells below 30 dBZ are not tracked at all, so light is the weakest band returned. A zone with min_dbz set (see above) fires a radar_threshold webhook when a cell at or above that reflectivity overlaps it.

Quota

Cell endpoints are billed more densely than flashes: one unit per 10 cells returned, rather than one per 100. A tracked cell carries position, size, intensity, heading, and speed, so it is a much larger unit of information than a single flash. The same rate applies to /v1/storm-cells. As everywhere else, a response costs at least one unit, and the exact charge comes back on the X-Quota-Cost header.

Your plan's limit and since_minutes caps apply here exactly as they do on /v1/flashes. See Plan Limits for the per-plan figures.

Storm Rotation

Where a storm is turning, near the ground and higher up. Each rotating area is tracked over time and carries how strongly it is turning at both heights, which way it is turning, and where it is heading. Refreshed every 2 minutes. Available on Ultimate plans and above, covering the continental United States.

Rotation is a measured field, not a warning. It is derived from how sharply wind speed changes across a storm, it has known artifacts, and a rotating storm does not always produce a tornado. Treat it as one strong signal among several, and not as a substitute for official warnings from your national weather service.

GEThttps://api.lightningapi.dev/v1/rotation-tracks

Query Parameters

Parameter

Type

Default

Description

since_minutes

integer

15

Return rotation tracks last seen within the last N minutes (includes recently-dissipated tracks still in this window).

min_lat

float

Minimum latitude of bounding box. Must be provided with max_lat, min_lon, max_lon.

max_lat

float

Maximum latitude of bounding box.

min_lon

float

Minimum longitude of bounding box.

max_lon

float

Maximum longitude of bounding box.

limit

integer

Maximum number of tracks to return.

direction

string

cyclonic or anticyclonic. Omit it to get both.

raw

boolean

false

Return stored values without rounding. Shear, coordinates, radius, heading, speed and max_dbz are rounded for reading by default.

Response

curl "https://api.lightningapi.dev/v1/rotation-tracks?since_minutes=30&direction=cyclonic" \
  -H "X-API-Key: YOUR_API_KEY"

{
  "count": 1,
  "since_utc": "2026-09-15 13:00:00.000000",
  "rotation_tracks": [
    {
      "id": 41,
      "centroid_lat": 35.21,
      "centroid_lon": -97.44,
      "radius_km": 3.8,
      "low_level_shear": 0.0184,
      "mid_level_shear": 0.0121,
      "direction": "cyclonic",
      "category": "strong",
      "max_dbz": 58.5,
      "heading_deg": 47.0,
      "speed_kmh": 62.0,
      "status": "active",
      "first_seen_at": "2026-09-15 12:44:00.000000",
      "last_seen_at": "2026-09-15 13:00:00.000000"
    }
  ]
}

low_level_shear is how sharply the storm is turning near the ground, and mid_level_shear the same measurement higher up, both in inverse seconds. A positive value turns one way and a negative value the other, which is what direction names so you do not have to read the sign.

mid_level_shear can be null, and null is not zero. Null means the higher altitude measurement was unavailable for that update. A value at or near zero means there genuinely is no rotation up there, which is real information and worth acting on differently.

Numbers are rounded for reading: shear to four decimal places, coordinates to four (about 11 metres), radius_km to two, and max_dbz, heading_deg and speed_kmh to one. The source grid is quantised to 0.001 s^-1 and 1 km, so nothing real is lost. If you would rather round yourself, pass raw=true and every stored value comes back untouched.

max_dbz is the peak rainfall intensity over the rotating area. Rotation is only reported where there is real precipitation above it, and this is the reading that qualified it. status is active or dissipated. category is the band low_level_shear falls into, in inverse seconds, by strength and regardless of direction:

weak

0.006 to 0.010

moderate

0.010 to 0.015

strong

0.015 to 0.020

extreme

0.020 and above

Rotation below the weak threshold is not tracked at all, so every track you receive is at or above it.

Coverage

Rotation covers the continental United States only. A query for anywhere else returns an empty list rather than an error, the same way the flash endpoints behave outside their own coverage.

Quota

Rotation tracks are billed at one unit per 10 tracks returned, the same rate as /v1/radar-cells and /v1/storm-cells, with the usual minimum of one unit per response. Your plan's limit and since_minutes caps apply here as they do on /v1/flashes. Filtering with direction narrows what you are charged for, so a caller who only wants one rotation direction should set it rather than discarding half the response.

Live Streaming (WebSocket)

Hold one connection open and have flashes pushed to you as they are detected, instead of polling on a timer. Available on Ultimate and above. The stream is resumable: if the connection drops, you can pick up from the last flash you received and lose nothing in between.

WSSwss://api.lightningapi.dev/v1/stream

Authentication

Send your key in the X-API-Key header on the handshake. Browser clients cannot set headers on a WebSocket, so they may instead send a first frame of { "api_key": "..." } within 10 seconds of connecting. The key is never accepted as a query parameter, because query strings are recorded in server and proxy logs.

Query Parameters

Parameter

Type

Default

Description

cg_only

boolean

false

Only push cloud-to-ground strikes, skipping intra-cloud activity.

min_lat

float

Minimum latitude of bounding box. Must be given with max_lat, min_lon, max_lon.

max_lat

float

Maximum latitude of bounding box.

min_lon

float

Minimum longitude of bounding box.

max_lon

float

Maximum longitude of bounding box.

resume_from_flash_id

integer

The last flash_id you received. Everything after it is replayed before the live feed resumes, so a reconnect loses nothing. Omit it and the feed starts from the present.

Example

import asyncio, json, websockets

URL = "wss://api.lightningapi.dev/v1/stream?cg_only=true"

async def main():
    async with websockets.connect(
        URL, additional_headers={"X-API-Key": "YOUR_API_KEY"}
    ) as ws:
        async for raw in ws:
            msg = json.loads(raw)
            if msg["type"] == "flashes":
                for flash in msg["flashes"]:
                    print(flash["lat"], flash["lon"], flash.get("type"))

asyncio.run(main())

Messages

The first message confirms the connection and echoes the filters that were applied. Flashes then arrive in batches as they are detected, so a busy period is one message rather than hundreds.

{ "type": "connected", "tier": "ultimate", "filters": { "cg_only": true } }

{
  "type": "flashes",
  "count": 1,
  "flashes": [
    {
      "flash_id": 27535,
      "lat": 19.773296,
      "lon": -104.554557,
      "flash_timestamp_utc": "2026-08-25 01:13:59.600977",
      "type": "CG",
      "energy_fj": 142.35
    }
  ]
}

A rejected connection receives { "type": "error", "message": "..." } and is then closed: the key was invalid, the plan does not include streaming, or a bounding box was incomplete or inverted. type and energy_fj appear only when known, matching the REST response.

Quota

Streaming is metered at one call per 50 flashes pushed. The REST endpoints are metered at one call per 100 flashes returned, so a flash delivered over the stream costs twice what the same flash costs when you poll for it. You are paying for delivery as it happens rather than on your next request, and for a connection held open on your behalf for as long as you keep it.

The remainder carries between batches rather than being rounded up each time, so a busy second costs the same whether it arrives as one batch or ten. A partial batch still outstanding when the connection closes is charged as one call. Replayed flashes from a catch-up are metered at the streaming rate, the same as live ones.

Coverage areas

Lightning data provided as-is; not for safety-critical use. Commercial use is permitted on every current plan. Read the EULA →