# Cannabis License Monitor — New, Changed & Expiring, No Login (`outstanding_vegetable/cannabis-license-monitor`) Actor

Watch cannabis licenses in 10 states (CA, NY, MA, WA, CO, NV, MD, ME, CT, FL) and get only NEW licenses, status changes (suspended, revoked, expired), removals and licenses expiring soon, with business, type and dates. Daily schedule. No login. MCP-ready. $20 per 1,000 alerts.

- **URL**: https://apify.com/outstanding\_vegetable/cannabis-license-monitor.md
- **Developed by:** [Peter Skotte](https://apify.com/outstanding_vegetable) (community)
- **Categories:** Lead generation, Automation, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $20.00 / 1,000 license alerts

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

## Cannabis License Monitor — new licenses, status changes & expirations in 12 states

Schedule it daily and get **only what changed** in official cannabis license data: newly issued licenses,
status changes (active → suspended / revoked / expired / surrendered), licenses about to expire, and licensees
that dropped off a regulator's current list — filtered by state, license type and status. Built for vendors and
ancillary suppliers prospecting new licensees, insurers and lenders tracking enforcement, and compliance teams
watching their own portfolio's renewals.

The monitor downloads each state's full regulator list on every run, diffs it against what it saw last time,
emits one row per change, posts a summary to your webhook and remembers the new state. No browser, no proxies,
no login: every source is a public regulator API, open-data feed or published spreadsheet.

### Supported states

| State | Source (fetched live) | Licenses | Status detail | Freshness |
|---|---|---|---|---|
| **CA** | Dept. of Cannabis Control search API (`search.cannabis.ca.gov`) | ~21k incl. surrendered/expired/revoked | Active / Expired / Surrendered / Revoked / Suspended / Canceled | Refreshed daily by DCC |
| **NY** | Office of Cannabis Management on data.ny.gov | ~2.3k current OCM licenses | Active / Inactive / In-Process / Under review | Weekly |
| **MA** | Cannabis Control Commission open-data extract | ~950 approved licenses | Active / Payment Pending + suspension/revocation dates | Weekly |
| **WA** | LCB retailer spreadsheet + producer/processor renewals on data.wa.gov | ~1.4k retailers, producers, processors | ACTIVE / CLOSED / EXPIRED / TITLE CERTIFICATE | Weekly (LCB) |
| **CO** | MED licensed-facility lists (all 9 facility types) | ~600 current facilities | Current licensees only → `removed` when delisted | Monthly |
| **NV** | Cannabis Compliance Board establishment list | ~640 active, conditional, surrendered/revoked | Active / Conditional / Surrendered / Revoked | Monthly |
| **MD** | Maryland Cannabis Administration data-dashboard workbook | ~210 growers, processors, dispensaries, ancillary | Current licensees only → `removed` when delisted | Monthly |
| **ME** | Office of Cannabis Policy adult-use workbook + medical establishments & caregivers | ~1.9k | Active / Conditional / Pending | Monthly |
| **CT** | Dept. of Consumer Protection establishments on data.ct.gov | ~115 | Current licensees only → `removed` when delisted | Weekly |
| **FL** | OMMU licensed MMTC table | ~50 MMTC license holders | Initial Licensure / Dispensing Authorization | As published |
| **MI** | Cannabis Regulatory Agency Accela portal, full "Download results" CSV export (adult-use + medical facility modules) | ~10k incl. void/closed/suspended/revoked | Active / Late Renewal / License Void / Closed / Suspended / Revoked | Live (exported from the CRA portal on every run) |
| **OH** | Division of Cannabis Control license lists (dispensary, cultivator, processor, testing-lab workbooks) | ~320 current dual-use licenses | Current licensees only → `removed` when delisted | As published (list dated in the file, updated roughly weekly) |

Not supported (and why): **OR** (OLCC moved its license table
into a Socrata story with no public tabular feed as of Sep 2026), **MO** (Tableau only), **AZ** (regulator PDF last
updated 2021), **IL** (PDF updated Jan 2026, layout too unstable), **NM** (feed has no license numbers or status),
**MN / VT / NJ** (browser-only portals), **RI** (blocks cloud requests).

Michigan is the largest list (~10k records across the adult-use and medical modules, about a minute to export);
its records have no issue date, so a first `emitAll` run orders Michigan by license number. Ohio publishes current
licensees only, so a license that drops off a DCC list surfaces as `removed`.

### Change types

| `changeType` | Emitted when | Extra fields |
|---|---|---|
| `new_license` | A license number appears that the monitor has not seen before. Newest `issueDate` first, capped by `maxNewPerState`. | `firstSeenAt` |
| `status_change` | The normalised status differs from the one stored last run (e.g. `active` → `suspended`). | `previousStatus` |
| `expiring_soon` | `expirationDate` is within `expiringWithinDays` of today and the license is not already expired / revoked / inactive. Alerted **once per expiration date**: a renewal that moves the date re-arms it. | `daysToExpiration` |
| `removed` | CO, MD and CT publish *current licensees only*, so a license vanishing from the list is the only signal that it lapsed, was surrendered or was revoked. Emitted with the stored name, type and last known status. | `previousStatus` |

#### Status normalisation

| `status` | Raw regulator values mapped here |
|---|---|
| `active` | Active, ACTIVE (ISSUED), Active (renewal on file), Open, Licensed, Approved, Limited Operations, Initial Licensure, Dispensing Authorization |
| `pending` | Pending, Conditional, Pending Conditional, In-Process, Payment Pending, PCA / Amendment Under Review, Provisional |
| `inactive` | Inactive, Surrendered, Canceled, CLOSED (PERMANENT/TEMPORARY), Not Operating, Former Title Certificate, Withdrawn, Denied |
| `expired` | Expired |
| `suspended` | Suspended |
| `revoked` | Revoked |
| `unknown` | anything else — `statusRaw` always keeps the original text |

### Input

| Field | Default | Notes |
|---|---|---|
| `states` | `["NY","MA"]` | Two-letter codes from the table above. Every listed state is downloaded in full each run. |
| `licenseTypes` | `[]` (all) | Case-insensitive substrings matched against the regulator's license type: `"retail"`, `"dispensary"`, `"cultivat"`, `"manufactur"`, `"microbusiness"`, `"delivery"`… |
| `statuses` | `[]` (all) | Only report rows whose **current** normalised status is in this list. For `status_change` the new status is tested, so `["suspended","revoked","expired"]` gives an enforcement-only feed. `removed` rows have no current status and pass this filter. |
| `expiringWithinDays` | `30` | Window for `expiring_soon`. |
| `maxNewPerState` | `5` | Cap on `new_license` rows per state per run. On the very first run *every* license is new, so this keeps it to a sample of the newest issue dates. On later runs, extras beyond the cap stay unseen and come out next run. |
| `firstRunMode` | `emitAll` | `emitAll`: report the newest licenses per state on the first run. `baseline`: silently record everything and emit nothing, so the next run reports only what changed. |
| `webhookUrl` | `""` | Optional POST target for the run summary. |
| `monitorId` | `default` | One state store per ID — run several watchlists side by side. |
| `maxItems` | `10` | Hard cap on rows per run across all change types. Rows beyond the cap are **not** marked seen and come out next run. |

Rows are emitted in priority order across all states — new licenses, then status changes, then removals, then
expirations — so an expiration backlog in one state never crowds out an enforcement alert in another.

### Recommended setup for a daily feed

1. Create a task with your `states`, `licenseTypes` and `statuses`, and give `monitorId` a meaningful name (`ny-retail`).
2. **First run: set `firstRunMode` to `baseline`.** It records every current license and emits nothing, so day one is not a
   thousand-row backlog. (The default `{}` run uses `emitAll` instead so you see real rows on a first test — the five newest
   licenses in NY and MA.)
3. Raise `maxItems` (500+) and `maxNewPerState` (50+) so a busy week is never truncated.
4. **Schedule the task daily.** CA refreshes nightly; NY, MA, WA and CT update weekly; CO, NV, MD and ME monthly. A daily
   run costs only the start fee on days nothing changed.
5. Point `webhookUrl` at Slack (incoming webhook), Zapier, Make or your own endpoint.

State lives in a named key-value store `cannabis-monitor-<hash of monitorId>` in your Apify account
(`STATE` record: `seen[state][licenseNumber] → {status, expirationDate, …}`, capped at 50,000 licenses per state).
Delete the store to reset a monitor. If a state's source fails or returns a partial list (fewer than half the
licenses seen last time), that state is skipped for the run and its saved state is left untouched, so a regulator
outage never produces a wave of false `removed` rows.

### Example profiles

**Vendor prospecting — new retail licenses in NY, MA and CT**

```json
{ "states": ["NY", "MA", "CT"], "licenseTypes": ["retail", "dispensary", "microbusiness"], "statuses": ["active"],
  "maxNewPerState": 50, "maxItems": 500, "firstRunMode": "baseline", "monitorId": "northeast-retail" }
```

**Insurer / lender — enforcement actions across every state**

```json
{ "states": ["CA", "NY", "MA", "WA", "NV", "ME"], "statuses": ["suspended", "revoked", "expired", "inactive"],
  "maxItems": 1000, "firstRunMode": "baseline", "monitorId": "enforcement" }
```

**Compliance — renewals due in the next 60 days for California cultivators**

```json
{ "states": ["CA"], "licenseTypes": ["cultivation"], "expiringWithinDays": 60, "maxItems": 2000, "monitorId": "ca-cult-renewals" }
```

### Output

One row per change:

```json
{
  "state": "NY",
  "licenseNumber": "OCM-PT3B-26-000185",
  "businessName": "S'Lo Down LLC",
  "dbaName": null,
  "licenseType": "Adult-Use Processor Type Three-Branding",
  "status": "active",
  "statusRaw": "Active",
  "previousStatus": null,
  "issueDate": "2026-09-24",
  "expirationDate": "2028-09-24",
  "daysToExpiration": 727,
  "address": null,
  "city": null,
  "zip": null,
  "county": null,
  "sourceUrl": "https://data.ny.gov/Economic-Development/Current-OCM-Licenses/jskf-tt3q",
  "changeType": "new_license",
  "firstSeenAt": "2026-09-28T21:45:53.606Z",
  "monitorId": "default"
}
```

A status change looks like `{"changeType": "status_change", "status": "suspended", "statusRaw": "Suspended", "previousStatus": "active", …}`;
an expiration alert like `{"changeType": "expiring_soon", "expirationDate": "2026-10-18", "daysToExpiration": 20, …}`.
Address fields are `null` where the regulator does not publish them (see the coverage table).

### Webhook payload

POSTed once per run as `application/json`, also saved as the `SUMMARY` record in the run's key-value store:

```json
{
  "monitorId": "northeast-retail",
  "runAt": "2026-09-29T10:00:03.118Z",
  "baseline": false,
  "states": ["NY", "MA", "CT"],
  "failedStates": [],
  "newCount": 3,
  "statusChangeCount": 1,
  "expiringCount": 4,
  "removedCount": 0,
  "seenTotal": { "NY": 2265, "MA": 953, "CT": 115 },
  "changes": [ { "...first 50 rows, same shape as the dataset..." } ]
}
```

### Pricing

Pay per event: a small start fee plus a per-row fee **only for changes emitted**. A daily monitor that finds nothing new
costs just the start fee. Need to check a specific list of licensees instead of watching for changes? Use the companion
**Cannabis License Verifier** actor.

# Actor input Schema

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

Two-letter codes of the states to watch. Supported: CA, NY, MA, WA, CO, NV, MD, ME, CT, FL, MI, OH. Each state's full regulator list is downloaded on every run.

## `licenseTypes` (type: `array`):

Case-insensitive substrings matched against the regulator's license type, e.g. "retail", "dispensary", "cultivat", "manufactur", "microbusiness". Empty = all types.

## `statuses` (type: `array`):

Only report licenses whose current normalised status is one of: active, pending, inactive, expired, suspended, revoked, unknown. For status changes the NEW status is tested (e.g. \["suspended","revoked"] alerts only on enforcement actions). Empty = all.

## `expiringWithinDays` (type: `integer`):

Emit an expiring\_soon row for licenses whose expiration date falls within this many days. Each license is alerted once per expiration date (a renewal resets it).

## `maxNewPerState` (type: `integer`):

Cap on new\_license rows per state per run, newest issue date first. On the very first run every license is new, so this keeps the first run to a sample; later runs carry uncapped extras over to the next run.

## `firstRunMode` (type: `string`):

What to do when the monitor has no saved state yet. emitAll: report the newest licenses per state as new\_license (good for a first test). baseline: silently record every current license and emit nothing, so the next scheduled run reports only what changed since.

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

Optional. After each run a JSON summary {monitorId, runAt, newCount, statusChangeCount, expiringCount, removedCount, changes\[first 50]} is POSTed here (Slack/Zapier/Make/your API).

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

Name of this watchlist. Each monitor ID keeps its own seen-state in a key-value store named cannabis-monitor-<hash>, so you can run several profiles (e.g. "ny-retail", "ca-enforcement") side by side.

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

Hard cap on rows emitted per run across all change types. Changes beyond the cap are not marked as seen and come out on the next run.

## Actor input object example

```json
{
  "states": [
    "NY",
    "MA"
  ],
  "licenseTypes": [],
  "statuses": [],
  "expiringWithinDays": 30,
  "maxNewPerState": 5,
  "firstRunMode": "emitAll",
  "webhookUrl": "",
  "monitorId": "default",
  "maxItems": 10
}
```

# Actor output Schema

## `records` (type: `string`):

Dataset of license changes found in this run: new licenses, status changes, expiring soon, removed (JSON).

# 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 = {
    "states": [
        "NY",
        "MA"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("outstanding_vegetable/cannabis-license-monitor").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 = { "states": [
        "NY",
        "MA",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("outstanding_vegetable/cannabis-license-monitor").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 '{
  "states": [
    "NY",
    "MA"
  ]
}' |
apify call outstanding_vegetable/cannabis-license-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,outstanding_vegetable/cannabis-license-monitor"
        }
    }
}
```

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/YXp00yiBbRjcGfvYZ/builds/DsVXoqa48GBzlGPaw/openapi.json
