# Florida Property Tax Roll - DOR NAL Assessments (`j0401/fl-tax-roll-nal`) Actor

Florida DOR statewide property tax roll (NAL), all 67 counties (public record): just/assessed/taxable values, land and homestead value, owner and mailing address, situs address, building attributes and the latest sale with its DOR code. Search by owner, county, value or use code.

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

## Pricing

from $0.03 / 1,000 florida property tax roll (nal) parcel 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

## Florida Property Tax Roll - DOR NAL Assessments (All 67 Counties)

The Florida Department of Revenue publishes each county's property assessment roll - the **NAL** (Name-Address-Legal) file - as free public data. This actor unifies **all 67 counties** into one queryable parcel register and turns it into a **charged-per-record lookup and screening tool**.

**Built for:** property and title diligence, tax-lien and assessment research, owner and absentee-owner screening, land-assembly and development analysis, and statewide tax-base analysis - one query across the whole state instead of 67 separate county downloads.

### What it covers

**11,093,710 parcels across all 67 Florida counties** (as of the last refresh, 2026-10-07):

> **Final or preliminary?** DOR publishes a county's roll as a **Preliminary** file first and replaces it with a **Final** (certified) file once the appraiser certifies it. The actor always takes a county's Final file where one exists - but at the last refresh **40 counties were on their Final roll and 27 were still preliminary** (about 35% of the parcels). So a figure for a given county can be a preliminary number until DOR supersedes it. The state's roll year (`assessmentYear`) is 2026 throughout.

| County | Parcels | County | Parcels |
|---|---|---|---|
| Miami-Dade | 938,308 | Orange | 494,750 |
| Broward | 754,549 | Polk | 435,650 |
| Palm Beach | 657,490 | Pinellas | 433,770 |
| Lee | 561,559 |  all 67 counties | |
| Hillsborough | 527,636 | | |

Each record carries its **county**, **parcel id**, assessment year, **DOR land-use code** and appraiser use code, the **value set** - **just (market) value**, **assessed value (school / non-school)**, **taxable value**, **land value**, **homestead value** - the **owner name and mailing address**, the **physical (situs) address**, building attributes (**construction class, effective and actual year built, total living area, building and unit counts**), the **legal description**, township/range/section, the appraiser's status code - and, where the roll records one, the **most recent sale** (price, year, month and its DOR **sale qualification code**). Most parcels carry no recorded sale (about 87% of the register), and the optional columns - building attributes, situs address, appraiser status - are filled unevenly from county to county, sometimes on most parcels and sometimes on very few. The identity and value columns (`parcelId`, `county`, `justValue`, `ownerName`) are complete; treat the rest as sparse.

> **How the values work.** The roll is the appraiser's figures for the assessment year shown (Final where certified, otherwise preliminary). `justValue` is market value; `assessedValueNonSchool` / `taxableValueNonSchool` are the assessed and taxable figures after the Save-Our-Homes cap and exemptions (so a long-time homestead parcel often shows a market value far above its taxable value). `homesteadValue` is populated on parcels carrying a Florida homestead exemption - **a positive homestead value is the quickest signal of an owner-occupied property**, and it separates homestead from investment parcels in one filter.

### Typical questions

- "What is the **assessed and market value** of this parcel, and who owns it?"
- "Show every **Miami-Dade parcel with a just value of $1,000,000 or more**."
- "Find **homestead parcels over $1M** in this county."
- "Who owns **this physical address**?" (owner screening, absentee detection)
- "Aggregate the roll **by county and land-use code**."
- "What is the **most recent sale** of a parcel, and was it a qualified arm's-length sale?"

### Inputs

| Input | What it does |
|---|---|
| `parcelId` | exact county parcel id (combine with `county`) |
| `county` | county name, or the 2-digit DOR county number. Spacing and punctuation are ignored (`Miami Dade` = `Miami-Dade`, `St Johns` = `St. Johns`, `Broward County` = `Broward`); a value matching no county is rejected |
| `ownerName` | owner name substring, case-insensitive |
| `city` / `zipCode` | situs (physical) city / ZIP prefix |
| `dorUseCode` | Florida DOR land-use code (e.g. `001` single family, `004` condo) |
| `appraiserStatusCode` | appraiser assessment status code |
| `valueMin` / `valueMax` | just-value range (USD) |
| `landValueMin` / `landValueMax` | land-value range (USD) |
| `homesteadOnly` | true = homestead parcels only |
| `saleYearFrom` / `saleYearTo` | most-recent-sale year window |
| `aggregate` / `groupBy` | one row per group with a count - county / use code / status / year / construction class. Returns **every** group and is not bounded by `maxResults` (the dimensions are small: 67 counties, ~100 use codes) |
| `maxResults` | cap **parcel** records in a search/lookup (default 200; 0 = the same 200; maximum 20,000). Ignored in aggregate mode |

**Default run = a tiny aggregate** (parcel count by county) - fast and safe for the daily auto-test, never a surprise full dump of a 10M-row register. For records, add a `county` / `ownerName` / `valueMin` filter or a `parcelId`.

> **A `county` filter is what makes a query fast.** It pins one county's file. **Without one, the run has to stream all 67 files (~11M parcels) - roughly 15-20 minutes** - and returns the first `maxResults` matches in county order, not the best ones. Every example below carries a `county` for that reason. A bare `parcelId` with no `county` is a full statewide scan too: parcel ids are only unique within a county, so pass both.

### Example inputs

**Look up one parcel** - combine the id with its county.

```json
{ "county": "Miami-Dade", "parcelId": "0101000000020" }
```

**High-value homestead parcels** - a positive homestead value marks owner-occupied.

```json
{ "county": "Broward", "homesteadOnly": true, "valueMin": 1000000, "maxResults": 10 }
```

**Owner screening** - find every parcel recorded to a name.

```json
{ "ownerName": "SMITH", "county": "Lee", "maxResults": 10 }
```

**The county roll** - parcel counts per county, statewide (all 67 groups).

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

### Low cost

**From $0.00005 per record, down to $0.00003 at Gold** - billed only for the rows you use, and each record is metered individually.

Florida's tax roll is **67 separate county files, ~830 MB of zips** totalling a multi-gigabyte register, with no query server behind them - there is no way to ask the state for "just Broward homestead parcels over $1M". Rather than make every lookup pay for the whole statewide download, the register is **mirrored**, and each run queries the mirror in a single pass. That mirror is what lets an 11-million-parcel register answer a county-scoped query in seconds at per-record pricing. The mirror is **integrity-checked as it is built** - a statewide parcel-count floor, a county-count floor and key-column coverage - so a degraded or schema-changed source is rejected instead of being published as a mirror.

### Example output

**A single-family homestead parcel** (`groupBy`/`mode` blank; one record):

```json
{
 "platform": "fl-tax-roll-nal",
 "source": "fl-dor-nal",
 "state": "FL",
 "refreshed": "2026-10-07",
 "mode": "record",
 "groupKey": "",
 "groupCount": "",
 "groupBy": "",
 "countyNumber": "40",
 "county": "Holmes",
 "parcelId": "032600000000012100",
 "assessmentYear": "2026",
 "dorUseCode": "001",
 "paUseCode": "00",
 "justValue": "206311",
 "assessedValueSchool": "206311",
 "assessedValueNonSchool": "206311",
 "taxableValueSchool": "181311",
 "taxableValueNonSchool": "104900",
 "homesteadValue": "206311",
 "landValue": "37500",
 "landSquareFeet": "4356000",
 "improvementQuality": "5",
 "constructionClass": "4",
 "effectiveYearBuilt": "2025",
 "actualYearBuilt": "2025",
 "totalLivingArea": "1482",
 "buildingCount": "1",
 "residentialUnits": "1",
 "salePrice": "45500",
 "saleYear": "2025",
 "saleMonth": "03",
 "saleQualificationCode": "03",
 "saleChangeCode": "1",
 "multiParcelSale": "",
 "ownerName": "WOODHAM GALE M",
 "ownerAddress": "3500 BAXLEY RD",
 "ownerCity": "GRACEVILLE",
 "ownerState": "FL",
 "ownerZip": "32425",
 "physicalAddress": "3500 BAXLEY RD",
 "physicalCity": "GRACEVILLE",
 "physicalZip": "32440",
 "legalDescription": "A LOT OR PRCL OF LAND ON HOCO",
 "neighborhoodCode": "",
 "township": "07S",
 "range": "14W",
 "section": "26",
 "appraiserStatusCode": "B",
 "taxAuthorityCode": "1"
}
```

**`aggregate=true`, `groupBy=county`** - parcels per county (top):

```
Miami-Dade     938,308
Broward        754,549
Palm Beach     657,490
Lee            561,559
Hillsborough   527,636
```

### Source

- [Florida DOR Property Tax Data Portal - Tax Roll Data Files](https://floridarevenue.com/property/dataportal/) - the official free public county roll files this actor reads (Fla. Stat. ch. 119).
- Assessment data is each county appraiser's roll - Final where DOR has certified it, otherwise the preliminary file - published by the Department of Revenue. It reflects the roll as of the last mirror refresh shown in `refreshed` on every record.

# Actor input Schema

## `mode` (type: `string`):

lookup = one parcel id's record. aggregate = one row per group. Leave blank to auto-route: a bare parcelId -> lookup, otherwise search (or aggregate when aggregate=true).

## `parcelId` (type: `string`):

Exact county parcel id (the NAL PARCEL_ID field). Combine with `county` to disambiguate, since parcel ids are only unique within a county.

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

Florida county name, substring, case-insensitive (e.g. "Miami-Dade", "Broward", "Lee"). Or pass the 2-digit DOR county number (e.g. "23"). Spacing and punctuation are ignored, so "Miami Dade", "St Johns" and "Broward County" all work; a value matching no county is rejected. Filtering by county also pins a single file, which is what keeps a run fast - without it the actor streams all 67 files (about 15-20 minutes).

## `ownerName` (type: `string`):

Owner name substring, case-insensitive (e.g. "SMITH" or "ACME HOLDINGS").

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

Property (physical) city, substring, case-insensitive.

## `zipCode` (type: `string`):

Property (physical) ZIP prefix, digits only.

## `dorUseCode` (type: `string`):

Florida DOR land use code, exact match (3 digits, zero-padded). Codes run in blocks: 000-009 residential (e.g. "001" single family, "004" condominium), 010-039 commercial, 040-049 industrial, 050-069 agricultural, 070-099 institutional, government and miscellaneous.

## `appraiserStatusCode` (type: `string`):

Appraiser assessment status code, exact match. The code is the county appraiser's own status marker; it is blank on most parcels (about 56% statewide), so it is a narrow filter rather than a primary dimension.

## `valueMin` (type: `number`):

Lowest just (market) value in USD. Value filters only return parcels that record a value.

## `valueMax` (type: `number`):

Highest just (market) value in USD.

## `landValueMin` (type: `number`):

Lowest land value in USD.

## `landValueMax` (type: `number`):

Highest land value in USD.

## `homesteadOnly` (type: `boolean`):

true = only parcels carrying a homestead value (a Florida homestead exemption); false = only non-homestead; leave unset for all.

## `saleYearFrom` (type: `string`):

Earliest sale year (inclusive), 4 digits. Filters on the most recent recorded sale year in the roll.

## `saleYearTo` (type: `string`):

Latest sale year (inclusive), 4 digits.

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

When on, returns one summary record per group (with a count) instead of individual parcel records.

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

county -> parcels per county; dorUseCode -> the land-use mix; assessmentYear -> the roll year; saleYear -> sales by year; constructionClass -> building class.

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

Cap the number of parcel records pushed in search/lookup mode (default 200; 0 = the same 200; maximum 20,000). Does not apply in aggregate mode, which returns every group of the chosen dimension.

## Actor input object example

```json
{
  "aggregate": false,
  "groupBy": "county",
  "maxResults": 200
}
```

# Actor output Schema

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

Florida DOR NAL property tax roll records - 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/fl-tax-roll-nal").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/fl-tax-roll-nal").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/fl-tax-roll-nal --silent --output-dataset

```

## MCP server setup

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

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/T0z2wJW6FHcYp5iBH/builds/Nc0vuicK5r3rpvYRa/openapi.json
