Delaware Professional License Disciplinary Actions Delta
Pricing
$10.00 / 1,000 new disciplinary action detecteds
Delaware Professional License Disciplinary Actions Delta
Tracks Delaware's official professional license disciplinary action registry (nursing, real estate, electricians, medicine, cosmetology and more) and alerts only on genuinely new or revised revocations, suspensions, probations, and fines. Built for employers, HR, insurers, and compliance teams.
Pricing
$10.00 / 1,000 new disciplinary action detecteds
Rating
0.0
(0)
Developer
Radu Furtuna
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
a day ago
Last modified
Categories
Share
Durable monitor for Delaware's public Disciplinary Actions for Professional and Occupational Licensees registry — one dataset covering ALL 37 professions the Delaware Division of Professional Regulation (DPR) regulates (nursing, real estate, electricians, medicine, cosmetology, plumbing/HVAC, pharmacy, and more). Watch by profession, license number, and/or name, and get notified only about disciplinary actions genuinely new or revised since your last check. No API key needed, no login, no captcha.
Source
https://data.delaware.gov/resource/dz6p-akeq.json — Socrata SODA JSON API for dataset dz6p-akeq
("Disciplinary Actions for Professional and Occupational Licensees"), confirmed live 13.09.2026: 8,475 rows,
updated regularly (22 new rows since 01.08.2026 at verification time, latest action dated 2026-09-03).
This is an append-only registry of discrete disciplinary actions, not a point-in-time license status
lookup. One license/person can have several rows (up to 16 for a single license as of 13.09.2026) — each
row is one action (Probation, Revocation, Fine, Letter of Reprimand, Suspension, etc., in item_text).
There is no field for "this license's current status" by itself, only the list of actions ever taken
against it. This actor therefore watches for new or revised rows appearing in the registry, the same
architecture as ncua-credit-union-enforcement-watch — not for a status field flipping value, the
architecture used by tx-nursing-board-license-status-delta and similar point-query monitors. See
ROADMAP.md for the full reasoning behind this choice.
How it works
- Each
watchis an optionalprofessionId(exact match, e.g."Nursing","Electrical Examiners","Real Estate","Plumbing/HVACR"— see the full list below),licenseIdContains(case-insensitive substring against the license number, e.g."L1-0043682"), and/ornameContains(case-insensitive substring against the licensee's name or business name) — or none of the three, to watch the entire statewide registry. - Every run downloads the entire registry in a single HTTP request (cached for the whole run regardless of how many watches you define) and filters it per watch on our side — the API has no server-side filter scoped to a single watch, but the whole registry is only ~8,500 rows and growing slowly, and one fetch serves every watch in the run. A single request over the whole snapshot — rather than paging through it — also means the read is consistent: the source database's own snapshot isolation guarantees we see one coherent point-in-time state, not a mix of before/after a mutation that happened to land between two page requests.
- A stable identity is kept per row using Socrata's own row identifier (
:id), plus a content hash of the substantive fields (license number, profession, license type, action text, start/end dates, name). If Delaware ever edits an existing row in place, the content hash changes and it is honestly treated as a revision, never silently ignored or double-billed for no reason; if the exact same row reappears unchanged, nothing is re-billed. - Genuinely new or revised rows are pushed to the dataset and billed once each
(
new-disciplinary-action-detected); a check that finds nothing new costs nothing beyond the fixed platform run cost.
Input
{"monitorId": "my-de-license-watch","userAgentContact": "you@example.com","watches": [{ "watchId": "electricians", "professionId": "Electrical Examiners" },{ "watchId": "one-license", "licenseIdContains": "L1-0043682" }],"notifyOn": "new_alerts","webhookUrl": "https://example.com/webhook"}
userAgentContact is sent as a descriptive User-Agent on every request (polite-scraping practice) —
Socrata's open-data API does not require a key, but we still identify ourselves.
Add more watches later under the same monitorId — each watch keeps its own independent history. A
watchId is permanently bound to the filter it first saw; pointing the same watchId at a different
professionId/licenseIdContains/nameContains later fails the run instead of silently mixing histories.
All 37 regulated professions (profession_id, verified live 13.09.2026)
Accountancy, Adult Entertainment, Architecture, Charitable Gaming, Chiropractic, Combative Sports, Controlled Substances, Cosmetology and Barbering, Deadly Weapons Dealers, Dentistry, Dietitians/Nutritionists, Electrical Examiners, Elevator Mechanics, Funeral Services, Geologists, Home Inspectors, Landscape Architecture, Land Surveyors, Manufactured Home Installation, Massage Bodywork, Medical Practice, Mental Health, Nursing, Nursing Home Administrators, Occupational Therapy, Optometry, Pharmacy, Physical Therapy/Athletic Trg, Pilots, Plumbing/HVACR, Podiatry, Psychology, Real Estate, Real Estate Appraisers, Social Work Examiners, Speech and Hearing, Veterinary Medicine.
Output row (per action)
watchId, stableRowId, licenseId, professionId, licenseType, subjectType ("individual"|"business"), lastName, firstName, combinedName, itemText, dispStart, dispEnd, monitorId, runId, discoveredAt, eventId, billedsubjectType is "business" (e.g. a cosmetology salon, an elevator-mechanic company) when the row has no
individual name — only 98 of 8,475 rows as of 13.09.2026, all of Delaware's other 8,377 rows name a person.
Billing
Pay-per-event: new-disciplinary-action-detected — charged only for a row genuinely new or revised since
the previous check of that watch. The first check of a new watch establishes a baseline (no charge).
Failed/blocked checks are never charged.
Important — read before relying on this for hiring/compliance decisions
This actor is an informational monitor of NEW or REVISED publications to Delaware's official
disciplinary-action registry — it is NOT a current license-status check, NOT a background-check report,
and NOT legal advice. The registry itself does not state whether an action has since been resolved,
appealed, or superseded beyond what dispEnd (often absent — see Honest limits) records; names may not
reflect a person's current legal name. Always confirm directly with Delaware DPR (dpr.delaware.gov) or
counsel before acting on any single entry, especially for employment or licensing 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 watch's next disciplinary action. 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
- 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), 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 is not retried 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 itsrun_summaryrow) — but a default-dataset row does not by itself prove the row was billed: the row is written beforeActor.charge()runs, so if charging then fails or comes backcharge_unknown, the row is present but not confirmably paid.run_summary.eventsBilledand Apify's own billing ledger are the source of truth for confirmed payment, not the presence of a row in either dataset. - Single-request snapshot integrity guard. The whole registry is fetched in one HTTP request rather
than paginated with
$offset/$limit— verified live that the source's$limitis effectively unbounded for this registry's size (a$limitof 999,999 and even 50,000,000 both returned all 8,475 rows in a single ~1.5-1.8s/2.46 MB request). This closes an entire class of integrity risk that page-based fetching cannot: a page-by-page read only has an integrity boundary between pages, so a row inserted or deleted strictly inside an already-read page (not touching the page boundary or the final row count) would go undetected. A single request has no "inside a page" for a mutation to hide in — the source database's own snapshot isolation guarantees the whole response reflects one coherent point in time. What remains, and is still checked (seede_client.py::_ensure_docs): every row's:idis tracked across the single response — any duplicate raisesstructure_changedfor the whole run instead of silently collapsing into one entry; and a separate$select=count(*)request, made immediately before the snapshot request, must match the number of rows the snapshot actually returned exactly (in either direction) — a mismatch means the registry changed in the narrow window between the two requests, and the run honestly reportsstructure_changedrather than build on an unverifiable snapshot. A defensive ceiling (SODA_MAX_LIMIT, currently 100,000 — about 12x today's live row count) also guards against silent truncation if the registry ever grows past what a single request can safely retrieve: if the independentcount(*)already exceeds that ceiling, the run fails closed before even requesting the snapshot, rather than silently accepting a truncated$limit-capped response as if it were complete. - No server-side filter exists for a single watch (verified live) —
professionId/licenseIdContains/nameContainsare always applied client-side over one shared snapshot per run; they never reduce the actual network cost (the whole registry is fetched regardless), only the rows delivered to you. - Row identity relies on Socrata's
:idfield being stable across time for the same underlying record — verified live that it is NOT a pure content hash (5 rows with fully identical visible field content received 5 distinct:idvalues, ruling out hash-collapse of duplicates), consistent with a persistent internal surrogate key, but this could not be proven across repeated snapshots over time within a single verification session. As a defensive measure (not a documented Socrata guarantee), we still track a content hash and a change-sequence counter per:id— if Delaware ever edits an existing row in place, it is honestly reported as a revision rather than silently lost or double-charged. - Each watch's window is capped at
MAX_ROWS_PER_WATCH(10,000) matching rows, newest-first bydispStart(rows with nodispStart— 121 of 8,475 live — sort last).coverage.windowFullCountand each watch's row incoverage.watches(windowFull/matchedCount) flag when a watch's real match count exceeded that window — informational, not silently dropped data.coverage.sourceAccessLimitedCountis a separate, unrelated counter: it only counts watches where the registry snapshot itself could not be trusted this run (missing/empty required fields — see below), never a watch that simply had more matches than the window. - Row-level identity and integrity depend on the registry always supplying a non-empty
:id,license_id_l,profession_id, andlicense_typefor every row — all four verified true for all 8,475 rows live on 13.09.2026 (0 nulls/empties). If any row is missing one of these (or a nullable field likedispEnd/itemText/name fields is present but contains only whitespace — not a legitimate empty value), the whole run for that check is reported honestly assource_access_limited— no partial baseline/delta/billing happens on a snapshot whose completeness can't be trusted. dispEndis legitimately absent for the majority of rows (5,608 of 8,475 live — most actions have no recorded end date, e.g. permanent or still-open).dispStart(121/8,475),itemText(12/8,475), name fields (98/8,475, all business-only rows), andcombinedName(1,429/8,475) are also legitimately nullable. None of these participate in thesource_access_limitedintegrity check above.- Two subject types, not one convention.
subjectTypeis"individual"only when bothlastNameandfirstNameare present; otherwise"business"(onlycombinedName, e.g. a salon or company). Verified live: on every one of the 98 rows without a personal name, both name fields are absent together — no case of only one being empty was found, so no thirdsubjectTypeis defined. - History is kept per row (not a capped seen-ids list): each row's own state (its latest content hash and a change-sequence counter) is stored indefinitely per watch, so a row that changes and later reverts to an earlier state is billed correctly on every genuine transition, never silently deduplicated against an older occurrence of the same content.
- We don't invent data: if the API's response shape changes, or the registry grows past the defensive
SODA_MAX_LIMITceiling for a single request, the run reports it honestly (structure_changed: ...) instead of silently returning zero results or a truncated snapshot.
Author: OmniCoder (https://t.me/OmniCoder)