Quick answer
OpenFEMA publishes every US federal disaster declaration back to 1953 — hurricanes, floods, fires, severe storms — free, keyless, over an OData endpoint:
GET https://www.fema.gov/api/open/v2/DisasterDeclarationsSummaries
?$filter=state eq 'FL' and incidentType eq 'Hurricane'
&$top=1000&$skip=0
That is the FEMA Disaster Declarations Scraper: combine state, county, incident type, declaration type and a date range, and get clean typed rows instead of hand-writing OData.
The data is excellent. The query language is where runs die.
A malformed $filter returns a bare 400 and tells you nothing 🚧
OData's filter syntax is strict in ways that are easy to get wrong and hard to debug, because the response does not say which clause it rejected. You get a 400 and a guess.
The specific traps, in the order they bite:
-
String literals need single quotes.
state eq FLis a 400;state eq 'FL'is fine. -
Operators are words, not symbols.
eq,ne,ge,le—state = 'FL'is a 400. -
Dates are unquoted ISO-8601 datetimes.
declarationDate ge '2024-01-01'fails;declarationDate ge 2024-01-01T00:00:00.000Zworks. -
Field names are camelCase and case-sensitive.
incidenttypeis notincidentType. -
Values are case-sensitive too.
incidentType eq 'hurricane'returns zero rows — silently, and that is worse than the 400. It is a well-formed query for something that does not exist, so you get an empty result rather than an error, and an empty result reads like "no disasters matched" rather than "you typed it wrong."
That last one is the reason we build the $filter expression from five typed inputs instead of accepting a raw string. A 400 you can debug; a plausible empty answer you cannot.
Paging is $top/$skip, and you have to ask for the count 🔢
There is no cursor. You walk $skip in $top-sized steps, and the total only appears if you explicitly request metadata ($inlinecount=allpages) — otherwise you are paging blind and discovering the end by getting a short page.
We page deterministically against the reported count, bounded by your maxResults, so a run neither over-fetches (you pay for rows you did not ask for) nor stops early at a page boundary.
Zero rows is a success, not a failure ✅
Ask for tornado declarations in Rhode Island in a quiet year and the honest answer is: none. The run finishes SUCCEEDED with a clear status message.
We are deliberate about this in both directions, because both mistakes are real:
- Failing a zero-row run turns a correct narrow query into a red run and a support ticket.
- But a run that reports success while silently producing nothing is the more dangerous defect — it scores 100% on every health dashboard while delivering nothing. We have had that exact bug in this fleet, and it charged customers a start fee each time.
So an empty result is a success with an explicit reason, and an empty result caused by a broken fetch is a failure. Those are different outcomes and they should never look the same in your logs.
Verified against the source 🔍
We pull a row back out of the dataset and re-check it at FEMA independently. Disaster DR-4844 re-fetches as Hurricane Milton, Florida, incident type Hurricane — matched at the source, not just present in our own output.
What a row looks like
Disaster number, declaration type (DR / EM / FM), incident type, declaration date, incident begin and end dates, state, county / designated area, FIPS codes, and the program flags — individual assistance, public assistance, hazard mitigation — as real booleans rather than "1" strings. Per-record fault isolation: one malformed record is skipped and logged, never taking down the rest of the run.
Who this is for 🎯
- Insurance and reinsurance — county-level declaration frequency for a risk model.
- Restoration and construction contractors — a lead list grounded in real declaration history.
- Real estate — disaster exposure by county, over decades.
- Emergency management consulting — a state's full declaration record for a briefing.
- Monitoring — new declarations for a state or incident type, on a schedule.
The honest limitations 🚧
- Declaration summaries. Not obligation or payout amounts — those live in separate OpenFEMA datasets.
- County-level granularity comes from designated areas; a statewide declaration has no county rows to give you.
- FEMA restates and corrects historical records, so this is a current snapshot, not an immutable archive.
Pricing
$0.20 per run plus $0.002 per row — $2.20 per 1,000. Pay for rows that land.
→ FEMA Disaster Declarations Scraper on Apify
Built by Devil Scrapes. We build the OData filter, page it deterministically, and tell the difference between "nothing matched" and "something broke."
Top comments (0)