← Guides

How-to

How to run Minnesota Contractor License Lookup & Verify (DLI)

Step-by-step: the exact input, what each field does, and the rows that come back from MN DLI.

You have a Minnesota residential contractor in front of you — on a bid, on an onboarding form, on a list of subs someone else vetted — and you need to know whether their DLI licence is real, what subtype it is, whether it has expired, and whether the state has ever taken an enforcement action against them. Minnesota makes this unusually easy, and you should know that up front: DLI publishes its entire residential-contractor roster as one public CSV, refreshed nightly, no login and no key. You can download it right now. What you get if you do is a ~12MB file in a Windows encoding that Excel will happily mangle, with the name split across a business column and a DBA column, the address across two, and an enforcement flag hiding as a bare 1 in a column named for a database. This actor fetches that same file, filters it, and hands you back the matching licences as clean, typed rows — one search instead of one download, one import and one afternoon of column-wrangling per question you want to ask.

What you’ll need

Nothing but an Apify account. The actor talks to MN DLI directly.

No API key, no CAPTCHA solver, no proxy. The roster is a static file the department serves to anyone who asks, so the actor is a single plain HTTP GET. There’s a proxyConfiguration field in the input, but it is off by default and there is no reason to touch it.

Step 1 — Open the actor

Open Minnesota Contractor License Lookup & Verify (DLI) on the Apify Store and press Try for free. The actor’s own page on this site lists its live health and pricing.

The Minnesota Contractor License Lookup & Verify (DLI) listing on the Apify Store
The listing on the Apify Store. Pricing and the nightly health record are both public.

Step 2 — Fill in the input

The input form for Minnesota Contractor License Lookup & Verify (DLI) in the Apify Console
The input form, as it appears in the Apify Console.
FieldTypeRequiredWhat it does
querystringyesA Minnesota DLI residential-contractor license number, business name, or person name to verify or search. Searches by exact license number and by business/DBA name substring.
maxResultsintegernoCap on matches returned (bounds cost). (default 50)
enforcementOnlybooleannoWhen on, return only licensees with an active DLI enforcement action (revocation, suspension, etc.). Use this to monitor a name/keyword for disciplinary flags.

query is the field that decides everything, and it does two different things depending on what you type. If what you type matches a licence number exactly (case-insensitive — BC807688, RR531208), you get that one licence. Anything else is treated as a case-insensitive substring of the business name or the DBA name. Substring, not fuzzy search: SMITH ROOFING finds SMITH ROOFING LLC and AAA SMITH ROOFING, but it will not find SMITH ROOFING, INC if you typed SMITH ROOFING INC, because that comma is in the middle of the string DLI stored. When a name search comes back empty, shorten it — type the distinctive word and let the substring do the rest. The matched_by column on every row tells you which of the two paths produced it.

The consequence of substring matching is that broad queries match a lot. CONSTRUCTION matches several thousand Minnesota businesses. That is what maxResults is for: it caps how many matches come back (default 50, max 500) and therefore caps what the run costs. It is a cap, not a ranking — the actor stops at the first N matches in the file’s own order, so a broad query with a low cap gives you an arbitrary slice, not the best one. Narrow the query rather than trusting the cap to choose for you.

enforcementOnly is a filter applied after the search: leave it off and every match comes back with its enforcement flag in the row; turn it on and only the licensees carrying an enforcement action come back. That is the difference between “tell me about this contractor” and “tell me if anyone matching this name is in trouble” — the second is what you want on a schedule.

A working input:

{
  "query": "CONSTRUCTION",
  "maxResults": 25
}

Step 3 — Run it

Press Start. Rows are pushed to the dataset as they’re found, so the run log fills in as it works.

The first thing every run does is pull the whole roster down — one ~12MB file — before it filters a single row. So there is a pause at the start that has nothing to do with how many matches you asked for, and a licence-number lookup that returns exactly one row does the same download as a name search that returns fifty. Once the file is in memory the filtering is instant. Actual wall-clock depends on DLI’s server on the day, so start it once and watch the log rather than trusting a number quoted here.

An empty run is nearly always the query, not the actor. A licence number with a typo matches nothing (there is no near-miss matching), and a full legal name typed the way you’d write it rarely survives contact with the way DLI punctuates it. Shorten the query and try again. Empty runs are free — the actor charges per match returned — so getting it wrong twice costs nothing.

Step 4 — Read the output

Each run returns a labelled table, not raw JSON:

The output fields returned by Minnesota Contractor License Lookup & Verify (DLI)
The output view — every field the actor returns, named and typed.

Every row looks like this — a real row from a real run:

{
  "state": "MN",
  "license_number": "RR531208",
  "business_name": "RASSMUSSEN METAL ROOFING AND CONSTRUCTION INC (dba RMR CONSTRUCTION INC)",
  "person_name": null,
  "license_type": "Residential Contractors - Residential Roofer Contractor",
  "status": "REVOKED",
  "issue_date": "06/20/2005",
  "expiration_date": "03/31/2008",
  "city": "MINNEAPOLIS",
  "address": "5536 27TH AVE S",
  "zip_code": "55417",
  "phone": null,
  "enforcement_action": "1",
  "renewal_in_progress": null,
  "matched_by": "business_name",
  "source_url": "https://secure.doli.state.mn.us/ccld/data/MNDLILicRegCertExport_Residential_Contractors.csv"
}
FieldExample valueMeaning
stateMNLicensee’s registered state (usually ‘MN’)
license_numberRR531208DLI license number (e.g. BC807688)
business_nameRASSMUSSEN METAL ROOFING AND CONSTRUCTION INC (dba RMR CONSTRUCTION INC)Licensed business name (DBA appended when present)
person_nameIndividual’s name when the licensee is a person, else null
license_typeResidential Contractors - Residential Roofer ContractorLicense type + subtype (e.g. ‘Residential Contractors - Qualifying Builder’)
statusREVOKEDLicense status (e.g. Issued, EXPIRED, REVOKED, SUSPENDED)
issue_date06/20/2005Original license date
expiration_date03/31/2008License expiration date

Most people came for two columns: status and enforcement_action. Read them together, and read them carefully — the sample row above is a good warning. It matched a name search, it has a licence number, it looks like a licensee, and it is REVOKED with an expiry date in 2008 and an enforcement action against it. A match is not a licence. The roster carries historical and inactive records alongside live ones, so “this contractor is in the file” tells you nothing on its own; status is the field that tells you whether they can legally take the job.

enforcement_action is DLI’s own flag, and it is exactly that — a flag. It comes through as the raw value the department set (a 1 in the sample) and is null when there is no action on record. It means an enforcement action exists, not what it was, when, or how it resolved; the case detail is not in this file, so treat the flag as a prompt to go and read DLI’s enforcement record, not as a verdict.

Things to watch in the rest of the columns. business_name folds the DBA in when DLI has one — RASSMUSSEN METAL ROOFING AND CONSTRUCTION INC (dba RMR CONSTRUCTION INC) is one field, not two, so split on (dba if you need them apart. person_name is only populated when DLI classifies the licensee as an individual rather than a business, so it is null on most rows and that is correct, not missing data. license_type is the department’s type and subtype joined — the type is Residential Contractors on essentially every row, so the part you actually want is after the dash (Residential Roofer Contractor, Qualifying Builder). Dates are MM/DD/YYYY strings, names shout in capitals, and phone is frequently empty because DLI only publishes it sometimes. There are no email addresses: the roster has an email column and this actor deliberately skips it.

Step 5 — Export it

Open the Dataset tab and export to CSV, JSON, or Excel — or pull the same rows from the API, which is what you want if this is going to run on a schedule.

What it costs

$0.004 per licence record returned. A licence-number verification returns one row and costs $0.004. A name search capped at the default 50 matches costs at most $0.20. Searches that match nothing are not charged at all, so a typo costs you nothing. Apify’s platform compute is billed separately, per your plan.

Be honest with yourself about the comparison. The data is free and the file is one click away — if you have one contractor to check, once, go and use DLI’s own site and pay nothing. The fee buys the parts the file doesn’t give you: the filtering, the normalised columns, and the run being repeatable from an API or a schedule without a person opening a spreadsheet.

Where the data comes from

This reads MN DLI directly. The target is Official bulk data file - one GET of the department’s nightly ~12MB roster CSV (cp1252-encoded), filtered in memory. Every night a canary runs this actor against that live source and diffs the result against a frozen fixture — what “verified” means.

This is the easiest target in the suite, and pretending otherwise would be silly: DLI publishes the roster itself, as a static CSV, with no anti-bot wall, no session, no form to drive and no key to hold. There is nothing to beat here. The annoyance is entirely in the file. It is a whole-state dump — you take all of it or none of it, because there is no query API behind it, so every question you want to ask means downloading the lot and filtering it yourself. It is encoded in cp1252 rather than UTF-8, which is the kind of detail that silently corrupts a name with an accent in it and shows up three steps later as a mismatch. And the schema is the department’s internal one: split names, split addresses, a licence type that says the same thing on every row, and an enforcement flag encoded as a bare digit. The nightly canary exists for exactly the part that is not under anyone’s control — DLI renaming a column or moving the file — because when a bulk file changes shape, nothing errors, it just quietly stops matching.

See it used

Find Minnesota Roofing Contractors with Enforcement Actions screens the DLI roster down to the licences carrying an enforcement action.

There are no worked scenarios for this actor yet. If the Minnesota roster isn’t the file you need, Michigan Contractor License Lookup & Verify (LARA) is the one to reach for when the contractor you’re checking is licensed next door rather than in Minnesota; if you don’t know which state issued the licence at all, the Multi-State Contractor & Trade License Lookup checks the same name across several states in one run.

Last updated 2026-07-13