Colorado DORA Professional License Status Delta
Under maintenancePricing
$10.00 / 1,000 license status changeds
Colorado DORA Professional License Status Delta
Under maintenanceTracks Colorado DORA's statewide professional license registry (nurses, cosmetologists, plumbers, engineers, 300+ license types) and alerts only on new licenses, status changes (active/expired/revoked/suspended), and confirmed removals since your last check.
Pricing
$10.00 / 1,000 license status changeds
Rating
0.0
(0)
Developer
Radu Furtuna
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
6 days ago
Last modified
Categories
Share
Durable monitor for Colorado's official statewide Professional and Occupational Licenses registry (Colorado Department of Regulatory Agencies / DORA) — one dataset covering 300+ license types in a single file: registered nurses, cosmetologists, plumbers, real estate brokers, professional engineers, CPAs, jewelers, architects, and many more. Watch one license type at a time and get notified only about genuinely new licenses, status changes (Active/Expired/Revoked/Suspended/Surrendered/Summary Suspension/...), and confirmed removals since your last check. No API key needed, no login, no captcha.
Source
https://data.colorado.gov/resource/7s5z-vewr.json — Socrata SODA JSON API for dataset 7s5z-vewr
("Professional and Occupational Licenses in Colorado"), confirmed live 14.09.2026: 1,608,059 rows,
313 distinct license type codes, updated daily (data_updated_at one day before verification). The
disciplinary pool (Revoked + Suspended + Surrendered + Voluntary Surrender + Summary Suspension) is
18,283 historical cases, with roughly 187 new discipline-dated records per month.
This is a large, single-snapshot registry — not an incremental feed. There is no field for "changed
since date X" and no cursor; the only honest way to catch a status change is to re-fetch the full current
population of one license type and diff it against the last known state. That is exactly what this actor
does, server-side filtered per watch ($where=licensetype='...'), not a client-side filter over the whole
1.6M-row file — the registry is too large to cache fully in one run (see ROADMAP.md for the full sizing
reasoning).
How it works
- Each
watchnames onelicenseType— DORA's own short code for a license category, e.g.RN(Registered Nurse),COS(Cosmetologist),PE(Professional Engineer),REG(Real Estate Broker). See How to find a license type code below. - Every run for that watch downloads the entire current population of that one license type in a single
HTTP request (server-side
$where=licensetype='...', no pagination — see Honest limits) — not a truncated window. Watches sharing the samelicenseTypein one run reuse a single fetch. - A stable identity is kept per row using Socrata's own row identifier (
:id), plus a content hash of the substantive fields (status, dates, discipline case number/action/dates, license number, subcategory). A row's cosmetic fields (name, city, state, zip) never trigger a billed event on their own. - Genuinely new rows, status/discipline changes on an already-known row, and confirmed removals (a row
missing from two consecutive checks — see below) are pushed to the dataset and billed once each
(
license-status-changed). A check that finds nothing new costs nothing beyond the fixed platform run cost.
Why two consecutive misses before "removed"
A single check where a previously-known row is not found in the current snapshot is structurally
indistinguishable from a transient snapshot problem (a page request that failed to complete, a momentary
source hiccup). Reporting it as "removed" on the first miss would risk a false alarm. The first miss is
recorded internally (missingStreak) but never billed; only a second consecutive miss — meaning the
row was absent on two separate runs in a row — is billed as a confirmed removed event. If the row
reappears at any point before that second miss, the streak resets with no event at all; if it reappears
after being confirmed removed, that is honestly billed again as reappeared.
Input
{"monitorId": "my-co-dora-watch","userAgentContact": "you@example.com","watches": [{ "watchId": "cosmetology", "licenseType": "COS" },{ "watchId": "nurses", "licenseType": "RN" }],"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 licenseType it first saw; pointing the same watchId at a different
license type later fails the run instead of silently mixing histories.
Pick a narrow license type for faster, cheaper runs. Live sizes (14.09.2026, top types): RN 238,749,
NA 195,495, APE 92,052, COS 77,197, DR 69,185, PE 63,335, PN 50,763, AP 41,364, CPA 39,545,
JW 33,044 — but the large majority of the 313 codes are small (dozens down to a single row).
How to find a license type code
Browse or query the source directly, e.g.:
https://data.colorado.gov/resource/7s5z-vewr.json?$select=licensetype,count(*)&$group=licensetype&$order=count(*)%20DESC
or look up the DORA "Lookup a License" page for the profession you care about and note the two-letter/code prefix shown against similar license numbers.
Output row (per change)
watchId, changeType ("new"|"changed"|"reappeared"|"removed"), stableRowId, licenseType, licenseNumber, subCategory, subjectType ("individual"|"business"|"unknown"), lastName, firstName, middleName, suffix, entityName, city, state, mailZipCode, licenseStatus, previousStatus, licenseFirstIssueDate, licenseLastRenewedDate, licenseExpirationDate, specialty, title, degrees, caseNumber, programAction, disciplineEffectiveDate, disciplineCompleteDate, verifyLicenseUrl, healthcareProfileUrl, monitorId, runId, discoveredAt, eventId, billedFor changeType: "removed", the row's current-snapshot fields (licenseNumber, licenseStatus, name
fields, etc.) are null by design — the row is no longer in the source, so there is nothing current to
report beyond previousStatus (the last known status before it disappeared) and licenseType (the watch's
own filter, always known). See Honest limits.
Billing
Pay-per-event: license-status-changed — charged once for each row genuinely new, status/discipline-changed,
reappeared, or confirmed-removed 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/vendor decisions
This actor is an informational monitor of the Colorado DORA public license registry — it is NOT a
real-time regulatory feed, NOT a background-check report, and NOT legal advice. Always confirm directly
with the relevant DORA board, or the verifyLicenseUrl in the row itself, before acting on any single entry
for employment, licensing, 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. 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. Every early exit
(lease busy, claim-protocol error, or a deadline hit before any watch was even attempted) still writes a
coverage/digest record to the key-value store, so a FAILED run is never silent about why.
Run timeout — cloud platform limit vs. the actor's own deadline
The actor's own internal worst-case deadline (src/config.py::UPPER_TIME_S * DEADLINE_MARGIN_MULT =
2,415s × 1.1 = 2,656.5s ≈ 44min, covering MAX_WATCHES_PER_RUN=5 watches each doing exactly two HTTP
requests — one count(*) and one single-request snapshot fetch — with one retry per request) is only
meaningful if the Apify platform actually lets the run live that long. Left unset, Apify silently applies its
own default run timeout (3,600s / 1 hour, confirmed live 14.09.2026) — close to but still short of the
actor's own worst case with margin, which would risk killing the run before its own degradation/reconcile
logic (_reconcile_if_deadline_passed, the webhook time-budget check) ever gets a chance to run honestly.
.actor/actor.json therefore sets defaultRunOptions.timeoutSecs explicitly to 4,000s (~67min). That
number is checked against src/config.py::REQUIRED_CLOUD_TIMEOUT_SECS = 3,256s (the actor's own deadline
plus lease.py's LEASE_DEADLINE_MARGIN_S = 600s lease-release margin) by both a module-level assert in
config.py and tests/test_main.py::test_actor_json_declares_explicit_timeout_covering_worst_case_with_margin
— the two numbers (config's requirement and actor.json's actual value) are kept in sync by test, not by eye.
This dropped by more than 5x from the previous offset-pagination architecture (was ~5h12m/21,600s) because a
single-request fetch replaces up to 84 paginated HTTP requests per watch with exactly one.
Honest limits
- Row-level identity is Socrata's own
:idfield, notlicenseNumber+licenseType. A live check before building this actor found real collisions within a singlelicenseType(the samelicenseNumberappearing under multiple rows — up to 8 rows for one number underCOS), so the license number/type pair cannot be trusted as a unique row identity.:idis Colorado DORA's Socrata-assigned surrogate row key (rowIdentifierColumnIdisnullfor this dataset, confirmed live) and is the only field this actor treats as identity.licenseNumber/licenseTyperemain delivered as context fields on every row. licenseNumberis legitimately absent for a small number of rows (374 of 1,608,059 live, mostly temporary/local permits without an assigned number) — this is not treated as a structural error.- Full-snapshot integrity via a single request, not paginated. Each watch's fetch is one HTTP GET
(
$where=licensetype='...',$order=:id, a single large$limit) over a live, mutable registry — not offset-pagination across multiple HTTP requests. This is a deliberate architectural choice, not just an optimization: a single SQL query executes under the source database's own snapshot isolation, so the set of rows visible to that one request is fixed at the moment the query starts and cannot be partially mutated while the response streams — a row is either wholly present (if it existed at query start) or wholly absent, never half-read-then-changed. Multiple separate HTTP requests (the old page-by-page design) cannot offer that guarantee in principle, since each request only sees its own snapshot at its own start time, and the source can mutate freely in the window between requests — including strictly inside an already-read page's range, which a purely page-boundary check (row counts, adjacent-page overlap) cannot detect. The one remaining integrity check is: the single response's row count must exactly match what an independent$select=count(*)(taken immediately before the fetch) says the registry holds for that license type — not just "not fewer": a response returning more rows than expected is treated exactly as seriously as one returning fewer. A duplicate:idwithin that one response is also treated as a structural anomaly. If the registry has grown past the single request's configured$limitceiling (SODA_MAX_LIMIT), the run fails closed before even issuing the fetch, rather than silently accepting a$limit-truncated partial response as if it were the whole snapshot. None of these checks invent or drop data silently; all report the run honestly (source_access_limited) instead. - The durable index has no capacity limit and never evicts old entries. Every
:idever seen for a watch stays in that watch's durable index indefinitely (a plain, unbounded map, not a fixed-size or FIFO-capped list) — an entry that fell out of a capped window could otherwise come back as "never seen before" and be billed a second time for a row that existed before monitoring started. The only limit is on how many new/changed/removed transitions are processed for billing in a single run (MAX_NEW_ITEMS_PER_WATCH_PER_RUN= 300) — any excess is simply picked up on the next run, never dropped. - A whole-registry ID re-issue (rekey) is detected and treated as a run failure, never as a mass delta.
Socrata occasionally republishes a dataset with a brand-new internal
:id/tableIdspace on a structural change; if that ever happens to7s5z-vewr, every previously known row would look "removed" and every current row would look "new" in the same run — the existing per-run page-integrity checks above don't catch this because they only verify one run's snapshot, not continuity between runs. Two independent fail-closed checks guard against silently mass-billing that as a normal delta: (1) each run best-effort reads Socrata's own dataset metadata (GET /api/views/7s5z-vewr.json, fieldtableId) and compares it to the value stored from the previous run for that watch — a change is treated as a confirmed re-issue signal on its own; (2) independently, if 50% or more of a watch's previously known rows are missing from the current snapshot in a single run (and that watch had at least 20 known rows, to avoid false positives on tiny watches), that is treated the same way. Either check tripping stops that watch for the run withrekey_suspected— no rows delivered, no charge, and the watch's durable index is not updated, so the next run re-checks against the same known-good baseline instead of losing it. This is the same class of outcome assource_access_limitedabove (a structural anomaly, not a transient network hiccup). - No server-side filter exists for anything narrower than one license type.
licenseTypeis the only filter this actor supports, applied server-side; there is no per-watch filter on license number, name, or status — watch one license type and read the delivered rows for what you care about within it. - Nullable billing-relevant fields (status, dates, discipline fields) are validated by physical key
presence in the source's own
X-SODA2-Fieldsresponse header, not just "value is not null" — Socrata omits a null field's key entirely, so the only honest way to tell "this field is legitimately empty on this row" from "this column disappeared from the schema" is the header, checked on every single fetch, independent of whether any row in that response happens to have a value. - We don't invent data: if the API's response shape changes, a required field is empty/whitespace-only where
it should not be, or the registry has grown past the single request's
$limitceiling, the run reports it honestly (structure_changed: ...→source_access_limited) instead of silently returning zero results or a truncated partial snapshot.
Author: OmniCoder (https://t.me/OmniCoder)