# TX Sales Tax Permits - Active Permit Holders (`j0401/tx-sales-tax-permits`) Actor

Texas Comptroller active sales-tax permit register (public data, 887k permits across 702k taxpayers): outlet name and address, NAICS industry code, city-limits flag, permit and first-sales dates - plus the taxpayer's own registration address and county.

- **URL**: https://apify.com/j0401/tx-sales-tax-permits.md
- **Developed by:** [Wenhao Yang](https://apify.com/j0401) (community)
- **Categories:** Business
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.03 / 1,000 tx sales tax permit records

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## TX Sales Tax Permits - Active Permit Holders

### Low cost

**From $0.00005 per record, down to $0.00003 at Gold** - pay per record delivered, and nothing for the query. Every active sales-tax permit in Texas, billed by the permit.

The Texas Comptroller's register of businesses permitted to collect sales tax: 887,244 permits held by 702,193 taxpayers, refreshed daily.

### What you get

Each row is one **outlet permit** - a business can hold many.

| | |
|---|---|
| who | taxpayer number and registered name, plus the outlet's trade name and number |
| where it sells | outlet street address, city, state, ZIP, county, and whether it sits inside city limits |
| where it is registered | the taxpayer's own mailing address, city, state, ZIP and county |
| industry | the outlet's NAICS code |
| when | permit issue date and first-sales date |

### The two addresses are the point

Every row carries two addresses, and they are not copies of each other. One is where the business is registered; the other is where this particular permit is. They disagree on **518,696 rows** for the street address - **over half the register** - and on **259,054** for the county, of which 162,162 are a genuine county-to-county difference (the rest are rows where the taxpayer's county was never reported).

That gap is the product: "registered in Austin, selling in Houston" is answerable here, and a dataset carrying one address would silently tell you the wrong city for more than half of Texas permits.

### Modes

- **permits** (default) - individual permits, newest permit first
- **aggregate** - one count row per group: by county, NAICS sector, organization type, city-limits status, or permit year

Filter by taxpayer number, business name, city, ZIP, county (by name or Comptroller code), NAICS industry, inside/outside city limits, and issue / first-sales dates.

### Example inputs

**Every outlet permit one taxpayer holds** - `taxpayerNumber` is the exact 11-digit Comptroller number, so one call returns that business's whole permit list.

```json
{ "taxpayerNumber": "17515166332", "maxResults": 10 }
```

**Full-service restaurants in Houston** - `naicsCode` takes the 6-digit industry, `city` is a case-insensitive substring.

```json
{ "city": "HOUSTON", "naicsCode": "722511", "maxResults": 10 }
```

**Retail outlets inside Harris County city limits** - `countyCode=101` is Harris, `inCityLimits=Y` is the city-limits flag.

```json
{ "countyCode": "101", "inCityLimits": "Y", "maxResults": 10 }
```

**The industry mix** - `aggregate=true` and `groupBy=naicsSector` roll the register up by NAICS sector.

```json
{ "aggregate": true, "groupBy": "naicsSector" }
```

### Why this is hard

**The county is a code, and the codes are numbered the hard way.** The Comptroller numbers Texas's 254 counties by name with the spaces removed - so LA SALLE collates as `LASALLE` and lands after Lampasas, not up with the other La- counties. Sorting the 254 names alphabetically puts ten of them in the wrong slot, which is how a county lookup quietly returns the neighbouring county's permits. This register resolves the code to a county name against a table checked both ways, and the aggregate carries the name alongside the code.

**96,896 permit rows (23,820 taxpayers) have no county reported** - the code comes through as `000`. That is not a missing value to paper over; it is the register saying the county was not given, and it is kept distinguishable from a real county rather than folded into one.

**The industry code is a number, not a string.** It looks like a text identifier and behaves like one to a reader, but treating it as text fails at the source - every text operation on it is rejected. Groups and prefixes have to be expressed numerically.

**And the organization-type field is 62 two-letter codes with no legend anywhere in the dataset.** There is no published mapping from `CL` / `IS` / `CT` to anything a buyer could act on, so it is offered only as a count dimension - never as a filter you would have to guess the codes for.

### Output

One schema in every mode: each record carries the full field set, empty where the source has nothing, and dates arrive as `YYYY-MM-DD`. Every record carries `sourceUpdatedAt`, the register's own last-refresh timestamp. Aggregate rows use the same schema, with `groupKey` / `groupCount` filled.

### Example output

**`aggregate=true`, `groupBy=naicsSector`** - one row per industry sector; sector 45 is retail trade. Aggregate rows keep the permit schema, with `groupKey` / `groupCount` / `groupBy` filled and the permit fields empty:

```json
{
 "firstSalesDate": "",
 "inCityLimits": "",
 "naicsCode": "",
 "outletAddress": "",
 "outletCity": "",
 "outletCountyCode": "",
 "outletCountyName": "",
 "outletName": "",
 "outletNumber": "",
 "outletState": "",
 "outletZip": "",
 "permitIssueDate": "",
 "sourceUpdatedAt": "2026-09-19T08:05:21Z",
 "taxpayerAddress": "",
 "taxpayerCity": "",
 "taxpayerCountyCode": "",
 "taxpayerCountyName": "",
 "taxpayerId": "",
 "taxpayerName": "",
 "taxpayerState": "",
 "taxpayerZip": "",
 "platform": "tx-sales-tax-permits",
 "source": "tx-comptroller-sales-tax-permits",
 "groupKey": "45",
 "groupCount": "191979",
 "groupBy": "naicsSector"
}
```

### Notes

- Public open data from the Texas Comptroller. No login, no scraping.
- Charges are metered per record delivered, so a single taxpayer lookup costs a fraction of a cent.

# Actor input Schema

## `taxpayerNumber` (type: `string`):

Exact Texas taxpayer number (11 digits) -> every outlet permit that taxpayer holds. Example: 17515166332.

## `name` (type: `string`):

Business-name substring, case-insensitive. Matches either the taxpayer's registered name or the outlet's trade name.

## `city` (type: `string`):

Outlet city substring, case-insensitive. Example: HOUSTON.

## `zip` (type: `string`):

Outlet ZIP prefix, e.g. 770 or 78701.

## `county` (type: `string`):

Texas county name where the outlet sits, e.g. Harris. Give the full name - a partial name is rejected when it matches several counties.

## `countyCode` (type: `string`):

Comptroller county code (the same numbers used across Texas Comptroller data), e.g. 101 = Harris. Use either this or the county name.

## `naicsCode` (type: `string`):

NAICS industry of the outlet, as a number: 6 digits for the exact industry (722511 = full-service restaurants), or fewer for a whole group - 72 = accommodation & food services, 722 = food services. Codes in this register are six digits (two stray short values exist).

## `inCityLimits` (type: `string`):

Y = the outlet is inside city limits (~685k), N = outside (~202k). Blank = both.

## `issuedFrom` (type: `string`):

Only permits issued on/after this date (YYYY-MM-DD).

## `issuedTo` (type: `string`):

Only permits issued on/before this date (YYYY-MM-DD).

## `firstSalesFrom` (type: `string`):

Only permits whose first sales date is on/after this date (YYYY-MM-DD).

## `firstSalesTo` (type: `string`):

Only permits whose first sales date is on/before this date (YYYY-MM-DD).

## `aggregate` (type: `boolean`):

When on, returns one summary record per group (see groupBy) with count - instead of individual permits.

## `groupBy` (type: `string`):

Which dimension to aggregate over. countyCode -> permits per county; naicsSector -> the industry mix (44/45 retail, 72 food & accommodation, ...); permitYear -> how many permits were issued each year.

## `maxResults` (type: `integer`):

Cap the number of permit records pushed (0 = up to ~10k per run; each record is metered individually, so there is no per-run charge cap). Does not apply in aggregate mode, which returns every group of the chosen dimension.

## Actor input object example

```json
{
  "taxpayerNumber": "",
  "name": "",
  "city": "",
  "zip": "",
  "county": "",
  "countyCode": "",
  "naicsCode": "",
  "inCityLimits": "",
  "issuedFrom": "",
  "issuedTo": "",
  "firstSalesFrom": "",
  "firstSalesTo": "",
  "aggregate": false,
  "groupBy": "countyCode",
  "maxResults": 50
}
```

# Actor output Schema

## `recordsUrl` (type: `string`):

Texas active sales-tax permit records or aggregates - as JSON

## `datasetUrl` (type: `string`):

No description

## `runUrl` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {};

// Run the Actor and wait for it to finish
const run = await client.actor("j0401/tx-sales-tax-permits").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {}

# Run the Actor and wait for it to finish
run = client.actor("j0401/tx-sales-tax-permits").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{}' |
apify call j0401/tx-sales-tax-permits --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,j0401/tx-sales-tax-permits"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/p8AQq9qVRfdfe8KQo/builds/AF6kP1VQcqMPW4fXg/openapi.json
