openapi: 3.1.0
info:
  title: DistrictFacts Public Data API
  version: 1.8.0
  description: |-
    Static public records projected from the same per-district artifacts used by
    DistrictFacts pages. Measures are the values each agency published, unrounded.
    Line items marked aggregate: true are DistrictFacts sums of the agency's rows,
    as the row's locator says. Line items span several categories (for example
    spending and tax apportionment in New Jersey); sum only within one category
    and never across aggregate and non-aggregate rows. New York carries Fiscal Profile
    history and FY2025 finance components with source hashes; coverage_note identifies
    website fields not yet exported. Its state-only district has leaid: null and an
    explicit district_key; never interpret that key as an NCES identifier.

    Salary figures: Role-level averages are included. Leadership single-position
    figures are included and flagged as such. Assistant-principal salaries and
    aggregates of two or fewer positions drawn from a source list that names each
    person are withheld. No named individuals are included. The website may show
    more than the open-data files.

    School history: Georgia school discipline figures are carried for the five
    most recent school years only, so every district file stays under 8 MiB; the
    record's school_history_limits says which periods it keeps, and the full
    history stays on each school's page.

    Files listed in the API index are available under CC BY 4.0. The jurisdiction
    status file covers every jurisdiction on the website, including those held
    from file distribution and the CC BY offer. Connecticut figures come
    from the Connecticut State Department of Education (CT EdSight, listed as
    public domain on data.ct.gov) and are published without warranty; small counts
    are suppressed under CSDE's student-privacy rules. The exceptions list is kept
    for any future state whose terms are under review; it is empty today.

    Version policy: Additive changes (new fields, files, states) ship within v1
    without notice. Anything that removes, renames or changes the meaning of a field
    ships as v2 at a new path, with v1 kept for at least 6 months.

    How to cite: Cite the district page URL, the record's generated_at date, and,
    for exact reproducibility, the file's sha256 from districts.json.

    The www.districtfacts.com server is the keyless static host. The
    api.districtfacts.com server serves the same paths through the keyed service;
    requests there require an API key in Authorization: Bearer <key> or X-API-Key.
    API keys in the query string are refused and a verified key in a query string
    is revoked. The free tier permits 60 requests per minute (120-request burst),
    5,000 requests per day, 50,000 requests per month, and 20 bulk requests per
    day. A keyed bulk GET request redirects to a signed download URL valid for
    600 seconds; a keyed bulk HEAD request returns metadata without a body. The
    keyed API resources accept GET and HEAD on every path and answer other
    resource methods with 405 method_not_allowed; CORS preflight OPTIONS is
    answered separately with 204.
  license:
    name: CC BY 4.0
    identifier: CC-BY-4.0
    x-status: final
    x-effective: '2026-09-28'
    x-terms: https://www.districtfacts.com/open-data/#terms
    x-exceptions: []
servers:
  - url: https://www.districtfacts.com
    description: Static production host
  - url: https://api.districtfacts.com
    description: >-
      Keyed production host; the same paths require an API key in
      Authorization: Bearer <key> or X-API-Key.
paths:
  /data/v1/openapi.yaml:
    get:
      operationId: getApiV1OpenApi
      summary: Get the OpenAPI 3.1 contract for this API
      responses:
        '200':
          description: OpenAPI 3.1 contract
          content:
            'application/yaml; charset=utf-8':
              schema:
                type: string
          headers: &commonSuccessHeaders
            X-RateLimit-Limit: { $ref: '#/components/headers/X-RateLimit-Limit' }
            X-RateLimit-Remaining: { $ref: '#/components/headers/X-RateLimit-Remaining' }
            X-RateLimit-Reset: { $ref: '#/components/headers/X-RateLimit-Reset' }
            X-RateLimit-Resource: { $ref: '#/components/headers/X-RateLimit-Resource' }
            RateLimit-Policy: { $ref: '#/components/headers/RateLimit-Policy' }
            X-Request-Id: { $ref: '#/components/headers/X-Request-Id' }
            X-DistrictFacts-Release: { $ref: '#/components/headers/X-DistrictFacts-Release' }
        '400':
          $ref: '#/components/responses/QueryKeyRejected'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '304':
          $ref: '#/components/responses/NotModified'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalError'
        '429':
          $ref: '#/components/responses/RateLimited'
  /data/v1/changelog.json:
    get:
      operationId: getApiV1Changelog
      summary: Get the API v1 release history
      responses:
        '200':
          description: Ordered API v1 changelog
          content:
            'application/json; charset=utf-8':
              schema:
                $ref: '#/components/schemas/Changelog'
          headers: *commonSuccessHeaders
        '400':
          $ref: '#/components/responses/QueryKeyRejected'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '304':
          $ref: '#/components/responses/NotModified'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalError'
        '429':
          $ref: '#/components/responses/RateLimited'
  /data/v1/index.json:
    get:
      operationId: getApiV1Index
      summary: List available states
      responses:
        '200':
          description: State index
          content:
            'application/json; charset=utf-8':
              schema:
                $ref: '#/components/schemas/StateIndex'
          headers: *commonSuccessHeaders
        '400':
          $ref: '#/components/responses/QueryKeyRejected'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '304':
          $ref: '#/components/responses/NotModified'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalError'
        '429':
          $ref: '#/components/responses/RateLimited'
  /data/v1/jurisdictions.json:
    get:
      operationId: getApiV1Jurisdictions
      summary: List website, API, bulk-file and licence status for every jurisdiction
      responses:
        '200':
          description: Distribution status, hold reasons and recorded source-terms review dates
          content:
            'application/json; charset=utf-8':
              schema:
                $ref: '#/components/schemas/Jurisdictions'
          headers: *commonSuccessHeaders
        '400':
          $ref: '#/components/responses/QueryKeyRejected'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '304':
          $ref: '#/components/responses/NotModified'
        '404':
          $ref: '#/components/responses/NotFound'
        '500':
          $ref: '#/components/responses/InternalError'
        '429':
          $ref: '#/components/responses/RateLimited'
  /data/v1/{state}/districts.json:
    get:
      operationId: getApiV1StateDistricts
      summary: List districts available for a state
      parameters:
        - $ref: '#/components/parameters/AvailableState'
      responses:
        '200':
          description: District directory
          content:
            'application/json; charset=utf-8':
              schema:
                $ref: '#/components/schemas/DistrictDirectory'
          headers: *commonSuccessHeaders
        '304':
          $ref: '#/components/responses/NotModified'
        '404':
          $ref: '#/components/responses/NotFound'
        '400':
          $ref: '#/components/responses/QueryKeyRejected'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /data/v1/{state}/bulk.ndjson:
    get:
      operationId: getApiV1StateBulk
      summary: Get every district record in a state as newline-delimited JSON
      description: >-
        Records are in slug order. Each line is byte-identical to the content of
        that district's individual JSON file, and the file is newline-terminated.
      parameters:
        - $ref: '#/components/parameters/AvailableState'
      responses:
        '200':
          description: One ApiDistrict JSON object per line
          content:
            'application/x-ndjson; charset=utf-8':
              schema:
                type: string
                description: Newline-delimited ApiDistrict objects.
          headers: *commonSuccessHeaders
        '404':
          $ref: '#/components/responses/NotFound'
        '302':
          description: Only on the keyed host; redirects to a signed bulk download URL valid for 600 seconds.
          headers:
            Location:
              schema:
                type: string
                format: uri
          content: {}
        '400':
          $ref: '#/components/responses/QueryKeyRejected'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
  /data/v1/{state}/districts/{slug}.json:
    get:
      operationId: getApiV1District
      summary: Get one district and its published, withheld, and not-collected claims
      parameters:
        - $ref: '#/components/parameters/AvailableState'
        - name: slug
          in: path
          required: true
          schema:
            type: string
            minLength: 1
      responses:
        '200':
          description: Projected district record
          content:
            'application/json; charset=utf-8':
              schema:
                $ref: '#/components/schemas/ApiDistrict'
          headers: *commonSuccessHeaders
        '304':
          $ref: '#/components/responses/NotModified'
        '404':
          $ref: '#/components/responses/NotFound'
        '400':
          $ref: '#/components/responses/QueryKeyRejected'
        '401':
          $ref: '#/components/responses/Unauthorized'
        '403':
          $ref: '#/components/responses/Forbidden'
        '429':
          $ref: '#/components/responses/RateLimited'
        '500':
          $ref: '#/components/responses/InternalError'
components:
  securitySchemes:
    bearerKey:
      type: http
      scheme: bearer
      description: >-
        API key sent as Authorization: Bearer <key> on the keyed host.
    apiKeyHeader:
      type: apiKey
      in: header
      name: X-API-Key
      description: >-
        API key sent in X-API-Key on the keyed host. A key in the query string
        is refused and the verified key is revoked.
  headers:
    X-RateLimit-Limit:
      description: Only sent by the keyed host. Limit for the rate-limit resource named by X-RateLimit-Resource.
      schema: { type: integer, minimum: 0 }
    X-RateLimit-Remaining:
      description: Only sent by the keyed host. Requests remaining for the rate-limit resource named by X-RateLimit-Resource.
      schema: { type: integer, minimum: 0 }
    X-RateLimit-Reset:
      description: Only sent by the keyed host. Unix time in seconds when the rate-limit resource resets, or when the next request can be made for a minute bucket.
      schema: { type: integer, minimum: 0 }
    X-RateLimit-Resource:
      description: >-
        Only sent by the keyed host. Rate-limit resource selected for these
        headers: minute, day, month, or bulk.
      schema: { type: string, enum: [minute, day, month, bulk] }
    RateLimit-Policy:
      description: Only sent by the keyed host. The keyed free-tier policy, expressed as minute, day, and month windows.
      schema: { type: string }
    Retry-After:
      description: Seconds to wait before retrying a rate-limited request.
      schema: { type: integer, minimum: 1 }
    X-Request-Id:
      description: Only sent by the keyed host. Request identifier for support and log correlation.
      schema: { type: string, format: uuid }
    X-DistrictFacts-Release:
      description: Only sent by the keyed host. Release identifier serving the response.
      schema: { type: string, minLength: 1 }
  responses:
    QueryKeyRejected:
      description: Only on the keyed host; the service refuses API keys in the query string and revokes a verified key found there.
      content:
        application/problem+json:
          schema: { $ref: '#/components/schemas/Problem' }
    Unauthorized:
      description: Only on the keyed host; the API key is missing or invalid.
      headers:
        X-Request-Id: { $ref: '#/components/headers/X-Request-Id' }
      content:
        application/problem+json:
          schema: { $ref: '#/components/schemas/Problem' }
    Forbidden:
      description: Only on the keyed host; the API key is revoked or the developer account is suspended.
      content:
        application/problem+json:
          schema: { $ref: '#/components/schemas/Problem' }
    NotFound:
      description: >-
        On the static host, a plain 404 means the state directory was not found,
        the state bulk file was not found, or the district record was not found.
        On the keyed host, the same missing resource is returned as
        application/problem+json with code not_found.
      content:
        application/problem+json:
          schema: { $ref: '#/components/schemas/Problem' }
    NotModified:
      description: >-
        Only on the keyed host and only for non-bulk paths. If If-None-Match
        matches the current ETag, the service returns 304. It consumes one
        minute-bucket request, but does not increment daily or monthly usage.
      headers:
        X-RateLimit-Limit: { $ref: '#/components/headers/X-RateLimit-Limit' }
        X-RateLimit-Remaining: { $ref: '#/components/headers/X-RateLimit-Remaining' }
        X-RateLimit-Reset: { $ref: '#/components/headers/X-RateLimit-Reset' }
        X-RateLimit-Resource: { $ref: '#/components/headers/X-RateLimit-Resource' }
        RateLimit-Policy: { $ref: '#/components/headers/RateLimit-Policy' }
        X-Request-Id: { $ref: '#/components/headers/X-Request-Id' }
        X-DistrictFacts-Release: { $ref: '#/components/headers/X-DistrictFacts-Release' }
    RateLimited:
      description: >-
        Only on the keyed host. A 429 means the API key exceeded its minute,
        daily, monthly, or bulk limit, usage data is temporarily unavailable,
        or one address made more than 20 invalid-key attempts in the same
        minute (the 21st attempt and later are throttled).
      headers:
        X-RateLimit-Limit: { $ref: '#/components/headers/X-RateLimit-Limit' }
        X-RateLimit-Remaining: { $ref: '#/components/headers/X-RateLimit-Remaining' }
        X-RateLimit-Reset: { $ref: '#/components/headers/X-RateLimit-Reset' }
        X-RateLimit-Resource: { $ref: '#/components/headers/X-RateLimit-Resource' }
        RateLimit-Policy: { $ref: '#/components/headers/RateLimit-Policy' }
        X-Request-Id: { $ref: '#/components/headers/X-Request-Id' }
        X-DistrictFacts-Release: { $ref: '#/components/headers/X-DistrictFacts-Release' }
        Retry-After: { $ref: '#/components/headers/Retry-After' }
      content:
        application/problem+json:
          schema: { $ref: '#/components/schemas/Problem' }
    InternalError:
      description: Only on the keyed host; the service failed while validating a key, preparing a bulk redirect, or reading the requested resource.
      content:
        application/problem+json:
          schema: { $ref: '#/components/schemas/Problem' }
  parameters:
    AvailableState:
      name: state
      in: path
      required: true
      description: A state currently present in the static v1 corpus.
      schema:
        $ref: '#/components/schemas/AvailableStateCode'
  schemas:
    Jurisdictions:
      type: object
      additionalProperties: false
      required: [api_version, generated_at, licence_terms, note, reviewed_at_note, jurisdictions]
      properties:
        api_version:
          $ref: '#/components/schemas/ApiVersion'
        generated_at:
          type: string
          format: date-time
        licence_terms:
          type: string
          const: https://www.districtfacts.com/open-data/#terms
        note:
          type: string
          const: "A held jurisdiction's pages remain on districtfacts.com; held means its files are not offered for download or under CC BY."
        reviewed_at_note:
          type: string
          const: "reviewed_at is the date of a source-terms review recorded in our files. Null means no dated review is recorded; the reason says whether the terms have been read."
        jurisdictions:
          type: array
          description: Every public website jurisdiction, sorted by name.
          items:
            $ref: '#/components/schemas/JurisdictionStatus'
    JurisdictionStatus:
      type: object
      additionalProperties: false
      required: [state, name, web_status, web_url, api_status, api_url, bulk_status, bulk_url, license_status, reason, reviewed_at]
      description: >-
        Held jurisdictions have null API and bulk URLs, a non-empty reason and
        a held licence status. Published jurisdictions have both file URLs,
        CC-BY-4.0 licence status and a null reason. A null reviewed_at means no
        dated source-terms review is recorded here; it does not change distribution status.
      properties:
        state:
          type: string
          pattern: '^[a-z]{2}$'
        name:
          type: string
          minLength: 1
        web_status:
          type: string
          const: published
        web_url:
          type: string
          format: uri
        api_status:
          type: string
          enum: [published, held]
        api_url:
          type: [string, 'null']
          format: uri-reference
          minLength: 1
        bulk_status:
          type: string
          enum: [published, held]
        bulk_url:
          type: [string, 'null']
          format: uri-reference
          minLength: 1
        license_status:
          type: string
          enum: [CC-BY-4.0, held]
        reason:
          type: [string, 'null']
          minLength: 1
        reviewed_at:
          type: [string, 'null']
          format: date
    ApiVersion:
      type: string
      const: 1.8.0
    Problem:
      type: object
      additionalProperties: false
      description: >-
        Problem response from the keyed service. Error codes are identified by
        the final fragment of type. The service returns today: missing_key,
        invalid_key, key_in_query, key_revoked, developer_suspended,
        rate_limited, not_found, method_not_allowed, and internal.
      required: [type, title, status, detail, request_id]
      properties:
        type:
          type: string
          format: uri
        title:
          type: string
        status:
          type: integer
        detail:
          type: string
        request_id:
          type: string
          format: uuid
    LicenceStatus:
      type: string
      enum: [final, under_review]
    StateCode:
      type: string
      enum: [NJ, CT, PA, DE, NH, MD, RI, VT, DC, GA]
    AvailableStateCode:
      type: string
      enum: [nj, ct, pa, de, nh, md, ri, vt, dc, ga]
    Category:
      type: string
      enum: [finance, students, staff, outcomes, programs, climate, funding, fiscal]
    WithheldReason:
      type: string
      enum:
        - agency_suppressed
        - failed_distribution_gate
        - definition_not_comparable
        - source_unverified
        - superseded_pending_review
        - privacy_review
    AbsenceCapability:
      type: string
      enum:
        - finance_detail
        - funding_taxes
        - staffing
        - outcomes
        - students
        - programs
        - climate
        - fiscal_transparency
        - trends
        - comparisons
        - schools
    ApiSource:
      type: object
      additionalProperties: false
      required: [id, url, sha256, locator, retrieved_at, source_host]
      properties:
        id:
          type: string
          minLength: 1
        url:
          type: string
          format: uri
          pattern: '^[Hh][Tt][Tt][Pp][Ss]?://'
        sha256:
          type: string
          pattern: '^[A-Fa-f0-9]{64}$'
        locator:
          type: string
          minLength: 1
        retrieved_at:
          type: string
          format: date-time
        source_host:
          type: string
          minLength: 1
    PublishedCell:
      type: object
      additionalProperties: false
      required: [status, key, label, category, period, definition, value, unit, source_id, locator]
      properties:
        status:
          type: string
          const: published
        key:
          type: string
          minLength: 1
        label:
          type: string
          minLength: 1
        category:
          $ref: '#/components/schemas/Category'
        period:
          type: string
          minLength: 1
        definition:
          type: string
          minLength: 1
        value:
          type: number
          description: Exact stored value; monetary units remain integer cents.
        unit:
          type: string
          minLength: 1
        source_id:
          type: string
          minLength: 1
          description: Key in the district record's sources registry.
        locator:
          type: string
          minLength: 1
        position_class:
          type: string
          const: single_position
          description: Identifies a published leadership salary held by one position.
    WithheldCell:
      type: object
      additionalProperties: false
      required: [status, key, label, category, period, definition, reason, note, source_id, locator]
      properties:
        status:
          type: string
          const: withheld
        key:
          type: string
          minLength: 1
        label:
          type: string
          minLength: 1
        category:
          $ref: '#/components/schemas/Category'
        period:
          type: string
          minLength: 1
        definition:
          type: string
          minLength: 1
        reason:
          $ref: '#/components/schemas/WithheldReason'
        note:
          type: string
          minLength: 1
        source_id:
          type: string
          minLength: 1
          description: Key in the district record's sources registry.
        locator:
          type: string
          minLength: 1
    NotCollectedCell:
      type: object
      additionalProperties: false
      required: [status, key, label, category, note]
      properties:
        status:
          type: string
          const: not_collected
        key:
          type: string
          minLength: 1
        label:
          type: string
          minLength: 1
        category:
          $ref: '#/components/schemas/Category'
        note:
          type: string
          minLength: 1
        capability:
          $ref: '#/components/schemas/AbsenceCapability'
    Cell:
      oneOf:
        - $ref: '#/components/schemas/PublishedCell'
        - $ref: '#/components/schemas/WithheldCell'
        - $ref: '#/components/schemas/NotCollectedCell'
      discriminator:
        propertyName: status
        mapping:
          published: '#/components/schemas/PublishedCell'
          withheld: '#/components/schemas/WithheldCell'
          not_collected: '#/components/schemas/NotCollectedCell'
    SchoolCell:
      oneOf:
        - $ref: '#/components/schemas/PublishedCell'
        - $ref: '#/components/schemas/WithheldCell'
      discriminator:
        propertyName: status
        mapping:
          published: '#/components/schemas/PublishedCell'
          withheld: '#/components/schemas/WithheldCell'
    Notice:
      type: object
      additionalProperties: false
      description: Context that changes how the record's figures should be read.
      required: [text, href, link_label, locator, source_id]
      properties:
        text:
          type: string
          minLength: 1
        href:
          type: string
          pattern: '^/[a-z]{2}/[a-z0-9-]+/$'
        link_label:
          type: string
          minLength: 1
        locator:
          type: string
          minLength: 1
        source_id:
          type: string
          minLength: 1
          description: Key in the district record's sources registry.
    LineItem:
      type: object
      additionalProperties: false
      required:
        - code
        - label
        - category
        - category_code
        - location
        - period
        - aggregate
        - general_fund_cents
        - grants_revolving_cents
        - total_cents
        - per_pupil_cents
        - locator
        - source_id
      properties:
        code:
          type: string
        label:
          type: string
        category:
          type: string
        category_code:
          type: string
        location:
          type: string
        period:
          type: string
        aggregate:
          type: boolean
        general_fund_cents:
          type: [integer, 'null']
          minimum: -9007199254740991
          maximum: 9007199254740991
        grants_revolving_cents:
          type: [integer, 'null']
          minimum: -9007199254740991
          maximum: 9007199254740991
        total_cents:
          type: [integer, 'null']
          minimum: -9007199254740991
          maximum: 9007199254740991
        per_pupil_cents:
          type: [integer, 'null']
          minimum: -9007199254740991
          maximum: 9007199254740991
        locator:
          type: string
        source_id:
          type: string
          minLength: 1
          description: Key in the district record's sources registry.
    School:
      type: object
      additionalProperties: false
      required: [code, name, grades, page_url, measures]
      properties:
        code:
          type: string
          pattern: '^[0-9A-Za-z-]{3,16}$'
        name:
          type: string
          minLength: 1
        grades:
          type: string
        page_url:
          type: string
          format: uri
        measures:
          type: array
          items:
            $ref: '#/components/schemas/SchoolCell'
        notice:
          $ref: '#/components/schemas/Notice'
    ApiDistrict:
      type: object
      additionalProperties: false
      required:
        - api_version
        - state
        - leaid
        - slug
        - name
        - generated_at
        - page_url
        - licence_status
        - attribution
        - sources
        - measures
        - schools
        - line_items
      properties:
        api_version:
          $ref: '#/components/schemas/ApiVersion'
        state:
          $ref: '#/components/schemas/StateCode'
        leaid:
          type: [string, 'null']
          pattern: '^\d{7}$'
          description: NCES LEAID; null only for a NY state-only identity with district_key.
        district_key:
          type: string
          pattern: '^(\d{7}|ny-beds-\d{12})$'
        slug:
          type: string
          minLength: 1
        name:
          type: string
          minLength: 1
        state_district_id:
          type: string
          description: Exact state agency district identifier where provided.
        coverage_note:
          type: string
          description: Explicit limits of this export versus website coverage.
        generated_at:
          type: string
          format: date-time
        page_url:
          type: string
          format: uri
        licence_status:
          $ref: '#/components/schemas/LicenceStatus'
        attribution:
          type: string
          const: 'Source: DistrictFacts (districtfacts.com), compiled from public records listed in sources.'
        sources:
          type: object
          description: Only sources referenced by this district's cells, school cells, line items, and notices.
          additionalProperties:
            $ref: '#/components/schemas/ApiSource'
        measures:
          type: array
          items:
            $ref: '#/components/schemas/Cell'
        schools:
          type: array
          items:
            $ref: '#/components/schemas/School'
        line_items:
          type: array
          items:
            $ref: '#/components/schemas/LineItem'
        notice:
          $ref: '#/components/schemas/Notice'
        school_history_limits:
          type: array
          description: Added in 1.3.0. Present only where the file carries fewer periods of a school-level series than the website shows (Georgia school discipline, the five most recent school years).
          items:
            $ref: '#/components/schemas/SchoolHistoryLimit'
    SchoolHistoryLimit:
      type: object
      additionalProperties: false
      required: [measures, periods, note]
      properties:
        measures:
          type: string
          description: Key pattern of the school measures the limit applies to, for example school_discipline_*.
        periods:
          type: array
          minItems: 1
          description: The periods the file carries for those measures, oldest first.
          items:
            type: string
        note:
          type: string
    StateIndexEntry:
      type: object
      additionalProperties: false
      required: [state, name, districts, api_url, bulk_url, bulk_bytes, districts_sha256]
      properties:
        state:
          $ref: '#/components/schemas/AvailableStateCode'
        name:
          type: string
          minLength: 1
        districts:
          type: integer
          minimum: 0
        api_url:
          type: string
          format: uri-reference
        bulk_url:
          type: string
          format: uri-reference
        bulk_bytes:
          type: integer
          minimum: 0
        districts_sha256:
          type: string
          pattern: '^[a-f0-9]{64}$'
        source_notice:
          type: string
          description: Attribution a source licence requires for this state's records (for example dc.gov's CC BY 3.0). Present only where one applies.
    StateIndex:
      type: object
      additionalProperties: false
      required: [api_version, generated_at, licence, attribution, docs, changelog, version_policy, states, excluded]
      properties:
        api_version:
          $ref: '#/components/schemas/ApiVersion'
        generated_at:
          type: string
          format: date-time
        licence:
          type: object
          additionalProperties: false
          required: [id, url, status, effective, terms, exceptions]
          properties:
            id:
              type: string
              const: CC-BY-4.0
            url:
              type: string
              format: uri
              const: https://creativecommons.org/licenses/by/4.0/
            status:
              type: string
              const: final
            effective:
              type: string
              format: date
              const: '2026-09-28'
            terms:
              type: string
              format: uri
              const: https://www.districtfacts.com/open-data/#terms
            exceptions:
              type: array
              items:
                type: object
                additionalProperties: false
                required: [state, status, note]
                properties:
                  state:
                    type: string
                  status:
                    type: string
                    enum: [under_review]
                  note:
                    type: string
        attribution:
          type: string
          const: 'Source: DistrictFacts (districtfacts.com), compiled from public records listed in sources.'
        docs:
          type: string
          const: /data/v1/openapi.yaml
        changelog:
          type: string
          const: /data/v1/changelog.json
        version_policy:
          type: string
          const: 'Additive changes (new fields, files, states) ship within v1 without notice. Anything that removes, renames or changes the meaning of a field ships as v2 at a new path, with v1 kept for at least 6 months.'
        states:
          type: array
          items:
            $ref: '#/components/schemas/StateIndexEntry'
        excluded:
          type: array
          items:
            type: string
          description: Explicit excluded record classes; current exclusions are listed in the index.
    DistrictDirectoryEntry:
      type: object
      additionalProperties: false
      required: [slug, name, leaid, page_url, api_url, sha256, bytes]
      properties:
        leaid:
          type: [string, 'null']
          pattern: '^\d{7}$'
          description: NCES LEAID; null only for a NY state-only identity with district_key.
        district_key:
          type: string
          pattern: '^(\d{7}|ny-beds-\d{12})$'
        slug:
          type: string
          minLength: 1
        name:
          type: string
          minLength: 1
        page_url:
          type: string
          format: uri
        api_url:
          type: string
          format: uri-reference
        sha256:
          type: string
          pattern: '^[a-f0-9]{64}$'
        bytes:
          type: integer
          minimum: 0
    DistrictDirectory:
      type: array
      items:
        $ref: '#/components/schemas/DistrictDirectoryEntry'
    ChangelogEntry:
      type: object
      additionalProperties: false
      required: [date, version, changes]
      properties:
        date:
          type: string
          format: date
        version:
          type: string
          pattern: '^1\.\d+\.\d+$'
        changes:
          type: array
          minItems: 1
          items:
            type: string
            minLength: 1
    Changelog:
      type: array
      items:
        $ref: '#/components/schemas/ChangelogEntry'
