API documentation v1

Day-level school calendar data for every US public school district — one row per district per day, every break named and typed, every row scored for confidence.

13,700+ districts, 44.6M students, a rolling three-year window. Flat $99/monthget a key, then read on.

Overview

Responses are read-only JSON (the CSV export returns text/csv), versioned under /v1. Every calendar row carries a source_method and a confidence so you always know whether a date was read from a real calendar or estimated. It's the same archive that powers this site.

Authentication

All data endpoints require a valid API key, passed as a Bearer token in the Authorization header. Keys start with ssd_live_ and are shown once, after purchase and in your account — we store only a SHA-256 hash and can't recover it for you.

# include your key in every request curl https://api.hazeydata.ai/ssd/v1/districts \ -H "Authorization: Bearer ssd_live_your_key_here"

Base URL

https://api.hazeydata.ai/ssd/v1/

All endpoints return application/json unless noted (the CSV export returns text/csv).

Rate limits

Each subscription includes 50,000 API calls per month, resetting on the 1st. Exceed it and requests return 429 Too Many Requests until reset. Track usage live in your account dashboard. Need higher volume? Contact us for enterprise pricing.

GET /districts auth

Search and list districts, ordered by enrollment (largest first).

GET /districts?state=FL&min_enrollment=10000
ParamTypeDescription
statestringTwo-letter code (case-insensitive)
searchstringPartial name match, e.g. Houston
min_enrollmentintEnrollment floor
limit / offsetintPagination (limit default 100, max 1000)
{ "total": 32, "limit": 100, "offset": 0, "districts": [ { "district_id": "FL_1200390", "district_name": "Miami-Dade County Public Schools", "state": "FL", "enrollment": 334235 } ] }

GET /days auth

The core endpoint — day-level rows showing whether each district is in session on each date, with break names and confidence.

GET /days?district_id=FL_1200390&school_year=2026-2027
GET /days?state=FL&date=2026-12-25
ParamTypeDescription
district_idstringA specific district
statestringTwo-letter code
datestringYYYY-MM-DD — all districts for that date
date_from / date_tostringDate range
school_yearstringe.g. 2026-2027
limitintDefault 365, max 5000
# what does spring break look like in Texas? curl "…/v1/days?state=TX&date_from=2027-03-08&date_to=2027-03-19" \ -H "Authorization: Bearer ssd_live_your_key" { "total": 2, // rows matching the query "returned": 2, "limit": 365, "offset": 0, "days": [ { "district_id": "TX_4823640", "district_name": "HOUSTON ISD", "state": "TX", "date": "2027-03-08", "school_year": "2026-2027", "is_in_session": 0.0, "day_type": "BREAK", "break_name": "Spring Recess", "confidence": 0.98, "source_method": "official_calendar_pdf_verified" } ] }

GET /breaks auth

Just the breaks, holidays, and non-school days (weekends excluded) — for "when is spring break across these districts?"

GET /breaks?state=CA&break_name=Spring
ParamTypeDescription
statestringTwo-letter code
break_namestringPartial match, e.g. Spring, Thanksgiving
date_from / date_tostringDate range
limitintDefault 100, max 1000

Key scope. A trial key is restricted to one state and one school year. An out-of-scope request returns 403 naming the restriction — it is never silently narrowed to what the key can see (Issue #211).

GET /export auth

Bulk CSV, same fields as /days, flattened for a warehouse or spreadsheet.

GET /export?format=csv&state=FL&school_year=2026-2027
ParamTypeDescription
school_yearstringDefault current year
statestringOmit for all states
formatstringcsv (default) or json; anything else returns 400
limitintDefault 50000, max 100000

Returns text/csv with a suggested filename — pipe with -o or load with pd.read_csv().

GET /sample no auth

Free — no key required. The top 100 districts by enrollment, to preview the data model before subscribing.

GET /sample/districts

Data model

School day object

FieldTypeDescription
is_in_sessionfloat1.0 full day · 0.5 half day · 0.0 no school · -1 no basis established (see note below). A real number, not a boolean — never sum it; count days with is_in_session > 0.
day_typestringSCHOOL_DAY, WEEKEND, BREAK, HOLIDAY, TEACHER_WORKDAY, HALF_DAY, SUMMER
break_namestring | nulle.g. Spring Break, Thanksgiving
confidencefloat0.0–1.0 — see below
source_methodstringHow we know: markitdown_extraction, pdf_extraction (both observed), imputation (estimated), deterministic (weekends, certain)
Observed vs estimated, in the payload: extraction methods are observed; imputation methods are estimates (honest low confidence). Days with no established basis ARE served, as the no-basis sentinel: is_in_session = -1, source_method = "unresolved", day_type = "UNKNOWN", confidence = 0.0. The sentinel means we don’t know, never closed — a complete grid with honest labels beats a hole in your join. Never sum is_in_session; count days where it is > 0.

School years

YearPeriodStatus
2024-2025Aug 2024 – Jun 2025Previous (historical)
2025-2026Aug 2025 – Jun 2026Most complete
2026-2027Aug 2026 – Jun 2027As published / estimated

The oldest year drops off each September. Requests outside the window return 403.

Confidence scores

RangeMeaning
0.98official_calendar_pdf_verified — the ceiling for any evidence method
0.95observed, r1_extract_driver, human_anchor_email
0.90inferred, state_median_imputationestimates
1.00deterministic and legacy — weekends and summer, classified by rule. Also not evidence.
Confidence does not rank methods against each other. It ranks certainty within a method, so a higher number is not a stronger provenance — the highest values in the archive belong to rule-derived estimates. Derive observed-vs-estimated from source_method, never from confidence. The evidence methods are observed, r1_extract_driver, official_calendar_pdf_verified and human_anchor_email.

Errors

StatusMeaning
200Success
400Bad request — check your parameters
401Missing or invalid API key
403Subscription inactive, or year outside the window
429Monthly rate limit exceeded
500Server error — let us know
{ "error": "Monthly rate limit exceeded (10000 calls). Resets on the 1st." }

Support

Something not working? Email us — you'll get a real human, usually within a few hours. Include your API key prefix (first 16 characters) so we can find your account. Most first-request issues are a missing Bearer prefix in the header.

Email support →