# TX Medical Board License Status Delta (`titan_coder/tx-medical-board-license-status-delta`) Actor

Watches named Texas Medical Board physician licenses in the official registry, alerting only on a genuine change: registration status (revoked, suspended, cancelled), disciplinary status, or board action. For hospitals, staffing agencies, insurers, malpractice due-diligence, HR. Free when unchanged.

- **URL**: https://apify.com/titan\_coder/tx-medical-board-license-status-delta.md
- **Developed by:** [Radu Furtuna](https://apify.com/titan_coder) (community)
- **Categories:** Business, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$10.00 / 1,000 physician license status change delivereds

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?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## TX Medical Board License Status Delta

Durable monitor for the official Texas Medical Board (TMB) bulk registry — `DataSet-01-All Licenses` —
published as open data on `data.texas.gov`. Watch specific **Physician License** (MD/DO) numbers and get
notified only when the license's registration status, disciplinary status, license status, or a board
action genuinely changes. No API key needed, no login, no captcha.

### Source and scope

One Socrata Open Data API dataset: `https://data.texas.gov/resource/tm3v-pfq9.json` — 511,438 rows total
(confirmed live 13.09.2026), covering 37 different TMB-regulated license types (physicians, physician
assistants, radiologic technologists, respiratory care practitioners, acupuncturists, perfusionists, and
more).

**This actor covers only the `Physician License` pool** (185,801 rows) — a deliberate scope decision, not
an oversight. Live verification 13.09.2026 found that `license_number` is a `text` field that is **not
unique across the whole dataset**: the value `"ACTEMP"` alone repeats 1,458 times (a placeholder code for
`Acupuncturist Temporary License`, not a personal number), and even the pair `(license_number,
license_type)` is not always unique for other license types (e.g. `Acudetox Certificate`). Within
`Physician License` specifically, `license_number` **is** confirmed globally unique (`count(*)` ==
`count(distinct license_number)` == 185,801, live query). Extending this actor to the other 36 license
types would require a separate live uniqueness audit for each one — not done, and not assumed. Every
server-side request explicitly filters `license_type='Physician License'` in addition to `license_number`,
and the client independently rejects any response with more than one matching row as a structural anomaly.

`license_number` format observed live within this pool: either purely numeric (4-7 digits, e.g. `10031`,
`8085`) or one uppercase letter followed by 4 digits (e.g. `H0001`, `B2944`, `Z4634`). A leading zero right
after the letter (`H0001`) is a legitimate part of the identifier and is preserved exactly, not stripped.

Updated live (`rowsUpdatedAt` observed 2026-09-08, 5 days before this actor was built) — cadence is a
property of the source, not guaranteed daily.

### Fields tracked for billing

Four fields confirmed live (13.09.2026) to be **required and non-blank for 100% of the 185,801 Physician
License rows** (0 null/empty each): `license_number`, `registration_status` (the live "is this license
active/cancelled/suspended right now" signal — `ACTIVE` 107,690, `CANC NON-RENEWAL` 30,978, `CANCELLED BY
REQUEST` 12,773, `DECEASED` 11,914, `SUSPENDED, ACTIVE` 38, etc.), `disciplinary_status` (`NONE` 179,639,
`SEE PREVIOUS ORDER` 3,890, `CANCELLED BY BOARD` 1,231, `UNDER BOARD ORDER` 277, `SUSPENDED BY BOARD` 94,
etc.), and `license_status` (99.93% literal `"NONE"`, with rare non-`NONE` values also disciplinary in
nature, e.g. `SUSPENDED BY ATTORNEY GENERAL`).

**A separate, independent disciplinary signal:** `current_board_action_date` and
`current_board_action_description` — confirmed live to be legitimately null for ~95.8% of Physician License
rows, but a live example (license `M5333`) shows `disciplinary_status: NONE` **and**
`current_board_action_date` populated at the same time — a board action can exist independently of the
plain-text disciplinary status. This actor treats a change in **any** of the five fields
(`registration_status`, `disciplinary_status`, `license_status`, `current_board_action_date`,
`current_board_action_description`) as billable.

### How it works

1. Each `watch` names exactly **one** Physician License by `licenseNumber` — look it up at
   [profile.tmb.state.tx.us](https://profile.tmb.state.tx.us/) if you only have a name. One watch = one
   point query (`$where=license_number='<N>' AND license_type='Physician License'`) = one HTTP request per
   run.
2. The first check of a new watch establishes a baseline (no charge). Every later check compares the
   current combination of the five tracked fields against the durable record of what it was last time.
3. Billing deliberately excludes administrative/date fields that shift routinely (`license_issue_date`,
   `license_expiration_date`, `registration_status_date`, `license_status_date`, `disciplinary_status_date`)
   and non-status fields (practice address, degree, specialties). A status that changes and later reverts
   bills every genuine transition, not just the first one.
4. Genuinely new (first found) or changed records are pushed to the dataset and billed once each
   (`tmb-physician-license-status-changed`); a check that finds nothing new costs nothing beyond the fixed
   platform run cost.

### Input

```json
{
  "monitorId": "my-tmb-watch",
  "watches": [
    { "watchId": "new-hire-robbins", "licenseNumber": "C2582" }
  ],
  "notifyOn": "new_alerts",
  "webhookUrl": "https://example.com/webhook"
}
```

Add more physicians later under the same `monitorId` — each watch keeps its own independent history. A
`watchId` is permanently bound to the `licenseNumber` it first saw; pointing the same `watchId` at a
different license number later fails the run instead of silently mixing histories.

`socrataAppToken` is optional — `data.texas.gov` does not require a key for this dataset, but a free
Socrata app token from your own account raises the anonymous request-rate ceiling if you run many watches
across many monitors.

### Output row (per change)

`watchId, licenseNumber, changeType ("new"|"status_changed"), registrationStatus,
previousRegistrationStatus, disciplinaryStatus, previousDisciplinaryStatus, licenseStatus,
previousLicenseStatus, disciplinaryStatusDate, currentBoardActionDate, previousCurrentBoardActionDate,
currentBoardActionDescription, previousCurrentBoardActionDescription, firstName, lastName, suffix, yearOfBirth, licenseIssueDate,
licenseExpirationDate, registrationStatusDate, licenseStatusDate, degree, specialties, practiceAddress,
practiceCity, practiceState, practiceZip, currentlyLicensed, monitorId, runId, discoveredAt, eventId,
billed`

### Billing

Pay-per-event: `tmb-physician-license-status-changed` — charged only for a watch's first found record
(baseline is free) or a genuine change in `registrationStatus`/`disciplinaryStatus`/`licenseStatus`/
`currentBoardActionDate`/`currentBoardActionDescription` since the previous check. Failed/blocked checks
are never charged.

### Important — read before relying on this for any hiring/staffing/insurance/compliance decision

**This registry is a publication of license status, not a certification of good standing or a complete
disciplinary record.** The Texas Medical Board maintains its own authoritative enforcement files; the
public bulk dataset reflects what TMB has published, which can lag real-world disciplinary events.
**This actor is an informational monitor of CHANGES to that public publication — it is NOT a current
"license to practice" certification, NOT a disciplinary-history lookup, and NOT legal/HR/malpractice
due-diligence advice.** Always confirm directly at
[profile.tmb.state.tx.us](https://profile.tmb.state.tx.us/) (or with TMB directly) before acting on any
single entry — especially for hiring, staffing placement, credentialing, insurance underwriting, or
compliance decisions.

### Delivery guarantee: at-most-once (we would rather lose an alert than bill you twice)

Each computed change is delivered to the dataset and charged **at most once**, for as long as the
monitor's claim log exists (see the boundary below). Before any irreversible step (writing the row,
charging the event) the run takes an **atomic claim** on that exact change, using the only atomic
primitive the Apify platform offers: a request queue's unique-key insert. Exactly one run can win that
claim for a given change. The claim log is never consumed, deleted or rotated by this actor; it is a
permanent record of what was already attempted, and `coverage.claimJournalSize` reports its size each run
so you can watch it grow (the platform's counter is eventually consistent, so treat it as a lagging
estimate, not an exact count).

**Where that guarantee ends — the honest boundary.** The claim log lives in a *named* request queue
(`<prefix>-<monitorId>-claims`) in your own account. The at-most-once guarantee holds as long as that
queue keeps existing. If you — or any process holding your account credentials — delete, rename or
re-create it from the Console or API, the log starts empty and previously delivered changes can be
delivered and charged again. That is the unavoidable boundary of *any* durable storage, not a loophole in
the protocol. For the same reason, the actor's storage prefix and internal claim namespace are frozen
after release: changing either would create a fresh, empty log with exactly the same effect.

The response the platform returns for each claim is interpreted **strictly**: only a real boolean `false`
grants the right to write and charge, only a real boolean `true` denies it, and anything else — a missing
field, `null`, `0`, an empty string, a changed SDK response shape — aborts the run's delivery for that
item with `claim_protocol_error` **before** any row or charge. An answer we do not fully understand is
never read as "you may charge".

One thing we deliberately do **not** claim: the monitor's lease makes overlapping runs a fail-closed
exception rather than a fact of life, but between the moment a run verifies it still holds the lease and
the moment the dataset write or charge actually lands there is an unavoidable time gap (the platform
offers no fencing token for datasets or billing). So "a run that lost the lease can never write another
row" would be an overstatement. What actually protects your money is the claim above: the key is already
taken, so even a ghost run cannot charge for the same change twice.

The honest consequence, stated plainly: **if a run dies after taking the claim but before finishing, that
one change is lost**. It is recorded as `dataset_unknown` or `charge_unknown` and it is **not**
re-delivered on the next run — the next run moves on to the physician's next status change. We deliberately chose possible loss
of one alert over the possibility of charging you twice for the same event. This is *at-most-once*
delivery, not *exactly-once*; any actor that claims exactly-once over a store without compare-and-swap is
overstating what the platform can do.

Practically this only happens if the Apify run is killed mid-delivery (platform abort, timeout, migration).
Every such case is visible: the run's `coverage` and `run_summary` report it, and `run_summary.eventsBilled`
plus Apify's own billing ledger remain the source of truth for what you actually paid for.

### Honest limits

- **This actor covers the `Physician License` pool only** — not physician assistants, radiologic
  technologists, or any of the other 36 license types in the same source dataset. See "Source and scope"
  above for the live evidence (duplicate/non-unique `license_number` values in other pools) behind this
  decision.
- **The durable dataset is a delivery-attempt log, not a guaranteed mirror of the default dataset.** Each
  row is written to the durable dataset first, then mirrored to the run's default dataset before billing
  proceeds for that row. If the durable write succeeds but the default-dataset mirror write fails (e.g. a
  transient Apify storage error interrupts the run between the two writes), the item is marked
  `dataset_unknown`, billing for it is permanently blocked (fail-closed — we never charge for a row we
  can't confirm was delivered), and the run does not retry into re-creating that exact row. The durable
  dataset can therefore end up with a small number of orphan rows that were never mirrored and never
  billed. The **default dataset is the canonical log of rows successfully written to this run's output**
  (see its `run_summary` row) — but a default-dataset row does not by itself prove the row was billed: the
  row is written before `Actor.charge()` runs, so if charging then fails or comes back `charge_unknown`,
  the row is present but not confirmably paid. **`run_summary.eventsBilled` and Apify's own billing ledger
  are the source of truth for confirmed payment**, not the presence of a row in either dataset.
- **One watch = one physician license, one request.** There is no bulk/roster mode — to track a whole unit
  or medical staff, add one watch per license (up to 30 per run). This keeps the network cost fixed and
  predictable regardless of the registry's total size (511k+ rows / 185k+ in the Physician License pool),
  and keeps each license's history independently auditable.
- **A "not found" result on the very first check of a watch is reported honestly, not as an error** —
  `coverage.watches[].matched: false`. TMB does not delete records (confirmed live 13.09.2026: a
  1915-issued, 1980-deceased physician license is still returned by a direct point query), so a typo'd
  `licenseNumber`, or a license number that belongs to a different license type in the source dataset, will
  simply never match; it costs nothing and is safe to correct and retry under the same `watchId`.
- **A license number that WAS found on a previous check but is NOT found on a later one** is treated as
  `source_access_limited` for that watch this run — no baseline/history update, no billing. Since TMB does
  not delete rows (see above), this should never happen from a genuine data change; it's the honest
  fallback if the source ever answers unexpectedly.
- **Billing tracks `registrationStatus` + `disciplinaryStatus` + `licenseStatus` +
  `currentBoardActionDate` + `currentBoardActionDescription` only**, deliberately excluding
  `licenseIssueDate`/`licenseExpirationDate`/`registrationStatusDate`/`licenseStatusDate`/
  `disciplinaryStatusDate` (administrative dates that shift routinely) and practice-address/degree/
  specialty fields (which can change administratively without any change to license standing). Those
  fields are still delivered in every row for context.
- **All four required fields (`license_number`, `registration_status`, `disciplinary_status`,
  `license_status`) are confirmed live to be non-blank for 100% of Physician License rows.** If the source
  ever returns a blank/whitespace-only value for one of them, or omits `current_board_action_date`/
  `current_board_action_description` as a whitespace-only string rather than a true absence, the run treats
  it as a structural anomaly (`source_access_limited`), not a silent guess.
- We don't invent data: if the API ever returns something other than a bare JSON array, more than one row
  for a single `license_number` within the Physician License pool (violating the dataset's own uniqueness
  contract), or a row whose returned `license_number` doesn't exactly match the one requested (including
  case and whitespace), the run reports it honestly (`source_access_limited`) instead of guessing.

Author: OmniCoder (https://t.me/OmniCoder)

# Actor input Schema

## `monitorId` (type: `string`):

Name of this monitor's durable history (a-z, 0-9, dash; up to 40 chars).

## `watches` (type: `array`):

1-30 objects: {"watchId": "new-hire-robbins", "licenseNumber": "C2582"}. licenseNumber is the plain Texas Medical Board Physician License number as published in the registry (an optional single uppercase letter followed by 4-7 digits, e.g. C2582 or 10031) — look it up at https://profile.tmb.state.tx.us/ if you only have a name. This actor covers the Physician License pool only (MD/DO) — not physician assistants, radiologic technologists, or other TMB-regulated professions in the same source dataset, which use separate, unverified numbering. One watch = one license holder. New licenses can be added later under the same monitorId.

## `socrataAppToken` (type: `string`):

Optional. data.texas.gov does not require a key for this dataset, but a free Socrata app token (from your own data.texas.gov account) raises the anonymous request-rate ceiling if you run many watches across many monitors. Leave empty for normal use — a handful of watches per run works fine without one.

## `notifyOn` (type: `string`):

new\_alerts — post the webhook only when new/changed paid status changes were delivered; always — post it every run; never — do not call webhookUrl at all.

## `webhookUrl` (type: `string`):

Optional. Receives a digest of delivered (paid) status changes as JSON. HTTPS only.

## Actor input object example

```json
{
  "monitorId": "my-tmb-watch",
  "watches": [
    {
      "watchId": "example-physician",
      "licenseNumber": "C2582"
    }
  ],
  "notifyOn": "new_alerts"
}
```

# Actor output Schema

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

Every row this run produced. Key fields: watchId, licenseNumber, changeType (new|status\_changed), registrationStatus, previousRegistrationStatus, disciplinaryStatus, previousDisciplinaryStatus, licenseStatus, previousLicenseStatus, currentBoardActionDate, currentBoardActionDescription, previousCurrentBoardActionDescription, firstName, lastName, practiceCity. Informational only — not a legal certification of current standing.

## `coverage` (type: `string`):

What this run actually covered and what it charged for: per-watch status/reason/matched, records delivered and billed, requested/attempted/succeeded/failed watch counts. Enough to reconcile every charge against every row.

## `digest` (type: `string`):

A short human-readable summary of what this run found, written every run.

# 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 = {
    "monitorId": "my-tmb-watch",
    "watches": [
        {
            "watchId": "example-physician",
            "licenseNumber": "C2582"
        }
    ],
    "notifyOn": "new_alerts"
};

// Run the Actor and wait for it to finish
const run = await client.actor("titan_coder/tx-medical-board-license-status-delta").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 = {
    "monitorId": "my-tmb-watch",
    "watches": [{
            "watchId": "example-physician",
            "licenseNumber": "C2582",
        }],
    "notifyOn": "new_alerts",
}

# Run the Actor and wait for it to finish
run = client.actor("titan_coder/tx-medical-board-license-status-delta").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 '{
  "monitorId": "my-tmb-watch",
  "watches": [
    {
      "watchId": "example-physician",
      "licenseNumber": "C2582"
    }
  ],
  "notifyOn": "new_alerts"
}' |
apify call titan_coder/tx-medical-board-license-status-delta --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,titan_coder/tx-medical-board-license-status-delta"
        }
    }
}
```

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/CkoWqleDEsioAoF5T/builds/CsrnlrtlpeJMq7A7H/openapi.json
