www2.dre.ca.gov

ca-real-estate-license-verify

Installation

Adds this website's skill for your agents

 

Summary

Verify a California real estate license by ID against the DRE Public License Lookup. Returns licensee name, license type (broker/salesperson/corporation), status, expiration, issue date, mailing/office addresses, branches, affiliated corporations, and the full disciplinary-action timeline with PDF links. Zod-validated.

FIG. 01
FIG. 02
FIG. 03
FIG. 04
FIG. 05
SKILL.md
258 lines

CA DRE Real Estate License Verification

Purpose

Verify a California real estate license by its 8-digit license ID against the California Department of Real Estate (DRE) Public License Lookup at https://www2.dre.ca.gov/publicasp/pplinfo.asp. Returns the licensee's name, license type (BROKER, SALESPERSON, or CORPORATION), license status (LICENSED, EXPIRED, SURRENDERED, REVOKED, CANCELED, etc.), expiration date, original issue date, mailing address, main office, DBAs, branches, affiliated corporations, and the full disciplinary-action timeline including links to any formal-action PDF documents. Read-only.

When to Use

  • Verify a real estate agent / broker before signing a representation agreement.
  • Compliance pipelines that need to confirm an agent's license is currently active before referrals, MLS access, or commission payouts.
  • Surfacing disciplinary history (revocations, restricted licenses, petitions for reinstatement) for due-diligence on a counterparty.
  • Resolving the affiliated brokerage / corporation graph (each affiliated corp link is a clickable license ID — chain-followable for org-tree mapping).

Workflow

The DRE public lookup is a 2012-era ASP application served by Microsoft-IIS/10.0. It is not JavaScript-rendered, has no anti-bot, no CAPTCHA, no auth, and no rate-limiting headers, and the form's <form method="post" action="pplinfo.asp?start=1"> server-side handler accepts the same parameters via GET when the input is a LICENSE_ID. A single HTTPS GET returns the complete result page — no session, no proxy, no browser required. Lead with fetch; the browser fallback at the end is ~30× more expensive and exists only for the name-search variant (see Site-Specific Gotchas — LICENSEE_NAME requires POST).

Recommended path — direct GET fetch

  1. Construct the URL (license number zero-padded to 8 digits):

    GET https://www2.dre.ca.gov/publicasp/pplinfo.asp?start=1&h_nextstep=SEARCH&LICENSE_ID={8-digit-id}
    

    Both start=1 (action route discriminator) and h_nextstep=SEARCH (hidden form field that triggers the search branch in the ASP code) are required. Omitting h_nextstep=SEARCH re-renders the empty form. The endpoint returns 200 OK text/html either way; success vs. not-found is encoded in the response body, never via status code.

  2. Branch on the response body:

    • Contains >>>>> Public information request complete <<<<found — parse the field table.
    • Contains No matching public record was found for License ID:not found — emit {found: false, license_id, reason: "not_found"}.
    • Contains Please enter either a licensee name or a license idinput rejectedLICENSE_ID was empty or malformed; check that you passed exactly 8 digits.
  3. Parse the field table. On found responses the page body is a flat <table> with one row per field. Each row's left <td> carries <strong>{Label}:</strong> (sometimes wrapped in <A HREF="..."> for the help-link fields License Status, Comment, and the issuance label) and the right <td> carries the value inside <FONT FACE="Arial,Helvetica" size=2>...</FONT>. Robust extraction regex for any label L:

    <strong>(?:<A HREF[^>]*>)?L(?:</A>)?:</strong><font><br/></td><td><FONT[^>]*>([\s\S]*?)</font></td>
    

    Then strip <br/> (replace with \n or |), strip remaining tags, and collapse whitespace.

    Labels observed across all license types:

    • License TypeBROKER, SALESPERSON, or CORPORATION. (For a corporation the issuance label becomes Corporation License Issued; for a broker it's Broker License Issued; for a salesperson it's Salesperson License Issued.)
    • Name"Last, First Middle" for individuals, full company name for corporations.
    • Mailing Address — multi-line, <br/>-separated. Sometimes carries the trailing line (Above address is marked unreliable in DRE database) — preserve it; it's a signal to downstream callers.
    • License ID — the canonical zero-padded 8-digit ID.
    • Expiration DateMM/DD/YY (two-digit year — see gotchas).
    • License StatusLICENSED (active and current), EXPIRED, SURRENDERED, REVOKED, CANCELED, SUSPENDED (and a few rarer states — surface the raw string).
    • Broker License Issued / Salesperson License Issued / Corporation License Issued — original issue date, often suffixed (Unofficial -- taken from secondary records) for older licenses.
    • Former Name(s) — usually NO FORMER NAMES.
    • Main Office — may be NO CURRENT MAIN OFFICE ADDRESS ON FILE, or a multi-line address. For corporations, also contains chained anchor links to division/branch managers — each <A HREF = "pplinfo.asp?License_id=XXXXXXXX">XXXXXXXX</A> is an affiliated license ID followed by name - role.
    • DBANO CURRENT DBAS or a list.
    • BranchesNO CURRENT BRANCHES or a multi-line list, again with chained anchor links to the branch manager's license ID.
    • Affiliated Licensed Corporation(s) — similar chained-anchor list, each entry is LICENSE_ID - Officer Expiration Date: MM/DD/YY | Corporation Name.
    • Commentthis is the disciplinary timeline. Either the single string NO DISCIPLINARY ACTION, or one or more dated lines like MM/DD/YY - H-NNNNN SF (case hearing number), MM/DD/YY - BROKER LICENSE REVOKED-RIGHT TO RESTRICTED SALESPERSON LICENSE, MM/DD/YY - PETITION FOR REINSTATEMENT OF BROKER LICENSE GRANTED, MM/DD/YY - H-NNNNN SF RELEASED, etc. Each line is in a separate <tr> row — the first row carries the Comment: strong label, subsequent rows have empty left <td></td> and the next comment line in the right <td>. Terminate the collection at the row containing NO OTHER PUBLIC COMMENTS.
    • Disciplinary or Formal Action Documents — appears only when disciplinary action exists. One or more <A HREF="/hearingfiles/HNNNNN_*.pdf" target="_blank">filename.pdf</A> rows. Resolve to absolute URLs by prefixing with https://www2.dre.ca.gov.
  4. Terminate at the sentinel. The body always ends with the row >>>>> Public information request complete <<<< — anything before it is the licensee record, anything after is </table></body></html>. Use the sentinel to validate that the response was not truncated mid-document.

  5. Validate against a Zod schema (illustrative; field names map 1:1 to the labels above):

    import { z } from "zod";
    const LicenseStatus = z.enum([
      "LICENSED", "EXPIRED", "SURRENDERED", "REVOKED", "CANCELED", "SUSPENDED"
    ]).or(z.string());                          // fall back to raw string for unknown states
    const LicenseRecord = z.object({
      license_id: z.string().regex(/^\d{8}$/),
      license_type: z.enum(["BROKER", "SALESPERSON", "CORPORATION"]),
      name: z.string(),
      mailing_address: z.string(),
      mailing_address_unreliable: z.boolean(),  // true if "(Above address is marked unreliable...)" is present
      expiration_date: z.string(),              // MM/DD/YY as given; do NOT auto-parse to ISO without 4-digit-year heuristic
      status: LicenseStatus,
      license_issued: z.string(),               // MM/DD/YY, may have "(Unofficial ...)" suffix
      license_issued_unofficial: z.boolean(),
      former_names: z.array(z.string()),        // [] when "NO FORMER NAMES"
      main_office: z.string().nullable(),       // null when "NO CURRENT MAIN OFFICE ADDRESS ON FILE"
      dba: z.array(z.string()),
      branches: z.array(z.object({              // anchor-link entries; empty array when "NO CURRENT BRANCHES"
        license_id: z.string(),
        text: z.string(),
      })),
      affiliated_corporations: z.array(z.object({
        license_id: z.string(),
        officer_expiration_date: z.string().nullable(),
        name: z.string(),
      })),
      disciplinary_actions: z.array(z.string()),  // [] when "NO DISCIPLINARY ACTION"
      disciplinary_documents: z.array(z.object({
        filename: z.string(),
        url: z.string().url(),                    // absolute https://www2.dre.ca.gov/hearingfiles/...
      })),
      retrieved_at: z.string(),                   // "License information taken from records of the Department of Real Estate on M/D/YYYY H:MM:SS AM/PM" — parsed off the page header
    });
    const Result = z.discriminatedUnion("found", [
      z.object({ found: z.literal(true),  license: LicenseRecord }),
      z.object({ found: z.literal(false), license_id: z.string(), reason: z.literal("not_found") }),
      z.object({ found: z.literal(false), reason: z.literal("invalid_input"), message: z.string() }),
    ]);
    

Browser fallback

Required only when the lookup is by licensee name rather than license ID (the ASP handler reads LICENSEE_NAME from Request.Form, not Request.QueryString, so a name-search GET silently returns "Please enter either a licensee name or a license id"). For license-ID lookups the GET path above is strictly superior — use it.

  1. sid=$(browse cloud sessions create --keep-alive | node -e "let s='';process.stdin.on('data',c=>s+=c).on('end',()=>{const j=JSON.parse(s.replace(/.*?\n\{/,'{'));process.stdout.write(j.id)})")
  2. browse open "https://www2.dre.ca.gov/publicasp/pplinfo.asp" --remote --session "$sid"
  3. browse fill 'input[name=LICENSEE_NAME]' "Last, First" --remote --session "$sid"
  4. browse fill 'input[name=CITY_STATE]' "{optional city}" --remote --session "$sid"
  5. browse click 'input[type=submit][value=Find]' --remote --session "$sid"
  6. browse wait load --remote --session "$sid"
  7. If multiple matches → site renders an interstitial result list (each row links to pplinfo.asp?License_id=NNNNNNNN); harvest the anchors with browse get html body, present to the caller for disambiguation, then GET the chosen ID via the recommended path.
  8. If single match → site renders the same field table as the GET path; parse identically.
  9. browse cloud sessions update "$sid" --status REQUEST_RELEASE

Bare session (no --verified, no --proxies) is sufficient — this is a 2012-era CA.gov ASP site with no anti-bot.

Site-Specific Gotchas

  • LICENSE_ID accepts GET; LICENSEE_NAME does NOT. The ASP handler reads LICENSE_ID from Request.QueryString but LICENSEE_NAME only from Request.Form. A GET with LICENSEE_NAME=... silently behaves as if no name was submitted and prints "Please enter either a licensee name or a license id." Confirmed by direct fetch comparison. License-ID is the canonical fast path; for name searches use the browser POST (form submit) fallback.
  • Both start=1 and h_nextstep=SEARCH query params are required. Omitting either re-renders the empty form. Other hidden routes (start=2, alternate h_nextstep values) are unused server-side.
  • The response is always 200 OK — including not-found, including malformed input. Branch on the body text (>>>>> Public information request complete <<<< for found, No matching public record was found for not-found, Please enter either a licensee name or a license id for input rejection), never on HTTP status.
  • LICENSE_ID must be exactly 8 digits, zero-padded. Older licenses are 00XXXXXX. The input has maxlength="8". A 7-digit or unpadded ID is silently treated as malformed.
  • Expiration / issuance dates are MM/DD/YY with a two-digit year. Do not naïvely prepend 2000696976's issue date 02/07/79 is 1979, not 2079. Use a sliding-window heuristic (e.g., if YY >= current_year + 5 then 19YY, else 20YY) and surface the raw two-digit string alongside any parsed ISO date.
  • (Unofficial -- taken from secondary records) suffix on issuance dates is present for licenses originally issued before DRE's modern recordkeeping (~mid-2000s cutoff). Preserve it as a boolean flag — it's a confidence signal callers care about.
  • (Above address is marked unreliable in DRE database) suffix on mailing addresses is a DRE-flagged signal that the address is stale or undeliverable. Preserve as a boolean flag.
  • The Comment section is multi-row, not multi-line. When a license has disciplinary history, every dated event is in its own <tr> with an empty left <td></td> and the event text in the right <td><FONT...>...</FONT></td>. The first row of the section carries the Comment: label; collection terminates at the row containing NO OTHER PUBLIC COMMENTS. A naïve "match the value after Comment:" parser captures only the first event line and misses the full timeline.
  • The Disciplinary or Formal Action Documents section only appears when there is disciplinary history. Same multi-row pattern as Comment. Hrefs are root-relative (/hearingfiles/HNNNNN_P.pdf); prefix with https://www2.dre.ca.gov to absolutize.
  • For CORPORATION records, Main Office carries an embedded chain of officer license links (e.g., LICENSE_ID - Name - Division Manager, repeated). Each anchor's text is the 8-digit license ID — chain-followable to map the org tree. Same for Branches.
  • License status enum is open-ended. Confirmed values from probes: LICENSED, EXPIRED. Documented elsewhere on DRE static pages: SURRENDERED, REVOKED, CANCELED, SUSPENDED, restricted variants. The Zod schema should accept the known enum or fall back to the raw string so an unknown status doesn't break validation.
  • There is no JSON / GraphQL / structured API endpoint. secure.dre.ca.gov/addresslookup is a sibling address-based search (also browser-only); pplinfo2.dre.ca.gov is referenced in a commented-out <meta refresh> tag in the source but currently inactive (don't rely on it). The HTML scrape is the public contract.
  • No rate limiting observed. Microsoft-IIS/10.0, no Retry-After, no X-RateLimit-* headers. Be courteous (≤ 2 req/s sustained) and the site is happy.
  • A residential proxy and stealth mode are NOT required. Bare browse cloud fetch (or even a plain Node fetch) works end-to-end. Don't waste a --proxies allocation on this site.
  • Co-browsing script (Median) and Google Translate widget load on every response — they're cosmetic and don't affect the field table. Ignore.

Expected Output

Three distinct outcome shapes:

// 1. Found — individual licensee (BROKER, with no disciplinary history)
{
  "found": true,
  "license": {
    "license_id": "01365477",
    "license_type": "BROKER",
    "name": "Riley, Joshua Luke",
    "mailing_address": "20406 BRIAN WAY\nSUITE 3A\nTEHACHAPI, CA 93561",
    "mailing_address_unreliable": false,
    "expiration_date": "09/26/29",
    "status": "LICENSED",
    "license_issued": "01/31/03",
    "license_issued_unofficial": false,
    "former_names": [],
    "main_office": "20406 BRIAN WAY\nSUITE 3A\nTEHACHAPI, CA 93561",
    "dba": [],
    "branches": [],
    "affiliated_corporations": [
      {
        "license_id": "01910265",
        "officer_expiration_date": "02/08/28",
        "name": "Associated Real Estate"
      }
    ],
    "disciplinary_actions": [],
    "disciplinary_documents": [],
    "retrieved_at": "5/21/2026 11:46:44 AM"
  }
}
// 2. Found — corporation with chained officers / branches
{
  "found": true,
  "license": {
    "license_id": "01527235",
    "license_type": "CORPORATION",
    "name": "Compass California II, Inc.",
    "mailing_address": "9454 WILSHIRE BLVD STE 100\nBEVERLY HILLS, CA 90212",
    "expiration_date": "08/15/28",
    "status": "LICENSED",
    "license_issued": "08/16/16",
    "main_office_officers": [
      { "license_id": "02106770", "name": "Finlay, Emma Kathleen", "role": "Division Manager" },
      { "license_id": "01392561", "name": "Patsel, Kevin Blaine",   "role": "Division Manager" },
      { "license_id": "01868574", "name": "Brand, Kristian Frederik", "role": "Division Manager" }
    ],
    "branches": [
      { "license_id": "01421420", "name": "Supica, Stacey", "role": "Branch/Division Manager",
        "address": "0 JUNIPERO BETWEEN 5TH & 6TH\nCARMEL BY THE SEA, CA 93921" }
    ],
    "disciplinary_actions": [],
    "disciplinary_documents": []
  }
}
// 3. Found — with disciplinary action timeline + PDF documents
{
  "found": true,
  "license": {
    "license_id": "01000200",
    "license_type": "BROKER",
    "name": "...",
    "status": "LICENSED",
    "disciplinary_actions": [
      "10/24/09 - H-10787 SF",
      "09/14/10 - BROKER LICENSE REVOKED-RIGHT TO RESTRICTED SALESPERSON LICENSE",
      "12/22/14 - (H-10787 SF) PETITION FOR REINSTATEMENT OF BROKER LICENSE GRANTED",
      "06/09/16 - H-10787 SF  RELEASED"
    ],
    "disciplinary_documents": [
      { "filename": "H10787SF_141222_P.pdf", "url": "https://www2.dre.ca.gov/hearingfiles/H10787SF_141222_P.pdf" },
      { "filename": "H10787SF_P.pdf",        "url": "https://www2.dre.ca.gov/hearingfiles/H10787SF_P.pdf" }
    ]
  }
}
// 4. Not found — license ID does not match any DRE record
{ "found": false, "license_id": "00000000", "reason": "not_found" }
// 5. Invalid input — empty or malformed license ID
{ "found": false, "reason": "invalid_input", "message": "Please enter either a licensee name or a license id." }