Guide · JavaScript · beginner
This walks through adding a live lightning layer to an existing Leaflet map. You fetch recent flashes for whatever the map is currently showing, draw each one as a circle marker, and refresh on a timer. It is roughly thirty lines and needs no plugin.
Do not put your API key in browser code. Anything in a script tag is public. Proxy the request through your own backend and keep the key there.
The browser calls your server, your server calls the API with the key, and the response is passed straight back. This example is a Next.js route handler, but the shape is the same anywhere.
app/api/flashes/route.ts
export async function GET(req: Request) {
const { searchParams } = new URL(req.url);
const qs = new URLSearchParams({
since_minutes: searchParams.get("since_minutes") ?? "15",
min_lat: searchParams.get("min_lat") ?? "",
max_lat: searchParams.get("max_lat") ?? "",
min_lon: searchParams.get("min_lon") ?? "",
max_lon: searchParams.get("max_lon") ?? "",
});
const res = await fetch(`https://api.lightningapi.dev/v1/flashes?${qs}`, {
headers: { "X-API-Key": process.env.LIGHTNING_API_KEY! },
next: { revalidate: 20 },
});
return new Response(await res.text(), {
status: res.status,
headers: { "content-type": "application/json" },
});
}The twenty second revalidate window matches how often the underlying data changes, so several users sharing a map do not each spend a call.
Keep the flashes in their own layer group so you can clear and redraw without touching the base map. Query the map's current bounds so you never ask for data outside the viewport.
Lightning layer
const lightning = L.layerGroup().addTo(map);
async function refresh() {
const b = map.getBounds();
const qs = new URLSearchParams({
since_minutes: "15",
min_lat: b.getSouth().toFixed(3),
max_lat: b.getNorth().toFixed(3),
min_lon: b.getWest().toFixed(3),
max_lon: b.getEast().toFixed(3),
});
const res = await fetch("/api/flashes?" + qs);
if (!res.ok) return;
const { flashes = [] } = await res.json();
lightning.clearLayers();
for (const f of flashes) {
L.circleMarker([f.lat, f.lon], {
radius: 4,
weight: 0,
fillColor: "#ffd166",
fillOpacity: 0.85,
}).addTo(lightning);
}
}
refresh();
setInterval(refresh, 20000);
map.on("moveend", refresh);Refreshing on both a timer and every map move is convenient and expensive. A user panning around burns calls quickly. Debounce the moveend handler, and consider dropping the timer interval when the tab is hidden.
Pay As You Go includes 5,000 calls a month at no cost and bills $0.003 per call after that, with a 100,000 call monthly cap. If you are building something with real users behind it, Pro is where browser access and a larger allowance start.
Frequently asked questions
Your key would be visible to anyone who opens developer tools. A one-route proxy keeps it on the server and costs you almost nothing in complexity.
The fetch half is identical. Only the drawing changes, since those libraries use GeoJSON sources rather than circle markers. The radar tiles guide shows the MapLibre pattern.
Twenty seconds matches how often the data changes. Faster than that spends quota without showing you anything new.
Up to the per-call row cap for your plan, which is 20,000 on Pro and 50,000 on Ultimate. A single map viewport over fifteen minutes is normally far below that.
Yes. Each flash carries its classification, so you can filter client side, and the stream endpoint accepts a cg_only parameter if you move to push delivery later.
Related
Last checked 2026-09-20
Coverage areas
Lightning data provided as-is; not for safety-critical use. Commercial use is permitted on every current plan. Read the EULA →