# State trade licence records, with change detection (`quietfetch/state-license-records`) Actor

Normalised trade-licence records built from the licensing authority's own published data file, with per-run change flags (new, renewed, expired, status-changed, removed). Official downloads only — no lookup-form automation. Street addresses and telephone numbers are never emitted.

- **URL**: https://apify.com/quietfetch/state-license-records.md
- **Developed by:** [Quietfetch](https://apify.com/quietfetch) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$2.00 / 1,000 licence records

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## State trade licence records, with change detection

Normalised trade-licence records built from the **licensing authority's own published licence list**, read from the state's **official open-data API**, with a change flag on every row: what is new, what was renewed, what lapsed, what dropped out of the register since your last run.

### Spec (10 lines)

1. **What:** deduplicated, normalised trade-licence records for one or more US states, sourced from each authority's own published licence list, with per-run change flags.
2. **Buyer:** property managers, insurers, marketplaces and lead tools that must verify or *monitor* licences at scale and cannot poll a lookup form.
3. **Input:** `programs` (which licence types), `licenseTypes` (optional filter), `onlyChanged`, `includeRemoved`, `snapshotStoreName`, `maxRecordsPerProgram`, `pageSize`.
4. **Output:** one row per licence — `state, authority, program, licenseNumber, licenseType, licenseSubtype, status, statusBasis, businessName, city, stateCode, county, issuedDate, expiresDate, continuingEducation, sourceUrl, datasetPage, sourceFileDate, fetchedAt, recordKey, change, previousStatus, previousExpiresDate, complete`.
5. **Pricing:** pay-per-event `license-record` at $0.002 ($2 per 1,000 rows). `onlyChanged` charges only for rows that moved.
6. **Legal floor:** official open data only — the state portal's read-only API, robots.txt and its Crawl-delay checked and obeyed, terms recorded per state. No lookup-form automation, no enrichment, no personal-data resale.
7. **How it works:** page through one licence type on the state's open-data API → normalise → diff against your own snapshot of the previous run → write rows with change flags.
8. **Tests:** golden normalisation, date and status derivation, paging and robots handling, change detection, and a full run against a local stand-in for the portal. No network needed.
9. **Health:** a nightly cheap run reads the first couple of thousand licences of one programme and checks the row shape and the dataset's last-updated date.
10. **Not built:** anything that automates a state's individual-verification lookup form. Where an authority publishes no licence list with acceptable terms, that state is simply not supported.

### Source

Texas licences come from **"TDLR - All Licenses"** on the Texas Open Data Portal (https://data.texas.gov/dataset/TDLR-All-Licenses/7358-krk7), published under the attribution of the Texas Department of Licensing and Regulation and refreshed daily. Each programme is one licence type in that list:

| Programme | Licence type in the source | Licences (Sept 2026, approx.) |
|---|---|---|
| `tx-ac-contractors` | A/C Contractor | 20,400 |
| `tx-electrical-contractors` | Electrical Contractor | 14,000 |
| `tx-water-well-drillers` | Water Well Driller/Pump Installer | 1,750 |
| `tx-electrical-sign-contractors` | Electrical Sign Contractor | 650 |
| `tx-elevator-contractors` | Elevator Contractor | 370 |

Only business (contractor) licence types are offered. The same list also covers individual practitioners; those are deliberately not included.

### What is never in the output

**Street addresses, telephone numbers, owner names, postcodes and email addresses are never in the output — they are not even requested.** The source list carries the licence holder's business and mailing addresses, both telephone numbers, the owner's personal name and a map point. This actor asks the API only for the licence columns it needs, so those fields never cross the wire, never reach the actor, and are not available behind any option. For a sole trader, an address and a phone number are a home address and a personal number, and this actor is not a channel for reselling them.

What is published is the licence: who trades under it, in which town and county, of what type and subtype, and until when. That is what the authority publishes for public verification. The town is taken from the business address block **without its postcode**, because a town alone is coarse and a town plus a postcode is not.

`city` is empty wherever the authority publishes no business town — which, in the current source, is every A/C contractor. `county` is filled for about 96% of those licences and is the better geographic field for that programme.

### Status is derived, and says so

The authority's licence list carries no status column — only an expiry date. This actor does not invent one. It sets `status` to `active` or `expired` by comparing the expiry date with the dataset's own last-updated date, and every record carries `statusBasis` spelling out exactly that comparison. `issuedDate` is `null` because the list does not publish one; it is never estimated.

### Change flags

| `change` | Meaning |
|---|---|
| `new` | Not in the previous read (every row on your first run) |
| `renewed` | Expiry date moved later |
| `expired` | Now past its expiry date, and was not before |
| `status-changed` | Status moved some other way |
| `updated` | Trading name, town or expiry changed without being a renewal |
| `removed` | Was in the previous complete read, absent from this one |
| `unchanged` | Nothing moved |

Two honesty rules apply to the diff:

- **A partial run never reports removals.** If you cap records or filter by licence type, everything past the cut would otherwise look as though it had vanished from the register. Such a run is marked `complete: false` and reports no removals.
- **A partial, refused or failed run never overwrites your snapshot.** Otherwise the following good run would report the whole programme as newly added.

A licence whose town or trading name is blank in the source stays `unchanged` from run to run while nothing about it moves — a blank field is not a change, and you are not charged for it under `onlyChanged`.

### Input example

```json
{
  "programs": ["tx-ac-contractors"],
  "onlyChanged": true,
  "includeRemoved": true,
  "snapshotStoreName": "state-license-snapshots"
}
```

### Output example (one row, second run)

```json
{
  "state": "TX",
  "authority": "Texas Department of Licensing and Regulation",
  "program": "Electrical Contractors",
  "programId": "tx-electrical-contractors",
  "licenseNumber": "31522",
  "licenseType": "Electrical Contractor",
  "licenseSubtype": null,
  "status": "active",
  "statusBasis": "derived: LICENSE EXPIRATION DATE (2028-03-01) compared with the dataset's last-updated date (2026-09-26); this authority's licence list publishes no status column",
  "businessName": "Lone Oak Electric LLC",
  "city": "Waco",
  "stateCode": "TX",
  "county": "McLennan",
  "issuedDate": null,
  "expiresDate": "2028-03-01",
  "continuingEducation": "N",
  "change": "renewed",
  "previousStatus": "expired",
  "previousExpiresDate": "2026-03-01",
  "sourceUrl": "https://data.texas.gov/resource/7358-krk7.json",
  "datasetPage": "https://data.texas.gov/dataset/TDLR-All-Licenses/7358-krk7",
  "sourceFileDate": "2026-09-26",
  "complete": true
}
```

*(The licence number and business in this example are illustrative, not a real record.)*

### When a programme cannot be read

A programme that is refused, disallowed by robots.txt, returns something other than the expected JSON, or is unreachable produces a single row with `outcome: "problem"` and the reason, visible in the **Problems** dataset view. Those rows are **never charged for**, and nothing is worked around: no proxy rotation, no fingerprint spoofing, no CAPTCHA handling. A refusal is reported and the run moves on.

### Pricing (pay-per-event)

| Event | When charged | Price |
|---|---|---|
| `license-record` | after a record is written to the dataset | $0.002 ($2 / 1,000 records) |

A full first read charges once per record — about **$41** for the whole A/C contractor programme (~20,400 licences), about $28 for electrical contractors, and under $4 for each of the three smaller programmes. After that, `onlyChanged: true` charges only for the licences that actually moved. Set a maximum charge on the run if you want a hard ceiling; the actor stops writing, and stops charging, when it is reached.

### Attribution

The `ATTRIBUTION` record in the run's key-value store names, per programme, the authority, the dataset page, the dataset's last-updated date and the date it was copied, together with a statement that no state endorsement is implied. The source authority's site policy asks anyone republishing its web content to carry exactly that — **if you republish these records onward, carry it too**.

### Lawful and responsible use

- **Official open data only.** Read-only requests to the state portal's public API for the licence list the authority publishes. No lookup-form automation, no session, no login.
- **robots.txt is fetched for every host and obeyed** (`QuietfetchBot`, then `*`), including its `Crawl-delay`. A disallowed path is not fetched, and the run says so.
- **Rate:** at least one second between requests to a host, or the host's Crawl-delay if longer. Pages are ordered on the portal's own row id, so no page is fetched twice.
- **Identification:** the user agent names the bot and a contact address. Bot token `QuietfetchBot`; contact page https://quietfetch.com/bots, mailbox hello@quietfetch.com.
- **Optional app token:** if you set a `SOCRATA_APP_TOKEN` environment variable on the actor, it is sent as the portal requests, in a header and never in a URL. It is not needed for normal use.
- **Data:** the licence facts an authority publishes for public verification. No street addresses, no telephone numbers, no owner names, no postcodes, no email addresses, no enrichment from other sources. Buyers are responsible for their own use of the output, including any obligations that attach to the source data.
- **Terms:** each supported state's terms are recorded verbatim, with their source URL and the date they were read, before that state ships.

### Local development

```bash
npm test        # the full suite, no network required
npm start       # a local run (needs the Apify SDK and an input)
```

***

*Built and maintained with AI assistance under human review.*

# Actor input Schema

## `programs` (type: `array`):

Which licence programmes to read. Leave empty for all supported programmes. Each is one licence type in the authority's own published licence list, read from the state's official open-data API — this actor never automates a lookup form.

## `onlyChanged` (type: `boolean`):

Write (and charge for) only records whose status, expiry, name or town moved since your last run, plus records that appeared or disappeared. First run writes everything.

## `includeRemoved` (type: `boolean`):

Emit a row (change = 'removed') for a licence that was in the previous published file and is absent from this one. Only possible on a complete read.

## `licenseTypes` (type: `array`):

Optional. Keep only these licence types (as they appear in the dataset's LICENSE TYPE column, e.g. "A/C Contractor"). Each programme is already one licence type, so this is rarely needed. Note: filtering makes the run a partial read, so no snapshot is written and removals are not reported.

## `snapshotStoreName` (type: `string`):

Named key-value store, in your account, holding the previous run's index for change detection. Keep the same name between runs.

## `maxRecordsPerProgram` (type: `integer`):

Safety cap. Hitting it makes the run a partial read: no snapshot is written and removals are not reported.

## `pageSize` (type: `integer`):

How many licences to ask the open-data API for in each request while paging through a programme. The default suits almost everyone.

## `requestDelaySecs` (type: `integer`):

Minimum gap between two requests to the data portal. Never goes below 1 second, and the portal's own robots.txt Crawl-delay wins whenever it is longer.

## Actor input object example

```json
{
  "programs": [
    "tx-ac-contractors"
  ],
  "onlyChanged": false,
  "includeRemoved": true,
  "licenseTypes": [],
  "snapshotStoreName": "state-license-snapshots",
  "maxRecordsPerProgram": 200000,
  "pageSize": 1000,
  "requestDelaySecs": 1
}
```

# Actor output Schema

## `results` (type: `string`):

One row per licence: state, authority, program, programId, licenseNumber, licenseType, licenseSubtype, status, statusBasis, businessName, city, stateCode, county, issuedDate, expiresDate, continuingEducation, sourceUrl, datasetPage, sourceFileDate, fetchedAt, recordKey, change (new | renewed | expired | status-changed | updated | removed | unchanged), previousStatus, previousExpiresDate, complete. Street addresses, telephone numbers, owner names and postcodes are never included — they are not even requested from the source. `status` is derived from the expiry date because the authority's licence list publishes no status column — `statusBasis` says exactly how. `city` is empty where the authority publishes no business town (for example, every A/C contractor); `county` is the better geographic field there.

## `changesView` (type: `string`):

The same dataset, pre-filtered to the columns that matter when monitoring: change, licence number, name, town, county, status, expiry.

## `problemsView` (type: `string`):

Programmes that could not be read this run (refused, disallowed by robots.txt, unreadable response, or a network error), with the reason. These rows are never charged for.

## `runSummary` (type: `string`):

RUN\_SUMMARY record in the default key-value store: per-programme record counts, change tallies, the dataset's last-updated date, requests made and the delay honoured, whether the read was complete, and the robots.txt result.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {};

// Run the Actor and wait for it to finish
const run = await client.actor("quietfetch/state-license-records").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {}

# Run the Actor and wait for it to finish
run = client.actor("quietfetch/state-license-records").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{}' |
apify call quietfetch/state-license-records --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,quietfetch/state-license-records"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/DruvuXay2P5Qyshso/builds/ZlULgg2rdNTpMQeYh/openapi.json
