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

Optional
  • 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.

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 keyMeaning
search-term, fieldText to find; field is "all" (default) or "company"
companyExact company names; find them with suggest-companies
product, issueExact labels; Product•Sub-product and Issue•Sub-issue select the second level
state, zip-codeConsumer mailing state and ZIP code
tags, timely, submitted-viae.g. "Servicemember", "Yes", "Web"
company-response, company-public-responsee.g. "Closed with explanation"
date-received-min, date-received-maxYYYY-MM-DD, both ends included
company-received-min, company-received-maxYYYY-MM-DD, both ends included
sortcreated_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.