Get a free API key
- Create a free API account (name, email, password) and click the link in the confirmation email. Already have a ListRiver account? Sign in instead.
- 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.
- 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"
import requests
r = requests.get(
"https://listriver.com/api/data.ashx",
headers={"X-Api-Key": "YOUR_KEY"},
params={"action": "states"},
timeout=120,
)
r.raise_for_status()
print(r.json())
// Node 18+ (save as an .mjs file) or a browser console
const params = new URLSearchParams({ action: "states" });
const res = await fetch(`https://listriver.com/api/data.ashx?${params}`, {
headers: { "X-Api-Key": "YOUR_KEY" },
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);
console.log(data);
// .NET 6+ (top-level statements)
using System;
using System.Net.Http;
var http = new HttpClient();
http.DefaultRequestHeaders.Add("X-Api-Key", "YOUR_KEY");
var res = await http.GetAsync("https://listriver.com/api/data.ashx?action=states");
var json = await res.Content.ReadAsStringAsync();
Console.WriteLine((int)res.StatusCode + " " + json);
<?php
$ch = curl_init("https://listriver.com/api/data.ashx?" . http_build_query(["action" => "states"]));
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => ["X-Api-Key: YOUR_KEY"],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 120,
]);
$data = json_decode(curl_exec($ch), true);
if (curl_getinfo($ch, CURLINFO_HTTP_CODE) != 200) die($data["error"] . "\n");
print_r($data);
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-Allowance | Records included this month (free key) or this billing period (plan). |
X-ListRiver-Monthly-Remaining | Of those, how many are left after this call. |
X-ListRiver-Allowance-Resets | When they reset (UTC, ISO 8601). Free keys reset on the 1st of the month. |
X-ListRiver-Credits-Remaining | Prepaid 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.
| Parameter | Example | What it does |
|---|
states | FL,TX | Registration states from action=states. Leave it out for every available state. |
city | MIAMI,DORAL | Business address city. Needs states. |
county | MIAMI-DADE or FL:MIAMI-DADE | County of the business address. A bare name applies to each state in states. Needs states. Not every state publishes counties. |
zip | 33139,33140 or 331 | 5-digit ZIP codes or 3-digit ZIP prefixes. Needs states. |
naics | 23, 2361, 236115 | Industry by NAICS code prefix, 2 to 6 digits (23 = all construction). Only records with a known NAICS code match. |
entityType | LLC,CORP | One or more of LLC, CORP, LP, LLP, NONPROFIT, OTHER. |
hasPhone | 1 | Only businesses with a phone number on file. |
hasEmail | 1 | Only businesses with an email on file (onlyEmail=1 also works). |
formedWithin | 3 | Formed in the current month or the N months before it (1 to 120). formedWithin=1 is the same as preset=recent. |
formedFrom / formedTo | 2026-06 / 2026-09 | Formed in this range of months (both ends included). Either end may be left out. |
preset | recent (default), year, all | Shortcut 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"
import requests
r = requests.get(
"https://listriver.com/api/data.ashx",
headers={"X-Api-Key": "YOUR_KEY"},
params={"action": "count", "states": "FL", "city": "MIAMI", "hasPhone": 1, "formedWithin": 3},
timeout=120,
)
r.raise_for_status()
print(r.json())
// Node 18+ (save as an .mjs file) or a browser console
const params = new URLSearchParams({ action: "count", states: "FL", city: "MIAMI", hasPhone: 1, formedWithin: 3 });
const res = await fetch(`https://listriver.com/api/data.ashx?${params}`, {
headers: { "X-Api-Key": "YOUR_KEY" },
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);
console.log(data);
// .NET 6+ (top-level statements)
using System;
using System.Net.Http;
var http = new HttpClient();
http.DefaultRequestHeaders.Add("X-Api-Key", "YOUR_KEY");
var res = await http.GetAsync("https://listriver.com/api/data.ashx?action=count&states=FL&city=MIAMI&hasPhone=1&formedWithin=3");
var json = await res.Content.ReadAsStringAsync();
Console.WriteLine((int)res.StatusCode + " " + json);
<?php
$ch = curl_init("https://listriver.com/api/data.ashx?" . http_build_query(["action" => "count", "states" => "FL", "city" => "MIAMI", "hasPhone" => 1, "formedWithin" => 3]));
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => ["X-Api-Key: YOUR_KEY"],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 120,
]);
$data = json_decode(curl_exec($ch), true);
if (curl_getinfo($ch, CURLINFO_HTTP_CODE) != 200) die($data["error"] . "\n");
print_r($data);
{
"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:
| Parameter | Values | What it does |
|---|
limit | 1 to 1000 (default 1000) | Records per call. |
cursor | from the last answer | Leave it out on the first call; then pass the cursor (or a saved resume) with the same filters. |
email | 1 (default) or 0 | 0 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"
import requests
r = requests.get(
"https://listriver.com/api/data.ashx",
headers={"X-Api-Key": "YOUR_KEY"},
params={"action": "records", "states": "FL", "city": "MIAMI", "hasPhone": 1, "formedWithin": 3, "limit": 100},
timeout=120,
)
r.raise_for_status()
print(r.json())
// Node 18+ (save as an .mjs file) or a browser console
const params = new URLSearchParams({ action: "records", states: "FL", city: "MIAMI", hasPhone: 1, formedWithin: 3, limit: 100 });
const res = await fetch(`https://listriver.com/api/data.ashx?${params}`, {
headers: { "X-Api-Key": "YOUR_KEY" },
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);
console.log(data);
// .NET 6+ (top-level statements)
using System;
using System.Net.Http;
var http = new HttpClient();
http.DefaultRequestHeaders.Add("X-Api-Key", "YOUR_KEY");
var res = await http.GetAsync("https://listriver.com/api/data.ashx?action=records&states=FL&city=MIAMI&hasPhone=1&formedWithin=3&limit=100");
var json = await res.Content.ReadAsStringAsync();
Console.WriteLine((int)res.StatusCode + " " + json);
<?php
$ch = curl_init("https://listriver.com/api/data.ashx?" . http_build_query(["action" => "records", "states" => "FL", "city" => "MIAMI", "hasPhone" => 1, "formedWithin" => 3, "limit" => 100]));
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => ["X-Api-Key: YOUR_KEY"],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 120,
]);
$data = json_decode(curl_exec($ch), true);
if (curl_getinfo($ch, CURLINFO_HTTP_CODE) != 200) die($data["error"] . "\n");
print_r($data);
{
"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
import csv, requests
API = "https://listriver.com/api/data.ashx"
HEADERS = {"X-Api-Key": "YOUR_KEY"}
params = {"action": "records", "limit": 1000, "states": "FL", "city": "MIAMI", "hasPhone": 1, "formedWithin": 3}
with open("miami.csv", "w", newline="", encoding="utf-8") as f:
writer = None
while True:
r = requests.get(API, headers=HEADERS, params=params, timeout=120)
r.raise_for_status()
data = r.json()
for row in data["rows"]:
if writer is None:
writer = csv.DictWriter(f, fieldnames=list(row.keys()))
writer.writeheader()
writer.writerow(row)
print("records left:", r.headers.get("X-ListRiver-Monthly-Remaining"))
if not data["cursor"]:
break
params["cursor"] = data["cursor"] # save this to resume later without paying twice
// Node 18+ (save as an .mjs file)
import { writeFileSync, appendFileSync } from "node:fs";
const API = "https://listriver.com/api/data.ashx";
const headers = { "X-Api-Key": "YOUR_KEY" };
const params = { action: "records", limit: 1000, states: "FL", city: "MIAMI", hasPhone: 1, formedWithin: 3 };
const cell = (v) => (v === null ? "" : `"${String(v).replace(/"/g, '""')}"`);
let header = null;
while (true) {
const res = await fetch(`$https://listriver.com/api/data.ashx?${new URLSearchParams(params)}`, { headers });
const data = await res.json();
if (!res.ok) throw new Error(data.error);
for (const row of data.rows) {
if (!header) {
header = Object.keys(row);
writeFileSync("miami.csv", header.join(",") + "\n");
}
appendFileSync("miami.csv", header.map((k) => cell(row[k])).join(",") + "\n");
}
console.log("records left:", res.headers.get("X-ListRiver-Monthly-Remaining"));
if (!data.cursor) break;
params.cursor = data.cursor; // save this to resume later without paying twice
}
// .NET 6+ (top-level statements)
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text.Json;
const string API = "https://listriver.com/api/data.ashx";
var http = new HttpClient { Timeout = TimeSpan.FromMinutes(2) };
http.DefaultRequestHeaders.Add("X-Api-Key", "YOUR_KEY");
var query = new Dictionary<string, string> { ["action"] = "records", ["limit"] = "1000", ["states"] = "FL", ["city"] = "MIAMI", ["hasPhone"] = "1", ["formedWithin"] = "3" };
using var csv = new StreamWriter("miami.csv");
List<string> header = null;
string Cell(JsonElement v) => v.ValueKind == JsonValueKind.Null ? ""
: "\"" + (v.ValueKind == JsonValueKind.String ? v.GetString() : v.GetRawText()).Replace("\"", "\"\"") + "\"";
while (true)
{
var url = API + "?" + string.Join("&", query.Select(kv => kv.Key + "=" + Uri.EscapeDataString(kv.Value)));
var res = await http.GetAsync(url);
var body = await res.Content.ReadAsStringAsync();
if (!res.IsSuccessStatusCode) throw new Exception(body);
using var doc = JsonDocument.Parse(body);
foreach (var row in doc.RootElement.GetProperty("rows").EnumerateArray())
{
if (header == null)
{
header = row.EnumerateObject().Select(p => p.Name).ToList();
csv.WriteLine(string.Join(",", header));
}
csv.WriteLine(string.Join(",", header.Select(h => Cell(row.GetProperty(h)))));
}
var cursor = doc.RootElement.GetProperty("cursor");
if (cursor.ValueKind == JsonValueKind.Null) break;
query["cursor"] = cursor.GetString(); // save this to resume later without paying twice
}
<?php
$params = ["action" => "records", "limit" => 1000, "states" => "FL", "city" => "MIAMI", "hasPhone" => 1, "formedWithin" => 3];
$out = fopen("miami.csv", "w");
$header = null;
do {
$ch = curl_init("https://listriver.com/api/data.ashx?" . http_build_query($params));
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => ["X-Api-Key: YOUR_KEY"],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 120,
]);
$data = json_decode(curl_exec($ch), true);
if (curl_getinfo($ch, CURLINFO_HTTP_CODE) != 200) die($data["error"] . "\n");
foreach ($data["rows"] as $row) {
if ($header === null) {
$header = array_keys($row);
fputcsv($out, $header);
}
fputcsv($out, array_map(fn($k) => is_bool($row[$k]) ? ($row[$k] ? "true" : "false") : $row[$k], $header));
}
$params["cursor"] = $data["cursor"]; // save this to resume later without paying twice
} while ($data["cursor"]);
fclose($out);
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"
import requests
r = requests.get(
"https://listriver.com/api/data.ashx",
headers={"X-Api-Key": "YOUR_KEY"},
params={"action": "count", "states": "FL", "city": "MIAMI", "hasPhone": 1, "formedWithin": 3},
timeout=120,
)
r.raise_for_status()
print(r.json())
// Node 18+ (save as an .mjs file) or a browser console
const params = new URLSearchParams({ action: "count", states: "FL", city: "MIAMI", hasPhone: 1, formedWithin: 3 });
const res = await fetch(`https://listriver.com/api/data.ashx?${params}`, {
headers: { "X-Api-Key": "YOUR_KEY" },
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);
console.log(data);
// .NET 6+ (top-level statements)
using System;
using System.Net.Http;
var http = new HttpClient();
http.DefaultRequestHeaders.Add("X-Api-Key", "YOUR_KEY");
var res = await http.GetAsync("https://listriver.com/api/data.ashx?action=count&states=FL&city=MIAMI&hasPhone=1&formedWithin=3");
var json = await res.Content.ReadAsStringAsync();
Console.WriteLine((int)res.StatusCode + " " + json);
<?php
$ch = curl_init("https://listriver.com/api/data.ashx?" . http_build_query(["action" => "count", "states" => "FL", "city" => "MIAMI", "hasPhone" => 1, "formedWithin" => 3]));
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => ["X-Api-Key: YOUR_KEY"],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 120,
]);
$data = json_decode(curl_exec($ch), true);
if (curl_getinfo($ch, CURLINFO_HTTP_CODE) != 200) die($data["error"] . "\n");
print_r($data);
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"
import requests
r = requests.get(
"https://listriver.com/api/data.ashx",
headers={"X-Api-Key": "YOUR_KEY"},
params={"action": "records", "states": "FL", "city": "MIAMI", "hasPhone": 1, "formedWithin": 3, "limit": 2},
timeout=120,
)
r.raise_for_status()
print("left:", r.headers.get("X-ListRiver-Monthly-Remaining"))
print(r.json())
// Node 18+ (save as an .mjs file) or a browser console
const params = new URLSearchParams({ action: "records", states: "FL", city: "MIAMI", hasPhone: 1, formedWithin: 3, limit: 2 });
const res = await fetch(`https://listriver.com/api/data.ashx?${params}`, {
headers: { "X-Api-Key": "YOUR_KEY" },
});
const data = await res.json();
if (!res.ok) throw new Error(data.error);
console.log("left:", res.headers.get("X-ListRiver-Monthly-Remaining"));
console.log(data);
// .NET 6+ (top-level statements)
using System;
using System.Net.Http;
var http = new HttpClient();
http.DefaultRequestHeaders.Add("X-Api-Key", "YOUR_KEY");
var res = await http.GetAsync("https://listriver.com/api/data.ashx?action=records&states=FL&city=MIAMI&hasPhone=1&formedWithin=3&limit=2");
var json = await res.Content.ReadAsStringAsync();
if (res.Headers.TryGetValues("X-ListRiver-Monthly-Remaining", out var left))
Console.WriteLine("left: " + string.Join("", left));
Console.WriteLine((int)res.StatusCode + " " + json);
<?php
$ch = curl_init("https://listriver.com/api/data.ashx?" . http_build_query(["action" => "records", "states" => "FL", "city" => "MIAMI", "hasPhone" => 1, "formedWithin" => 3, "limit" => 2]));
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => ["X-Api-Key: YOUR_KEY"],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 120,
]);
$data = json_decode(curl_exec($ch), true);
if (curl_getinfo($ch, CURLINFO_HTTP_CODE) != 200) die($data["error"] . "\n");
print_r($data);
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
import csv, requests
API = "https://listriver.com/api/data.ashx"
HEADERS = {"X-Api-Key": "YOUR_KEY"}
params = {"action": "records", "limit": 1000, "states": "TX", "county": "HARRIS", "naics": "23", "formedWithin": 6}
with open("harris-construction.csv", "w", newline="", encoding="utf-8") as f:
writer = None
while True:
r = requests.get(API, headers=HEADERS, params=params, timeout=120)
r.raise_for_status()
data = r.json()
for row in data["rows"]:
if writer is None:
writer = csv.DictWriter(f, fieldnames=list(row.keys()))
writer.writeheader()
writer.writerow(row)
print("records left:", r.headers.get("X-ListRiver-Monthly-Remaining"))
if not data["cursor"]:
break
params["cursor"] = data["cursor"] # save this to resume later without paying twice
// Node 18+ (save as an .mjs file)
import { writeFileSync, appendFileSync } from "node:fs";
const API = "https://listriver.com/api/data.ashx";
const headers = { "X-Api-Key": "YOUR_KEY" };
const params = { action: "records", limit: 1000, states: "TX", county: "HARRIS", naics: "23", formedWithin: 6 };
const cell = (v) => (v === null ? "" : `"${String(v).replace(/"/g, '""')}"`);
let header = null;
while (true) {
const res = await fetch(`$https://listriver.com/api/data.ashx?${new URLSearchParams(params)}`, { headers });
const data = await res.json();
if (!res.ok) throw new Error(data.error);
for (const row of data.rows) {
if (!header) {
header = Object.keys(row);
writeFileSync("harris-construction.csv", header.join(",") + "\n");
}
appendFileSync("harris-construction.csv", header.map((k) => cell(row[k])).join(",") + "\n");
}
console.log("records left:", res.headers.get("X-ListRiver-Monthly-Remaining"));
if (!data.cursor) break;
params.cursor = data.cursor; // save this to resume later without paying twice
}
// .NET 6+ (top-level statements)
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text.Json;
const string API = "https://listriver.com/api/data.ashx";
var http = new HttpClient { Timeout = TimeSpan.FromMinutes(2) };
http.DefaultRequestHeaders.Add("X-Api-Key", "YOUR_KEY");
var query = new Dictionary<string, string> { ["action"] = "records", ["limit"] = "1000", ["states"] = "TX", ["county"] = "HARRIS", ["naics"] = "23", ["formedWithin"] = "6" };
using var csv = new StreamWriter("harris-construction.csv");
List<string> header = null;
string Cell(JsonElement v) => v.ValueKind == JsonValueKind.Null ? ""
: "\"" + (v.ValueKind == JsonValueKind.String ? v.GetString() : v.GetRawText()).Replace("\"", "\"\"") + "\"";
while (true)
{
var url = API + "?" + string.Join("&", query.Select(kv => kv.Key + "=" + Uri.EscapeDataString(kv.Value)));
var res = await http.GetAsync(url);
var body = await res.Content.ReadAsStringAsync();
if (!res.IsSuccessStatusCode) throw new Exception(body);
using var doc = JsonDocument.Parse(body);
foreach (var row in doc.RootElement.GetProperty("rows").EnumerateArray())
{
if (header == null)
{
header = row.EnumerateObject().Select(p => p.Name).ToList();
csv.WriteLine(string.Join(",", header));
}
csv.WriteLine(string.Join(",", header.Select(h => Cell(row.GetProperty(h)))));
}
var cursor = doc.RootElement.GetProperty("cursor");
if (cursor.ValueKind == JsonValueKind.Null) break;
query["cursor"] = cursor.GetString(); // save this to resume later without paying twice
}
<?php
$params = ["action" => "records", "limit" => 1000, "states" => "TX", "county" => "HARRIS", "naics" => "23", "formedWithin" => 6];
$out = fopen("harris-construction.csv", "w");
$header = null;
do {
$ch = curl_init("https://listriver.com/api/data.ashx?" . http_build_query($params));
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => ["X-Api-Key: YOUR_KEY"],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 120,
]);
$data = json_decode(curl_exec($ch), true);
if (curl_getinfo($ch, CURLINFO_HTTP_CODE) != 200) die($data["error"] . "\n");
foreach ($data["rows"] as $row) {
if ($header === null) {
$header = array_keys($row);
fputcsv($out, $header);
}
fputcsv($out, array_map(fn($k) => is_bool($row[$k]) ? ($row[$k] ? "true" : "false") : $row[$k], $header));
}
$params["cursor"] = $data["cursor"]; // save this to resume later without paying twice
} while ($data["cursor"]);
fclose($out);
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"]
// Node 18+ (save as an .mjs file)
import { existsSync, readFileSync, writeFileSync } from "node:fs";
const API = "https://listriver.com/api/data.ashx";
const headers = { "X-Api-Key": "YOUR_KEY" };
const SAVED = "listriver-feed.json";
const filters = { action: "records", entityType: "LLC", preset: "recent", limit: 1000 };
const positions = existsSync(SAVED) ? JSON.parse(readFileSync(SAVED, "utf8")) : {};
for (const state of ["FL", "GA", "TX"]) { // one state per feed
const params = { ...filters, states: state };
if (positions[state]) params.cursor = positions[state];
while (true) {
const res = await fetch(`$https://listriver.com/api/data.ashx?${new URLSearchParams(params)}`, { headers });
const data = await res.json();
if (!res.ok) throw new Error(data.error);
for (const row of data.rows) console.log(row.listriverId, row.businessName); // or save to your CRM
if (data.resume) {
positions[state] = data.resume;
writeFileSync(SAVED, JSON.stringify(positions)); // saved after every page: a crash never pays twice
}
if (!data.cursor) break;
params.cursor = data.cursor;
}
}
// .NET 6+ (top-level statements)
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text.Json;
const string API = "https://listriver.com/api/data.ashx";
const string SAVED = "listriver-feed.json";
var http = new HttpClient { Timeout = TimeSpan.FromMinutes(2) };
http.DefaultRequestHeaders.Add("X-Api-Key", "YOUR_KEY");
var positions = File.Exists(SAVED)
? JsonSerializer.Deserialize<Dictionary<string, string>>(File.ReadAllText(SAVED))
: new Dictionary<string, string>();
foreach (var state in new[] { "FL", "GA", "TX" }) // one state per feed
{
var query = new Dictionary<string, string>
{
["action"] = "records", ["entityType"] = "LLC", ["preset"] = "recent", ["limit"] = "1000", ["states"] = state
};
if (positions.TryGetValue(state, out var saved)) query["cursor"] = saved;
while (true)
{
var url = API + "?" + string.Join("&", query.Select(kv => kv.Key + "=" + Uri.EscapeDataString(kv.Value)));
var res = await http.GetAsync(url);
var body = await res.Content.ReadAsStringAsync();
if (!res.IsSuccessStatusCode) throw new Exception(body);
using var doc = JsonDocument.Parse(body);
foreach (var row in doc.RootElement.GetProperty("rows").EnumerateArray())
Console.WriteLine(row.GetProperty("listriverId").GetString() + " " + row.GetProperty("businessName").GetString());
var resume = doc.RootElement.GetProperty("resume");
if (resume.ValueKind == JsonValueKind.String)
{
positions[state] = resume.GetString();
File.WriteAllText(SAVED, JsonSerializer.Serialize(positions)); // saved after every page
}
var cursor = doc.RootElement.GetProperty("cursor");
if (cursor.ValueKind == JsonValueKind.Null) break;
query["cursor"] = cursor.GetString();
}
}
<?php
$saved = "listriver-feed.json";
$positions = file_exists($saved) ? json_decode(file_get_contents($saved), true) : [];
foreach (["FL", "GA", "TX"] as $state) { // one state per feed
$params = ["action" => "records", "entityType" => "LLC", "preset" => "recent", "limit" => 1000, "states" => $state];
if (!empty($positions[$state])) $params["cursor"] = $positions[$state];
do {
$ch = curl_init("https://listriver.com/api/data.ashx?" . http_build_query($params));
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => ["X-Api-Key: YOUR_KEY"],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 120,
]);
$data = json_decode(curl_exec($ch), true);
if (curl_getinfo($ch, CURLINFO_HTTP_CODE) != 200) die($data["error"] . "\n");
foreach ($data["rows"] as $row) echo $row["listriverId"], " ", $row["businessName"], "\n"; // or save to your CRM
if ($data["resume"]) {
$positions[$state] = $data["resume"];
file_put_contents($saved, json_encode($positions)); // saved after every page: a crash never pays twice
}
$params["cursor"] = $data["cursor"];
} while ($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")
const API = "https://listriver.com/api/data.ashx";
const headers = { "X-Api-Key": "YOUR_KEY" };
async function getPage(params) {
for (let attempt = 0; attempt < 5; attempt++) {
const res = await fetch(`$https://listriver.com/api/data.ashx?${new URLSearchParams(params)}`, { headers });
if (res.status === 503) { // busy: nothing was charged
const wait = Number(res.headers.get("Retry-After") || 60);
await new Promise((r) => setTimeout(r, wait * 1000));
continue;
}
const data = await res.json();
if (res.status === 429) throw new Error(data.error); // out of records: the message says how to get more
if (!res.ok) throw new Error(data.error);
console.log("left:", res.headers.get("X-ListRiver-Monthly-Remaining"), "+ packs:", res.headers.get("X-ListRiver-Credits-Remaining"));
return data;
}
throw new Error("ListRiver stayed busy; try again later");
}
using System;
using System.Linq;
using System.Net.Http;
using System.Text.Json;
using System.Threading.Tasks;
static async Task<JsonDocument> GetPage(HttpClient http, string url)
{
for (var attempt = 0; attempt < 5; attempt++)
{
var res = await http.GetAsync(url);
if ((int)res.StatusCode == 503) // busy: nothing was charged
{
await Task.Delay(res.Headers.RetryAfter?.Delta ?? TimeSpan.FromSeconds(60));
continue;
}
var body = await res.Content.ReadAsStringAsync();
if ((int)res.StatusCode == 429) // out of records: the message says how to get more
throw new Exception(JsonDocument.Parse(body).RootElement.GetProperty("error").GetString());
if (!res.IsSuccessStatusCode) throw new Exception(body);
if (res.Headers.TryGetValues("X-ListRiver-Monthly-Remaining", out var left))
Console.WriteLine("left: " + left.FirstOrDefault());
return JsonDocument.Parse(body);
}
throw new Exception("ListRiver stayed busy; try again later");
}
<?php
function get_page(array $params): array {
for ($attempt = 0; $attempt < 5; $attempt++) {
$headers = [];
$ch = curl_init("https://listriver.com/api/data.ashx?" . http_build_query($params));
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => ["X-Api-Key: YOUR_KEY"],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 120,
CURLOPT_HEADERFUNCTION => function ($ch, $line) use (&$headers) {
$p = explode(":", $line, 2);
if (count($p) == 2) $headers[strtolower(trim($p[0]))] = trim($p[1]);
return strlen($line);
},
]);
$data = json_decode(curl_exec($ch), true);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($code == 503) { sleep((int)($headers["retry-after"] ?? 60)); continue; } // busy: nothing was charged
if ($code == 429) die($data["error"] . "\n"); // out of records: the message says how to get more
if ($code != 200) throw new Exception($data["error"] ?? "HTTP $code");
echo "left: ", $headers["x-listriver-monthly-remaining"] ?? "?", "\n";
return $data;
}
throw new Exception("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.
| Field | Format | What it holds |
|---|
businessName | text | Legal name on the state registry. |
dbaName | text | Trade name (doing business as), where filed. |
entityType | text | LLC, corporation, nonprofit, limited partnership, etc., as the state words it. |
status | text | Status on the registry, upper case (e.g. ACTIVE). |
standing | text | Good-standing status, where the state publishes it. |
formed | YYYY-MM | Month the business was formed or registered. |
registrationState | 2 letters | State registry the record comes from. |
stateFilingNumber | text | The state's filing (entity) number. |
ein | NN-NNNNNNN | Federal EIN, where known. |
address | text | Business street address. |
address2 | text | Suite, unit, etc. |
city | text | Business city. |
state | 2 letters | Business address state. |
zip | text | Business ZIP code. |
county | text | County of the business address (when it is in the registration state). |
mailingAddress | text | Mailing street address, where different. |
mailingAddress2 | text | Mailing suite, unit, etc. |
mailingCity | text | Mailing city. |
mailingState | 2 letters | Mailing state. |
mailingZip | text | Mailing ZIP code. |
phone | text | Business phone, where on file. U.S. numbers as (NNN) NNN-NNNN. |
email | text | Business email, where on file (null when you ask for email=0). |
website | text | Business website, where on file. |
registeredAgent | text | Registered agent name, where the state publishes it. |
naics | text | NAICS industry code, where known. |
industry | text | NAICS industry description. |
womanOwned | true/false | Woman-owned, where known (false = not known to be). |
veteranOwned | true/false | Veteran-owned, where known. |
minorityOwned | true/false | Minority-owned, where known. |
disabilityOwned | true/false | Disability-owned, where known. |
lgbtqiOwned | true/false | LGBTQI-owned, where known. |
latitude | number | Latitude of the business address, where geocoded. |
longitude | number | Longitude of the business address, where geocoded. |
registryDataAsOf | YYYY-MM-DD | Date of the state file the record was last taken from. |
listriverId | text | Stable 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:
400 | A parameter is wrong (unknown preset, unavailable state, a ZIP without states, bad cursor). The message says which. |
401 | Missing or unknown API key. |
403 | AWS Marketplace keys: the subscription isn't active yet (try again in a few minutes) or it has ended. |
429 | Your records are used up (see the headers above), or too many count calls in a minute. |
503 | We 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. |
500 | A 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].