DEV Community

Cover image for Parsing California's data broker registry CSV
Edward Fancher
Edward Fancher

Posted on Originally published at github.com

Parsing California's data broker registry CSV

California requires data brokers to register every year with the California Privacy Protection Agency (CPPA) and, since the Delete Act (SB 362), to disclose what they collect, who they sold it to, and how many consumer requests they handled. The result is a public table at https://cppa.ca.gov/data_broker_registry/ with one row per registered broker. In the 2026-09-01 snapshot that is 603 companies, from Acxiom to firms you have never heard of, each answering the same 77 questions. It is where people-search sites such as TruthFinder and Whitepages state, in their own words, how many deletion requests they received in 2024 and how long they took to respond.

Getting the file

The registry page has a "Download Registration Information" button that serves a file called registry.csv. We pulled it on 2026-09-01 and committed the file, byte for byte, to https://github.com/delistmydata/ca-data-broker-registry so that anyone can work from the same data without re-scraping the state site. The snippets below assume you are in that repo's root.

The shape of the file

Seventy-seven columns, and the headers are the registry's own labels, verbatim. Copy header names out of the file rather than typing them due to minor stylistic differences between columns.

The columns fall into four groups:

  • Identity (columns 1 to 11): name, DBA, website, contact details, address, and the URL of the broker's own privacy-rights page.
  • Yes/no disclosure flags (12 to 26): which sensitive categories the broker collects (minors' data, biometrics, precise geolocation, and so on) and who it shared or sold data to in the past year, including a foreign actor, the federal government, law enforcement, and a developer of a generative AI system.
  • Sector regulation (27 to 46): whether the broker or a subsidiary falls under the FCRA, GLBA, IIPPA, CMIA, or HIPAA, each with three free-text fields describing what is covered.
  • 2024 consumer-request metrics (47 to 76): five families of requests (delete, know what is collected, know what is sold or shared, opt out of sale or sharing, limit sensitive PI), each with requests received, complied in whole, complied in part, denied, and mean and median days to respond.

Column 77 is a free-text comments field, and it is where most of the trouble lives.

Gotcha one: multi-line fields

Fifty-five of those comment fields contain line breaks, correctly quoted per RFC 4180. A few of the regulation description fields do too. Line-oriented tools have no idea.

$ wc -l ca_data_broker_registry_2026-09-01.csv
     866 ca_data_broker_registry_2026-09-01.csv
Enter fullscreen mode Exit fullscreen mode

Subtract the header and you get 865. We did exactly that and published "865 registered data brokers" before running the file through a real CSV parser, which says 603. The same mistake bites grep -c, awk, sed and Excel's row numbers. Use a parser.

Python:

import csv

with open("ca_data_broker_registry_2026-09-01.csv", encoding="utf-8-sig", newline="") as fh:
    rows = list(csv.DictReader(fh))

print(len(rows))
print(rows[0]["Data broker name:"])
Enter fullscreen mode Exit fullscreen mode
603
01Advertising Inc.
Enter fullscreen mode Exit fullscreen mode

Ruby:

require "csv"

rows = CSV.read("ca_data_broker_registry_2026-09-01.csv", headers: true, encoding: "bom|utf-8")
puts rows.size
puts rows.headers.first.inspect
Enter fullscreen mode Exit fullscreen mode
603
"Data broker name:"
Enter fullscreen mode Exit fullscreen mode

Gotcha two: the byte order mark

The file starts with the bytes EF BB BF, a UTF-8 BOM. Both snippets above account for it (utf-8-sig in Python, bom|utf-8 in Ruby). Open it as plain UTF-8 and the first header quietly becomes Data broker name:, so row["Data broker name:"] raises KeyError on every row while the printed header list looks fine.

import csv

with open("ca_data_broker_registry_2026-09-01.csv", encoding="utf-8", newline="") as fh:
    print(repr(next(csv.reader(fh))[0]))
Enter fullscreen mode Exit fullscreen mode
'Data broker name:'
Enter fullscreen mode Exit fullscreen mode

Gotcha three: free text where you want categories and numbers

The state column is whatever the registrant typed. "CA" and "California" are both present, as are "NY", "New York" and " NY" with a leading space, plus "NA", "N-A", "London" and "England" from non-US registrants who had to put something in the box. Normalise before you group.

import csv
from collections import Counter

FULL = {"california": "CA", "new york": "NY", "florida": "FL", "texas": "TX",
        "illinois": "IL", "massachusetts": "MA", "virginia": "VA", "georgia": "GA"}

def state(value):
    v = value.strip()
    return FULL.get(v.lower(), v.upper())

with open("ca_data_broker_registry_2026-09-01.csv", encoding="utf-8-sig", newline="") as fh:
    rows = list(csv.DictReader(fh))

raw = Counter(r["Data broker state:"] for r in rows)
print({k: raw[k] for k in ("CA", "California", "NY", "New York", " NY")})

clean = Counter(state(r["Data broker state:"]) for r in rows
                if r["Data broker country:"] == "UNITED STATES")
print(clean.most_common(8))
Enter fullscreen mode Exit fullscreen mode
{'CA': 100, 'California': 26, 'NY': 57, 'New York': 27, ' NY': 1}
[('CA', 126), ('NY', 84), ('FL', 48), ('TX', 32), ('IL', 32), ('MA', 29), ('VA', 23), ('GA', 21)]
Enter fullscreen mode Exit fullscreen mode

That mapping covers the top eight; a real one needs all fifty states plus DC.

The metric columns are cleaner than I expected. In this snapshot there are no thousands separators and no decimals, so int() works on every filled cell. Two things still need handling: one broker left the deletion-request count blank, and five cells across the other request families hold small negative numbers (-1 to -4), presumably placeholders. Cast through a function that returns None for blanks, and decide what a negative count means to you before summing.

Summing the deletion requests

import csv
from statistics import median

RECEIVED = "Requests to delete - Total requests received"
MEDIAN_DAYS = ("Requests to delete - The number of days to respond "
               "substantively to a request to delete in 2024 - Median")

def num(value):
    value = value.strip()
    return int(value) if value else None

with open("ca_data_broker_registry_2026-09-01.csv", encoding="utf-8-sig", newline="") as fh:
    rows = list(csv.DictReader(fh))

received = [num(r[RECEIVED]) for r in rows]
answered = [v for v in received if v is not None]
print("answered:", len(answered))
print("total deletion requests:", f"{sum(answered):,}")
print("reported zero:", sum(1 for v in answered if v == 0))

medians = [num(r[MEDIAN_DAYS]) for r in rows if (num(r[RECEIVED]) or 0) > 0]
print("brokers with requests:", len(medians))
print("median of medians:", median(medians), "days")
Enter fullscreen mode Exit fullscreen mode
answered: 602
total deletion requests: 58,031,327
reported zero: 163
brokers with requests: 439
median of medians: 5 days
Enter fullscreen mode Exit fullscreen mode

So 602 of 603 brokers filled in the field. Between them they reported over 58 million deletion requests for 2024, 163 of them reported receiving none at all, and among the 439 that received at least one, the typical broker's median response time was 5 days against the 45 days the CCPA allows. The zeros deserve a second look: several people-search operators that we publish opt-out guides for reported zero deletion requests for the entire year. This requires further investigation.

What the metrics are and are not

Every number in columns 47 to 76 is the broker's own declaration for calendar year 2024. The CPPA does not audit them at registration. Volumes at the top of the table almost certainly include automated signals such as Global Privacy Control headers and authorized-agent submissions rather than people filling in forms, which would explain how one location-data firm can report 26 million deletion requests while a well-known people-search site reports 8. That is an inference; the file does not say. Write "reported" or "declared" in anything you publish from it, and avoid comparing two brokers' totals as if they measured the same thing.

Three things you could work out from this file

Which registrants list the most DBAs, and how many consumer-facing brands the 603 rows actually collapse into. Column 2 is free text; most registrants separate brands with semicolons, some with commas, so expect to split on both and clean by hand. What else the 32 brokers that declared sharing or selling data to a generative AI developer disclosed about themselves. And which brokers reported a median response time above the 45-day limit (there are 8), and whether their comment fields explain it.

The repo is delistmydata/ca-data-broker-registry. If you find a parsing error, a normalisation I got wrong, or a broker whose declared numbers you can check against your own records, open a GitHub issue. We will refresh the snapshot when the registry changes.

Edward Fancher is a software engineer in Seattle and the founder of Delist My Data, a data removal service that publishes free opt-out guides for the people-search sites in this registry.

Top comments (0)