Article type: How-to guide (one task: check whether a Texas company still has its right to transact business)

Prerequisites: Python 3 with requests. No API key or account is needed for the open data. An Apify account only if you use the hosted version at the end.

Tested on: 14 November 2026

If you onboard Texas vendors or customers, sooner or later someone asks: "is this company still in good standing in Texas?" In Texas the useful answer comes from the Texas Comptroller of Public Accounts, not just the Secretary of State. The Comptroller tracks each company's franchise tax account, and a company that falls behind can lose its right to transact business in Texas.

The good news is that the Comptroller publishes this as open data. All the data in this post comes from the Texas Comptroller of Public Accounts' Active Franchise Taxpayers dataset on data.texas.gov, the state's open data portal. It lists taxpayers set up for franchise tax under Tax Code Chapter 171: 3,533,311 rows on 14 November 2026, refreshed that day. The Comptroller says the information on its sites is public domain (policy). You can query it with one GET request, without a key.

This post covers the three columns that matter, a short Python status check, a name search, and the gotchas we hit in the raw data.

What does "good standing" mean in Texas?

The Comptroller no longer uses the phrase. The Texas Secretary of State explains that "certificate of good standing" is sometimes used for the Comptroller's certificate about an entity's franchise tax account status, now called a "certificate of account status". The Comptroller says that certificate is only required when a company terminates, and that banks don't need it. For proof that a franchise tax account has an "active" right to transact business, it points to its own search.

So in practice, "good standing in Texas" usually means two things, and this dataset has a column for each:

Column What it is (from the Comptroller's record layout)
right_to_transact_business_code The franchise tax side: A = Active, D = Active - Eligible for Termination/Withdrawal, N = Forfeited, I = Franchise Tax Involuntarily Ended, U = Franchise Tax Not Established, blank = Franchise Tax Ended
sos_status_code The Secretary of State charter or registration: A = Active, R = Reinstated, D = Dissolved, F = Forfeited Franchise Tax, M = Merger, W = Withdrawn, and others
record_type_code U = a Texas SOS file number, V = an SOS certificate of authority (an out-of-state company registered in Texas), X = a Comptroller-assigned number with no current SOS charter

The full code lists are in the record layout file attached to the dataset ("Franchise Layout.docx" on the dataset page).

How do I look up one company?

Each row has the Comptroller's 11-digit taxpayer number and, for most rows, the Secretary of State file number. Filter on either with SoQL's $where:

curl -sG https://data.texas.gov/resource/9cir-efmm.json \ --data-urlencode "\$where=taxpayer_number='17415632409'" \ --data-urlencode '$select=taxpayer_number,taxpayer_name,record_type_code,secretary_of_state_sos_or_coa_file_number,sos_status_code,right_to_transact_business_code' 
[{"taxpayer_number":"17415632409","taxpayer_name":"SOUTHWEST AIRLINES CO.","record_type_code":"U","secretary_of_state_sos_or_coa_file_number":"0023294100","sos_status_code":"A","right_to_transact_business_code":"A"}] 

Right to transact A, SOS status A: active on both sides.

How do I check the status in Python?

This function accepts either ID, strips the stray whitespace the file contains (see the gotchas), reads both codes and returns one status plus the Comptroller's own wording. pip install requests, then save as tx_status.py:

import requests URL = "https://data.texas.gov/resource/9cir-efmm.json" # Texas Comptroller: Active Franchise Taxpayers COLS = ("taxpayer_number,taxpayer_name,taxpayer_organizational_type,record_type_code," "secretary_of_state_sos_or_coa_file_number,sos_charter_date," "sos_status_code,right_to_transact_business_code") RTB = {"A": "Active", "D": "Active - Eligible for Termination/Withdrawal", "N": "Forfeited", "I": "Franchise Tax Involuntarily Ended", "U": "Franchise Tax Not Established", "": "Franchise Tax Ended"} SOS = {"A": "Active", "R": "Reinstated", "B": "Consolidated", "C": "Converted", "D": "Dissolved", "E": "Expired", "F": "Forfeited Franchise Tax", "G": "Miscellaneous", "I": "Closed by FDIC", "J": "State Charter Pulled", "K": "Forfeited Registered Agent", "L": "Forfeited Registered Office", "M": "Merger", "N": "Forfeited Hot Check", "P": "Forfeited Court Order", "T": "Terminated", "W": "Withdrawn", "Y": "Dead at Conversion 69", "Z": "Dead at Conversion 83", "": "none (no current SOS charter)"} SOS_CLOSED = set("BCDEIJMTWYZ") | set("FKLNP") # ended, or charter/registration forfeited PERSON_TYPES = {"IS", "S", "ES", "PI", "PZ", "TR", "GL"} # tied to individuals: skip def fetch(texas_id): texas_id = texas_id.strip() if not texas_id.isdigit(): raise ValueError("Texas IDs are digits only") # 11 digits = Comptroller taxpayer number; up to 10 = SOS file number. col = "taxpayer_number" if len(texas_id) == 11 else "secretary_of_state_sos_or_coa_file_number" # starts_with, not '=': some IDs in the file carry a trailing space. r = requests.get(URL, params={"$select": COLS, "$where": f"starts_with({col}, '{texas_id}')", "$order": "sos_status_date DESC", "$limit": 10}, timeout=30) r.raise_for_status() return [row for row in r.json() if row.get(col, "").strip() == texas_id] def status(texas_id): rows = fetch(texas_id) if not rows: return {"id": texas_id, "status": "not_listed"} row = rows[0] # a few taxpayer numbers appear twice; newest SOS status first if row.get("taxpayer_organizational_type", "").strip() in PERSON_TYPES: return {"id": texas_id, "status": "skipped (individual)"} rtb = row.get("right_to_transact_business_code", "").strip() sos = row.get("sos_status_code", "").strip() if sos in SOS_CLOSED: result = "inactive" # charter or registration ended or forfeited elif rtb in ("A", "D") and sos in ("A", "R", ""): result = "active" elif rtb == "N" and sos in ("A", "R", ""): result = "forfeited" # right to transact forfeited, charter still on file else: result = "unknown" return {"id": texas_id, "name": row["taxpayer_name"].strip(), "status": result, "detail": f"Right to transact: {RTB.get(rtb, rtb)}; SOS status: {SOS.get(sos, sos)}"} if __name__ == "__main__": for texas_id in ["17415632409", "0023294100", "17502899705", "00000000000"]: print(status(texas_id)) 

Output on 14 November 2026:

{'id': '17415632409', 'name': 'SOUTHWEST AIRLINES CO.', 'status': 'active', 'detail': 'Right to transact: Active; SOS status: Active'} {'id': '0023294100', 'name': 'SOUTHWEST AIRLINES CO.', 'status': 'active', 'detail': 'Right to transact: Active; SOS status: Active'} {'id': '17502899705', 'name': 'TEXAS INSTRUMENTS INCORPORATED', 'status': 'active', 'detail': 'Right to transact: Active; SOS status: Active'} {'id': '00000000000', 'status': 'not_listed'} 

The second line is the same company found by its Secretary of State file number. Keep the leading zeros: the file stores the IDs as text.

Why the rule looks like this:

  • A closed or forfeited charter wins. If the Secretary of State record is dissolved, merged, withdrawn or forfeited, the company is inactive, whatever the franchise tax code says.
  • Right to transact decides the rest. A or D with an active or reinstated charter is active. N is forfeited: the charter is still on file, but the right to transact business has been forfeited.
  • Everything else is unknown, including "Franchise Tax Not Established". Treat it as "check by hand", not as a pass.
  • Rows for individuals are skipped. A few organisation types in the file are tied to people (sole owners, sole proprietorships, estates, trusts, individual partnerships, local officials). You don't need them for a company check, so don't store them.

The function returns the Comptroller's own wording in detail, so you can make the rule stricter for your use case. We ran the forfeited, inactive and unknown branches on real rows too; they return as described.

How common is a forfeited right to transact?

Very. The dataset is called "Active Franchise Taxpayers", but on 14 November 2026 its right-to-transact codes were:

right_to_transact_business_code Meaning Rows
A Active 2,997,113
N Forfeited 464,864
U Franchise Tax Not Established 45,925
D Active - Eligible for Termination/Withdrawal 25,409

In 347,193 rows, the right to transact is forfeited while the Secretary of State record still reads Active or Reinstated. So "it's in the dataset" or "the SOS says Active" is not enough on its own. Read the right-to-transact code.

How do I search by name?

Names are stored in upper case (every row on 14 November 2026), so upper-case the prefix and use starts_with:

import requests def search_tx(prefix, limit=5): prefix = prefix.upper().replace("'", "''") # names are stored upper-case; SoQL escapes ' as '' r = requests.get( "https://data.texas.gov/resource/9cir-efmm.json", params={ "$select": "taxpayer_number,taxpayer_name,record_type_code,sos_charter_date," "sos_status_code,right_to_transact_business_code", "$where": f"starts_with(taxpayer_name, '{prefix}')", "$order": "taxpayer_name", "$limit": limit, }, timeout=30, ) r.raise_for_status() return r.json() for row in search_tx("Southwest Airlines"): print(row["taxpayer_number"], row["taxpayer_name"], row["record_type_code"], row.get("sos_charter_date", "")[:10], row.get("sos_status_code"), row.get("right_to_transact_business_code")) 
17415632409 SOUTHWEST AIRLINES CO. U 1967-03-09 A A 30113904590 SOUTHWEST AIRLINES EMPLOYEES CATASTROPHIC ASSISTAN U 1990-01-30 A A 32078600023 SOUTHWEST AIRLINES GOLDEN EAGLES U 2021-04-06 A A 

Once you've found the right row, switch to the taxpayer number for every later check. The second line shows why.

Gotchas from the raw data

  1. Names are cut off at 50 characters. 39,628 names are exactly 50 characters long, and some are visibly truncated ("...CATASTROPHIC ASSISTAN" above). An exact-name match on a long name can fail. Match on the taxpayer number.
  2. Stray whitespace in IDs and codes. 287 taxpayer numbers have a trailing space, so taxpayer_number='...' misses them. That's why the function uses starts_with and then compares the stripped value. Codes have it too: 270 right-to-transact codes are "A " and 2 are "A\n". Strip before you compare.
  3. The two codes can disagree. 8,890 rows have SOS status F (Forfeited Franchise Tax) while the right-to-transact code still reads A. Decide which one wins (the function above treats a forfeited charter as inactive) and keep both in your logs.
  4. Out-of-state companies look different. Texas Instruments Incorporated is record type V: its number is a certificate of authority, the registration of a company formed in another state. 277,936 rows are type V. Another 243,122 are type X with no SOS file number at all, so look those up by taxpayer number.
  5. A few taxpayer numbers appear twice (31 on 14 November 2026). The function sorts by sos_status_date and takes the newest.
  6. Some charter dates are in the future. 129 rows have an SOS charter date after 14 November 2026, the latest being 2027-01-20. Don't use the charter date as "formed on or before today" without checking.

Rate limits and freshness

You don't need a token, but the portal throttles anonymous requests by IP address, and its robots.txt asks for a 1-second crawl delay. For more than a handful of checks, register a free app token on the portal, send it in the X-App-Token header, and keep to about one request a second.

The dataset is a copy the Comptroller publishes on its own schedule (this one was refreshed on 14 November 2026). Before you act on a single result, such as refusing a vendor, confirm it on the Comptroller's Franchise Tax Account Status Search, which takes the 11-digit taxpayer number or the 9-digit federal EIN.

The hosted version (Disclosure: we built this)

We built an Apify Actor, Secretary of State Business Entity Search, that runs this Texas lookup alongside New York, Colorado, Oregon and Connecticut, in one schema. For Texas it reads the same Comptroller dataset. You can look up by taxpayer number or SOS file number, and status comes from the right-to-transact code and the SOS status code, with both wordings in statusDetail. Rows for individuals are skipped and never charged.

It can also check a list of up to 500 company names. Matching ignores case, punctuation and the legal ending (LLC, Inc. and so on); other spelling has to match. Each match comes back as an entity row, and a name with no match gets a free no_match row. For example (not run for this post):

{ "mode": "nameCheck", "states": ["TX"], "names": ["Southwest Airlines Co.", "Texas Instruments Incorporated"], "maxResults": 50 } 

It's pay per event: $0.0025 per entity record (base price; higher Apify plans pay less), plus Apify's $0.00005 Actor start per run (per GB of memory; a default run counts once). Status rows such as no_match are free. maxResults (default 50) caps a run, so raise it for long lists. In our test on 14 November 2026, 100 names across all five states took about 80 seconds; picking only Texas checks fewer states. The Python above is all you need if you'd rather run it yourself.

Secretary of State Business Entity Search on Apify

Data: Texas Comptroller of Public Accounts, Active Franchise Taxpayers (data.texas.gov). This post and the Actor are not affiliated with or endorsed by the Comptroller or the Texas Secretary of State.

Your Evenings