New here?Your first list, up to 10,000 businesses, for $50Get my code

ListRiver Business Data API

Active U.S. businesses from official state business registries, as JSON over HTTPS. Filter by state, city, county, ZIP, industry, entity type, formation month and whether a phone or email is on file, then page through the results. Start free with 500 records a month.

Get a free API keyTry it GitHub

Get a free API key

  1. Create a free API account (name, email, password) and click the link in the confirmation email. Already have a ListRiver account? Sign in instead.
  2. On API access, click Get my free API key. The key is shown once: copy it somewhere safe. One free key per person; temporary email addresses can't get one.
  3. Lost it, or think someone else has it? Click Make a new key on the same page. The old key stops working at once.

Prefer to start from code? The ListRiver API repository on GitHub has the Python client, runnable examples, an OpenAPI spec and a free data sample.

Your free key includes 500 records each calendar month (UTC). Need more? On the same page:

  • Monthly plans (cancel any time): $50/month for 5,000 · $100/month for 12,000 · $200/month for 28,000 · $450/month for 75,000 records per billing period. Unused records don't carry over.
  • Prepaid packs (one-time): $50 for 5,000 · $75 for 7,500 · $100 for 10,000 records, used after your free or plan records. They expire 12 months after purchase.
  • AWS Marketplace: larger volumes billed on your AWS invoice. Subscribe to ListRiver Business Data API, click Set up your account, and AWS sends you to ListRiver for a key (these keys start with lr_live_).

Authentication

Every call is an HTTPS GET to https://listriver.com/api/data.ashx with your key in the X-Api-Key header. Keys start with lr_key_ (or lr_live_ for AWS Marketplace). Keep yours private: anyone with it uses your records. Pick your language once on any example below and every example switches to it.

curl -H "X-Api-Key: YOUR_KEY" "https://listriver.com/api/data.ashx?action=states"

What counts against your records

  • Only records returned by action=records count. A search with no matches costs nothing, and action=states and action=count are free.
  • Every record counts each time it is returned. Running the same search twice, or two searches that overlap (say, all of Miami and then Miami LLCs), counts the shared records again. To avoid paying twice, save the cursor or resume position and carry on from it rather than starting over, and use listriverId to spot records you already have.
  • Records come from your free or plan records first, then from prepaid packs (soonest-expiring first). A call near the end of what you have returns fewer rows than limit; after that, calls get HTTP 429.

Every action=records answer says where you stand in its headers:

X-ListRiver-Monthly-AllowanceRecords included this month (free key) or this billing period (plan).
X-ListRiver-Monthly-RemainingOf those, how many are left after this call.
X-ListRiver-Allowance-ResetsWhen they reset (UTC, ISO 8601). Free keys reset on the 1st of the month.
X-ListRiver-Credits-RemainingPrepaid pack records left (self-serve keys).

Try it

Paste your key and send a real call from this page. states and count are free; records is limited to 10 here, and those records count. Your key stays in this browser tab and isn't saved.

Any of the filters, written as in a URL: name=value&name=value.

List the states: action=states

Which states are available and how many records each has for each formation window. Free. (The call itself is shown under Authentication above.)

{
  "ok": true,
  "states": [
    { "code": "FL", "name": "Florida", "recent": 61234, "year": 402118, "all": 3120455 },
    ...
  ]
}

Counts are refreshed every few hours. (Example figures.)

Filters

action=records and action=count take the same filters. Combine as many as you like: a record must match all of them. Where a filter takes several comma-separated values, a record may match any one of those values.

ParameterExampleWhat it does
statesFL,TXRegistration states from action=states. Leave it out for every available state.
cityMIAMI,DORALBusiness address city. Needs states.
countyMIAMI-DADE or FL:MIAMI-DADECounty of the business address. A bare name applies to each state in states. Needs states. Not every state publishes counties.
zip33139,33140 or 3315-digit ZIP codes or 3-digit ZIP prefixes. Needs states.
naics23, 2361, 236115Industry by NAICS code prefix, 2 to 6 digits (23 = all construction). Only records with a known NAICS code match.
entityTypeLLC,CORPOne or more of LLC, CORP, LP, LLP, NONPROFIT, OTHER.
hasPhone1Only businesses with a phone number on file.
hasEmail1Only businesses with an email on file (onlyEmail=1 also works).
formedWithin3Formed in the current month or the N months before it (1 to 120). formedWithin=1 is the same as preset=recent.
formedFrom / formedTo2026-06 / 2026-09Formed in this range of months (both ends included). Either end may be left out.
presetrecent (default), year, allShortcut formation windows: recent = this month or last; year = the last 12 months; all = every active business on file. Ignored when formedWithin or formedFrom/formedTo is given.

Formation dates are month-level (registries publish the month), so windows are whole calendar months (UTC), not exact day counts.

Count before you pull: action=count

How many records match your filters, per state, without returning or charging any. Use it to check a search's size before spending records. Limited to 20 calls a minute.

curl -H "X-Api-Key: YOUR_KEY" "https://listriver.com/api/data.ashx?action=count&states=FL&city=MIAMI&hasPhone=1&formedWithin=3"
{
  "ok": true,
  "total": 6120,
  "states": [ { "code": "FL", "count": 6120 } ],
  "approximate": true
}

Approximate: a business filed under two of the states you ask for is counted in each, but records returns it once. (Example figures.)

Get records: action=records

Takes every filter above, plus:

ParameterValuesWhat it does
limit1 to 1000 (default 1000)Records per call.
cursorfrom the last answerLeave it out on the first call; then pass the cursor (or a saved resume) with the same filters.
email1 (default) or 00 leaves the email field empty.
curl -H "X-Api-Key: YOUR_KEY" "https://listriver.com/api/data.ashx?action=records&states=FL&city=MIAMI&hasPhone=1&formedWithin=3&limit=100"
{
  "ok": true,
  "rows": [ { "businessName": "...", "formed": "2026-10", "city": "MIAMI", "listriverId": "FL-L26000012345", ... }, ... ],
  "cursor": "FL.123456789",
  "resume": "FL.123456789"
}

cursor is null on the last page. Records come state by state in a fixed order, so you can stop and carry on later with the last cursor.

resume is the position just after the last record in this answer, and it is there on the last page too. Save it, and later pass it as cursor (with the same filters) to get only the records ListRiver has added since: that is how to keep a feed up to date without paying for the same records again. Use one state per feed (see recipe 4).

Reading everything, to a CSV file

Pages through every matching record and writes one CSV file. Run count first to see how many records it will use.

# bash + jq: every page goes to records.jsonl, then one CSV
API="https://listriver.com/api/data.ashx"
cursor=""
: > records.jsonl
while :; do
  page=$(curl -s -H "X-Api-Key: YOUR_KEY" "$API?action=records&limit=1000&states=FL&city=MIAMI&hasPhone=1&formedWithin=3${cursor:+&cursor=$cursor}")
  echo "$page" | jq -c '.rows[]' >> records.jsonl
  cursor=$(echo "$page" | jq -r '.cursor // empty')   # save this to resume later without paying twice
  [ -z "$cursor" ] && break
done
jq -r -s '(.[0] | keys_unsorted) as $k | ($k | @csv), (.[] | [.[$k[]]] | @csv)' records.jsonl > miami.csv

Recipes

Complete examples you can copy. Replace YOUR_KEY with your key. Responses are shortened with ....

1. How many records will a search use? (free)

New businesses in Miami with a phone number, formed this month or in the 3 months before. This is the same call as under Count:

curl -H "X-Api-Key: YOUR_KEY" "https://listriver.com/api/data.ashx?action=count&states=FL&city=MIAMI&hasPhone=1&formedWithin=3"

Run this before every large pull. It costs nothing and shows whether a filter (county, NAICS, email) is well filled in for the place you want.

2. The first page of a search, with the headers

curl -i -H "X-Api-Key: YOUR_KEY" "https://listriver.com/api/data.ashx?action=records&states=FL&city=MIAMI&hasPhone=1&formedWithin=3&limit=2"
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
X-ListRiver-Monthly-Allowance: 500
X-ListRiver-Monthly-Remaining: 498
X-ListRiver-Allowance-Resets: 2026-12-01T00:00:00Z
X-ListRiver-Credits-Remaining: 0

{
  "ok": true,
  "rows": [
    {
      "businessName": "ANGELI TRAVEL LLC",
      "dbaName": null,
      "entityType": "LIMITED LIABILITY COMPANY",
      "status": "ACTIVE",
      "standing": "GOOD",
      "formed": "2026-08",
      "registrationState": "FL",
      "stateFilingNumber": "L26000446708",
      "ein": null,
      "address": "2125 BISCAYNE BLVD, STE 204 #27572",
      "city": "MIAMI",
      "state": "FL",
      "zip": "33137",
      "county": "MIAMI-DADE",
      "phone": "(786) 298-9566",
      "email": "[email protected]",
      "website": "HTTPS://ANGELITRAVELS.COM",
      "registeredAgent": "ULLOA SANCHEZ YUDELSA",
      "naics": null,
      "registryDataAsOf": "2026-08-27",
      "listriverId": "FL-L26000446708",
      ...
    },
    { "businessName": "EMPATHY IN ACTION ABA, LLC", "formed": "2026-08", "city": "MIAMI", "zip": "33173", "listriverId": "FL-L26000448631", ... }
  ],
  "cursor": "FL.6604750",
  "resume": "FL.6604750"
}

The cursor means there are more. Ask again with &cursor=FL.6604750 and the same filters for the next page; you're only charged for the new records it returns.

3. New construction companies in one county, to a CSV file

# bash + jq: every page goes to records.jsonl, then one CSV
API="https://listriver.com/api/data.ashx"
cursor=""
: > records.jsonl
while :; do
  page=$(curl -s -H "X-Api-Key: YOUR_KEY" "$API?action=records&limit=1000&states=TX&county=HARRIS&naics=23&formedWithin=6${cursor:+&cursor=$cursor}")
  echo "$page" | jq -c '.rows[]' >> records.jsonl
  cursor=$(echo "$page" | jq -r '.cursor // empty')   # save this to resume later without paying twice
  [ -z "$cursor" ] && break
done
jq -r -s '(.[0] | keys_unsorted) as $k | ($k | @csv), (.[] | [.[$k[]]] | @csv)' records.jsonl > harris-construction.csv

NAICS codes are on file for only part of the records in most states; count first (recipe 1) to see how many you'll get.

4. A daily feed of new businesses that never pays twice

Run this once a day. For each state it saves the resume position, and the next run asks only for records ListRiver has added since. The first run takes everything that matches; later runs only what's new.

import json, os, requests

API = "https://listriver.com/api/data.ashx"
HEADERS = {"X-Api-Key": "YOUR_KEY"}
SAVED = "listriver-feed.json"
filters = {"action": "records", "entityType": "LLC", "preset": "recent", "limit": 1000}

positions = json.load(open(SAVED)) if os.path.exists(SAVED) else {}

for state in ["FL", "GA", "TX"]:                       # one state per feed
    params = dict(filters, states=state)
    if positions.get(state):
        params["cursor"] = positions[state]
    while True:
        r = requests.get(API, headers=HEADERS, params=params, timeout=120)
        r.raise_for_status()
        data = r.json()
        for row in data["rows"]:
            print(row["listriverId"], row["businessName"])   # or save to your CRM, keyed by listriverId
        if data["resume"]:
            positions[state] = data["resume"]
            json.dump(positions, open(SAVED, "w"))     # saved after every page: a crash never pays twice
        if not data["cursor"]:
            break
        params["cursor"] = data["cursor"]

New records get higher positions, so a saved position only ever returns records added after it. A record that changes later (a new phone number, say) keeps its position and is not returned again.

5. Handling limits and busy moments

Wait and retry on 503 (nothing was charged); stop on 429 and show the message, which says how to get more records.

import time, requests

API = "https://listriver.com/api/data.ashx"
HEADERS = {"X-Api-Key": "YOUR_KEY"}

def get_page(params):
    for attempt in range(5):
        r = requests.get(API, headers=HEADERS, params=params, timeout=120)
        if r.status_code == 503:                       # busy: nothing was charged
            time.sleep(int(r.headers.get("Retry-After", "60")))
            continue
        if r.status_code == 429:                       # out of records (or too many count calls)
            raise SystemExit(r.json()["error"])          # the message says how to get more
        r.raise_for_status()
        print("left:", r.headers.get("X-ListRiver-Monthly-Remaining"), "+ packs:", r.headers.get("X-ListRiver-Credits-Remaining"))
        return r.json()
    raise RuntimeError("ListRiver stayed busy; try again later")

Fields

Every record has the same 35 fields. A field is null when the state doesn't publish it or we don't have it; nothing is estimated. Contact fields (phone, email, website), county and NAICS are on file for only part of the records, and how much varies by state: use action=count with hasPhone, hasEmail or naics to see. Text that starts with =, +, - or @ gets a leading apostrophe so spreadsheets can't run it as a formula.

FieldFormatWhat it holds
businessNametextLegal name on the state registry.
dbaNametextTrade name (doing business as), where filed.
entityTypetextLLC, corporation, nonprofit, limited partnership, etc., as the state words it.
statustextStatus on the registry, upper case (e.g. ACTIVE).
standingtextGood-standing status, where the state publishes it.
formedYYYY-MMMonth the business was formed or registered.
registrationState2 lettersState registry the record comes from.
stateFilingNumbertextThe state's filing (entity) number.
einNN-NNNNNNNFederal EIN, where known.
addresstextBusiness street address.
address2textSuite, unit, etc.
citytextBusiness city.
state2 lettersBusiness address state.
ziptextBusiness ZIP code.
countytextCounty of the business address (when it is in the registration state).
mailingAddresstextMailing street address, where different.
mailingAddress2textMailing suite, unit, etc.
mailingCitytextMailing city.
mailingState2 lettersMailing state.
mailingZiptextMailing ZIP code.
phonetextBusiness phone, where on file. U.S. numbers as (NNN) NNN-NNNN.
emailtextBusiness email, where on file (null when you ask for email=0).
websitetextBusiness website, where on file.
registeredAgenttextRegistered agent name, where the state publishes it.
naicstextNAICS industry code, where known.
industrytextNAICS industry description.
womanOwnedtrue/falseWoman-owned, where known (false = not known to be).
veteranOwnedtrue/falseVeteran-owned, where known.
minorityOwnedtrue/falseMinority-owned, where known.
disabilityOwnedtrue/falseDisability-owned, where known.
lgbtqiOwnedtrue/falseLGBTQI-owned, where known.
latitudenumberLatitude of the business address, where geocoded.
longitudenumberLongitude of the business address, where geocoded.
registryDataAsOfYYYY-MM-DDDate of the state file the record was last taken from.
listriverIdtextStable ListRiver id: registration state + filing number (e.g. FL-L26000012345).

Coverage notes

  • Only states listed by action=states are available, and some states are offered for all but not for the new-business windows. Asking for one that isn't returns HTTP 400 with the list of states that are.
  • Formation dates are month-level. Formation windows follow the calendar month (UTC), so they move forward on the 1st.
  • A business filed under two states in your request is returned once.
  • Sources, update schedules and known limits are on the methodology page.

Errors

Errors come back as { "ok": false, "error": "..." } with one of these HTTP codes:

400A parameter is wrong (unknown preset, unavailable state, a ZIP without states, bad cursor). The message says which.
401Missing or unknown API key.
403AWS Marketplace keys: the subscription isn't active yet (try again in a few minutes) or it has ended.
429Your records are used up (see the headers above), or too many count calls in a minute.
503We couldn't finish the search in time (usually just after an update). Nothing was charged. Wait a minute and retry with the same cursor, or narrow the search.
500A problem on our side. Wait a minute and retry with the same cursor.

Terms and support

Use of the API and the data is covered by the ListRiver API subscription agreement. In short: the data is for your own business use, not for resale or for credit, insurance, employment or housing decisions.

Questions or problems: [email protected].