California DRE Real Estate License Verification
Purpose
Given an 8-digit California Department of Real Estate (DRE) license ID, return the licensee's name, license type (SALESPERSON | BROKER | CORPORATION), status, expiration date, mailing address, license-issued date, and any disciplinary-action comments. Read-only — never modifies any DRE record.
The skill targets the public license lookup at www2.dre.ca.gov/publicasp/pplinfo.asp — the same surface DRE links to from its "Verify a License" consumer page. No authentication, cookies, or anti-bot stealth is required.
When to Use
- A consumer or compliance flow verifying that a CA real-estate licensee is currently licensed before transacting.
- A brokerage onboarding agents and confirming each rep's license status + responsible-broker affiliation.
- A title/escrow workflow validating the listing agent's license number against name on a purchase contract.
- Background-check / due-diligence pipelines pulling disciplinary-action records.
Workflow
The public lookup serves the full record over plain HTTP GET — the form on pplinfo.asp is cosmetic. A single request retrieves the same HTML the form-POST flow produces.
Recommended: Single HTTP GET deep-link
curl -s "https://www2.dre.ca.gov/publicasp/pplinfo.asp?License_id=01400000"
Or via browse cloud fetch (no --proxies, no --verified — the site has no anti-bot):
browse cloud fetch "https://www2.dre.ca.gov/publicasp/pplinfo.asp?License_id=${ID}"
The response is a ~5–10 KB HTML page (200 OK, Content-Type: text/html, Server: Microsoft-IIS/10.0, X-Powered-By: ASP.NET). The license-information table sits between the License Type: label and the >>>> Public information request complete <<<< sentinel.
-
Build the URL.
https://www2.dre.ca.gov/publicasp/pplinfo.asp?License_id={ID}where{ID}is the license number. Param name is case-insensitive —License_id,license_id,LICENSE_ID, and the URL-encoded formLicense%5Fidall work. Zero-padding to 8 digits is conventional but not required (1400000and01400000both resolve; more than 8 digits returns NOTFOUND). -
Send a plain GET. No headers required. Session cookies (
ASPSESSIONID*) are issued but not validated forLicense_id=queries. -
Detect NOTFOUND. If the response contains the substring
No matching public record was found for License ID:→ emit{ "found": false, "license_id": "..." }. The HTML returns the search form, not a 404 status; success/failure must be detected from the body. -
Extract fields. All fields live in a
<table>of<tr valign="top"><td><strong>Label:</strong></td><td>Value</td></tr>rows. Some labels are wrapped in<A HREF>(License Status,Comment,{Type} License Issued,Former Responsible Broker). Multi-line values use<br/>separators (e.g., address line 1<br/>city/state/zip).Reliable regex per field (case-insensitive, dot-all):
const grab = (label) => { const re = new RegExp( `<strong>(?:<a[^>]*>)?${label}(?:</a>)?:</strong>[^]*?size=2>([^<]+)`, 'i' ); const m = html.match(re); return m ? m[1].trim() : null; }; const licenseType = grab('License Type'); // "SALESPERSON" | "BROKER" | "CORPORATION" const name = grab('Name'); const licenseId = grab('License ID'); const expiration = grab('Expiration Date'); // "MM/DD/YY" const status = grab('License Status'); // see status enum below const issuedDate = grab('(?:Salesperson|Broker|Corporation) License Issued'); const comment = grab('Comment'); // "NO DISCIPLINARY ACTION" or text const mailingAddr = grab('Mailing Address'); // "<line1><br/>CITY, ST ZIP" const formerNames = grab('Former Name\\(s\\)'); -
Type-specific extras. Parse the type-specific blocks:
- SALESPERSON →
Responsible Brokerrow (linked License_id + brokerage name + address) and optionalFormer Responsible Brokerrows. - BROKER (individual) →
Affiliated Licensed Corporation(s)— one or more<A HREF="pplinfo.asp?License_id=NNNNNNNN">NNNNNNNN</A>links followed by the corporation name and expiration. Inactive officer affiliations carry anOFFICER LICENSE EXPIRED AS OF MM/DD/YYsuffix. - CORPORATION →
Licensed Officer(s)row containingDESIGNATED OFFICERand a linked License_id for the broker-officer. - All types →
Main Office,DBA,Branches(corp/broker),NO OTHER PUBLIC COMMENTSsentinel.
- SALESPERSON →
-
Detect disciplinary action. Two independent signals:
Comment:row ≠ literal stringNO DISCIPLINARY ACTION→ disciplined. The cell text is a free-form history (e.g.,06/15/81 - DENIED W/RTR LIC ON T/C PER H-1660 SAC). Multiple comment rows may follow.License Status:value contains*** RESTRICTED ***(e.g.,EXPIRED *** RESTRICTED ***) → restricted/disciplined.- Treat the licensee as having a disciplinary record if either signal fires. Pass the raw comment text(s) through.
-
Stamp the as-of date. The page prints
License information taken from records of the Department of Real Estate on M/D/YYYY H:MM:SS AM/PM— capture this for output provenance. -
Validate with Zod. A reference schema for the canonical output is in Expected Output. Apply at the boundary so downstream consumers get a stable shape regardless of which license-type variant came in.
Browser fallback — name search
When the caller only has a name (no license ID), use a browser session against the form:
SID=$(browse cloud sessions create --keep-alive | node -e "let s='';process.stdin.on('data',c=>s+=c).on('end',()=>process.stdout.write(JSON.parse(s).id))")
export BROWSE_SESSION="$SID"
browse open "https://www2.dre.ca.gov/publicasp/pplinfo.asp" --remote
browse wait load --remote
browse snapshot --remote # textbox refs: LICENSEE_NAME, CITY_STATE, LICENSE_ID
# Use refs from the snapshot — typically [2-25] / [2-26] / [2-27] and Find button [2-341]
browse fill "@<NAME_REF>" "Last, First" --remote
browse click "@<FIND_BUTTON_REF>" --remote
browse wait load --remote
browse get markdown body --remote
browse cloud sessions update "$SID" --status REQUEST_RELEASE
The form is <form method="post" name="entry_form" action="pplinfo.asp?start=1"> with three text inputs (LICENSEE_NAME 40 chars, CITY_STATE 30 chars, LICENSE_ID 8 chars) and a hidden h_nextstep=SEARCH. A name search returns a results list (multiple matches) — click each row to reach the same single-license detail page the GET deep-link returns, then apply the parsing logic from steps 4–8.
Alternative direct-POST (no browser):
curl -s -X POST "https://www2.dre.ca.gov/publicasp/pplinfo.asp?start=1" \
--data-urlencode "h_nextstep=SEARCH" \
--data-urlencode "LICENSEE_NAME=Last, First" \
--data-urlencode "CITY_STATE=" \
--data-urlencode "LICENSE_ID="
(The form-POST returns the same HTML the browser does; the only reason to use a browser is when you need to click into a result row from the list.)
Site-Specific Gotchas
- The form is cosmetic for ID lookups.
pplinfo.asp?License_id={ID}(GET) returns the same detail page that the form's POST flow produces — no session cookie, no CSRF, no Referer required. Don't drive the form unless you need name search. - Plain HTTP, no anti-bot. The site is classic IIS/ASP. Bare
curlworks; do NOT use--proxies --verified— it just adds latency. Verified with directbrowse cloud fetchfrom sandbox IP, 200 OK in ~200ms per request. License_idparam name is case-insensitive.License_id,license_id,LICENSE_ID, andLicense%5Fid(URL-encoded underscore — seen in DRE's own internal links) all resolve. PreferLicense_idfor consistency with DRE's published URLs.- Leading zeros optional, but >8 digits = NOTFOUND.
1400000and01400000both resolve to the same record.000001400000(12 digits) returns NOTFOUND. Normalize input by stripping non-digits then accepting 1–8 digit values. - NOTFOUND is 200 OK, not 404. Detect failure from the body string
No matching public record was found for License ID:, not from HTTP status. The not-found response also re-renders the search form above the message. License Statusenum is rich. Observed values:LICENSED,LICENSED NBA(broker with No Broker Affiliation — i.e., not currently sponsoring salespersons),EXPIRED,EXPIRED *** RESTRICTED ***,WITHHELD DENIED, plus less-common combos (*** RESTRICTED ***may appear with other prefixes). Treat anything containing*** RESTRICTED ***as a disciplinary signal; treatLICENSED(with or withoutNBA) as currently valid.Expiration Dateis not always a date. For records under temporary licenses it can be a sentence likeTemporary License issued through 01/04/96. Schema must accept a free-form string here, not justMM/DD/YY.- Disciplinary signal lives in two places. The
Comment:row carries the disciplinary history text (free-form, multi-line), and theLicense Statusmay also include*** RESTRICTED ***. Either-or — confirm with both. The literal sentinel for "no discipline" is exactly the stringNO DISCIPLINARY ACTION; anything else in the Comment field is real discipline content. - Type-specific row sets differ. SALESPERSON has
Responsible Broker(linked License_id of the brokerage). BROKER hasAffiliated Licensed Corporation(s)(zero or more linked License_ids; expired ones taggedOFFICER LICENSE EXPIRED AS OF MM/DD/YY). CORPORATION hasLicensed Officer(s)with aDESIGNATED OFFICERlinked License_id. Don't assume all rows exist on every record. - Multi-value cells use
<br/>. Mailing-address and former-DBA cells concatenate lines with<br/>and no comma —109 BAYSIDE PL<br/>CORONA DEL MAR, CA 92625. Split on<br/>and re-assemble for normalization. - License Issued row is type-prefixed and "Unofficial" for old records. Field is labelled
Salesperson License Issued,Broker License Issued, orCorporation License Issued. For pre-1980-ish issuances, value carries the suffix(Unofficial -- taken from secondary records). Strip the suffix or surface it as a confidence flag. - Page footer "Public information request complete" sentinel. The literal string
>>>> Public information request complete <<<<always terminates a successful response. Useful as a parsing fence and as a signal that the page rendered fully. - As-of timestamp is in the page body.
License information taken from records of the Department of Real Estate on M/D/YYYY H:MM:SS AM/PM— capture for provenance. DRE notes it does not include pending licensing changes. - There is a separate site at
pplinfo2.dre.ca.gov. The currentpplinfo.aspHTML has a commented-out<meta http-equiv="refresh">pointing athttps://pplinfo2.dre.ca.gov/. It's not active today (2026-05-26) — the comment confirms it was disabled. If DRE re-enables the redirect, the new site's URL pattern may differ; check for a redirect before assuming this skill still works. - No formal rate limit observed, but keep requests ≤ 2 req/s — this is a state government IIS box. The ASP session cookie issued on each request is not validated for
License_id=lookups but holding requests to a polite rate is courteous. - Disciplinary-action documents are not on this page. The DRE main site links to PDFs under
/Licensees/DisciplinaryActions.html, but those are not cross-referenced from the per-license detail page. TheComment:field summarizes discipline; full case documents must be fetched separately if needed. - Address-lookup is a different surface. The form links to
https://secure.dre.ca.gov/addresslookupfor searching by office address — distinct skill, not covered here.
Expected Output
Zod-validated shape (TypeScript reference):
import { z } from 'zod';
const AffiliatedEntity = z.object({
license_id: z.string(),
name: z.string(),
expiration_date: z.string().nullable(),
active: z.boolean(), // false if marked "OFFICER LICENSE EXPIRED AS OF ..."
expired_as_of: z.string().nullable(), // present when active === false
});
const DRELicenseRecord = z.discriminatedUnion('found', [
z.object({
found: z.literal(false),
license_id: z.string(), // echoed back from the input
as_of: z.string(), // server-side "License information taken from records ... on M/D/YYYY ..." capture
}),
z.object({
found: z.literal(true),
license_id: z.string(), // canonical 8-digit form
license_type: z.enum(['SALESPERSON', 'BROKER', 'CORPORATION']),
name: z.string(),
mailing_address: z.string(), // raw multi-line concatenated
expiration_date: z.string(), // "MM/DD/YY" or free-form sentence for temporary licenses
license_status: z.string(), // e.g. "LICENSED", "LICENSED NBA", "EXPIRED", "EXPIRED *** RESTRICTED ***", "WITHHELD DENIED"
issued_date: z.string().nullable(), // "MM/DD/YY"; may carry "(Unofficial -- taken from secondary records)" suffix
issued_date_unofficial: z.boolean(),
former_names: z.array(z.string()), // [] if "NO FORMER NAMES"
main_office: z.string().nullable(), // present for BROKER/CORPORATION
dbas: z.array(z.string()), // [] if "NO CURRENT DBAS"
branches: z.array(z.string()), // [] if "NO CURRENT BRANCHES"
// Type-specific blocks:
responsible_broker: AffiliatedEntity.nullable(), // SALESPERSON only
former_responsible_brokers: z.array(AffiliatedEntity), // SALESPERSON only
affiliated_corporations: z.array(AffiliatedEntity), // BROKER only
licensed_officers: z.array(AffiliatedEntity), // CORPORATION only
// Discipline:
has_disciplinary_action: z.boolean(), // status contains "*** RESTRICTED ***" OR comment != "NO DISCIPLINARY ACTION"
is_restricted: z.boolean(), // status contains "*** RESTRICTED ***"
disciplinary_comments: z.array(z.string()), // [] if "NO DISCIPLINARY ACTION"
other_public_comments: z.array(z.string()), // entries between "Comment:" block and the sentinel
as_of: z.string(), // server-side timestamp from page body
}),
]);
Example: active SALESPERSON (no discipline)
{
"found": true,
"license_id": "01400000",
"license_type": "SALESPERSON",
"name": "Cohen, Matthew Jacob",
"mailing_address": "3232 QUANDT ROAD, LAFAYETTE, CA 94549",
"expiration_date": "08/02/28",
"license_status": "LICENSED",
"issued_date": "11/08/03",
"issued_date_unofficial": false,
"former_names": ["Cohen, Matthew Jacob"],
"main_office": null,
"dbas": [],
"branches": [],
"responsible_broker": {
"license_id": "01999173",
"name": "Synesis Advisors, Inc.",
"expiration_date": null,
"active": true,
"expired_as_of": null
},
"former_responsible_brokers": [
{
"license_id": "01999173",
"name": "Synesis Advisors, Inc. (08/03/2020–02/05/2024)",
"expiration_date": null,
"active": false,
"expired_as_of": null
}
],
"affiliated_corporations": [],
"licensed_officers": [],
"has_disciplinary_action": false,
"is_restricted": false,
"disciplinary_comments": [],
"other_public_comments": [],
"as_of": "5/26/2026 9:49:04 AM"
}
Example: active BROKER with multiple affiliated corporations
{
"found": true,
"license_id": "00372156",
"license_type": "BROKER",
"name": "Moss, Jay Lawrence",
"mailing_address": "109 BAYSIDE PL, CORONA DEL MAR, CA 92625",
"expiration_date": "02/15/28",
"license_status": "LICENSED",
"issued_date": "08/31/73",
"issued_date_unofficial": true,
"former_names": [],
"main_office": "109 BAYSIDE PL, CORONA DEL MAR, CA 92625",
"dbas": ["Development Dimensions (ACTIVE FROM 11/03/1975 TO 04/30/1993)"],
"branches": [],
"responsible_broker": null,
"former_responsible_brokers": [],
"affiliated_corporations": [
{ "license_id": "01527203", "name": "Real Estate Dimensions, Inc.", "expiration_date": "12/01/27", "active": true, "expired_as_of": null },
{ "license_id": "00242327", "name": "KB HOME Sales-Southern California Inc", "expiration_date": "02/02/91", "active": false, "expired_as_of": "02/03/91" },
{ "license_id": "00746787", "name": "Housing Dimensions Inc", "expiration_date": "08/14/87", "active": false, "expired_as_of": "08/15/87" }
],
"licensed_officers": [],
"has_disciplinary_action": false,
"is_restricted": false,
"disciplinary_comments": [],
"other_public_comments": [],
"as_of": "5/26/2026 9:46:13 AM"
}
Example: CORPORATION with designated officer
{
"found": true,
"license_id": "01527203",
"license_type": "CORPORATION",
"name": "Real Estate Dimensions, Inc.",
"mailing_address": "109 BAYSIDE PL, CORONA DEL MAR, CA 92625",
"expiration_date": "12/01/27",
"license_status": "LICENSED",
"issued_date": "12/02/15",
"issued_date_unofficial": false,
"former_names": [],
"main_office": "109 BAYSIDE PL, CORONA DEL MAR, CA 92625",
"dbas": [],
"branches": [],
"responsible_broker": null,
"former_responsible_brokers": [],
"affiliated_corporations": [],
"licensed_officers": [
{
"license_id": "00372156",
"name": "Moss, Jay Lawrence (DESIGNATED OFFICER)",
"expiration_date": "12/01/27",
"active": true,
"expired_as_of": null
}
],
"has_disciplinary_action": false,
"is_restricted": false,
"disciplinary_comments": [],
"other_public_comments": [],
"as_of": "5/26/2026 9:45:55 AM"
}
Example: restricted / disciplined SALESPERSON
{
"found": true,
"license_id": "00800000",
"license_type": "SALESPERSON",
"name": "Hasek, Donna Marie",
"mailing_address": "ONE MANDARIN, IRVINE, CA 92714",
"expiration_date": "05/28/96",
"license_status": "EXPIRED *** RESTRICTED ***",
"issued_date": "06/15/81",
"issued_date_unofficial": true,
"former_names": [],
"main_office": null,
"dbas": [],
"branches": [],
"responsible_broker": null,
"former_responsible_brokers": [],
"affiliated_corporations": [],
"licensed_officers": [],
"has_disciplinary_action": true,
"is_restricted": true,
"disciplinary_comments": ["06/15/81 - DENIED W/RTR LIC ON T/C PER H-1660 SAC"],
"other_public_comments": [],
"as_of": "5/26/2026 9:48:17 AM"
}
Example: not found
{
"found": false,
"license_id": "02000000",
"as_of": "5/26/2026 9:48:00 AM"
}