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.
Step 2 — Fill in the input
| Field | Type | Required | What it does |
|---|---|---|---|
query | string | yes | An Alabama LBGC general-contractor license number (exact) or a business name (substring). The board publishes a numeric license number; anything non-numeric is treated as a… |
maxResults | integer | no | Cap on matches returned (bounds cost and detail-page fetches). (default 50) |
includeDetails | boolean | no | Fetch each match’s detail page to add address, ZIP, and expiration date. Disable for a faster, roster-only run. (default True) |
query is the field that decides everything, and it has two modes chosen for you by what you type.
A bare integer — 8, 4510 — 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
}
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, around 9,600 licensees, arrives in a
single response — and filters it in memory, so the search itself is instant. 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, and a number typed
with a dash or a prefix stops looking like a number at all and is searched as a name instead. 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:
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"
}
| Field | Example value | Meaning |
|---|---|---|
state | AL | Always ‘AL’ for this actor |
license_number | 8 | LBGC general-contractor license number |
business_name | DUNN CONSTRUCTION CO INC | Licensed business name |
license_type | BC: BUILDING CONSTRUCTION, H/RR: HEAVY AND RAILROAD, HS: HIGHWAYS AND STREETS, MU: MUNICIPAL AND UTILITY | Specialty classification (e.g. BC: BUILDING CONSTRUCTION, HS: HIGHWAYS AND STREETS) |
city | BIRMINGHAM | Business city |
phone | (205) 592-3866 | Business phone (as published on the roster) |
address | P O BOX 11967 | Business street / mailing address (from the detail page) |
zip_code | 35202 | ZIP 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 license
status field and no bond, insurance or issue date on this registry — the board does not put them on
these pages, so the actor returns them as null rather than inventing them. 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.
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. 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 ~9,600-row licensee roster is inlined into a single table (paged client-side); detail pages carry their values in readonly input fields. 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-07-13