ScreenshotNeo

BlogUse cases

Real Estate License Verification Automation

Build an auditable real-estate license verification workflow with state APIs, normalized data, batch checks, exception review, and evidence capture.

By the ScreenshotNeo team1 October 202611 min read

Real-estate license verification automation combines a regulator lookup, a normalized license database, or a commercial verification API with matching rules, audit storage, exception review, and scheduled rechecks.

The reliable pattern is:

  1. Collect the jurisdiction, license number when available, and normalized name or business fields.
  2. Query an authoritative state source or a provider whose coverage and refresh schedule you have checked.
  3. Store the returned status, expiration date, license type, brokerage relationship, source, request inputs, and verification timestamp.
  4. Route ambiguous names, missing records, conflicts, and stale data to manual review.
  5. Keep operational verification separate from regulator-issued certified proof or disciplinary history documents.

A license-number match is stronger than an unresolved name match. An API response can support an operational decision, but it may not qualify as certified proof when another regulator requires a certified record.

What the automation should return

Design a stable internal result even when each state or vendor uses different field names and status values.

Field Purpose
jurisdiction State or other licensing authority queried.
license_number Preferred exact identifier.
subject_name Normalized first, middle, last, or business name.
status Normalized value such as active, inactive, expired, suspended, revoked, or unknown.
expiration_date Date used for renewal and recheck decisions.
license_type Salesperson, broker, company, or another jurisdiction-specific type.
brokerage Current or returned sponsoring/employing brokerage.
disciplinary_records Keep the provider’s value and source details; do not infer a clean history from a missing field.
match_confidence Exact identifier, strong identity match, ambiguous, or no match.
source Regulator, normalized provider, or commercial feed.
source_url URL or evidence pointer returned by the source.
requested_at When the lookup was made.
verified_at When your system accepted the result as verified.
raw_response Original response or immutable evidence reference for later review.

Choose an integration architecture

Direct state integrations

A direct integration queries the licensing authority for each jurisdiction. Indiana’s Professional Licensing Agency documents a REST API for organizations confirming employee, contractor, or facility licenses. This approach gives you a direct relationship with the regulator, but every state can differ in credentials, formats, status vocabulary, rate limits, and certification rules.

Use direct integrations when a small number of jurisdictions matter, the regulator provides a supported API, and you need the authority’s exact response. Build an adapter per state instead of exposing state-specific fields to the rest of your application.

Normalized multi-state API

A normalized provider reduces the number of state-specific integrations. RELD documents single verification, name and brokerage search, batch verification, affiliation endpoints, normalized fields, and source-aligned refresh schedules. Coverage, freshness, source traceability, and continuity still need to be evaluated for every jurisdiction you use.

ARELLO or commercial data feed

SourceRE documents an ARELLO API that accepts jurisdiction, license number, first name, and last name. This can fit an enterprise workflow that wants one request shape, but verify licensing rights, latency, jurisdiction coverage, and current service terms before committing.

Use a source hierarchy

Define which source wins when two systems disagree. A common policy is an official state response first, a normalized provider second, and a commercial enrichment feed third. Preserve every response rather than overwriting history, then record why the winning source was selected.

Data model and audit trail

Store an append-only verification event and a current projection. The event makes an onboarding or roster decision explainable months later; the projection makes ordinary reads fast.

CREATE TABLE license_verification_events (
  id UUID PRIMARY KEY,
  subject_id TEXT NOT NULL,
  jurisdiction TEXT NOT NULL,
  license_number TEXT,
  input_first_name TEXT,
  input_last_name TEXT,
  input_brokerage TEXT,
  source_type TEXT NOT NULL,
  source_name TEXT NOT NULL,
  source_url TEXT,
  request_payload JSONB NOT NULL,
  response_payload JSONB,
  normalized_status TEXT NOT NULL,
  expiration_date DATE,
  license_type TEXT,
  brokerage TEXT,
  match_confidence TEXT NOT NULL,
  decision TEXT NOT NULL,
  requested_at TIMESTAMPTZ NOT NULL,
  verified_at TIMESTAMPTZ,
  evidence_uri TEXT,
  error_code TEXT,
  error_message TEXT
);

Do not store only a boolean such as is_valid. Keep the original inputs, the returned status, the source, and both timestamps. Redact secrets, API credentials, and unnecessary personal data from logs.

Implement the request pipeline

  1. Normalize input. Trim whitespace, preserve the original spelling for audit, normalize case for matching, and canonicalize state names and abbreviations.
  2. Prefer an exact identifier. If a license number is present, query it with the jurisdiction. Treat an identity-only result as provisional until the match is unambiguous.
  3. Map statuses explicitly. Maintain a per-provider mapping table. Never assume that “current,” “clear,” or an empty disciplinary field means active.
  4. Validate dates. Parse the provider’s date and record whether it is already expired in your system’s timezone.
  5. Check affiliation separately. A valid license does not automatically prove that the person is currently affiliated with the submitted brokerage.
  6. Persist evidence. Save the raw response or a durable evidence pointer before returning a decision.
  7. Apply a decision policy. For example: active exact match may pass; active ambiguous match requires review; missing, expired, suspended, or conflicting records fail or require review according to your business rules.

Provider-neutral Python adapter

The following client is runnable once you set the provider URL and authentication method documented by your selected source. It deliberately does not invent a vendor endpoint.

import json
import os
from datetime import datetime, timezone
import requests

VERIFY_URL = os.environ["LICENSE_VERIFY_URL"]
API_KEY = os.environ.get("LICENSE_API_KEY")


def verify_license(jurisdiction, license_number=None, first_name=None,
                   last_name=None, brokerage=None):
    params = {
        "jurisdiction": jurisdiction,
        "license_number": license_number,
        "first_name": first_name,
        "last_name": last_name,
        "brokerage": brokerage,
    }
    params = {k: v for k, v in params.items() if v not in (None, "")}
    headers = {"Accept": "application/json"}
    if API_KEY:
        headers["Authorization"] = f"Bearer {API_KEY}"

    response = requests.get(VERIFY_URL, params=params, headers=headers, timeout=30)
    response.raise_for_status()
    payload = response.json()

    event = {
        "request": params,
        "response": payload,
        "source": VERIFY_URL,
        "requested_at": datetime.now(timezone.utc).isoformat(),
    }
    print(json.dumps(event, indent=2))
    return payload


if __name__ == "__main__":
    verify_license(
        jurisdiction=os.environ["LICENSE_JURISDICTION"],
        license_number=os.environ.get("LICENSE_NUMBER"),
        first_name=os.environ.get("LICENSE_FIRST_NAME"),
        last_name=os.environ.get("LICENSE_LAST_NAME"),
        brokerage=os.environ.get("LICENSE_BROKERAGE"),
    )

cURL template

curl --fail-with-body --get "$LICENSE_VERIFY_URL" \
  --header "Accept: application/json" \
  --header "Authorization: Bearer $LICENSE_API_KEY" \
  --data-urlencode "jurisdiction=$LICENSE_JURISDICTION" \
  --data-urlencode "license_number=$LICENSE_NUMBER" \
  --data-urlencode "first_name=$LICENSE_FIRST_NAME" \
  --data-urlencode "last_name=$LICENSE_LAST_NAME"

Node.js template

const params = new URLSearchParams({
  jurisdiction: process.env.LICENSE_JURISDICTION,
  license_number: process.env.LICENSE_NUMBER || '',
  first_name: process.env.LICENSE_FIRST_NAME || '',
  last_name: process.env.LICENSE_LAST_NAME || ''
});

const res = await fetch(`${process.env.LICENSE_VERIFY_URL}?${params}`, {
  headers: {
    accept: 'application/json',
    authorization: `Bearer ${process.env.LICENSE_API_KEY}`
  }
});
if (!res.ok) throw new Error(`Verification failed: ${res.status}`);
const result = await res.json();
console.log(JSON.stringify(result, null, 2));

Batch verification for broker rosters

Batch checks are useful during onboarding and recurring roster reviews. RELD documents batches of up to 100 license pairs in one request. Keep each submitted row tied to an internal subject ID so a partial response cannot be attached to the wrong person.

  1. Validate every row before sending: jurisdiction, license number, and required identity fields.
  2. Split larger rosters into provider-supported batches.
  3. Use an idempotency key based on roster version and batch number when the provider supports it.
  4. Persist each row’s outcome independently. One malformed license should not discard the other 99.
  5. Retry transient transport errors with exponential backoff; do not retry a deterministic validation error.
  6. Send ambiguous and conflicting rows to a review queue.
from concurrent.futures import ThreadPoolExecutor


def verify_row(row):
    try:
        result = verify_license(**row)
        return {"subject_id": row["subject_id"], "ok": True, "result": result}
    except requests.HTTPError as exc:
        return {"subject_id": row["subject_id"], "ok": False, "error": str(exc)}


def verify_roster(rows, workers=5):
    with ThreadPoolExecutor(max_workers=workers) as pool:
        return list(pool.map(verify_row, rows))

Matching and exception rules

Situation Recommended treatment
Exact license number and jurisdiction match Accept the record, then check status and expiration.
One unambiguous name result Accept only if your policy allows identity-only matching; record the weaker evidence.
Several name results Do not choose automatically. Request a license number or manual review.
No result Check spelling, alternate names, jurisdiction, and source coverage before marking unlicensed.
Conflicting brokerage affiliations Hold the decision and compare timestamps and source authority.
Expired or inactive status Fail or review according to the business rule; retain the exact source status.
Disciplinary information present Store the record and route it according to your compliance process. Do not summarize it as “clear” or “not clear” without a policy.

Freshness, scheduling, and reliability

Set recheck intervals by risk and by the source’s stated refresh cadence. A high-risk onboarding decision may require a fresh request, while a lower-risk roster display can use a cached result with a visible verified_at timestamp. Never present an old cached response as current.

  • Use timeouts and bounded retries.
  • Record provider request IDs when available.
  • Queue work so a provider outage does not block onboarding indefinitely.
  • Monitor error rates, latency, stale records, unmatched names, and manual-review volume.
  • Keep a dead-letter queue for responses that cannot be normalized.
  • Version your status mapping when a provider changes vocabulary.

Certification and evidence boundaries

Indiana’s Professional Licensing Agency states that it maintains a REST API for sharing licensure data, and separately warns that its License Data REST API does not provide proof of licensure when applying to another state. Treat that distinction as a system requirement: an operational API result and a regulator-issued certified history are different artifacts.

When a reviewer needs visual evidence of a public regulator page, you can capture the page after the API decision. ScreenshotNeo is a website screenshot API and MCP server; it can capture a regulator page as PNG, JPEG, WebP, or PDF. Use it as evidence capture, not as the licensing authority.

Or skip the browser setup

If you need a clean image or PDF of a regulator result for an audit record, call ScreenshotNeo directly. The API accepts one GET request and supports full-page capture, PDF output, custom headers and cookies, waiting for a selector or network idle, and signed links. See the ScreenshotNeo API documentation for the request options.

cURL

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://www.in.gov/pla/ -o shot.webp

Python

import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://www.in.gov/pla/"}, timeout=90)
open("shot.webp", "wb").write(r.content)

Node.js

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://www.in.gov/pla/' }); const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);

Cookie banners, newsletter popups, and chat widgets are removed before the shot. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and the response identifies the page verdict and billing result in X-Page-Verdict and X-Billed headers. An MCP server lets Claude, Cursor, and other MCP clients call take_screenshot, get_page_info, and capture_pdf. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. Create a free ScreenshotNeo account.

Performance, reliability, and cost

Performance

  • Prefer license-number queries to broad name searches.
  • Batch within the provider’s documented limit; RELD documents up to 100 license pairs per request.
  • Run independent jurisdictions concurrently while respecting rate limits.
  • Cache only for the period your policy permits and show the verification timestamp.
  • Capture evidence asynchronously when it is not needed for the immediate decision.

Reliability

  • Classify failures as invalid input, authentication, rate limit, provider outage, timeout, no match, or ambiguous match.
  • Retry only transient failures.
  • Make writes idempotent so a retry cannot create an apparently different verification event.
  • Keep raw responses or durable evidence pointers for audits.

Cost

Compare providers on jurisdiction coverage, refresh cadence, identity matching, status vocabulary, affiliation data, disciplinary fields, rate limits, evidence retention, and certification requirements. The cheapest request is not useful if it lacks the jurisdiction or freshness your decision requires. For screenshot evidence, ScreenshotNeo bills only clean shots; failed loads, bot checks, blank pages, timeouts, and cache hits cost nothing.

Troubleshooting

Error or symptom Likely cause Fix
Authentication failure Missing, expired, or incorrectly scoped credential. Check the provider’s credential format and rotate the secret without logging it.
Rate-limit response Too many concurrent or repeated requests. Honor retry headers, reduce concurrency, batch where supported, and add backoff.
No match Wrong jurisdiction, spelling variation, stale source, or missing license number. Validate inputs, try permitted identity fields, inspect coverage, then route to review.
Several matches Name is not unique. Require the license number or manual identity confirmation.
Status cannot be mapped Provider introduced a new value. Quarantine the result, preserve the raw value, and update the mapping table.
Expiration appears wrong Timezone or date-format conversion error. Parse the documented format and compare dates in one explicit timezone.
Affiliation conflict Sources have different refresh times or report different relationship types. Compare timestamps, prefer the authoritative source, and require review.
Screenshot is cluttered Consent banner, popup, or chat widget loaded before capture. Use ScreenshotNeo’s clean capture, or hide selectors and wait for the page to settle.
Screenshot response is not billed Page verdict is a bot check, blank page, timeout, failed load, or cache hit. Read X-Page-Verdict, fix the target or wait settings, and retry when appropriate.

Implementation checklist

  • Identify every jurisdiction you support and document source coverage.
  • Prefer license-number and jurisdiction inputs.
  • Normalize names, statuses, dates, and affiliation fields.
  • Persist raw evidence, source, request inputs, and timestamps.
  • Define ambiguous, missing, conflicting, expired, and disciplinary-result policies.
  • Separate operational verification from certified proof.
  • Batch roster checks within provider limits.
  • Schedule rechecks based on risk and source freshness.
  • Monitor stale data, provider errors, and manual-review queues.

FAQ

Is there one API for every US state?

No. State data is fragmented. A normalized provider can reduce integrations, but you still need to verify coverage and freshness by jurisdiction.

Does a name match prove a license is valid?

No. A name match can be ambiguous. A license-number and jurisdiction match is stronger, and unresolved matches should go to manual review.

Can I verify an entire brokerage roster?

Yes. Use a batch endpoint when available, keep each row tied to an internal subject ID, and process partial failures independently. RELD documents batches of up to 100 license pairs.

Should I store the whole provider response?

Store the raw response or an immutable evidence pointer when policy and privacy requirements allow it, along with normalized fields and timestamps.

Can an API response be submitted as certified proof?

Not necessarily. Indiana explicitly distinguishes its License Data REST API from proof of licensure for an application in another state. Check the receiving regulator’s certification requirement.