← Guides

How-to

How to run Alabama Contractor License Lookup & Verify (LBGC)

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

A subcontractor sends you a bid, or a name lands in your CRM, and you need to know whether Alabama actually licenses them as a general contractor — and in what. The board publishes that for free: Alabama’s LBGC license-search page lists its whole licensee roster, and clicking a row opens the contractor’s detail page with the address, ZIP and expiration on it. For one contractor, that is a minute of your life and you should just go and do it. It gets tedious at volume — the roster is one long client-side-paged table, so a batch of twenty subs is twenty searches, twenty clicks into a detail page, and forty copy-pastes back into a spreadsheet, with the classification codes retyped by hand. This actor is the same public record, as rows, addressable from an API.

What you’ll need

Nothing but an Apify account. The actor talks to AL LBGC directly.

No API key, no CAPTCHA-solving credentials, no proxy. The board’s site is plain server-rendered HTML with no anti-bot wall on it, and it serves ordinary datacenter IPs happily, so the actor just fetches it. There is a proxyConfiguration field in the input; it is off by default and you should not need to touch it.

Step 1 — Open the actor

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

The Alabama Contractor License Lookup & Verify (LBGC) 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 Alabama Contractor License Lookup & Verify (LBGC) in the Apify Console
The input form, as it appears in the Apify Console.
FieldTypeRequiredWhat it does
query / queriesstring / listnoAn Alabama LBGC license number (exact) or a business name (substring). Leave both empty to export the roster instead.
classification / classificationsstring / listnoExport only: an LBGC code (BC, MU-S, SUBCONTRACTOR, …) or a word from the description (roofing).
statusstringnoExport only: active (default), inactive or all.
addressStatestringnoExport only: two-letter state of the mailing address, e.g. AL.
city / zipstringnoExport only: mailing city (exact) or 5-digit ZIP; each also takes a list (cities, zips).
maxResultsintegernoPer query in a search (default 50); for the whole run in an export (default 500, up to 100000).
includeDetailsbooleannoSearch only: fetch each match’s detail page to add address, ZIP, and expiration date. (default True)

query decides which of two jobs the actor does. Fill it in and it verifies; leave it empty and it exports (next section). A search has two modes chosen for you by what you type. A licence-shaped value — 8, 4510, or a prefixed number like S-53030 — is read as an LBGC license number and matched exactly. Anything else is read as a business name and matched as a case-insensitive substring of the licensed name, so CONSTRUCTION returns every licensee with that word anywhere in its name, and Dunn returns Dunn Construction along with anything else containing “dunn”. There is no fuzzy matching and no punctuation forgiveness: search the shortest distinctive fragment of the name rather than the full legal string with its , INC. on the end. The mode that fired is stamped on every row as matched_by, so you can always see which one you got.

maxResults (default 50) is a cap, not a ranking. The actor walks the roster in the board’s own order and stops at the cap, so a broad name search returns the first fifty matches, not the fifty best ones. If a search feels arbitrary, that is why — narrow the query rather than raising the cap.

includeDetails is on by default and is what fills in address, zip_code and expiration_date: those three live on each contractor’s detail page, and the actor fetches one page per match to get them. Turn it off and the run is faster and lighter, but those three fields come back null. Leave it on unless you only want the roster-level columns.

A working input:

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

Exporting the whole roster

Leave query and queries empty and the actor stops searching and reads LBGC’s own roster file instead — the FullRosterReport CSV the board links from its search page, one download with every licensee in it. On 2026-10-09 that file held 8,993 licensees, 8,917 of them active. The rows already carry street address, ZIP, phone, fax, bid limit and expiry, so no detail pages are fetched, and the filters narrow it before anything is billed:

{
  "classification": "roofing",
  "addressState": "AL",
  "maxResults": 1000
}

That input returned 159 active roofing licensees with an Alabama mailing address on 2026-10-09, every one of them with a phone number; "classification": "BC" with the same state returned 733. Two things about the roster decide which filters you want. First, LBGC licenses a lot of firms from elsewhere: only 4,172 of the 8,917 active licensees had an Alabama address, so addressState: "AL" is the filter for “contractors based here”. Second, LBGC publishes no county at all, so narrow by city or zip. A code such as BC matches only that code (BC-S, the subclassification, is a different code); a word such as roofing matches any classification whose description contains a word starting with it. A filter value the actor does not recognise is dropped with a log line, and if nothing you gave is recognised you get one free row saying so — never the whole state.

maxResults defaults to 500 in an export, as a spend control. Set it to 100000 to take everything that matches; the log line states how many matched whenever the cap cut the run short.

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.

A healthy run does one big fetch first — the entire roster, 8,993 licensees on 2026-10-09, arrives in a single response — and filters it in memory, so the search itself is instant. An export is that one fetch and nothing else. The time after that is detail pages: one request per match when includeDetails is on. A license-number lookup is therefore one match and one detail fetch; a broad name search capped at 50 is up to fifty of them. That is the honest shape of the run; the wall-clock depends on the board’s servers on the day, so start one and watch the log.

An empty run is nearly always the query, not the actor. The license match is an exact string compare against the number as the board prints it, so 00008 will not find licensee 8. (Until 2026-10-09 a prefixed number such as S-53030 was searched as a name and could never match; 3,474 of the board’s 8,993 licence numbers carry a prefix like that, and they are now looked up exactly.) On the name side, a full legal string with punctuation the board spells differently matches nothing as a substring. Zero rows costs nothing — the actor charges per result — so drop back to the shorter fragment and look again.

Step 4 — Read the output

Each run returns a labelled table, not raw JSON:

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

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

{
  "state": "AL",
  "license_number": "8",
  "business_name": "DUNN CONSTRUCTION CO INC",
  "license_type": "BC: BUILDING CONSTRUCTION, H/RR: HEAVY AND RAILROAD, HS: HIGHWAYS AND STREETS, MU: MUNICIPAL AND UTILITY",
  "city": "BIRMINGHAM",
  "phone": "(205) 592-3866",
  "address": "P O BOX 11967",
  "zip_code": "35202",
  "expiration_date": "9/30/2026",
  "matched_by": "business_name",
  "source_url": "https://licensesearch.alabama.gov/genconbd/Details/2"
}
FieldExample valueMeaning
stateALAlways ‘AL’ for this actor
license_number8LBGC general-contractor license number
business_nameDUNN CONSTRUCTION CO INCLicensed business name
license_typeBC: BUILDING CONSTRUCTION, H/RR: HEAVY AND RAILROAD, HS: HIGHWAYS AND STREETS, MU: MUNICIPAL AND UTILITYSpecialty classification (e.g. BC: BUILDING CONSTRUCTION, HS: HIGHWAYS AND STREETS)
cityBIRMINGHAMBusiness city
phone(205) 592-3866Business phone (as published on the roster)
addressP O BOX 11967Business street / mailing address (from the detail page)
zip_code35202ZIP code (from the detail page)

Most people came for two things: license_type, which is the proof that this contractor is actually allowed to do the work you are about to hand them, and expiration_date, which is the proof it is still current. license_type is the board’s specialty classification, and it is a comma-separated string of codes with their labels, not an array — BC: BUILDING CONSTRUCTION, H/RR: HEAVY AND RAILROAD, HS: HIGHWAYS AND STREETS, MU: MUNICIPAL AND UTILITY is one field value for one contractor. If you want to filter on a classification, do a substring test on the code, and split it yourself if you need it structured.

The rest of the caveats are about what Alabama does and does not publish. There is no bond, insurance or issue date on this registry, and no status column — the actor returns those as null rather than inventing them. An export does fill status, but only from what the roster file says: Inactive for the 76 licensees LBGC files under an INACTIVE classification, Active (extended) for the 100 it lists with an extension date, Active otherwise. A present, unexpired row means the contractor is on the board’s roster with that classification; it is not a statement that the license is in good standing, and you should not read it as one. County is not published either. Export rows add fax, bid_limit, classification_codes (the codes as a list) and address_state. address, zip_code and expiration_date are null whenever includeDetails is off, and address is whatever the contractor filed — often a PO box, as in the row above, not a physical site. Everything else arrives as the board stores it: names shouted in capitals, phones as (205) 592-3866, dates as M/D/YYYY strings. Normalise before you mail-merge.

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 license record returned. A single verification — one license number, one match — is four tenths of a cent. A name search that comes back with 25 matches is $0.10, and a run capped at the default 50 can cost at most $0.20. An export at the default 500 rows is $2.00; the whole active roster (8,917 rows on 2026-10-09) is $35.67, and the 159 Alabama roofers above are $0.64. A search that matches nothing returns nothing and is not charged, so a typo costs you the run and no more. Apify’s platform compute is billed separately, under your plan.

The comparison is not against zero, because the board’s own site is free. What the per-result fee buys is the same record without the paging and the copy-paste: a batch of subs verified in one run, on a schedule, into an API you can call.

Where the data comes from

This reads AL LBGC directly. The target is Server-rendered HTML - the entire licensee roster (8,993 rows on 2026-10-09) is inlined into a single table (paged client-side); detail pages carry their values in readonly input fields. An export reads the board’s FullRosterReport CSV instead. Every night a canary runs this actor against that live source and diffs the result against a frozen fixture — what “verified” means.

As scrape targets go, this one is easy and it would be dishonest to dress it up: one GET returns the board’s entire licensee roster, there is no login, no token, no hidden state and no anti-bot wall, so the filtering happens in memory afterwards. The annoyance is what the shape does to a human. Because the whole roster is inlined into a single table and paged in your browser, “search” on the board’s site is really you scrolling someone else’s table, and the fields you actually need for a compliance check — address, ZIP, expiration — are not in it; they sit one page deeper, in readonly input boxes on each licensee’s detail page. That is the only reason this needs a tool at all: not difficulty, just the number of clicks between you and the columns. Easy targets still drift, though — a renamed column or a reordered table would silently change what lands in your rows, which is what the nightly canary is there to catch.

See it used

Verify an Alabama Contractor by License Number takes a licence number off a bid and turns it into a filed row.

Alabama’s LBGC only licenses commercial general contractors, and only Alabama — so if the contractor in front of you might be in any state, or you want the same company checked in several at once, Multi-State Contractor & Trade License Lookup is the one to reach for instead; and if you already know it is a California licence, California Contractor License Lookup & Verify (CSLB) returns status, bond and workers’ comp, which Alabama’s registry does not publish at all.

Last updated 2026-10-09