TX Franchise Tax Entity Status Delta avatar

TX Franchise Tax Entity Status Delta

Pricing

$10.00 / 1,000 entity status changeds

Go to Apify Store
TX Franchise Tax Entity Status Delta

TX Franchise Tax Entity Status Delta

Watches named Texas businesses in the Comptroller Franchise Tax registry, alerting only on a genuine SOS status or right-to-transact change (Forfeited, delinquent, reinstated). For lenders, title companies, due-diligence, procurement compliance. Free when nothing changes.

Pricing

$10.00 / 1,000 entity status changeds

Rating

0.0

(0)

Developer

Radu Furtuna

Radu Furtuna

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

9 days ago

Last modified

Share

Durable monitor for the official Active Franchise Taxpayers registry, published by the Texas Comptroller of Public Accounts as open data on data.texas.gov. Watch specific business entities by their Texas Taxpayer Number and get notified only when their SOS status or right-to-transact-business status genuinely changes. No API key needed, no login, no captcha.

Source

https://data.texas.gov/resource/9cir-efmm.json — the Socrata Open Data API for "Active Franchise Taxpayers" (dataset id 9cir-efmm). Confirmed live 13.09.2026: 3,454,402 rows, table last refreshed 2026-09-12T10:10:15Z (daily refresh). Despite the dataset's name, it is NOT limited to currently-active businesses — it publishes the full disciplinary spectrum: sos_status_code (A=Active 2,803,556, R=344,483, F=Forfeited 57,789, plus rarer codes, and 240,573 rows with no SOS status at all — legitimate for taxpayers never registered with the Secretary of State) and right_to_transact_business_code (A=OK 3,258,835+, D=Delinquent 20,303, N=Not in good standing 129,757, present on 100% of rows).

taxpayer_number (Texas Comptroller Taxpayer Number, not the Secretary of State file number and not an EIN) is the dataset's identifier for one taxpayer — confirmed live: RECORDS live 11 digits long, no leading zero observed across all 3.45M rows. It is almost, but not perfectly, unique: count(distinct taxpayer_number) = 3,454,371 vs count(*) = 3,454,402 — 31 values are duplicated (exact-content duplicate rows, confirmed live on one such value). The Secretary of State file number was evaluated as an alternative key and rejected: count(distinct ...) = 3,213,780 (240,622 short of total) — too many blank/colliding values to serve as a primary key.

How it works

  1. Each watch names exactly one business entity by taxpayerNumber (the Texas Comptroller Taxpayer Number, exactly 11 digits) — look it up at mycpa.cpa.state.tx.us/coa/ (Texas Taxable Entity Search) if you only have a company name. One watch = one point query ($where=taxpayer_number='<taxpayerNumber>') = one HTTP request per run.
  2. The first check of a new watch establishes a baseline (no charge). Every later check compares the current pair (sosStatusCode, rightToTransactCode) against the durable record of what it was last time.
  3. Billing is tied only to that pair — not to name, address, NAICS code, or any other administrative field that can change without affecting legal standing. A status that changes and later reverts (e.g. Forfeited, then reinstated, then Forfeited again for a new lapse) bills every genuine transition, not just the first one — never silently deduplicated against an earlier occurrence of the same status value.
  4. Genuinely new (first found) or status-changed records are pushed to the dataset and billed once each (entity-status-changed); a check that finds nothing new costs nothing beyond the fixed platform run cost.

Input

{
"monitorId": "my-tx-franchise-watch",
"watches": [
{ "watchId": "vendor-abc", "taxpayerNumber": "32095750629" }
],
"notifyOn": "new_alerts",
"webhookUrl": "https://example.com/webhook"
}

Add more businesses later under the same monitorId — each watch keeps its own independent history. A watchId is permanently bound to the taxpayerNumber it first saw; pointing the same watchId at a different taxpayer 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, taxpayerNumber, changeType ("new"|"status_changed"), sosStatusCode, previousSosStatusCode, rightToTransactCode, previousRightToTransactCode, taxpayerName, taxpayerAddress, taxpayerCity, taxpayerState, taxpayerZip, taxpayerCountyCode, taxpayerOrganizationalType, recordTypeCode, responsibilityBeginningDate, secretaryOfStateFileNumber, sosCharterDate, sosStatusDate, currentExemptReasonCode, exemptBeginDate, naicsCode, monitorId, runId, discoveredAt, eventId, billed

Billing

Pay-per-event: entity-status-changed — charged only for a watch's first found record (baseline is free) or a genuine transition of the (sosStatusCode, rightToTransactCode) pair since the previous check. Failed/blocked checks are never charged.

Important — read before relying on this for any lending/due-diligence/procurement decision

This registry is a publication of Comptroller franchise-tax and SOS-status data, not a certification of good standing or a complete corporate/legal record. The Texas Secretary of State and Comptroller maintain their own authoritative records; the public dataset reflects what has been published, which can lag real-world filings. This actor is an informational monitor of CHANGES to that public publication — it is NOT a current "good standing" certification, NOT a full UCC/lien/litigation check, and NOT legal advice. Always confirm directly at mycpa.cpa.state.tx.us/coa/ or SOSDirect before acting on any single entry — especially for lending, title/closing, vendor onboarding, or procurement 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 entity'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

  • 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.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 business entity, one request. There is no bulk/roster mode — to track a portfolio, add one watch per entity (up to 30 per run). This keeps the network cost fixed and predictable regardless of the registry's total size (3.45M+ rows), and keeps each entity'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. The registry does not delete records (confirmed live 13.09.2026: entities forfeited in 1974/1978/1980, and entities chartered in 1843/1860/1866, are all still returned by a direct point query), so a typo'd taxpayerNumber will simply never match; it costs nothing and is safe to correct and retry under the same watchId.
  • A taxpayerNumber 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 the registry doesn't delete rows (see above), this should never happen from a genuine data change; it's the honest fallback if the source ever answers unexpectedly.
  • A known, rare defect of this specific source, documented rather than silently patched over: 296 of 3,454,402 rows (confirmed live 13.09.2026) carry taxpayer_number with one trailing space baked into the value itself (e.g. "10101858008 " instead of "10101858008"). Socrata does exact string matching — a clean 11-digit query for that value returns zero rows; only a query including the literal trailing space finds it. Because taxpayerNumber input is validated as exactly 11 digits, these 296 entities (0.009% of the registry) cannot be watched by their number as printed elsewhere — this is a source data-quality gap, not an actor bug, and is called out here rather than worked around with a fuzzy match that would weaken the exact-id guarantee below.
  • Every response is checked that the raw taxpayer_number value returned equals, byte-for-byte, the value that was requested — before any trimming. This is not a theoretical concern for this source: see the trailing-space defect above. If the source were ever to answer a clean query with a padded or otherwise different raw id (a matching-behavior change on Socrata's end), that mismatch is treated as source_access_limited for this watch, not silently accepted.
  • taxpayerNumber is a near-unique key, not a perfectly unique one. 31 of 3,454,402 values are duplicated (exact-content duplicate rows, confirmed live). A watch on one of those 31 values reports source_access_limited (2+ rows on what should be a unique key) rather than silently picking one row.
  • Billing tracks only the pair (sosStatusCode, rightToTransactCode), deliberately excluding name, address, NAICS code, and date fields that can change administratively (a move, a rebrand, an updated annual report) without any change to the entity's legal standing before the state. Those fields are still delivered in every row for context.
  • sosStatusCode can legitimately be absent — confirmed live: 240,573 of 3,454,402 rows carry no sos_status_code at all, typically taxpayers never registered with the Secretary of State (sole proprietors, certain partnerships). This is not an error; the watch simply reports null until/unless the entity later registers.
  • We don't invent data: if the API ever returns something other than a bare JSON array, more than one row for a single taxpayer_number (violating the dataset's own near-uniqueness), or a row missing taxpayer_number, the run reports it honestly (source_access_limited) instead of guessing.

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