A free WARN Act API: 59,806 layoff notices, 48 states, one GET request

Every US state has to publish the mass-layoff notices employers file under the WARN Act, and every state publishes them differently — a portal here, a spreadsheet there, a PDF somewhere else. We scrape all 48 of them every morning, normalize them into one 11-field schema, resolve the employer names, and serve the result as plain JSON and CSV over HTTPS. No key, no signup, no rate limit, CORS open. If you have ever wanted to put real layoff data in something you are building, this is one curl away.

59,806notices, one GET away48 state agencies, back to 1988
0keys, tokens or signupsplain HTTPS, CORS open to any origin
59endpoints published44.9 MB of JSON/CSV, rebuilt daily
CC BY 4.0licenceuse it commercially, credit WARN Feed

Try it now — GET /data/latest.json Read the 11-field schema

Quickstart

# the whole archive as JSON (no key, no header)
curl -sL https://approjects-warn-act-notices.static.hf.space/data/warn_notices.json | jq '.[0]'

# just what is new — poll this one daily
curl -sL https://approjects-warn-act-notices.static.hf.space/data/latest.json | jq '.count, .generated_at'

# poll politely: send back the ETag you got last time — unchanged = 304, zero bytes
curl -sL -o latest.json -D - -H 'If-None-Match: "<etag from last run>"' https://approjects-warn-act-notices.static.hf.space/data/latest.json

# streaming: one notice per line, no 20 MB buffer
curl -sL https://approjects-warn-act-notices.static.hf.space/data/warn_notices.ndjson | head -n 3 | jq -c '{company_canonical, state, notice_date}'

# one state, as CSV
curl -sL https://approjects-warn-act-notices.static.hf.space/data/by-state/ca.csv | head -3
# python: the recent window straight into pandas
import pandas as pd
df = pd.read_csv("https://approjects-warn-act-notices.static.hf.space/data/latest.csv")
print(len(df), df.columns.tolist())
# duckdb: query the archive in place, no download step
SELECT state, count(*) AS notices
FROM read_csv_auto('https://approjects-warn-act-notices.static.hf.space/data/latest.csv')
GROUP BY 1 ORDER BY 2 DESC;
// browser javascript: CORS is open, so this works from any page
const r = await fetch("https://approjects-warn-act-notices.static.hf.space/data/coverage.json");
const cov = await r.json();
console.log(Object.keys(cov.states).length, "state agencies", cov.generated_at);

Endpoints

Base URL https://apventureengine.github.io/warn-act-notices. Sizes and row counts below are from the build that wrote this page, so they are what you will actually download today.

EndpointWhatRecordsSizeUse it for
/data/warn_notices.jsonWhole archive, JSON59,806 notices19.5 MBEvery notice we hold, one object per notice, 11 normalized fields. The file a backend or notebook wants.
/data/warn_notices.ndjsonWhole archive, NDJSON59,806 notices18.2 MBThe same notices, one JSON object per line, so a stream reader never buffers 20 MB: <code>curl … | head</code>, <code>jq -c</code>, log shippers, DuckDB.
/data/latest.csvRecent notices, CSV8,605 notices1.3 MBThe rolling window of notices added lately — the cheap poll for a daily job.
/data/latest.jsonRecent notices, JSON8,605 notices3.2 MBSame window as latest.csv with a header block: generated_at, window_days, count.
/datapackage.jsonSchema (Frictionless Table Schema)11 fields3 kBField names, types and descriptions for the CSVs. Read this before you map columns.
/data/coverage.jsonPer-state coverage + freshness48 states11 kBFor every state agency: rows held, last scrape time, latest notice date, status. This is the file to poll if you want to know whether a state is behind.
/data/stale.jsonSources behind their usual cadence9 late sources2 kBThe subset of coverage.json that is late, with how late. Machine-readable honesty.
/data/trends.jsonMonthly series24 months55 kBNotices and workers per month, national and by state — for charts without re-aggregating the archive.
/data/search-index.jsonEmployer search index29,131 employers2.6 MBCanonical employer names with their notice counts; powers client-side search.
/data/badge.jsonShields.io endpoint badge153 BDrop a live 'notices on record' badge in your own README.
/feed.xmlRSS of new notices50 items29 kBFor feed readers and RSS-to-anything plumbing. No key.

Per-state CSVs (48 states)

AK · AL · AZ · CA · CO · CT · DC · DE · FL · GA · HI · IA · ID · IL · IN · KS · KY · LA · MA · MD · ME · MI · MN · MO · MS · MT · NC · ND · NE · NJ · NM · NV · NY · OH · OK · OR · PA · RI · SC · SD · TN · TX · UT · VA · VT · WA · WI · WV

Pattern: https://approjects-warn-act-notices.static.hf.space/data/by-state/<two-letter-code-lowercase>.csv

California16,587 noticesNew York5,001 noticesIllinois4,845 noticesFlorida3,115 noticesTexas2,388 noticesNew Jersey2,323 noticesMichigan2,202 noticesPennsylvania1,895 noticesWashington1,504 noticesOregon1,370 noticesMaryland1,274 noticesIndiana1,182 notices
Notices on record per state agency, top 12 of 48 (59,806 in total). Each state also has its own CSV endpoint below. · Source: data/coverage.json, written by the same build as this page · as of 2026-09-13

Schema

The same fields in every endpoint, machine-readable at /datapackage.json:

FieldTypeMeaning
idstringStable dedupe id (source record number where available, else content hash)
statestring2-letter US postal code
companystringEmployer name exactly as published by the state
company_canonicalstringCleaned employer name; groups the same employer across states, renotices and store numbers
company_dbastringTrade name when the state published a dba/aka alias
locationstringCity/county/address, best effort
employees_affectedintegerWorkers affected; empty if the state omitted it
notice_datedateISO date the notice was received/posted (NJ: month precision, day pinned to 01)
effective_datedateISO date the layoff/closure takes effect
notice_typestringLayoff/closure label as published
first_seendatetimeUTC timestamp the pipeline first saw this notice

What this API does not do

Three things, and they are the honest reason a paid tier exists rather than a fake one:

If what you actually wanted was an alert, not a file: WARN Watch runs your employer list — up to 500 names, or whole states — against every daily refresh for a year and pushes the hits to a private page, an RSS feed, a calendar and your Slack, Discord or Teams webhook. WARN Watch — $49/year, one payment → Or take the free 30-day trial (3 employers or 1 state, no card).

FAQ

Is it really free, and what is the catch?

Really free, CC BY 4.0, no account. The catch is honest and structural: these are static files rebuilt once a morning, so there are no query parameters, no per-request filtering and no push. If that is enough, you never have to pay us.

How fresh is it?

The build that produced this page finished at 2026-09-13T01:40:52.945448Z and the newest notice in it is dated 2026-09-11. Every state is re-scraped in the same run; /data/coverage.json carries the per-state scrape time and /data/stale.json lists any source that is behind its usual cadence, so you never have to guess whether silence means 'no layoffs' or 'our scraper broke'.

Why does curl print 'Found. Redirecting to…' for the archive?

Files over about 10 MB (warn_notices.json, warn_notices.ndjson) answer 302 to a CDN copy; everything smaller is served directly. Follow redirects — curl -L, and every HTTP library does it by default. The CDN answers with the same access-control-allow-origin: * and its own ETag (measured 2026-09-13).

Can I call it from a browser?

Yes. The host answers access-control-allow-origin: * on these files, so fetch() works from any origin with no proxy of your own.

Rate limits?

None imposed by us, and we would rather you were polite than throttled. Every file is served with an ETag, and a request carrying If-None-Match with the last value you saw gets 304 Not Modified and no body (measured 2026-09-13: 304, 0 bytes). Last-Modified is not sent, so If-Modified-Since does nothing — use the ETag. Poll /data/latest.json (small) and only re-pull the archive when its generated_at moves. Mirroring the files is explicitly allowed — that is what CC BY 4.0 means.

What do I have to put in my app?

Credit "WARN Feed" with a link to this site. That is the whole obligation.

Will the fields change under me?

The schema lives in /datapackage.json and is the same contract the CSVs and the Hugging Face mirrors use. New columns get appended; if one ever has to change meaning it is written up in the repository's release notes, not silently.

I need something you do not publish.

Ask for it — open an issue and say which field, state or shape you need. Requests that are a small transform of what we already scrape usually ship the same week, because the pipeline runs every day anyway.

Mirrors and other shapes

The same data, maintained by the same daily build, if a different surface suits you better: the Git repository (versioned, every change is a commit) · the Hugging Face dataset (parquet-backed viewer, datasets.load_dataset) · RSS · the web search UI.