# NY Tax Warrant Status Delta (`titan_coder/ny-tax-warrant-status-delta`) Actor

Monitors named New York tax warrants (state tax liens) in the official NYS Tax Department registry and reports only real lifecycle changes: warrant satisfied, vacated, amended or closed. For lenders, title companies, collections and credit-risk due diligence. Free when nothing changes.

- **URL**: https://apify.com/titan\_coder/ny-tax-warrant-status-delta.md
- **Developed by:** [Radu Furtuna](https://apify.com/titan_coder) (community)
- **Categories:** Business, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$10.00 / 1,000 warrant status changeds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

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

## NY Tax Warrant Status Delta

Durable monitor for the official **New York State tax warrant registry** ("New York State Tax Warrants:
Beginning 7/1/2025"), published as open data on `data.ny.gov` by the **NYS Department of Taxation and
Finance**. Watch specific tax warrants (state tax liens) by their Warrant ID and get notified only when a
warrant's lifecycle genuinely changes — **satisfied**, **vacated**, **amended**, or closed. No API key
needed, no login, no captcha.

Built for **lenders, title companies, collections and recovery teams, credit-risk and KYB analysts, and
transaction due-diligence** — anyone who needs to know the day a New York tax warrant against a borrower,
seller, or counterparty is paid off or annulled, without re-pulling the whole registry every morning.

### Source

`https://data.ny.gov/resource/v7ua-z23v.json` — the Socrata Open Data API for "New York State Tax
Warrants: Beginning 7/1/2025" (dataset id `v7ua-z23v`), attributed to the NYS Department of Taxation and
Finance. Confirmed live 13.09.2026:

- **630,247 rows**, `X-SODA2-Truth-Last-Modified: Fri, 11 Sep 2026` — a near-daily maintained registry,
  not a static snapshot.
- `warrant_id` is the dataset's permanent identifier for one warrant, in a strictly fixed 18-character
  form `E-XXXXXXXXX-WXXX-X` (verified: zero rows deviate from that pattern across all 630,247).
- `warrant_id` is deliberately **not** one row per warrant: **577,134 distinct ids over 630,247 rows**.
  That is the registry's own mechanic — a warrant gains a second (occasionally third) row when its
  lifecycle moves. `status_code` is exactly three values registry-wide: `Added` (399,060), `Closed`
  (231,130), `Updated` (57). Verified live that no `warrant_id` has more than **3** rows.
- The lifecycle is in the data itself: `warrant_satisfaction_date` (153,243 rows populated),
  `warrant_vacate_date` (6,571), `warrant_amend_date` (192), `warrant_expiration_date` (71,316).
- Each warrant also carries a direct link to its PDF document on `tax.ny.gov`.

### How it works

1. Each `watch` names exactly **one** warrant by `warrantId`. One watch = one server-side point query =
   one HTTP request per run. No scanning, no scraping, no pagination.
2. All rows belonging to that `warrant_id` are folded into **one aggregate state** — the set of
   `status_code` values, the row count, and the latest satisfaction / vacate / amend / expiration dates.
   Row order does not affect the result.
3. The first check of a new watch establishes a baseline (no charge). Every later check compares the
   current aggregate against the durable record of what it was last time.
4. Billing is tied **only to the lifecycle**: status codes, row count, the four lifecycle dates, the
   latest filing timestamp, and the warrant amount. It deliberately excludes the debtor's name, address,
   city, country code, PDF link and geocode — the registry rewrites those cosmetically (we observed one
   warrant whose `country_code` flipped from `US` to `UNITED STATES` with no status change whatsoever).
   Those fields are still delivered in every row, as context.
5. A state that changes and later reverts bills every genuine transition, never silently deduplicated
   against an earlier occurrence of the same state.

### Input

```json
{
  "monitorId": "my-warrant-watch",
  "watches": [
    { "watchId": "acme-pizzeria", "warrantId": "E-012133297-W002-5" }
  ],
  "notifyOn": "new_alerts",
  "webhookUrl": "https://example.com/webhook"
}
```

Add more warrants later under the same `monitorId` — each watch keeps its own independent history. A
`watchId` is permanently bound to the `warrantId` it first saw; pointing the same `watchId` at a different
warrant later fails the run instead of silently mixing histories.

`socrataAppToken` is optional — `data.ny.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, warrantId, changeType ("new"|"lifecycle_changed"), lifecycle
("open"|"closed"|"amended"|"satisfied"|"vacated"), previousLifecycle, statusCodes, rowCount,
satisfactionDate, vacateDate, amendDate, expirationDate, latestFiledTimestamp, filedDate, filedAmount,
debtorName, city, state, zipCode, countyCode, documentUrl, contentHash, monitorId, runId, discoveredAt,
eventId, billed`

### Billing

Pay-per-event: `warrant-status-changed` — charged only for a watch's first found state (baseline is free)
or a genuine lifecycle change since the previous check. Failed, blocked, or structurally suspect checks
are never charged.

### Important — read before relying on this for any lending, title, or collections decision

**This is a publication of what the NYS Tax Department has filed, not a certification of a debtor's
current tax status, and not a title search.** A warrant appearing here is a filed state tax lien; a
satisfaction or vacate date appearing here means the Department has *published* that record, which can lag
the real-world event. Absence of a warrant is not evidence that no liability exists, and this dataset
begins 7/1/2025 as published, so it is not a complete historical lien record. **This actor is an
informational monitor of CHANGES to that public registry — it is NOT legal, credit, tax or title advice,
and NOT a substitute for a direct check at
[tax.ny.gov/warrants](https://www.tax.ny.gov/enforcement/warrants/) or a county-level lien search** before
funding, closing, releasing collateral, or reporting on any single entry.

### 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 warrant's next 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 warrant, one request.** There is no bulk mode, no search by debtor name, and no county
  sweep — to track a portfolio, add one watch per warrant (up to 30 per run). This keeps the network cost
  fixed and predictable and keeps each warrant's history independently auditable. You need the Warrant ID
  up front; look it up once in the dataset or at `tax.ny.gov`.
- **`lifecycle` is a coarse label, not the whole truth.** It reports the strongest outcome present
  (vacated > satisfied > amended > closed > open). All four lifecycle dates are delivered separately and
  all four are part of the billing hash — so an amendment to an already-satisfied warrant *is* a billable
  change even though the label stays `satisfied`.
- **A "not found" result on the very first check of a watch is reported honestly, not as an error** —
  `coverage.watches[].matched: false`. This almost always means the `warrantId` was mistyped; it costs
  nothing and is safe to correct and retry under the same `watchId`.
- **A `warrantId` that WAS matched on a previous check but is NOT found on a later one** is treated as
  `source_access_limited` for that watch this run — no history update, no billing. We have not verified
  live whether the Tax Department ever removes a warrant row from this publication, so this is the honest
  fallback rather than an unverified claim that a disappearance is always benign or always a real removal.
- **A run that checks some watches but not others tells you so.** If one warrant is checked fine and
  another is unreachable, the run still finishes as `SUCCEEDED`, but its reason becomes
  `partial_watch_failures: N/M`, `coverage.failedReasons` lists why, and the digest and webhook carry an
  explicit "result is INCOMPLETE" line. "No changes found" and "we could not look" are never reported as
  the same thing.
- **The monitor holds a lease, so a second run of the same `monitorId` stops instead of running in
  parallel.** If a run dies, the lease is released after a grace window (not after the run's full timeout),
  so the next scheduled run takes over promptly instead of failing as "monitor busy". The lease is a mutex
  for orderly behaviour, not the thing that protects your billing — see the time-gap note in the delivery
  guarantee above; duplicate charging is prevented by the atomic claim, not by the lease.
- **Billing tracks the lifecycle only**, deliberately excluding debtor name, address, city, country code,
  PDF link and geocode. Those fields are still delivered in every row for context.
- We don't invent data: if the API ever returns something other than a bare JSON array, more than 3 rows
  for one `warrant_id` (breaking the ceiling verified live across the whole registry), a non-object at any
  position, a row whose `warrant_id` doesn't exactly match the one requested (including stray whitespace
  or a different letter case, which we refuse rather than quietly clean up), a missing or
  whitespace-only required field, or a response whose `X-SODA2-Fields` header shows one of the lifecycle
  columns has disappeared entirely, the run reports it honestly (`source_access_limited`) instead of
  guessing what it actually found. Because Socrata omits null keys from JSON rows, that header is the only
  honest way to tell "this warrant has no satisfaction date" from "the satisfaction column is gone".

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

# Actor input Schema

## `monitorId` (type: `string`):

Name of this monitor's durable history (a-z, 0-9, dash; up to 40 chars). Reuse the same value on every scheduled run so the actor remembers what each warrant looked like last time.

## `watches` (type: `array`):

1-30 objects: {"watchId": "acme-pizzeria", "warrantId": "E-012133297-W002-5"}. warrantId is the NYS Tax Department Warrant ID, exactly in the form E-XXXXXXXXX-WXXX-X — look it up in the NYS Tax Warrants dataset on data.ny.gov or at tax.ny.gov/warrants if you only have a debtor name. One watch = one warrant. More warrants can be added later under the same monitorId.

## `socrataAppToken` (type: `string`):

Optional. data.ny.gov does not require a key for this dataset, but a free Socrata app token (from your own data.ny.gov account) raises the anonymous request-rate ceiling if you run many watches across many monitors. Leave empty for normal use.

## `notifyOn` (type: `string`):

new\_alerts — post the webhook only when new/changed paid warrant events were delivered; always — post it every run; never — do not call webhookUrl at all.

## `webhookUrl` (type: `string`):

Optional. Receives a digest of delivered (paid) warrant lifecycle changes as JSON. HTTPS only.

## Actor input object example

```json
{
  "monitorId": "my-warrant-watch",
  "watches": [
    {
      "watchId": "example-warrant",
      "warrantId": "E-012133297-W002-5"
    }
  ],
  "notifyOn": "new_alerts"
}
```

# Actor output Schema

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

Every row this run produced. Key fields: watchId, warrantId, changeType (new|lifecycle\_changed), lifecycle (open|closed|satisfied|vacated), previousLifecycle, statusCodes, rowCount, satisfactionDate, vacateDate, amendDate, expirationDate, filedDate, filedAmount, debtorName, city, state, zipCode, countyCode, documentUrl, contentHash. Informational only — not a legal, credit or title finding.

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

What this run actually covered and what it charged for: per-watch status/reason/matched, records delivered and billed, requested/attempted/succeeded/failed watch counts. Enough to reconcile every charge against every row.

## `digest` (type: `string`):

A short human-readable summary of what this run found, written every run.

# 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 = {
    "monitorId": "my-warrant-watch",
    "watches": [
        {
            "watchId": "example-warrant",
            "warrantId": "E-012133297-W002-5"
        }
    ],
    "socrataAppToken": "",
    "notifyOn": "new_alerts"
};

// Run the Actor and wait for it to finish
const run = await client.actor("titan_coder/ny-tax-warrant-status-delta").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 = {
    "monitorId": "my-warrant-watch",
    "watches": [{
            "watchId": "example-warrant",
            "warrantId": "E-012133297-W002-5",
        }],
    "socrataAppToken": "",
    "notifyOn": "new_alerts",
}

# Run the Actor and wait for it to finish
run = client.actor("titan_coder/ny-tax-warrant-status-delta").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 '{
  "monitorId": "my-warrant-watch",
  "watches": [
    {
      "watchId": "example-warrant",
      "warrantId": "E-012133297-W002-5"
    }
  ],
  "socrataAppToken": "",
  "notifyOn": "new_alerts"
}' |
apify call titan_coder/ny-tax-warrant-status-delta --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,titan_coder/ny-tax-warrant-status-delta"
        }
    }
}
```

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/F2hLdtvmWK4atD6zq/builds/XGdqJRXS3kbp4OZbC/openapi.json
