The CFPB Consumer Complaint Database API
(https://www.consumerfinance.gov/data-research/consumer-complaints/search/api/v1,
no key): filtered search, one complaint by ID, the aggregations the
search returns, match counts, and company name suggestions.
The API no longer has /trends or /geo/states, and complaint
narratives are gone: CFPB removed narratives and its visualizations
in September 2026 (Release 24). Build a trend from count over date
windows.
What the data shows. CFPB says the database is not a statistical sample of consumers' experiences, so complaints are not representative of all consumers' experiences with a product or company. Read volume against company size and market share. Only complaints sent to companies for response are published, after the company responds or after 15 days, whichever comes first. Complaints referred to other regulators are not published, such as those about depository institutions with less than $10 billion in assets, so no results does not mean no complaints.
Pages are size long (up to 100) and reach 10,000 records for one
query; next-page reads the API's break_points, and collect
follows them. The API's source sets 20 searches and 5 single
complaint reads per minute for each anonymous client.
Every function takes an optional trailing opts Map: the filters
listed at search-params, and base-url.
Context Vars
cfpb.complaints.base-url
Values
DEFAULT_SIZE
DEFAULT_SIZE: Int 25
MAX_SIZE
MAX_SIZE: Int 100
PARAMS
PARAMS: Map {
search-term: "search_term",
field: "field",
company: "company",
product: "product",
issue: "issue",
state: "state",
zip-code: "zip_code",
tags: "tags",
timely: "timely",
submitted-via: "submitted_via",
company-response: "company_response",
company-public-response: "company_public_response",
date-received-min: "date_received_min",
date-received-max: "date_received_max",
company-received-min: "company_received_min",
company-received-max: "company_received_max",
sort: "sort"
}
SUB_LEVELS
SUB_LEVELS: Vec [
"sub_product.raw",
"sub_issue.raw"
]
Functions
aggregations
fn (): Map
fn (opts: Map): Map
The aggregations for the filters in opts, with no complaints:
product, issue, company, state, timely, company_response,
company_public_response, submitted_via and tags. Read one with
buckets.
A dimension ignores its own filter. With company set,
aggregations.company still lists every company (so a UI can offer
other choices), and with state set, aggregations.state still
lists every state. All the other aggregations are filtered.
Example
aggs ::cfpb::complaints/aggregations({company: "JPMORGAN CHASE & CO.", date-received-min: "2026-01-01"})
::cfpb::complaints/buckets(aggs, "product") // [{key: "Checking or savings account", count: 7887, sub: [...]}, ...]
bucket-row
fn (bucket: Map): Map
buckets
fn (aggs: Any, name: Str): Vec<Map>
[{key, count}] for one aggregation, largest first (pure). product and issue rows also carry sub, the same shape for the sub-products or sub-issues. [] when the aggregation is absent.
ccdb-get
fn (path: Str, params: Map, opts: Map): Any
collect
fn (fetch: Fn, opts: Map, limit: Int): Vec
Follow pages until limit complaints or the last page. fetch takes
opts (with size, frm and search-after set) and returns one
page; size defaults to 100. Pass a sort of created_date_desc so the order is stable. A
query stops at 10,000 records, so split a larger one by date. It
makes one request per 100 complaints, and a throttled request ends
it with an Err.
Example
::c ::cfpb::complaints
rows ::c/collect((o) { ::c/search(o) }, {company: "JPMORGAN CHASE & CO.", sort: "created_date_desc"}, 500)
collect-pages
fn (fetch: Fn, opts: Map, limit: Int, acc: Vec): Vec
complaints
fn (page: Any): Vec<Map>
The complaints in a page: each hit's _source (pure).
count
fn (): Int
fn (opts: Map): Int
How many complaints match the filters in opts, without fetching
any. Call it once per date window for a trend. Both date bounds are
included, so windows must not share a day.
Example
::cfpb::complaints/count({
company: "JPMORGAN CHASE & CO.",
date-received-min: "2026-01-01",
date-received-max: "2026-01-31"
})
filter-params
fn (opts: Map): Map
The API parameters for the filters in opts (pure), unset ones left out. See search-params for the keys.
freshness
fn (page: Any): Map
The state of the data from a search page's _meta (pure):
{last-updated, last-indexed, stale, data-issue, record-count}.
stale means the newest data is over 5 business days old;
data-issue means the latest load had a problem.
get-complaint
fn (id: Any): Map?
fn (id: Any, opts: Map): Map?
One complaint by ID, or null when there is none (the API answers 200 with no hits). It carries date_indexed, which search results do not. A blank ID is an Err: it would otherwise request the search route.
next-page
fn (page: Any, opts: Map): Map?
The paging opts for the page after page, or null on the last one
(pure). opts are the ones that fetched page. Returns {frm, search-after}, built from the _meta.break_points entry for the
next page. The API stops giving break points at 10,000 records.
page-size
fn (opts: Map): Int
opts.size, capped at 100, or 25 when unset (pure). A size of 0 stays 0: it asks for counts and aggregations with no complaints.
search
fn (): Map
fn (opts: Map): Map
Search complaints. Returns the API's page: hits and _meta, plus
aggregations when opts.aggs is true. Read it with complaints,
total and freshness.
Example
page ::cfpb::complaints/search({
company: "JPMORGAN CHASE & CO.",
state: ["CA", "NY"],
product: "Mortgage",
date-received-min: "2026-01-01",
sort: "created_date_desc",
size: 50
})
::cfpb::complaints/total(page)
first(::cfpb::complaints/complaints(page)).issue
search-params
fn (opts: Map): Map
Query parameters for a search (pure). size, frm and
search-after page the results. Highlighting is always off, and
aggregations are off unless opts.aggs is true, since they add 100 KB
or more to a response.
| opts key | Meaning |
|---|---|
search-term, field | Text to find; field is "all" (default) or "company" |
company | Exact company names; find them with suggest-companies |
product, issue | Exact labels; Product•Sub-product and Issue•Sub-issue select the second level |
state, zip-code | Consumer mailing state and ZIP code |
tags, timely, submitted-via | e.g. "Servicemember", "Yes", "Web" |
company-response, company-public-response | e.g. "Closed with explanation" |
date-received-min, date-received-max | YYYY-MM-DD, both ends included |
company-received-min, company-received-max | YYYY-MM-DD, both ends included |
sort | created_date_desc, created_date_asc, relevance_desc (default), relevance_asc |
suggest-companies
fn (text: Str): Vec<Str>
fn (text: Str, opts: Map): Vec<Str>
Company names matching text, as the company filter needs them.
The filters in opts narrow the names, and size limits them.
Example
::cfpb::complaints/suggest-companies("jpmorgan") // ["JPMORGAN CHASE & CO."]
total
fn (page: Any): Int?
How many complaints match the query, across all pages (pure). Exact above 10,000 too.