# Early Vote & Mail Ballot Tracker — County Turnout & Results (`shelvick/early-vote-tracker`) Actor

County-level US election data in one sweep: daily early-vote & mail-ballot returns by county, party, method and day, voter-registration statistics with history, and county results history 2000-2024. Honest per-state coverage labels; built for forecasting and analysis agents.

- **URL**: https://apify.com/shelvick/early-vote-tracker.md
- **Developed by:** [Scott Helvick](https://apify.com/shelvick) (community)
- **Categories:** Business, Developer tools, News
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $4.25 / 1,000 county data row (standard)s

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

Election-forecasting and political-analysis agents need county-level early-vote
and mail-ballot numbers that no training corpus can contain — and today those
numbers live in fifty different state file formats. This Actor is an **early vote
tracker** that returns daily **mail ballot returns by county**, **voter
registration statistics by county and party**, and **county election results
2000–2024** as one normalized sweep, with honest per-state coverage labels.

### What this does

- **Early vote & mail ballots (daily).** County × party × method returns for
  covered states — **daily absentee ballot counts**, mail ballots requested /
  returned / accepted, and early in-person votes, one cumulative snapshot per day
  so you get the returns time series, not just today's number. Current-cycle
  files light up as each state publishes them; prior-cycle archives are there to
  baseline against.
- **Voter registration by county and party.** County-level registration
  statistics — registered voters per county × party — as a current snapshot plus
  the monthly/weekly history where the state publishes it. In no-party-registration
  states only a `TOTAL` row exists and the record says party breakdown is omitted
  by law, not missing.
- **County election results history.** The **county-level presidential results**
  panel 2000–2024 plus Senate 2022, keyed by 5-digit FIPS — a
  `county-level presidential results API` behind the same schema as the fresh data.
  Alaska's house-district reporting is kept honest (keyed to district, not
  force-mapped to a county).
- **Ballot-counting and outstanding-ballot progress (`ballot_progress`; AZ/CA/NJ/NC).** After polls close,
  Arizona is watched for days over how many ballots are still being counted. This
  dataset is that countdown as structured county rows — ballots already tabulated
  plus the county's estimates of early, provisional, and ready-to-count ballots
  still outstanding and a percent-complete — refreshed election night through the
  canvass. It is a **counting-progress** feed, not an early-vote return series, so
  it lives in its own dataset; blank county cells are null, not zero.
  North Carolina reports the mail-ballot outstanding countdown from mail-out
  through canvass, excluding election-day and provisional ballots.
- **One normalized schema across every source.** Every state source is a different
  parser; the output is one unified row shape discriminated by `dataset`, so an
  agent sums party keys and joins FIPS the same way in every state.
- **Honest coverage, never a silent empty.** Every requested state × dataset slice
  that is not live comes back as a labeled coverage row (`not_covered`,
  `in_season_pending`, `gateway_deferred`, `archive_only`, `failed`), and the run's
  `COVERAGE` key-value record always carries per-slice delivered counts and reasons.
- **Use cases:** feed a daily early-vote model with fresh county × party × method
  returns during the voting window; baseline this cycle's mail-ballot returns
  against the prior cycle's same-day archive; pull the full county presidential
  panel 2000–2024 to train or validate a county-level model; track registration
  drift by county and party between snapshots; assemble a single fresh-plus-historic
  county election dataset without stitching fifty state portals together.

### Why daily freshness matters

The demand this is built for is a pipeline that re-runs *daily* during the Sept–Nov
voting window. Daily early-vote counts are **post-training-cutoff by construction**
— today's ballots-returned number cannot be in any model's training data, so an
agent that needs it *must* call a live tool. That is the whole thesis: freshness a
static corpus can't match.

The expensive, brittle part is that every state ships its numbers differently — one
state's aggregate CSV, another's tab-delimited text, a third's open-data query, a
fourth's spreadsheet. Assembling county × party × method returns across states, day
after day, and keeping the schema stable while the underlying files churn mid-season
is exactly the work you don't want inside your model. This Actor is that assembly
layer: the slow, walled, format-churning sources are refreshed behind an index, and
your call is a fast sweep over normalized rows.

And it's honest about the edges. A source that only publishes during an active
election, a state that's behind a bot wall, a results panel that doesn't exist at
the county level — each comes back as a *labeled* row, so your pipeline can branch
on coverage instead of guessing whether an empty result means "no data" or "no
ballots." A smaller map that tells the truth beats a bigger one that returns silent
zeros.

### How it compares to raw sources and static datasets

| Approach | County-level | Fresh daily in-season | One schema across states | Honest coverage labels |
|---|---|---|---|---|
| Individual state portals / files | Yes | Varies | No (per-state formats) | No |
| Static county election datasets | Yes | No (historic only) | Within one dataset | N/A |
| National exit-poll / topline feeds | No (state/national) | Sometimes | No | No |
| **Early Vote & Mail Ballot Tracker** | **Yes (FIPS)** | **Yes** | **Yes** | **Yes** |

The bet: multi-jurisdiction assembly behind one schema, plus daily freshness and
honest coverage, is worth more to a forecasting workflow than any single state
portal scraped in isolation or any snapshot dataset frozen at last cycle.

### Input

| Field | Type | Required | Default | Description |
|---|---|---|---|---|
| `datasets` | array | | all four | Which datasets to sweep: any of `early_vote`, `registration`, `results`, `ballot_progress`. Empty = all four. Each is billed only for the county rows it returns. |
| `states` | array | | all covered | Two-letter postal codes, e.g. `["NC","FL","PA"]`. Empty = every covered state for each requested dataset. A requested-but-uncovered state returns a labeled coverage row, never a silent empty. |
| `dateFrom` | string | | — | ISO `YYYY-MM-DD`, inclusive. Bounds `early_vote` snapshot dates and `registration` as-of dates. Ignored for `results`. |
| `dateTo` | string | | — | ISO `YYYY-MM-DD`, inclusive upper bound for `early_vote` / `registration`. Ignored for `results`. |
| `counties` | array | | all | 5-digit FIPS filter, e.g. `["37183"]` (Wake County, NC). Empty = all counties. FIPS is the county key across every dataset; a single-county filter turns the sweep into a targeted lookup. |
| `resultsYears` | array | | all | Year filter for `results`, e.g. `["2020","2024"]` (numeric strings are coerced). Applies only to `results`. |
| `electionId` | string | | `latest` | Pin `early_vote` / `ballot_progress` to one election by election-day date, e.g. `"2026-11-03"`, or `"latest"` for each state's most recent election. When `electionId`, `dateFrom`, and `dateTo` are all empty the sweep defaults to `latest` — a plain state query returns the current race, never an old archived one. Set a date window (or an explicit id) to reach historical elections. |
| `districts` | array | | — | Scope `early_vote` to specific districts, e.g. `["NC-09"]` or `["PA-SH-101","PA-SS-05"]`. See [Districts](#districts) for the formats and what the numbers mean per state. Empty = no district filter, and the run returns exactly what it would have returned before. Applies only to `early_vote`. |
| `maxRows` | integer | | `200000` | Safety cap on total county data rows returned **and** billed per run. Coverage rows are free and don't count. The per-run max-charge cap is the hard budget stop. |

### Districts

Ask for a single race instead of a whole state. Formats:

| Form | Means | Examples |
|---|---|---|
| `ST-NN` | congressional district | `NC-09`, `PA-07`, `MT-00` (at-large) |
| `ST-SH-NNN` | state house district | `PA-SH-101`, `NC-SH-009` |
| `ST-SS-NN` | state senate district | `PA-SS-05`, `MD-SS-12` |

Loose spellings are accepted and normalized: `NC-9` becomes `NC-09`, and `NC-AL` becomes `NC-00`.

**What the number means depends on the state, and the run tells you which you got.**

- **Where the state publishes district information itself**, you get a true district total: the count for that district, aggregated across every county it covers. These rows carry `district`, a `district_type`, and no `county_fips`.
- **Where the state publishes county totals only**, a congressional district resolves to the counties it covers and you receive those counties' **whole** counts. That is a county sweep scoped by district, not a district total — a county split across several districts contributes its full count to each, so those rows must not be added up across districts. Every such row says so in its `field_notes`. State house and senate districts are not offered here, because they are far smaller than a county and the approximation would be meaningless.
- **Where neither is possible**, you get a labeled row naming the reason and what is available instead. Several states redrew their congressional maps for the 2026 election; where we cannot verify the county composition of the new map, we say so rather than return a plausible-looking answer from the old one.

A district we can't answer is always a free, labeled row explaining why — never a silent empty result, and never charged. The `/coverage` view reports, per state, whether districts come from the source, from county mapping, or not at all.

### Output

One dataset item per row — a unified superset discriminated by `dataset`;
dataset-specific fields are populated for that shape and `null` otherwise.

An `early_vote` row (daily mail-ballot returns, `status: "ok"`):

```json
{
  "dataset": "early_vote",
  "state": "NC",
  "status": "ok",
  "county_fips": "37183",
  "county": "Wake",
  "party": "DEM",
  "party_detail": null,
  "election_id": "2024-11-05",
  "election_name": "2024 General Election",
  "date": "2024-10-28",
  "method": "mail",
  "ballots_returned": 12987,
  "ballots_accepted": 12750,
  "in_person_votes": null,
  "source": "State Board of Elections (county absentee counts)",
  "as_of": "2024-10-28 11:00AM",
  "field_notes": []
}
```

A `registration` row (`registered` per county × party):

```json
{
  "dataset": "registration", "state": "FL", "status": "ok",
  "county_fips": "12086", "county": "Miami-Dade", "party": "REP",
  "snapshot_date": "2026-07-01", "registered": 462180,
  "source": "State Division of Elections (party affiliation by county)", "field_notes": []
}
```

A `results` row (county presidential history):

```json
{
  "dataset": "results", "state": "NC", "status": "ok",
  "county_fips": "37183", "county": "Wake", "year": 2024,
  "office": "US PRESIDENT", "candidate": "KAMALA D HARRIS",
  "votes": 340199, "total_votes": 621988, "field_notes": []
}
```

A `ballot_progress` row (Arizona counting countdown; blank source cells → null):

```json
{
  "dataset": "ballot_progress", "state": "AZ", "status": "ok",
  "county_fips": "04013", "county": "Maricopa",
  "election_id": "2026-07-21", "date": "2026-07-22",
  "tabulated": 518585, "early_pending": null, "provisional_pending": null,
  "ready_for_tabulation": null, "uncounted_total": null, "pct_complete": null,
  "county_last_updated": "2026-07-22T00:02", "field_notes": []
}
```

A coverage row (requested slice not live — free, never billed):

```json
{
  "dataset": "results", "state": "TX", "status": "gateway_deferred",
  "error_class": "not_covered",
  "retry": "never",
  "coverage_note": "This jurisdiction is not supported for this dataset yet; its source needs an integration we have not shipped. Re-running will not return rows for it.",
  "field_notes": []
}
```

A `stale` row (real county figures for a state whose source is not current — free,
never billed; every row for that state carries the same notice):

```json
{
  "dataset": "registration", "state": "OR", "status": "stale",
  "county_fips": "41051", "county": "Multnomah", "party": "DEM",
  "snapshot_date": "2026-09-01", "registered": 312345,
  "current_through": "2026-09-01",
  "source_status": "failing",
  "source_notice": "Oregon voter-registration figures have not been updated since 2026-09-29: the state's published data could not be retrieved on the latest daily updates. The figures here are current through 2026-09-01. Newer figures may already be available from the state."
}
```

Failure and coverage rows include `error_class`: `source_error` is a temporary source condition and is retryable; `not_covered` is unsupported and is not retryable; `pending` means data is not published yet and can be retried later but is not an outage; `malformed_input` means the input could not be used and the note names what to correct; `internal_error` means a fault on our side stopped the run. Successful rows set it to null. Each row also carries `retry`, that class as a single instruction — `later`, `with_changes`, or `never`, and `null` on a successful row — so an agent can route without holding the table above.

Key fields: `status` (row outcome — only `ok` bills; `ok` also means the state's
figures are current), `current_through` (the newest date the state's figures cover),
`source_status` (`current`, `failing`, `not_arrived`, or `stale`) with
`source_notice` (plain-language: which state, what is missing, since when), `party` (normalized
DEM/REP/UNA/OTH/TOTAL key, safe to sum), `party_detail` (the source's own party
code verbatim where it emits one), and `field_notes` (why a field is structurally
absent — e.g. a count column a state doesn't publish, or party omitted by law). Each
run also writes a `COVERAGE` key-value record listing every requested slice with its
delivered row count, reason, and freshness — authoritative per run, and coverage
expands over time. The `OUTPUT` record's `source_notices` lists every swept state
whose figures are not current, so one read tells you which states to treat with care.

### Example

Sweep this cycle's early-vote returns plus the county presidential history for three
states:

```bash
curl -X POST "https://api.apify.com/v2/acts/shelvick~early-vote-tracker/run-sync-get-dataset-items?token=YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"datasets":["early_vote","results"],"states":["NC","FL","PA"],"dateFrom":"2026-09-01","dateTo":"2026-11-03","resultsYears":["2020","2024"]}'
```

Returns the daily early-vote rows in the date window plus the 2020 and 2024 county
presidential rows for each state, with a labeled coverage row for anything requested
that isn't live yet.

### Calling from an AI agent

Agents are the primary customer. Three ways to call it:

- **Apify MCP server** (`mcp.apify.com`): expose `shelvick/early-vote-tracker` as a
  tool; the input schema and per-field descriptions are advertised to the model, so
  an agent can construct a valid sweep from the tool description alone. Pay per call
  via x402 USDC on Base or Skyfire managed tokens.

- **Apify SDK (Python):**

  ```python
  from apify_client import ApifyClient

  client = ApifyClient("YOUR_TOKEN")
  run = client.actor("shelvick/early-vote-tracker").call(
      run_input={"datasets": ["early_vote"], "states": ["NC", "FL", "PA"],
                 "dateFrom": "2026-09-01", "dateTo": "2026-11-03"}
  )
  for row in client.dataset(run["defaultDatasetId"]).iterate_items():
      print(row["state"], row["county"], row["party"], row["date"], row.get("ballots_returned"))
  ```

- **REST:** `POST /v2/acts/shelvick~early-vote-tracker/run-sync-get-dataset-items`
  for a synchronous sweep, or the async `/runs` endpoint plus polling for a large
  national pull.

### Pricing

Pay-per-event, billed only on success. Each **county data row** is charged once,
after it is pushed to the dataset (push-then-charge), metered per run. The rate
depends on what kind of row it is:

- **Live-tracking rows** (early-vote and registration) bill at a standard per-row
  rate, and a **lower volume rate applies automatically** to rows beyond a per-run
  threshold — large daily sweeps get cheaper per row, by design.
- **Results-history rows** bill at a flat rate lower than live-tracking rows, so
  pulling the full historical panel stays inexpensive.

Coverage rows (`not_covered`, `in_season_pending`, `gateway_deferred`,
`archive_only`, `failed`) are returned but **never charged**, and `stale` rows —
figures for a state whose source is not current — are delivered free too, and a run stopped by
your budget cap bills only the rows actually delivered — every charge is clamped to
your remaining budget in code, so a broad national sweep can't produce a runaway
bill. A single platform run-start fee applies per run.

See the **Pricing** tab on this Store page for the current per-row rates, the
volume threshold, and any active subscriber discounts.

### Behavior

- **Row statuses:** `ok` (a billed county data row whose state is current);
  `stale` (real county figures, free, for a state whose updates are failing, whose
  expected figures have not been published yet, or whose figures have not changed
  for longer than usual — `source_notice` says which and since when);
  `not_covered` (state/dataset
  not in scope yet); `in_season_pending` (source published only during an active
  election — e.g. some early-vote files off-season); `gateway_deferred` (a
  bot-walled source not yet integrated); `archive_only` (historical only, not
  currently refreshing); `failed` (the backing data could not be reached for that
  slice). Only `ok` bills.
- **Never blind on freshness.** Every swept row carries `current_through`; a state
  that is not current is labeled on every one of its rows, in the `COVERAGE`
  manifest, and in `OUTPUT.source_notices` — even when it returned no rows.
- **Never a false empty.** If the backing service is unreachable or a slice errors,
  that slice comes back as a `failed` source-error row and the run succeeds uncharged —
  the sweep never silently returns nothing. Zero-row live slices are
  explained in the `COVERAGE` manifest, and an empty slice inside an active election
  window raises a diagnostic rather than passing quietly.
- **Current election by default.** With no `electionId` and no date window, an
  `early_vote` / `ballot_progress` query returns each state's most recent
  election only — the index also holds prior cycles, reachable via an explicit
  `electionId` or a `dateFrom`/`dateTo` window. If `maxRows` truncates a
  series, the newest rows are kept.
- **Unusable inputs:** return one uncharged `malformed_input` correction row and a successful run —
  states must be 2-letter codes, counties 5-digit FIPS, dates ISO `YYYY-MM-DD`.
- **Latency:** a single-county or single-state sweep returns in a few seconds; a
  broad national sweep (tens of thousands of rows) is paged and pushed
  incrementally and scales with row count. `maxRows` and the per-run max-charge cap
  bound the largest pulls.
- **Telemetry:** to improve coverage and reliability, this Actor reports anonymous
  usage metrics and diagnostic events to the developer — run outcome counts, the
  state/dataset slice keys requested with delivered-row counts, coverage misses,
  and, only when something goes wrong, the relevant input fields. No account
  identifiers are collected, and telemetry never affects a run.

### FAQ

**Do I get today's ballot counts, or last week's?** The latest snapshot the source
has published, per state. In-season, states that publish daily are refreshed daily;
each day's cumulative snapshot is retained, so a date range gives you the returns
time series, not just the newest figure.

**What does a `null` party or count column mean?** It's structural, not a gap. In
no-party-registration states party is `TOTAL`-only and omitted by law; a count
column a state doesn't publish is left `null` rather than duplicated from another
column. The row's `field_notes` says which and why.

**Is the results data official?** It's the standard county-level returns panel —
president 2000–2024 and Senate 2022 — normalized to FIPS. County-level Governor and
House panels don't exist as a multi-cycle county series, and the coverage record
says so rather than inventing them.

**How do I keep a national sweep from over-charging?** Set `maxRows` and the run's
max-charge cap. Charges are clamped to your remaining budget, and dropped rows are
free and explained in the `COVERAGE` record.

**Which states are covered?** Read the run's `COVERAGE` record — it's authoritative
per run. Early-vote coverage centers on the states that publish clean daily county
files and expands as more come online; registration covers several
partisan-registration states; results covers all 50 states for the county
presidential panel.

### What this doesn't do

- **No individual voter data.** Aggregate county rows only — county × party × method
  counts, never a voter file, never a name, address, or ballot-level record. This is
  a deliberate design stance, not a coverage gap: where a source is individual-level,
  aggregation happens before anything is stored, and raw individual records are never
  retained or served.
- **No forecasting or modeling.** Raw normalized returns and the archived baseline
  series — the projection, turnout model, or seat forecast is yours to build on top.
- **No real-time streaming or monitoring.** A point-in-time sweep of the latest
  ingested data, not a live feed or a change-watcher.
- **No individual candidate-finance, precinct-level, or exit-poll data.** County is
  the finest grain; precinct and sub-county breakdowns are out of scope.
- **Not a 50-state guarantee for the fresh datasets.** Early-vote and registration
  coverage is honest and expanding; uncovered slices return labeled coverage rows.

For a turnout model or seat projection, build it on top of these rows — this Actor
is the fresh-data primitive, not the model. For individual voter registration
records or a voter file, this Actor deliberately does not serve them; use an
appropriately-authorized voter-data provider. For precinct-level or ballot-level
returns, use a precinct-returns source — county is the finest grain here.

***

# Actor input Schema

## `datasets` (type: `array`):

Which county-level election datasets to sweep. Any combination of: early_vote (daily early-vote & mail-ballot returns by county × party × method × day), registration (voter-registration stats by county × party, current + history), results (county results history — president 2000-2024 + Senate 2022), ballot_progress (post-election count and outstanding-ballot progress). Empty = all four. Coverage and source availability differ by dataset and change as sources come online, so the authoritative list is the run's COVERAGE record rather than any state list written here. Billed per county row; results rows bill lower, volume rate on large sweeps (see the Pricing tab).

## `states` (type: `array`):

Two-letter US state postal codes to scope the sweep, e.g. \["NC","FL","PA"]. Empty = every state currently covered for each requested dataset. Coverage differs per dataset and changes as states come online and as election seasons open, so the authoritative list is the run's COVERAGE record rather than any list written here: early_vote spans the states publishing daily county files, registration the states publishing party-registration snapshots, and results all 50 states plus DC. A requested state that is not covered returns a labeled coverage row (not_covered / gateway_deferred / in_season_pending) explaining which case it is — never a silent empty, and never charged.

## `dateFrom` (type: `string`):

Start date (ISO YYYY-MM-DD, inclusive) bounding early_vote returns (by snapshot date) and registration statistics (by 'as of' date). Setting a date window disables the default electionId="latest" filter, so historical elections inside the window are included. Ignored for results (use resultsYears). Empty = no lower bound. Example: "2026-09-01".

## `dateTo` (type: `string`):

End date (ISO YYYY-MM-DD, inclusive) bounding early_vote returns and registration statistics. Setting a date window disables the default electionId="latest" filter. Ignored for results. Empty = no upper bound. Example: "2026-11-03".

## `counties` (type: `array`):

Filter to specific counties by 5-digit FIPS code, e.g. \["37183"] (Wake County, NC). Empty = all counties in the requested states. FIPS is the county key across every dataset; a single-county filter turns the sweep into a targeted lookup over the same index.

## `resultsYears` (type: `array`):

Filter the results dataset to specific election years, e.g. \["2020","2024"]. Applies only to results (county president 2000, 2004, 2008, 2012, 2016, 2020, 2024; Senate 2022). Empty = all available years. Ignored for early_vote / registration.

## `electionId` (type: `string`):

Pin the early_vote / ballot_progress sweep to one election, keyed by election-day date (ISO YYYY-MM-DD, e.g. "2026-11-03"), or the sentinel "latest" for each state's most recent election. DEFAULT when electionId, dateFrom, and dateTo are all empty: "latest" — a plain state query serves the current race, never an old archived election. Set an explicit electionId or a date window for historical elections. Ignored for registration / results.

## `districts` (type: `array`):

Scope the early_vote sweep to specific districts, e.g. \["NC-09"] or \["PA-SH-101","PA-SS-05"]. Formats: congressional "NC-09" ("NC-9" and at-large "NC-AL" are accepted and normalized); state house "PA-SH-101"; state senate "PA-SS-05". What the number means depends on the state, and the run's COVERAGE record says which you got. Where the state publishes district information at source, rows are true district totals with no county FIPS. Where it publishes county totals only, a congressional district resolves to the counties it covers and you receive those counties' WHOLE counts — a county sweep scoped by district, not a district total, flagged in each row's field_notes; state house and senate districts are not available there. Some states redrew congressional maps for 2026; where the new map's county composition cannot be verified, the district is refused by name rather than answered from the old map. Any district we cannot answer returns a labeled row explaining why and is never charged. Applies only to early_vote; ignored for registration, results and ballot_progress. Empty = no district filter, and results are identical to previous runs.

## `maxRows` (type: `integer`):

Safety cap on the total number of county data rows returned AND billed in one run — protects a broad national sweep from an unbounded charge. When the cap truncates a time series, the NEWEST rows are kept (the current snapshot survives, history is dropped). Coverage rows (not_covered, etc.) are free and do not count against this. Default 200000 (a full national results-history sweep is ~110k rows); the per-run maximum charge (maxTotalChargeUsd) is the hard budget stop.

## Actor input object example

```json
{
  "datasets": [
    "early_vote",
    "registration",
    "results",
    "ballot_progress"
  ],
  "states": [
    "NC"
  ],
  "dateFrom": "2026-01-01",
  "dateTo": "2026-11-03",
  "counties": [
    "37183"
  ],
  "resultsYears": [
    "2020",
    "2024"
  ],
  "districts": [],
  "maxRows": 200000
}
```

# Actor output Schema

## `results` (type: `string`):

County-level rows for this sweep (early-vote returns, registration statistics, and results history, plus labeled coverage rows for anything requested but not live).

## `output` (type: `string`):

Run totals plus per-event charge counts and, on capped runs, what stopped the sweep.

## `coverage` (type: `string`):

Always-present coverage manifest: the delivered slices and index coverage for everything requested this sweep.

# 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 = {
    "datasets": [
        "early_vote",
        "registration",
        "results",
        "ballot_progress"
    ],
    "states": [
        "NC"
    ],
    "dateFrom": "2026-01-01",
    "dateTo": "2026-11-03",
    "counties": [
        "37183"
    ],
    "resultsYears": [
        "2020",
        "2024"
    ],
    "electionId": "",
    "districts": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("shelvick/early-vote-tracker").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 = {
    "datasets": [
        "early_vote",
        "registration",
        "results",
        "ballot_progress",
    ],
    "states": ["NC"],
    "dateFrom": "2026-01-01",
    "dateTo": "2026-11-03",
    "counties": ["37183"],
    "resultsYears": [
        "2020",
        "2024",
    ],
    "electionId": "",
    "districts": [],
}

# Run the Actor and wait for it to finish
run = client.actor("shelvick/early-vote-tracker").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 '{
  "datasets": [
    "early_vote",
    "registration",
    "results",
    "ballot_progress"
  ],
  "states": [
    "NC"
  ],
  "dateFrom": "2026-01-01",
  "dateTo": "2026-11-03",
  "counties": [
    "37183"
  ],
  "resultsYears": [
    "2020",
    "2024"
  ],
  "electionId": "",
  "districts": []
}' |
apify call shelvick/early-vote-tracker --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,shelvick/early-vote-tracker"
        }
    }
}
```

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/sNusf0DqUFL5AAuQz/builds/Yp4Ygk86efBAqHhDo/openapi.json
