WA Lobbyist Reporting Delta avatar

WA Lobbyist Reporting Delta

Pricing

$10.00 / 1,000 new or amended lobbyist filing detecteds

Go to Apify Store
WA Lobbyist Reporting Delta

WA Lobbyist Reporting Delta

Monitors Washington State PDC lobbyist registrations, expense reports, and amendments for journalists, compliance/govt-affairs teams, and watchdog orgs. Filter by entity, name, report type, or amendments only. Free when nothing changes.

Pricing

$10.00 / 1,000 new or amended lobbyist filing detecteds

Rating

0.0

(0)

Developer

Radu Furtuna

Radu Furtuna

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

10 days ago

Last modified

Share

Durable monitor for Washington State's public Lobbyist Reporting History — the Public Disclosure Commission's (PDC) complete log of lobbyist/firm/client registrations, monthly expense reports, annual reports, and — critically — amendments to any of the above. Watch by entity, lobbyist/firm/client name, or report type, and get notified only about filings genuinely new since your last check. No API key needed, no login, no captcha.

Source

https://data.wa.gov/resource/nuwx-ay5h.json (Socrata SODA API), dataset "Lobbyist Reporting History" (nuwx-ay5h), attribution "Public Disclosure Commission" — 201,431 filings confirmed live 13.09.2026, 435 new filings in the first 12 days of September 2026 alone. Unlike a small CSV export, this dataset is too large (200K+ rows, many carrying a full JSON blob of the original form) to fetch in full every run — each watch instead runs two lightweight SoQL queries: an honest count(*) under your filter, and a window of the newest filings by receipt_date matching it (see Honest limits for what that window does and doesn't cover).

Architecture: an append-only filing log, not a status registry

This registry does not work like "one license record whose status field changes." It is a log of discrete filing events: an L1 registration, an L2 monthly expense report, an L3/L3c annual report — and every single amendment to any of those — each gets its own permanent, never-reused id ("submission version number", confirmed live via the dataset's own field documentation) and its own receipt_date. When a filer amends a previously filed report, the amendment is not an edit of the old row: it is an entirely new row (origin containing "AMENDED", amends_report pointing back at the original). This means the amendment event is itself captured simply as "a new filing appeared" — the same mechanism that catches a first-time registration. (The reverse link, amended_by_report, does get added to the old row after the fact, but we don't need to re-poll old rows to detect the amendment — the new row is the signal.) This is the same append-only shape as ncua-credit-union-enforcement-watch and delaware-professional-disciplinary-actions, not the "one entity, mutable status" shape of e.g. ny-attorney-bar-status-delta.

How it works

  1. Each watch combines optional filters — entityId (matches entity_id, firm_id, or client_id; the PDC's own stable identifier for a lobbyist/firm/client across years), filerNameContains (case-insensitive substring, applied server-side), reportTypes (subset of L1_CLIENT|L1_CONTRACT|L1_FIRM|L2|L3|L3C|L5), and amendmentsOnly (only rows whose origin contains "AMENDED") — or none of them, to watch the newest filings across the entire registry.
  2. Every watch runs its own $where-filtered count(*) and its own newest-first window (receipt_date DESC, id DESC, capped at FETCH_WINDOW_SIZE = 1000 rows) — filters are applied server-side by Socrata, not downloaded-then-filtered.
  3. A stable identity is the filing's own id (never reused across the dataset's history) plus a content hash of its substantive fields. If the underlying data for an existing id were ever corrected in place (not observed live, but not assumed impossible either), the hash would change and it is honestly treated as a revision, not silently ignored or double-billed.
  4. Genuinely new (or, defensively, revised) filings are pushed to the dataset and billed once each (new-or-amended-filing-detected); a check that finds nothing new costs nothing beyond the fixed platform run cost.

Input

{
"monitorId": "my-lobbyist-watch",
"userAgentContact": "you@example.com",
"watches": [
{ "watchId": "gordon-thomas-honeywell", "entityId": 38721 },
{ "watchId": "all-amendments", "amendmentsOnly": true },
{ "watchId": "acme-client", "filerNameContains": "Acme Corp", "reportTypes": ["L1_CLIENT"] }
],
"notifyOn": "new_alerts",
"webhookUrl": "https://example.com/webhook"
}

userAgentContact is sent as a descriptive User-Agent on every request (polite-scraping practice) — data.wa.gov does not require it, but we still identify ourselves. An optional socrataAppToken raises Socrata's own throttling limits if you have one; not required for normal use.

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 different filters later fails the run instead of silently mixing histories.

Output row (per filing)

watchId, stableId, origin, isAmendment, filerName, filerType (Firm|Client|Public agency), entityId, firmId, clientId, year, receiptDate, filingMethod, reportFrom, reportThrough, amendsReport, amendedByReport, url, monitorId, runId, discoveredAt, eventId, billed

Billing

Pay-per-event: new-or-amended-filing-detected — charged only for a filing genuinely new (or, defensively, 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.

Not the same product as existing WA lobbying actors

A prior Apify actor covering Washington lobbying (j0401/wa-lobbying) pulls from two different datasets — "Lobbyist Compensation by Source" (9nnw-c693) and "PDC Enforcement" (a4ma-dq6s) — and bills $0.003/record as a one-off snapshot (confirmed live against its own listing and OpenAPI schema 13.09.2026: no mention anywhere of monitor/delta/sinceLastRun/scheduled behavior). This actor watches the Lobbyist Reporting History dataset (nuwx-ay5h) specifically, bills only newly-detected filings per watch per run, and is built around amendments as a first-class, explicitly filterable signal (amendmentsOnly) — a genuinely different source, filter, and billing model, not a repriced clone.

Important — read before relying on this for compliance decisions

The PDC's own dataset does not claim to be a real-time or complete record of lobbying activity in Washington — filings are self-reported by lobbyists/firms/clients, paper filings (filingMethod: "Paper", ~4,500 of 201,431 rows as of 13.09.2026) carry no machine-readable reportData, and a separate 180 rows are missing year/reportFrom/reportThrough entirely. This actor is an informational monitor of NEW or AMENDED filings in the official registry — it is NOT a compliance/risk verdict on any lobbyist, firm, or client, and it is NOT legal advice. Always confirm directly with the PDC (or counsel) before acting on any single filing.

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 new or amended filing. 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. 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 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 and Apify's own billing ledger are the source of truth for confirmed payment, not the presence of a row in either dataset.
  • Each watch sees only a bounded window of the newest matching filings per run (FETCH_WINDOW_SIZE = 1000, ordered receipt_date DESC then id DESC). coverage.watches[].windowFull flags when a watch's real match count (from its own count(*)) exceeded that window in this run — older matches beyond the window are honestly not shown this run, not silently dropped forever (the next run's window will still be newest-first, so a very high-volume unfiltered watch could in theory miss a burst of >1000 filings between two runs — narrow such a watch with entityId/filerNameContains/reportTypes if that matters to you). coverage.watches[].matchedCount is this watch's own count(*) and may differ by a handful of rows from what the window actually returned, purely because the two queries are separate HTTP requests against a live, continuously-growing dataset (not a transactional snapshot) — this is not treated as a structural error.
  • coverage.sourceAccessLimitedCount is a separate, unrelated counter from windowFullCount: it only counts watches where the Socrata response itself could not be trusted this run (malformed JSON, a non-numeric id, a required field missing/blank, or a nullable field present as a whitespace-only string instead of a real null — see below), never a watch that simply had more matches than the window.
  • Row-level identity depends on every filing in the window supplying a non-empty, purely numeric id and a non-empty url — both true for all 201,431 rows verified live on 13.09.2026. origin, filerName, filerType, receiptDate, and filingMethod are likewise required and never null on any live row checked. If any row in a watch's window fails any of these checks, the whole run for that watch is reported honestly as source_access_limited — no partial baseline/delta/billing happens on a window whose completeness can't be trusted, rather than silently skipping just that one row.
  • entityId is not always populated, unlike its own field description suggests — discovered by a live smoke test while building this actor, not assumed from documentation: 11,876 rows (all
    filerType: "Public agency"
    ) have no entity_id at all
    , only a client_id. entityId-filtered watches still match such rows via client_id; the output field itself is honestly null for them, not guessed.
  • Nullable fields (entityId, firmId, clientId, year, reportFrom, reportThrough, amendsReport, amendedByReport) are genuinely absent on a real share of live rows (confirmed by count per field, 13.09.2026) — but a whitespace-only string in any of them is not treated as an equivalent to null (Socrata itself never represents null that way): such a row fails closed into source_access_limited for that watch's run rather than being silently coerced to empty.
  • History is kept per filing id (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 filing that were ever revised and later reverted to an earlier state would be billed correctly on every genuine transition, never silently deduplicated against an older occurrence of the same content.
  • We don't invent data: if Socrata's response shape changes (not JSON, not a list, count(*) unparseable), the run reports it honestly (structure_changed: ...) instead of silently returning zero results.

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