Hobbs · Property Data Report

Cook County Property & Tax Data — Extraction Guide

How to pull Cook County, Illinois parcel, assessment, tax-rate, and equalization data directly from the public Socrata API — without scraping, without reCAPTCHA, without auth.

Prepared by: Dr. Strange 🔮 · Kaizen AI Lab Date: May 2026 Audience: Engineering / Data Status: Live · Verified

1. TL;DR

Cook County publishes its property, assessment, and tax-rate data on a Socrata-powered Open Data portal at datacatalog.cookcountyil.gov. Everything you need lives there as JSON/CSV with a documented query language. No authentication is required to read. A free app token raises the throttle ceiling but is not strictly required.

2. Why not scrape the public sites

The instinct is to point Playwright at cookcountyassessoril.gov/address-search and call it a day. Don't. Here's what you'd actually be fighting:

The shortcut

Cook County publishes the underlying tables directly. You skip every anti-bot layer by going to the same source the public-facing site is pulling from.

3. The actual sources of truth

SurfaceWhat it isUse for
Cook County Open Data Portal
datacatalog.cookcountyil.gov
Socrata-hosted catalog of every Cook County dataset. Free JSON/CSV API. No auth for read. Everything: parcels, assessments, sales, exemptions, tax rates, agency rates, geographic boundaries.
CCAO Data GitHub
github.com/ccao-data
Cook County Assessor's Office Data Science team. Open-source R/Python packages + the ptaxsim tax-bill-simulation SQLite database. Bulk historical analysis without API pagination. Tax-bill modeling.
cookcountypropertyinfo.com Treasurer's per-PIN portal. Static HTML detail pages keyed by 14-digit PIN. Live billing/payment status (paid/unpaid, due dates) — the one thing not in open data. PIN-by-PIN only.
cookcountyassessor.com/pin/<PIN> Assessor's PIN detail page. Static enough to parse. Fallback if a specific rendered field isn't in the API. Rarely needed.
IDOR annual order Illinois Department of Revenue's annual state multiplier announcement. The single State Equalization Factor per tax year.

4. Socrata API 101

Every Cook County dataset has a stable 4x4 resource ID (e.g. uzyt-m557) and is queryable at:

https://datacatalog.cookcountyil.gov/resource/{resource-id}.json
https://datacatalog.cookcountyil.gov/resource/{resource-id}.csv

You filter using SoQL (Socrata Query Language) via query-string parameters prefixed with $:

ParameterPurposeExample
$whereSQL-like filter$where=pin='32213210090000'
$selectPick columns$select=pin,certified_tot,year
$orderSort$order=year DESC
$limitPage size (max 50,000)$limit=50000
$offsetPagination cursor$offset=100000
$qFree-text search across columns$q=123 N MAIN

Optional: get a free App Token

Register at datacatalog.cookcountyil.gov/profile/edit/developer_settings and pass X-App-Token as a header. Without a token you share an anonymous throttling pool; with one you get your own much-higher ceiling. Either way, reads are free.

5. Assessor datasets (property & assessment)

DatasetResource IDKey columnsUse it for
Parcel Addresses3723-97qppin, prop_address_full, city, zipcodeAddress → PIN lookup
Parcel Universenj4t-kc8jpin, year, class, township_code, nbhd_code, tax_codeMaster list of every parcel
Parcel Universe (current year)pabr-t5khSame, current year onlyFaster queries for "now"
Assessed Valuesuzyt-m557pin, year, mailed_tot, certified_tot, board_totAssessment history by stage (mailed / certified / Board of Review)
Parcel Saleswvhk-k5uvpin, sale_date, sale_price, deed_typeSales transaction history
Single/Multi-Family Improvementsx54s-btdspin, bldg_sf, year_built, rooms, beds, bsmtBuilding characteristics
Residential Condo Unit Characteristics3r7i-mrz4Condo-specificCondo details
Appealsy282-6ig3pin, appeal_id, result, reasonAppeal history
Permits6yjf-dfxspin, permit_date, permit_typeBuilding permits
Property Tax-Exempt Parcelsvgzx-68gbExempt-status parcelsFilter out exempt properties
Neighborhood Boundaries (GeoJSON)pcdw-pxtgGeographic polygonsMapping
Commercial Valuationcsik-bswsCommercial-onlyIncome/cap-rate analysis

Example — address to PIN

GET https://datacatalog.cookcountyil.gov/resource/3723-97qp.json
    ?$where=upper(prop_address_full) like upper('%123 N MAIN%')
    &$limit=10

Example — assessment history for a PIN

GET https://datacatalog.cookcountyil.gov/resource/uzyt-m557.json
    ?pin=32213210090000
    &$order=year DESC

6. Treasurer / Clerk datasets (rates & equalizer)

The Treasurer collects the bill; the County Clerk publishes the tax rates and agency composition. All of it is on the same Open Data portal.

DatasetResource IDShapeUse it for
Tax codes, agencies, and rates9sqg-vznjLong format — one row per (year, tax_code, agency)★ Best for analytics — pivot/join/aggregate freely
Tax code Rates8vrk-e2ckWide format — one row per tax_code, columns per yearQuick year-over-year rate comparison
Tax Agency Rates9sgn-ibhuWide format — per agency, by year"How much of the bill goes to the school district?"
Residential Exemption Amounts (archived)a536-gjbqPer tax_code & year, includes equalization_factorHistorical equalization factor at tax-code granularity (2013-pre-2022)

Example — full tax composition for a tax code in a given year

GET https://datacatalog.cookcountyil.gov/resource/9sqg-vznj.json
    ?tax_year=2023
    &tax_code=10001

Returns one row per taxing body (county, schools, parks, library, etc.) plus the composite tax_code_rate. Sum the agency_rate values across the rows and you'll match the composite — that's the breakdown that shows up on a Cook County bill.

7. The equalization factor, explained

Why this exists

Illinois law requires property to be assessed at 33⅓% of fair market value statewide. Cook County, by ordinance, assesses at much lower fractions (10% residential, 25% commercial). The State Equalization Factor is the multiplier the Illinois Department of Revenue applies each year to bring Cook County's aggregate assessed value up to the statewide 33⅓% standard. It's how the state ensures Cook County pays its fair share toward state-funded programs.

It is exactly one number per tax year, applied county-wide. Recent values hover around 3.0.

The formula every Cook County bill follows:

EAV = AssessedValue × StateEqualizationFactor
NetEAV = EAV − Exemptions
Bill = NetEAV × (TaxCodeRate / 100)

Where to get the factor:

For programmatic use, the cleanest approach is a tiny static lookup table you maintain once a year:

# equalization_factors.json — update once per year
{
  "2019": 2.9160,
  "2020": 3.2234,
  "2021": 3.0027,
  "2022": 2.9237,
  "2023": 3.0163,
  "2024": 3.0103
  // Values shown are illustrative. Confirm against the IDOR order for production use.
}

8. Joining it together — address to estimated bill

Putting the joins in order, end to end:

# 1. Resolve address to PIN
3723-97qp  : address (free text)         → pin

# 2. Pull the parcel's tax code & class
nj4t-kc8j  : pin + year                  → tax_code, class, township

# 3. Pull the assessed value (latest stage available)
uzyt-m557  : pin + year                  → certified_tot (or board_tot)

# 4. Pull the tax code rate & agency breakdown
9sqg-vznj  : tax_code + year             → tax_code_rate, [agency, agency_rate]

# 5. Apply the state equalization factor (static lookup)
equalization_factors[year]               → state_eq_factor

# 6. (Optional) Subtract any exemptions
vgzx-68gb / exemption tables             → exemption_total

# 7. Compute
EAV     = certified_tot * state_eq_factor
NetEAV  = EAV - exemption_total
Bill    = NetEAV * (tax_code_rate / 100)

That's the entire bill. The Treasurer's website does the same math; you're just doing it from raw inputs.

9. Working Python example

Drop this in a file and run. No dependencies beyond requests.

import requests, urllib.parse

BASE = "https://datacatalog.cookcountyil.gov/resource"
HEADERS = {"X-App-Token": ""}  # optional, leave blank or add your token

def soda(resource, **params):
    url = f"{BASE}/{resource}.json"
    return requests.get(url, params=params, headers=HEADERS, timeout=15).json()

def find_pin_by_address(query):
    where = f"upper(prop_address_full) like upper('%{query}%')"
    return soda("3723-97qp", **{"$where": where, "$limit": 10})

def parcel_universe(pin, year=None):
    params = {"pin": pin}
    if year: params["year"] = year
    return soda("nj4t-kc8j", **params)

def assessed_values(pin):
    return soda("uzyt-m557", pin=pin, **{"$order": "year DESC"})

def tax_rate_composition(tax_code, year):
    return soda("9sqg-vznj", tax_code=tax_code, tax_year=str(year))

def estimate_bill(pin, year):
    parcel = parcel_universe(pin, year)[0]
    av     = float(assessed_values(pin)[0]["certified_tot"])
    rate   = float(tax_rate_composition(parcel["tax_code"], year)[0]["tax_code_rate"])
    eq     = EQUALIZATION_FACTORS[str(year)]  # static lookup
    eav    = av * eq
    bill   = eav * (rate / 100)
    return {"pin": pin, "year": year, "av": av, "eq": eq,
            "eav": eav, "rate_pct": rate, "est_bill": round(bill, 2)}

EQUALIZATION_FACTORS = {"2023": 3.0163, "2024": 3.0103}  # maintain yearly

if __name__ == "__main__":
    print(estimate_bill("32213210090000", 2023))

10. Working JavaScript example

Browser or Node — works either way. Useful for a thin client-side property-lookup form.

const BASE = "https://datacatalog.cookcountyil.gov/resource";

async function soda(resource, params = {}) {
  const qs = new URLSearchParams(params).toString();
  const r = await fetch(`${BASE}/${resource}.json?${qs}`);
  return r.json();
}

async function findPinByAddress(q) {
  return soda("3723-97qp", {
    "$where": `upper(prop_address_full) like upper('%${q}%')`,
    "$limit": 10
  });
}

async function taxComposition(taxCode, year) {
  return soda("9sqg-vznj", { tax_code: taxCode, tax_year: String(year) });
}

findPinByAddress("123 N MAIN").then(console.log);

11. Rate limits, freshness, & gotchas

12. When you must scrape (fallback)

One legitimate scraping scenario remains: live billing/payment status. This is not published on open data because it changes daily. If you need to know whether a specific PIN is paid or delinquent right now:

https://www.cookcountypropertyinfo.com/pin/<14-digit-pin>

The detail page renders server-side and the relevant fields (Total Amount Billed, Amount Paid, Last Payment Date, Status) are stable selectors. There is no reCAPTCHA on the GET path — only on POST forms. Polite scraping rules apply:

If you ever need to programmatically submit the Assessor's address-search form (you probably don't — the API does this better), you'd need to handle reCAPTCHA via 2Captcha or similar. Cost is ~$2 per 1,000 solves. Not worth it; use the API.

13. Quick-reference endpoint table

WhatResourceJSON URL
Parcel Addresses3723-97qp/resource/3723-97qp.json
Parcel Universenj4t-kc8j/resource/nj4t-kc8j.json
Parcel Universe (current)pabr-t5kh/resource/pabr-t5kh.json
Assessed Valuesuzyt-m557/resource/uzyt-m557.json
Parcel Saleswvhk-k5uv/resource/wvhk-k5uv.json
Improvement Characteristicsx54s-btds/resource/x54s-btds.json
Condo Characteristics3r7i-mrz4/resource/3r7i-mrz4.json
Appealsy282-6ig3/resource/y282-6ig3.json
Permits6yjf-dfxs/resource/6yjf-dfxs.json
Tax-Exempt Parcelsvgzx-68gb/resource/vgzx-68gb.json
Commercial Valuationcsik-bsws/resource/csik-bsws.json
Neighborhood Boundariespcdw-pxtg/resource/pcdw-pxtg.json
Tax codes, agencies, rates9sqg-vznj/resource/9sqg-vznj.json
Tax code Rates (wide)8vrk-e2ck/resource/8vrk-e2ck.json
Tax Agency Rates (wide)9sgn-ibhu/resource/9sgn-ibhu.json
Residential Exemption Amounts (archived; has eq. factor)a536-gjbq/resource/a536-gjbq.json
Next step

Wrap these endpoints in a small Python or TypeScript package keyed to the estimate_bill() flow in §8, and you have a complete address-to-bill engine without touching a single scraper. Maintain the State Equalization Factor lookup once per year and you're done.