# New York Liquor License Renewal & Expiry Watch (`thatmike1/ny-liquor-license-watch`) Actor

Check NY SLA serial numbers and permit IDs for published expiration and renewal-date changes. Preserve multiple permits, compare portfolios and flag upcoming dates in JSON.

- **URL**: https://apify.com/thatmike1/ny-liquor-license-watch.md
- **Developed by:** [Michal Pšenčík](https://apify.com/thatmike1) (community)
- **Categories:** Business, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.025 / actor start

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

## New York Liquor License Renewal & Expiry Watch

Check a portfolio of New York State Liquor Authority (SLA) legacy serial numbers and current permit IDs
against the official **Current Liquor Authority Active Licenses** dataset on data.ny.gov (Socrata dataset
`9s3h-dpkz`, posted daily). For each identifier you supply, the Actor returns one complete check with every
matching permit, the published issue, effective and expiration dates, days to the published expiry and
reminder flags. With a watch ID it compares each check with the last one it delivered and tells you what
changed.

Built for distributors, license consultants and hospitality operators who already know their NY
identifiers and recheck them weekly, or daily near expiry, from a schedule or an integration.

**What it does not do:** it does not discover licenses, file renewals, send email or SMS, derive legal
filing deadlines, follow parent/child links, or cover inactive licenses. An identifier missing from the
active feed does **not** prove a closure, revocation or that a license is invalid: it only means the
current active list does not contain it.

### Input

```json
{
    "identifiers": [
        { "kind": "legacy_serial", "value": "6085646" },
        { "kind": "legacy_serial", "value": "3171641" },
        { "kind": "permit_id", "value": "0370-24-131229-01" },
        { "kind": "legacy_serial", "value": "0000000" }
    ],
    "watchId": null,
    "reminderDays": [90, 30, 7],
    "maxItems": 2000,
    "requestIntervalMs": 1300
}
```

- `identifiers` (1 to 2,000): `legacy_serial` values are 1 to 20 digits and keep leading zeroes;
  `permit_id` values look like `0001-26-135216` or `0343-23-306684-01`. Matching is exact, never numeric
  or fuzzy. Exact repeats are checked once. A serial and a permit ID for the same license are two
  checks and both may bill.
- `watchId` (optional, letters, digits, `_` and `-`, up to 64): turns on comparison with the last
  delivered check for each identifier. Leave it empty for a stateless snapshot. Run one schedule per
  watch ID at a time; overlapping runs on one watch are refused when detected, but simultaneous starts
  can race. A new watch ID is a reset.
- `reminderDays` (1 to 3 values, 0 to 365): a permit is flagged when its published expiry is within that
  many calendar days.
- `maxItems` (1 to 2,000): stop after this many delivered checks.
- `requestIntervalMs` (1,300 to 10,000): idle time after each source response before the next request.

The default input checks one real serial (`6085646`) so the Actor always returns a row. A field left
out takes its default; a field set to `null` is rejected before any request, except `watchId`, where
`null` means no watch.

### Output

One `LICENSE_CHECK` item per matched identifier, in input order, including unchanged ones. A shortened
real example (serial 6085646, snapshot mode):

```json
{
    "recordType": "LICENSE_CHECK",
    "recordId": "ny-sla:legacy_serial:6085646",
    "watchId": null,
    "request": { "kind": "legacy_serial", "value": "6085646" },
    "matchStatus": "matched",
    "changeKind": "snapshot",
    "previousHash": null,
    "contentHash": "092715d9bbe403802be582f9d92f23c1274f30088d57f782801cb5ccfd65febf",
    "permitCount": 1,
    "sourceRowCount": 1,
    "matches": [
        {
            "licensePermitId": "0001-26-135216",
            "variants": [
                {
                    "legacySerialNumber": "6085646",
                    "parentLicenseId": null,
                    "legalName": "Guru Hari 394 Inc.",
                    "dba": null,
                    "classCode": "0001",
                    "description": "Wholesale Beer (Retail)",
                    "premisesCounty": "Suffolk",
                    "rawDates": {
                        "lastIssueDate": "2026-02-10T00:00:00.000",
                        "expirationDate": "2027-01-31T00:00:00.000"
                    },
                    "dates": { "lastIssueDate": "2026-02-10", "expirationDate": "2027-01-31" },
                    "sourceRowIds": ["row-3ij9-tbne~ek3x"],
                    "daysToPublishedExpiry": 107
                }
            ]
        }
    ],
    "changes": [],
    "reminders": [],
    "warnings": [],
    "observedAt": "2026-10-16T14:43:38.975Z",
    "calendarTimezone": "America/New_York",
    "sourceTimezone": null,
    "coverage": {
        "completeForRequestedIdentifier": true,
        "datasetId": "9s3h-dpkz",
        "sourceUrls": ["https://data.ny.gov/resource/9s3h-dpkz.json?..."]
    }
}
```

(`dates` and `rawDates` always carry all four fields: `originalIssueDate`, `lastIssueDate`,
`effectiveDate`, `expirationDate`; the example drops two for space.)

- **Multiple permits and variants.** A serial can return several current permits (3171641 returns a
  hotel permit and its additional bar). The source's "unique" permit ID is not always unique: when two
  rows for one permit differ (for example a null versus a populated parent link), both are kept as
  variants with a `multiple_source_variants:<permit>` warning. Nothing is merged, picked or filled in.
- **Dates.** The source publishes floating timestamps with no time zone. Raw strings are kept as published
  and the calendar date is used as is. Day counts use the New York calendar on the day the run started
  (an Actor choice, not a publisher declaration). Past dates give negative counts.
- **Watch mode.** `changeKind` is `baseline` on the first delivered check, then `unchanged` or `changed`
  (any correction, including backwards date changes, parent or name corrections). `changes[]` lists each
  permit that appeared, is not in the current result, or whose variants changed, with before and after
  date tuples. A permit that disappears is "not in current result", never "deleted" or "revoked".
- **Reminders.** For each permit with one published expiry, one entry per threshold it is within, with
  `newlyDue` true the first time a watch reports that permit, expiry and threshold (null in snapshot
  mode). Variants with conflicting expiries suppress that permit's reminders with an `expiry_conflict`
  warning. These are JSON flags for your own automation, not notifications.

The free `SUMMARY` record in the run's key-value store lists every input with its outcome
(`delivered`, `duplicate`, `not_in_current_active_feed`, `error`, `not_attempted_cap`,
`delivery_uncertain`), error details, source versions, request counts and the identifiers a later run
can retry. If the platform restarts a run, its counts and source versions carry over and an identifier
fetched again is counted once (`attemptedIdentifiers` and the input indexes in `attemptedIndexes`). No-match results and errors appear only there.

### Pricing

Pay per event:

- **Actor start: $0.025** per run start, charged even when the run finds nothing or fails. The higher
  start fee covers slow, empty and error runs that deliver no billable item.
- **License check: $0.001** per delivered matched identifier check, **including unchanged checks** (each
  is the checked current record and its receipt). One check can hold several permits and variants.

Identifiers not in the active feed, invalid input, incomplete or failed source batches, failed
deliveries and the `SUMMARY` record are not charged as items. A run with 500 matched identifiers costs
$0.025 + 500 × $0.001 = $0.525 in event fees.

With a maximum total charge set on the run, the Actor keeps room for the start fee and stores only the
checks the rest of the limit covers. Identifiers it could not afford are listed in `SUMMARY` as
`not_attempted_cap` with code `spending_limit`, the run ends as `capped`, and a later run can retry them.

### Limits and behaviour

- One request at a time to data.ny.gov, at least 1.3 s idle after each response, 20 s per attempt, at
  most 120 attempts, 2 MiB per response, 16 MiB per run and a 600-second work deadline. Identifiers are
  queried 25 at a time with `$limit=100` pages; a batch that needs more than 10 pages or returns more than
  50 rows for one identifier is reported as an error instead of being truncated.
- Timeouts and server errors are retried twice. HTTP 401, 402, 403, 429 or a bot challenge stops all
  requests for the run; a `Retry-After` is reported as `retryNotBefore`. There is no proxy, login, token
  or browser fallback.
- If the source version headers differ between pages of one batch, that batch fails and is listed for
  retry. Equal headers are a consistency check, not a guaranteed snapshot.
- Watch state is limited to 2,000 identifiers, 128 KiB per identifier and 32 MiB per watch, with at most
  four expiry dates and twelve reminder keys per permit; a check that would pass these limits is an
  error, nothing is evicted.
- Delivery and state are not one transaction: after a crash a later run can repeat a change or reminder
  (at-least-once).

### Source and data notes

Data comes from the New York State Liquor Authority's current active licenses dataset, published on
[data.ny.gov](https://data.ny.gov/d/9s3h-dpkz) through the Socrata open data API without a key. Legal
names are returned exactly as published; you are the controller of any processing of that data. The
SLA itself sends renewal advisories about three months before expiry; this Actor is a data check, not
legal advice or confirmation that a license is valid.

### Support

Open an issue on the Actor's Issues tab with the run ID and the `SUMMARY` record.

# Actor input Schema

## `identifiers` (type: `array`):

1 to 2,000 exact NY SLA identifiers, each {"kind": "legacy_serial" | "permit_id", "value": "..."}. Legacy serial numbers are 1 to 20 digits (leading zeroes kept); permit IDs look like 0001-26-135216 or 0343-23-306684-01. Exact repeats are checked once. A serial and a permit for the same license are two checks and may both bill.

## `watchId` (type: `string`):

Optional name (1 to 64 letters, digits, \_ or -) for a persistent watch. With it, each check is compared with the last delivered check for the same identifier and reminder thresholds are marked newly due once. Leave empty for a stateless snapshot. Do not run two schedules with the same watch ID at once.

## `reminderDays` (type: `array`):

1 to 3 distinct whole numbers from 0 to 365. A permit is flagged when its published expiry is that many calendar days away or fewer (New York calendar).

## `maxItems` (type: `integer`):

Stop after this many matched identifier checks are delivered (each is one billed dataset item). Remaining identifiers are listed in SUMMARY for a later run.

## `requestIntervalMs` (type: `integer`):

Idle time after each source response before the next request. Higher values are gentler but may leave a partial run under the fixed 600-second work deadline.

## Actor input object example

```json
{
  "identifiers": [
    {
      "kind": "legacy_serial",
      "value": "6085646"
    },
    {
      "kind": "legacy_serial",
      "value": "3171641"
    },
    {
      "kind": "permit_id",
      "value": "0370-24-131229-01"
    }
  ],
  "reminderDays": [
    90,
    30,
    7
  ],
  "maxItems": 2000,
  "requestIntervalMs": 1300
}
```

# Actor output Schema

## `checks` (type: `string`):

No description

## `summary` (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 = {
    "identifiers": [
        {
            "kind": "legacy_serial",
            "value": "6085646"
        },
        {
            "kind": "legacy_serial",
            "value": "3171641"
        },
        {
            "kind": "permit_id",
            "value": "0370-24-131229-01"
        }
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("thatmike1/ny-liquor-license-watch").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 = { "identifiers": [
        {
            "kind": "legacy_serial",
            "value": "6085646",
        },
        {
            "kind": "legacy_serial",
            "value": "3171641",
        },
        {
            "kind": "permit_id",
            "value": "0370-24-131229-01",
        },
    ] }

# Run the Actor and wait for it to finish
run = client.actor("thatmike1/ny-liquor-license-watch").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 '{
  "identifiers": [
    {
      "kind": "legacy_serial",
      "value": "6085646"
    },
    {
      "kind": "legacy_serial",
      "value": "3171641"
    },
    {
      "kind": "permit_id",
      "value": "0370-24-131229-01"
    }
  ]
}' |
apify call thatmike1/ny-liquor-license-watch --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,thatmike1/ny-liquor-license-watch"
        }
    }
}
```

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/ahiCr4QmVCAS6eUNR/builds/5iPuwynvJWN3GA6Y1/openapi.json
