Business Entity & UCC Lien Search — KYB Company Lookup
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.
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 thirty states for entity search: twenty-seven searched by default — Alabama, Alaska, Arkansas, Colorado, Connecticut, District of Columbia, Florida, Hawaii, Idaho, Iowa, Kentucky, Maine, Minnesota, Mississippi, Missouri, New Jersey, New Mexico, New York, North Dakota, Oregon, Pennsylvania, Rhode Island, South Carolina, Texas, Vermont, West Virginia, and Wisconsin — plus Montana, California, and Georgia, covered only when you name them in states (they’re reached through a warmed residential browser session with a real per-run cost, so they never ride along in the default sweep). The entity→lien join is live for fifteen of those: Alabama, Alaska, Arkansas, Colorado, Connecticut, Florida, Idaho, Kentucky, Mississippi, New Mexico, and Wisconsin in the default set, plus Montana and California — and now Pennsylvania and New York — on explicit request. Pennsylvania and New York are a newer 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, so the lien join is reached through a warmed residential browser session and runs only when you name the state with liens enabled. (Georgia is entity-only — its statewide lien index is a paid subscription service, so a resolved Georgia entity comes back with its full record but no lien join.) 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. Delaware — the country’s top incorporation state — has a free name search, but it returns only the name and file number (status, type, and agent sit behind a paid detail lookup), it’s an inconsistent handshake, and the page explicitly prohibits data mining. So Delaware is a documented miss, not a target: I don’t cross paywalls or fight a source that tells me not to scrape it. Several large states sit behind hard anti-bot walls. Three of those are covered now — Montana, California, and Georgia are served through the Actor’s own residential 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 all three are explicit-request only rather than part of the default sweep. Georgia was the demand-driven add: its portal sits behind an active challenge that flags a datacenter request within a few calls, but the warmed session clears it and reads the state’s own search and business-information pages — name, control number, status, formation date, registered agent, and the officer roster on a corporation — the same normalized record every other state returns. The rest of the walled set (Ohio, Washington, Nevada, Wyoming) stays mapped-but-uncovered until the same path proves out per state — not pretended-covered now. 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 three walled entity states (Montana, California, and Georgia), searched only when you name them, plus the Pennsylvania and New York lien joins, which bill the extended rate only on a record where the lien join was actually delivered (their entity search stays standard, and both are opt-in by naming the state). 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 three states cost structurally more to serve per run, and a flat price would have meant either overcharging the other twenty-seven 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 — fifteen states today (Alabama, Alaska, Arkansas, Colorado, Connecticut, Florida, Idaho, Kentucky, Mississippi, New Mexico, and Wisconsin in the default set, plus Montana, California, Pennsylvania, and New York on explicit request). Pennsylvania and New York are the most recent: 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 are gated (paywall or anti-bot). The compliant paths — licensed bulk data, or the same warmed-residential-session route that now serves Montana, California, and Georgia — are mapped but deliberately deferred. Florida, the other perennially-asked-for big state, shipped via its official bulk feed (see above), which is the shape I’d want a Delaware path to take too.
- 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.