Secretary of State Business Search API + UCC Lien Search — 45 States
Design notes for an Actor that resolves a US company from official state business registries and joins each entity to the UCC lien filings against it — entity search and lien search, connected, in one call.
Design notes for business-entity-lien-search, an Apify Actor that verifies a US company against official state business registries and, in the same call, joins each resolved entity to the UCC lien filings recorded against it. It’s built for the AI agents doing know-your-business (KYB), lending, and diligence work.
Also available through the MPP-payable Public Records API.
What this is
Give it a company name (optionally scoped to a list of states) and it returns one normalized record per matched entity: legal name, state registration ID, entity type, registration status (Good Standing, Active, Forfeited, Dissolved, and the rest, verbatim from the state), formation date, jurisdiction of formation, registered agent, principal/mailing address, and officers where the state publishes them. Then — the part nobody else does — each entity is joined to its Uniform Commercial Code filings as debtor: filing number, type, status, filed and lapse dates, the secured party (the lender), and the collateral description.
So one call answers the whole diligence question, not half of it: is this company real, in good standing, and does it already have liens against its assets?
Why I built it this way
The join is the product
The market already has business-entity search. What it doesn’t have is entity search connected to liens. Registries and UCC/secured-transaction filings live in different systems; analysts bridge them by hand — find the entity, switch to the UCC portal, search by name, eyeball whether it’s the same legal entity. That manual bridge is exactly the expensive, error-prone step, and it’s the one an agent most wants automated.
So the Actor resolves the entity from the state business register, then searches that state’s UCC index by the entity’s legal name and attaches the matching filings to the entity record. Entity search Actors stop at the first question. Standalone UCC scrapers answer the second but never link it to a verified entity. This does both and connects them.
Matching debtor to entity has to be precise
A loose name match would be worse than nothing — attach “Tesla Imaging LLC”’s lien to “Tesla Electric Company LLC” and you’ve manufactured a false risk signal. So the join compares the core of the legal name: uppercase, strip punctuation, and remove only unambiguous legal-form suffixes (LLC, Inc., Corp., LP, Ltd.). Descriptive words that are genuinely part of a name — Company, Co, Trust, Foundation — are kept, because stripping them would collapse distinct entities together (“Ford Motor Company” is not “Ford Motor”). The server-side search casts a slightly wider net with a contains query; the exact-core comparison then admits only the filings whose debtor really is this entity. A lien shows up on a record only when it belongs there.
One schema across states is the moat
A scraper for a single state’s registry is a commodity that breaks when that state redesigns its portal. The defensible thing is the unification: many states, identical output, one call. The architecture is a registry of per-state adapters behind a single search(name) -> [EntityRecord] contract, and it’s config-driven wherever the source allows. Where a state publishes its business register as clean open data (Socrata), a state is a single config row: a dataset id plus a column map onto the unified record. A larger group exposes a clean public JSON search API instead, and each gets a small bespoke adapter behind the same contract; a third group publishes only an HTML search, so those get a bespoke parser — same contract again. Adding a state never changes the call you make or the shape you parse.
The UCC side is config-driven too, in two dialects. Some states publish a single denormalized lien dataset where each row already carries debtor, secured party, status, and dates — one query, one config row. Others publish a relational UCC: a debtor table, a filing/status table, a collateral table, and a secured-party table, all joined by a filing id. That’s a second config shape (the four dataset ids plus their keys), and the adapter walks the join. Same contract, richer source.
Honest coverage beats an over-claimed map
I validated every source live before building on it, and the discipline mattered. The clean, reliable set shipped first and has since grown to forty-four states for entity search: twenty-six searched by default — Alabama, Alaska, Arkansas, Colorado, Connecticut, District of Columbia, Florida, Hawaii, Idaho, Iowa, Kentucky, Maine, Minnesota, Mississippi, New Jersey, New Mexico, New York, North Dakota, Oregon, Pennsylvania, Rhode Island, South Carolina, Texas, Vermont, West Virginia, and Wisconsin — plus eighteen covered only when you name them in states: California, Delaware, Georgia, Louisiana, Maryland, Massachusetts, Michigan, Missouri, Montana, Nevada, New Hampshire, North Carolina, Ohio, Oklahoma, Tennessee, Utah, Virginia, and Washington. Most of those eighteen are reached by driving the state’s own portal inside a warmed browser session with a real per-run cost, so they never ride along in the default sweep. (New Hampshire is the exception that keeps the pricing honest: it needs a browser but not the metered connection the walled states need, so it is named-only like the rest and still bills the ordinary rate.) The entity→lien join is live for twenty-eight: Alabama, Alaska, Arkansas, Colorado, Connecticut, Florida, Idaho, Kentucky, Mississippi, New Mexico, Rhode Island, South Carolina, and Wisconsin in the default set, plus California, Iowa, Maryland, Michigan, Montana, New Jersey, New York, North Carolina, Ohio, Oklahoma, Oregon, Pennsylvania, Tennessee, Virginia, and Washington on explicit request. Iowa, New Jersey, New York, Oregon, and Pennsylvania are a shape worth calling out: their entity search is part of the default sweep at the standard rate, but their lien indexes sit behind a bot wall or a host that refuses a direct request, so the lien join is reached through a warmed session and runs only when you name the state with liens enabled. (Eight of the named-only states are entity-only — Delaware, Georgia, Louisiana, Massachusetts, Missouri, Nevada, New Hampshire, and Utah — because each one’s statewide lien index is paid, account-gated, challenge-fronted, or on a portal I won’t drive; a resolved entity there comes back with its record and an explicit note that no lien join exists for that state.) Coverage runs deep where a state’s registry is rich and lighter where it isn’t — a few states publish a name/status/city index but gate the fuller record behind a per-entity view, so those records carry the core fields plus a note about what the source withholds. Every run also writes a COVERAGE record so a caller can read the live coverage rather than trust a stale doc.
Coverage also means being honest about what a source contains. Iowa is a good example: the state publishes its entire business register as a single public bulk download, but only for active entities — dissolved and historically-inactive companies simply aren’t in it. Rather than query that live (its live API is walled), I ingest the whole file once per publish cycle into a small lookup index and serve name searches from it in milliseconds. The catch a naive integration would miss: because the source is active-only, a “no match” for an Iowa name doesn’t mean the company never existed — only that no active Iowa entity matched. So every Iowa record is reported as active, and an Iowa no-match carries a note saying exactly that, so an agent doesn’t read absence as proof of non-existence.
Florida is the same bulk-index mechanism at a very different scale, and with the opposite semantics. The state’s live search is bot-walled, but its Division of Corporations publishes the entire corporate register — nearly thirteen million records — as an official bulk feed: a quarterly full snapshot plus daily delta files, in a fixed-width layout the state documents publicly. Each refresh rebuilds the index from the snapshot and upserts the deltas by document number, so the whole cycle is stateless and idempotent. And unlike Iowa, Florida’s register includes inactive and dissolved entities with their real status, registered agent, principal and mailing addresses, and up to six named officers per record. For a diligence workflow that matters twice: a Florida record can come back Inactive — a positive signal that the counterparty existed and was dissolved, which is a very different answer than “no match” — and the officer list gives the join-adjacent context (who ran it) that most open registers omit. One honest caveat, noted in the docs rather than hidden: new Florida filings arrive through the daily deltas within days, but a status change on an existing entity (a mid-quarter dissolution, say) surfaces at the next quarterly snapshot — the bulk feed’s own publication rhythm.
Just as important is what I didn’t ship — and where a documented miss later became an honest partial. Delaware — the country’s top incorporation state — has a free name search, but it returns only the file number and the legal name; status, type, formation date, agent, and officers all sit behind the state’s paid record lookup. For a long time that made Delaware a documented miss rather than a target, because I don’t cross paywalls. It now ships as a named-only entity state on exactly those terms: the two fields the free index publishes, and a field_notes entry saying plainly that everything else about a Delaware entity is behind a paid lookup this Actor does not purchase. Single per-request lookups only, never a sweep — the page posts a notice against data mining, and honoring it is the price of using the source at all. Several large states sit behind hard anti-bot walls. Eighteen states are named-only today, most of them served through the Actor’s own browser fetch layer, which warms a real session on the state’s portal and runs the site’s own search from inside it. That warm has a genuine per-search cost, which is exactly why they are explicit-request only rather than part of the default sweep. Maryland, Washington, and Oklahoma came earlier — Maryland brings both a full entity record and a free statewide lien index that publishes every debtor and secured party on a filing; Oklahoma’s entity search is walled while its lien index, run by the county clerk that is the state’s exclusive filing office, is openly readable; Washington shipped entity-only and has since gained its lien side too, from the separate agency portal that publishes it. Ohio, Virginia, and New Hampshire are the most recent, and each landed on a different lesson. Ohio was built once and then not shipped: its search portal opened fine, but the separate host that actually answers the searches refused this vantage outright — and a state that answers nothing is worse than a state I don’t claim. The second attempt stopped calling that host at all; the adapter drives Ohio’s own page instead, typing into the state’s name field, pressing the state’s search control, and reading the table the state renders, so the only request that host ever sees is the one the state’s own page makes. Virginia is anonymous-surface-only by construction — the cookie notice is recorded, never an account or a bulk-download page — and because the state emits one row per name type, former and fictitious names are folded back to a single entity by ID. New Hampshire changed the price rather than the coverage: it needs a browser but not a metered connection, so it is named-only and bills the standard rate anyway. What still isn’t covered stays that way and is documented rather than pretended: Wyoming, Illinois, Arizona, Kansas, Nebraska, Indiana, and South Dakota, each blocked by a refusing host, an interactive challenge I don’t answer, an account wall, or a consent gate I won’t click. A smaller set that actually works beats a bigger one that 500s on a real input.
Not all absences are the same
When a field comes back null, the record says why. Texas’s Comptroller search returns the entity name and taxpayer ID but not status or type — those live on a separate detail page — so a Texas record carries a field_notes entry marking those fields not_in_source with the explanation. A state we cover for entities but not yet for liens returns the entity with liens_checked: false and a note saying the lien join isn’t integrated there yet — distinct from a lien-covered state that genuinely found no liens (liens_checked: true, ucc_summary: {total: 0, active: 0}, a confident “clean”). An agent can tell “structurally unavailable” from “we don’t have it yet” from “checked, none found,” and act on each differently.
Billing that can’t surprise you
Charging is per completed entity record, pushed before it’s billed, and never charged on a no_match, not_covered, or failed result. A broad name like “Smith” is bounded two ways: a per-company record cap, and the run’s max-charge ceiling, with every charge clamped to the caller’s remaining budget in code. You can’t get a runaway bill from a common name.
How to use it
Verify a company and pull its liens in Colorado and Connecticut:
curl -X POST "https://api.apify.com/v2/acts/shelvick~business-entity-lien-search/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"companyName": "Tesla", "states": ["CO", "CT"], "includeLiens": true}'
From the Apify Python SDK, verifying a batch of counterparties at once:
from apify_client import ApifyClient
client = ApifyClient("<APIFY_TOKEN>")
run = client.actor("shelvick/business-entity-lien-search").call(
run_input={"companyNames": ["Acme Holdings", "Beta Logistics LLC"], "includeLiens": True}
)
for rec in client.dataset(run["defaultDatasetId"]).iterate_items():
print(rec["legal_name"], rec["state"], rec["entity_status"], rec["ucc_summary"])
Agents can call it as a tool through the Apify MCP server (mcp.apify.com), where the input schema and per-field descriptions are advertised to the model. Set matchMode to exact when you know the legal name, contains (the default) to discover every entity sharing a name, and includeLiens: false for a faster entity-only check.
How it compares to standalone registry tools
| This Actor | Standalone entity-search tools | Single-state UCC scrapers | |
|---|---|---|---|
| Entity search by name | Multi-state, one schema | Usually, coverage varies | No |
| UCC / lien filings | Joined to each entity | No | Yes, but unlinked |
| One call for entity + liens | Yes | No | No |
| Machine-readable status + coverage notes | Yes | Rarely | Rarely |
| Precise debtor→entity name match | Yes | N/A | N/A |
The bet is that multi-jurisdiction assembly behind one schema, plus the entity-to-lien join, is worth more to a diligence workflow than any single registry scraped in isolation.
Pricing model
Pay-per-event, billed only on success: one charge per completed entity record, after it’s pushed to the dataset. The charge covers the registry search, normalization, and the lien join where available; misses and failures are free. Two rates exist: a standard per-record rate for the default-covered states, and a higher extended rate for the extended-coverage work — the seventeen walled entity states, searched only when you name them, plus the Iowa, New Jersey, New York, Oregon, and Pennsylvania lien joins, which bill the extended rate only on a record where the lien join was actually delivered (their entity search stays standard, and all five are opt-in by naming the state). New Hampshire is named-only for the same cost-shape reason but bills the standard rate, because it doesn’t incur the metered cost the walled states do. Either way the extended rate is never charged for a state you didn’t ask about. The current rates and any subscriber discounts are on the Apify Store Pricing tab, which is authoritative.
Why two rates instead of one: those states cost structurally more to serve per run, and a flat price would have meant either overcharging the twenty-six default states or quietly eating the difference. Splitting the rate makes the asymmetry legible where customers actually read it — the price — and keeps the opt-in honest: naming the state is the consent to its rate.
Open questions / future work
- More UCC-join states. The join lives where a state publishes a register and a debtor-searchable UCC index I can reach — twenty-eight states today (Alabama, Alaska, Arkansas, Colorado, Connecticut, Florida, Idaho, Kentucky, Mississippi, New Mexico, Rhode Island, South Carolina, and Wisconsin in the default set, plus California, Iowa, Maryland, Michigan, Montana, New Jersey, New York, North Carolina, Ohio, Oklahoma, Oregon, Pennsylvania, Tennessee, Virginia, and Washington on explicit request). The newest arrivals each came with their own honest gap. Michigan returns every matching filing in one response but publishes no debtor list on the filing itself, so
debtor_namestays null and the source’s own exact debtor match is what establishes the link; its index also covers active filings plus those lapsed within roughly the past year, not older lapsed history. Ohio publishes filing number and date, transaction type, debtors, secured parties, and a lapse date, but no status column and no collateral text — collateral lives only in filing images — so effectiveness is read off the lapse date. Washington’s free results are the same shape, and the adapter deliberately stops at the results page because every control past it orders paid copies. Oregon answers on an exact normalized name, stripping punctuation, spacing, and trailing entity words, and prints the date its index is current through. Virginia’s search is anonymous-only, and its liveUnlapsedvalue is normalized toActiveso a caller’s inactive-status test can’t misread it. New Jersey rounds out the walled set on an ordinary public debtor search. South Carolina joined the default set on a free grid that publishes filing number, type, filed date, and lapse date only — status, secured party, and collateral are not exposed there and arrive null. Before them, Maryland and Oklahoma, each honest about a different gap: Maryland publishes no explicit filing-status column, so effectiveness is read off the lapse date (and a filing with no lapse date is one that does not lapse). Oklahoma’s index publishes debtor, secured party and full collateral text but neither a lapse date nor a status — so those two fields come back null with a note saying exactly why, rather than being guessed at from the filing chain. Before those, Rhode Island and Iowa, one from each side of the wall. Rhode Island’s UCC search lives on the same open estate as its entity search — the cheapest add the join has had — with one honesty wrinkle: the state’s “active” index quietly retains filing chains that were terminated by a later amendment, and publishes no lapse dates at all, so each filing’s status is derived from its own chain history rather than read off a column (counting that index’s rows as active liens would overstate them). Iowa is the opposite shape: its lien index sits behind a bot wall like Pennsylvania’s and New York’s, but once reached it’s the richest of the walled joins — a single query returns the complete result set with a real lapse date on every filing. Before them, Pennsylvania and New York: both publish a free debtor-name lien search, but each sits behind a bot wall, so — like the walled entity states — the lien side is reached through a warmed residential browser session and billed as extended-coverage, while their entity search rides the default sweep at the standard rate. New York’s index in particular runs oldest-first, so the join reads from the newest end to keep a heavy debtor’s current liens complete rather than burning the result window on decades-old lapsed filings. Florida is the join at its best-case: the state’s registry vendor publishes the entire Article 9 register — over five and a half million financing statements, with debtors, secured parties, statuses, and lapse dates — as free official bulk data files, so Florida’s lien side is an index built from the state’s own full publication, the same mechanism that already stands behind its thirteen-million-record entity register. The one gap is honest and structural: the bulk files publish filing images rather than collateral text, so Florida filings carry every field except the collateral description, refreshed on a periodic cycle rather than live. Before it, Mississippi followed Alaska’s playbook: its UCC side is a free public debtor search, but its live entity search sits behind a bot wall — so the entity side is an index built from the state’s own full public register (a million-plus records, dissolved entities included with their real standing), with the honest caveat that this particular register publishes no registered-agent or officer data, so those fields come back null with a note saying exactly why. Before it, Alaska took two sources to land: the statewide Recorder’s Office publishes a free, debtor-searchable UCC index, but Alaska’s live entity search is bot-walled — so the entity side is built from the state’s own official bulk data files rather than a live query, the same index-backed approach that already stands behind Iowa and Florida. Before it, New Mexico, Arkansas, Kentucky, and Wisconsin came from re-validating every “no free UCC” verdict against the states’ current portals; New Mexico in particular had been written off on a dead endpoint when a working keyless search existed one route over. Minnesota, covered for entity search, turned out not to qualify: its “Business & Lien System” bundles a UCC search, but lookup by debtor name sits behind a paid subscription and an account login — only file-number lookup is free, and a file number is exactly what a name-based join doesn’t have. I don’t build on paywalled or account-gated surfaces, so Minnesota ships entity-only. West Virginia is a similar story from the other direction: its free UCC index is debtor-searchable, but it’s reCAPTCHA-gated — required both from a test machine and, when I checked, from the production runner’s own IP — and I don’t solve interactive challenges, so West Virginia also ships entity-only for now. North Dakota’s registry API still hints at a lien endpoint worth a second look. - The formation hubs. Delaware, Wyoming, and Nevada are where a huge share of US entities actually form, and all three were gated (paywall or anti-bot). Two now ship: Nevada through the same warmed-residential-session route that serves Montana, California, and Georgia, and Delaware through its free public index — with the honest limit that Delaware’s free surface carries only a file number and a legal name, so a Delaware record confirms existence rather than profiling the entity. Wyoming stays gated and deliberately deferred. A fuller Delaware record would need licensed bulk data; Florida, the other perennially-asked-for big state, shipped via its official bulk feed (see above), which is still the shape I’d want a Delaware path to take.
- Officers and agents where they live in a second dataset. A few states keep principals in a separate companion table; the same best-effort join pattern that fills other gaps applies, once enough states queue behind it to justify the mechanism.