# Cannabis Dispensary Monitor: 47 US States & All of Canada (`registryfeeds/cannabis-dispensary-scraper`) Actor

Track cannabis dispensary openings, closures, and license changes across 47 US states and 13 Canadian provinces. Official registries plus platforms (Weedmaps, Leafly, Dutchie), deduplicated, with change detection and per-state license coverage: know which states have complete official data each run.

- **URL**: https://apify.com/registryfeeds/cannabis-dispensary-scraper.md
- **Developed by:** [Zach Wallace](https://apify.com/registryfeeds) (community)
- **Categories:** Lead generation, Automation
- **Stats:** 35 total users, 1 monthly users, 100.0% runs succeeded, 2 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.20 / 1,000 results

This Actor is paid per event and usage. You are charged both the fixed price for specific events and for Apify platform usage.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Cannabis Dispensary Monitor, 47 US States & All of Canada

**The only cannabis Actor that combines official government license registries with platform listings, across the US and all of Canada.** Every competing scraper covers one platform (Weedmaps, Leafly, or Dutchie) or, at best, one state's license data. This Actor merges **official license databases from 47 US states/territories and all 13 Canadian provinces & territories**, license number, status, issue/expiry dates, with **6 platform sources** (Weedmaps, Leafly, iHeartJane, Dutchie, Potguide, AllBud) into one deduplicated dataset, then diffs each run against the last to surface what changed: new stores, closures, license suspensions, and address updates. Change events include `previousValues` and `currentValues` so you can see exactly what changed without diffing records yourself.

**Also includes deep menu data** (optional), strain type (indica/sativa/hybrid), THC/CBD percentages, effects, terpenes, per-weight price tiers (gram → ounce), brand, dosage, and staff picks, across 4 menu sources. Market intelligence and license expiry alerts included on every run at no extra cost.

> ⭐ **Using this Actor?** A quick [rating or review on the Apify Store listing](https://apify.com/registryfeeds/cannabis-dispensary-scraper) is the main way other buyers find it over a generic scraper. It takes a minute and genuinely helps.

### What does Cannabis Dispensary Monitor do?

This Actor scrapes publicly available dispensary listings from major cannabis platforms plus official government licensing databases. It normalizes all records into a consistent schema, deduplicates across sources, and flags changes between runs.

### Demo

**Watch the 30-second overview:**

https://www.youtube.com/watch?v=CnjX\_MoOx-M

**Sample dataset output** (built from real Actor output, illustrating the schema, see the Output tab in Apify Console for the live table view):

![Sample dataset output showing dispensary records with name, chain, city, state, license number, and status columns](https://api.apify.com/v2/key-value-stores/FhYqkFe80tgK1UZLV/records/cannabis-output-table.svg)

**RUN\_HEALTH report** (illustrative example of the Key-Value store → RUN\_HEALTH key, showing the per-source status, item counts, and change-detection summary every run produces):

![RUN\_HEALTH showing per-source status, item counts, errors, and overall healthy verdict](https://api.apify.com/v2/key-value-stores/FhYqkFe80tgK1UZLV/records/cannabis-run-health.svg)

**Coverage:**

- **47 US states and territories** with official state license databases: PA, MD, DE, NY, NJ, CT, MA, CO, WA, CA, MI, IL, OR, NV, MO, OH, AZ, FL, MN, OK, NM, MT, VA, TX, AK, HI, LA, AR, MS, ND, ME, VT, RI, DC, NH, WV, SD, UT, IA, GA, AL, KY, NC, NE, PR, GU, VI
- **All 13 Canadian provinces and territories** with official regulator/retailer sources: Ontario (ON, ~1,884 stores, richest Canadian source, full address + coordinates + website), Saskatchewan (SK, ~204), Alberta (AB, ~751), British Columbia (BC, ~586), Manitoba (MB, ~247), Quebec (QC, ~113), Nova Scotia (NS, ~84, with coordinates, needs an Apify residential proxy in the cloud, since NSLC blocks datacenter IPs), Newfoundland & Labrador (NL, ~64), New Brunswick (NB, ~33), Yukon (YT, ~10), Northwest Territories (NT, 6), Prince Edward Island (PE, 5), Nunavut (NU, 2). Mix Canadian and US codes freely in one run, none collide. (Saskatchewan comes from the latest archived permit roster since SLGA blocks live automated access, so it can lag a few weeks; New Brunswick's government stores are scheduled to close 2026-08-03.)
- **6 platform sources**, Weedmaps, Leafly, iHeartJane, Dutchie, Potguide, AllBud, covering all US states with overlapping coverage for higher deduplication quality
- **Menu pricing** (optional), product counts, price ranges, per-category median/avg/min/max per dispensary
- **Market intelligence**, per-state analytics, chain concentration, top MSOs, delivery rates, automatically computed after every run
- **Social media handles** (optional), Instagram, Facebook, Twitter/X, TikTok extracted from dispensary websites
- **Social equity flags**, surfaced where a state's own license data documents a justice-involved/equity track (currently New York's CAURD program; other states don't publish this yet, see Known limitations)
- **Delivery-only detection**, dispensaries with no public storefront are flagged distinctly from ones you can walk into

#### State coverage at a glance

| State | Program | Est. licensed dispensaries | Official DB |
|-------|---------|---------------------------|-------------|
| California (CA) | Recreational + Medical | 1,250+ | CA DCC (reverse-engineered API) |
| Florida (FL) | Medical only | 550+ | FDOH OMMU |
| Oklahoma (OK) | Medical + Recreational | 2,000+ | OMMA (Socrata) |
| Colorado (CO) | Medical | 270+ | CO MED (public Google Sheet) |
| Washington (WA) | Recreational + Medical | 700+ | WA LCB (Socrata) |
| Michigan (MI) | Recreational + Medical | 600+ | MRA (Socrata) |
| Oregon (OR) | Recreational + Medical | 700+ | OLCC (Socrata) |
| Illinois (IL) | Recreational | 260+ | IDFPR (PDF export) |
| New York (NY) | Recreational + Medical | 250+ | OCM API |
| New Jersey (NJ) | No public directory | n/a | CRC (monitoring, site restructure retired the old directory) |
| Nevada (NV) | Recreational + Medical | 200+ | CCB (Socrata) |
| Massachusetts (MA) | Recreational + Medical | 115+ | CCC ("Where to Buy" public directory, incl. real coordinates) |
| Pennsylvania (PA) | Medical only | 180+ | DOH HTML |
| Maryland (MD) | Recreational + Medical | 100+ | MCA HTML |
| Arizona (AZ) | Recreational + Medical | 200+ | ADHS (Socrata) |
| Missouri (MO) | Recreational + Medical | 350+ | DHSS (Socrata) |
| Ohio (OH) | Recreational + Medical | 350+ | DOC (Socrata) |
| Texas (TX) | Medical only | ~50 | DSHS HTML |
| Nebraska (NE) | Medical | No public directory yet | NCRC (monitoring) |
| Guam (GU) | Recreational | 0 (cultivators only) | CCB (monitoring, no retail license issued yet) |
| US Virgin Islands (VI) | Recreational + Medical | 0 (10 conditional licenses, none operating) | OCR (monitoring, image-only list, no addresses yet) |
| Puerto Rico (PR) | Medical only | 300+ | JRCM (PDF export, Dec 2024) |
| + 25 more states | Various | Varies | State-specific |

Platform sources (Weedmaps, Leafly, iHeartJane) supplement official data and provide coverage for all US states.

**Quick smoke test:** Set `testMode: true`, caps every source (state and platform) at 3 results, runs in under 30 seconds, and costs a few cents regardless of how many states/sources you configure.

### Why use Cannabis Dispensary Monitor?

#### For developers and data teams

- **Cross-run change detection**, every run diffs against the previous snapshot and outputs `new_dispensary`, `dispensary_closed`, `license_suspended`, and `dispensary_updated` events. POST them to your webhook endpoint in real time
- **Official license data**, government Socrata APIs (CA DCC, CO MED, WA LCB, NY OCM, MA CCC, OR OLCC, NV CCB, AZ ADHS, and 34 more) provide verified license numbers, issue/expiry dates, and addresses the platforms often get wrong
- **Deep menu data** (optional), enable `includeMenuData: true` to get strain type (indica/sativa/hybrid/CBD), THC/CBD%, effects (up to 10), terpenes (up to 8), brand, dosage, per-weight price tiers (gram → ounce), staff picks, and in-stock status. Sourced from Weedmaps, Leafly, iHeartJane, and Dutchie across 4 overlapping APIs. Pushed to a separate `menu-items` dataset
- **Multi-source deduplication**, same location found on Weedmaps, Leafly, Dutchie, and the state DB → one clean merged record, not five duplicates. State source wins on address/license; platform source wins on ratings/hours. More overlap = higher confidence data
- **6 platform integrations**, Dutchie powers ~40% of US dispensaries (unique coverage). Full data needs a residential proxy; without one, Dutchie now falls back to sitemap metadata for partial records (name/city/state) rather than returning nothing. Potguide and AllBud are also integrated but currently non-functional upstream (see Known limitations), not part of the default source list
- **Historical trend tracking**, every healthy run appends a compact snapshot to `TREND_HISTORY`. `MARKET_INTELLIGENCE.growth` surfaces ready-made deltas vs. the previous run, 30 days ago, and 90 days ago, no need to diff raw exports yourself
- **License expiry alerts**, flags dispensaries whose license expires within 30/60/90 days via the `EXPIRING_LICENSES` KV key and webhook events
- **Per-state license-data coverage**, the `STATE_COVERAGE` output tells you, for every state you request, whether the dispensaries returned carry official license number/status or came through platform-only because that state's regulator feed was down. One `licenseDataComplete` boolean per state, so a silently-missing license field is an explicit signal, not something you discover by accident. States increasingly move their data off open portals; this is how you know the moment it happens
- **Enforcement/compliance actions** (optional), Colorado's public citation, fine, and suspension history in a separate `enforcement-actions` dataset, real compliance signal beyond a binary active/expired status
- **Run health monitoring + structural fingerprinting**, `RUN_HEALTH` shows per-source item counts, errors, `suspicious_zero`, and `below_expected` flags. That last one catches a known-large market (Oklahoma, Florida, Oregon, ...) that comes back far below its real size even on the very first run, when there's no error and no prior count to compare against, the exact signature of a state pulling its data off an open portal. On top of that, every source's field structure is fingerprinted after each healthy run. If Weedmaps silently drops the `name` field or Leafly's `address.city` null rate jumps 40+ percentage points, `overallStatus` immediately flips to `"degraded"` with the specific source and field named, you find out the same run the API broke, not after bad data reaches your pipeline
- **Coordinate geocoding** (optional), state databases publish addresses but not lat/lng. Enable `geocodeMissingCoordinates: true` to fill gaps via OpenStreetMap Nominatim, cached across runs so each address is only ever geocoded once. Coordinates include `geocodeSource` (`nominatim_full` or `nominatim_citystate`) so you know which are precise vs approximate
- **Any format**, JSON, CSV, Excel via Apify's native export

#### Use cases by buyer type

**Cannabis operators**, track competitor openings within 5 miles of your stores. Get a webhook when a new license is issued in your market. Export to CSV for your sales team.

**B2B sales teams**, build contact lists of licensed dispensaries with phone numbers, websites, addresses, and social media handles. Filter by state, category (medical vs recreational), and chain affiliation. 18,000–25,000 unique dispensaries with contact info across a full run.

**Investors and M\&A**, track chain expansion by monitoring `chain` field changes over time. Detect when a regional operator crosses a threshold. License status + expiry dates surface distressed assets before they make the news.

**Compliance and legal**, automatic license expiry notifications at 30/60/90 days. License status change webhooks (`active → suspended`) deliver via POST the same day the government database updates. Covers 42 state databases.

**Commercial real estate**, dispensary density maps using lat/lng coordinates. Find saturated markets and underserved zip codes. Cross-reference against zoning data. Export to GeoJSON.

**Market research**, per-dispensary product counts and pricing stats across flower, edibles, vapes, concentrates when `includeMenuData: true`. Median prices and sale counts enable competitive price benchmarking.

**App developers**, build a dispensary finder with fresh weekly data instead of maintaining your own scrapers. The deduplicated dataset + coordinates is production-ready.

### Use Case Examples

#### Dispensary operator: monitor competitor openings in your market

A Colorado dispensary chain wants to know within 24 hours when a new license is issued within 50 miles of their stores. They run this Actor on a weekly schedule with `states: ["CO"]`, `webhookUrl` pointing to their Slack integration, and `webhookStateFilter: ["CO"]`. Each Monday morning they receive a POST payload listing any `new_dispensary` events from the prior week, name, address, coordinates, license number, and license issue date. When a competitor opens near one of their locations, their ops team sees it before customers do.

#### B2B sales team: build a dispensary contact list for the Southeast

A cannabis software company (POS systems, compliance tools) wants to reach every active licensed dispensary in GA, AL, KY, NC, TN, FL, TX. They run the Actor with `states: ["GA","AL","KY","NC","FL","TX"]`, `maxResultsPerSource: 0`, `includeStateSources: true`, `enrichSocialMedia: true`. The output CSV gives them 3,500–5,000 records with business name, phone, website, social media handles, license number, license status, and category (medical vs recreational). They filter out `chain` !== null to target independents, and filter `licenseStatus: "active"` to skip expired licenses. Total cost: under $0.50.

#### Cannabis investor: track MSO footprint expansion

A private equity analyst tracks multi-state operator (MSO) expansion across quarterly runs. They run the Actor on all 47 states and territories with `maxResultsPerSource: 0` twice per quarter. Each run outputs a `CHANGES` KV key with `new_dispensary` events. By filtering `changes` where `dispensary.chain === "Trulieve"` (or any target MSO), they see exactly which markets each chain entered or exited. They also pull `MARKET_INTELLIGENCE.topChains` for national chain concentration numbers and `MARKET_INTELLIGENCE.growth`/`TREND_HISTORY` for quarter-over-quarter trajectory, data that traditional cannabis data vendors (Headset, BDSA) charge $5,000+/year to access.

#### App developer: dispensary finder with fresh weekly data

A developer building a dispensary-finder mobile app needs a dataset of all US dispensaries with coordinates, hours, services (pickup/delivery/storefront), and ratings, updated weekly without maintaining their own scrapers. They configure a weekly Apify schedule with `states` set to their target markets, `geocodeMissingCoordinates: true` (fills null coordinates via OpenStreetMap), and sync the dataset to their database via the Apify API after each run. The deduplicated dataset (~18,000 records for a full run) arrives as production-ready JSON with consistent schema across all sources.

#### Compliance team: license expiry monitoring

A cannabis law firm tracks renewal deadlines for 200+ dispensary clients across PA, MD, NJ, NY, CT, MA. They run the Actor weekly with `licenseExpiryAlertDays: [30, 60, 90]` and `webhookUrl` pointing to their case management system. The `license_expiring` events fire exactly 30, 60, and 90 days before each license expiration date, automatically, with no manual tracking spreadsheet. The `EXPIRING_LICENSES` KV key also gives them a full snapshot of every client with an upcoming renewal.

***

### How to use Cannabis Dispensary Monitor

1. Open the **Input** tab in Apify Console
2. Select which **Platform Sources** to scrape (Weedmaps, Leafly, iHeartJane enabled by default; Dutchie requires a residential proxy; Potguide and AllBud are selectable but currently non-functional upstream, see Known limitations)
3. Enter the **US States** you want, e.g. `["CA", "CO", "WA"]`
4. Keep **Include Official State Databases** enabled for license numbers and expiry dates
5. Optionally add a `webhookUrl` to receive change events after each run
6. Click **Start**, a typical 4-state run takes 5–15 minutes
7. Download from the **Output** tab in JSON, CSV, or Excel

**Run via API:**

```bash
curl -X POST "https://api.apify.com/v2/acts/YOUR_ACTOR_ID/runs" \
  -H "Authorization: Bearer YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"states": ["CA", "CO"], "sources": ["weedmaps", "iheartjane"], "maxResultsPerSource": 0}'
```

**Schedule weekly with change detection:**

1. Run once to build the baseline snapshot
2. Add a weekly schedule (Monday morning recommended)
3. Each subsequent run posts only what changed to your `webhookUrl`

### Input

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `testMode` | boolean | `false` | Collect only 3 results/source, skip snapshot update. Free smoke test |
| `sources` | array | `["weedmaps","leafly","iheartjane"]` | Platform sources to scrape. Options: `weedmaps`, `leafly`, `iheartjane`, `dutchie` (residential proxy required), `potguide`, `allbud` (currently non-functional upstream, see Known limitations) |
| `states` | array | `["PA","MD","DE","NY","NJ","CT","MA","CO","WA"]` | US state abbreviations. Supported state DBs: PA MD DE NY NJ CT MA CO WA CA MI IL OR NV MO OH AZ FL MN OK NM MT VA TX AK HI LA AR MS ND ME VT RI DC NH WV SD UT IA GA AL KY NC NE |
| `includeStateSources` | boolean | `true` | Include official state license databases (adds license numbers, expiry dates) |
| `maxResultsPerSource` | integer | `100` | Max dispensaries per source per state. `0` = unlimited |
| `sourceTimeoutSecs` | integer | `300` | Max seconds per source before cutoff (prevents one hung source from killing the run) |
| `includeMenuData` | boolean | `false` | Scrape deep menu data per dispensary, strain type (indica/sativa/hybrid), THC/CBD%, effects, terpenes, brand, dosage, per-weight price tiers, staff picks. Pushed to separate `menu-items` dataset |
| `includeEnforcementData` | boolean | `false` | Scrape Colorado MED's public enforcement/compliance actions (citations, fines, suspensions). Pushed to separate `enforcement-actions` dataset, not merged into dispensary records |
| `webhookUrl` | string | n/a | HTTPS endpoint to receive change/expiry events after each run |
| `webhookStateFilter` | array | n/a | Only send webhook events for these states (empty = all states) |
| `licenseExpiryAlertDays` | array | `[30,60,90]` | Days-before-expiry windows for license expiry alerts |
| `suppressInitialStateChanges` | boolean | `true` | Suppress `new_dispensary` events when adding a new state for the first time |
| `onlyOutputChanges` | boolean | `false` | Only write new/changed dispensaries to the dataset instead of the full dataset every run, built for recurring/scheduled monitoring, where re-billing for thousands of unchanged records each run doesn't make sense. `MARKET_INTELLIGENCE`, `TREND_HISTORY`, and the standby API still reflect the full current dataset regardless. The first run always outputs everything (nothing to diff against yet). Ignored in `testMode` |
| `geocodeMissingCoordinates` | boolean | `false` | Fill null coordinates via free OpenStreetMap Nominatim API |
| `enrichSocialMedia` | boolean | `false` | Scrape each dispensary's website to extract Instagram, Facebook, Twitter/X, TikTok, YouTube handles. Capped at 1,000 dispensaries. Adds run time. |
| `enrichEmail` | boolean | `false` | Scrape each dispensary's website to extract a contact email (mailto: links first, page text as fallback). Fills in `email` for dispensaries whose source didn't already publish one. Capped at 1,000 dispensaries. Adds run time. |
| `proxyConfiguration` | object | n/a | Apify proxy settings (recommended for large runs or rate-limited sources) |

**Quick test:**

```json
{
    "testMode": true,
    "states": ["CA", "CO"],
    "sources": ["weedmaps"]
}
```

**Full run with webhook alerts:**

```json
{
    "states": ["PA", "NY", "CO", "CA"],
    "sources": ["weedmaps", "leafly", "iheartjane"],
    "includeStateSources": true,
    "maxResultsPerSource": 0,
    "webhookUrl": "https://your-endpoint.com/cannabis-alerts",
    "licenseExpiryAlertDays": [30, 60, 90]
}
```

### Output

#### Main Dataset, Dispensary Records

Download in JSON, CSV, HTML, or Excel from the Output tab or via the Apify API.

**Example records:**

```json
[
  {
    "id": "wm_48291",
    "sources": ["weedmaps", "state-pa"],
    "name": "Maitri Medicinals",
    "slug": "maitri-medicinals",
    "url": "https://weedmaps.com/dispensaries/maitri-medicinals",
    "address": {
      "street": "6056 Broad St",
      "city": "Pittsburgh",
      "state": "PA",
      "zip": "15206",
      "country": "US"
    },
    "coordinates": { "lat": 40.4551, "lng": -79.9218 },
    "phone": "+14123625220",
    "email": null,
    "website": "https://maitrimedicinals.com",
    "hours": {
      "monday": "10am-7pm", "tuesday": "10am-7pm", "wednesday": "10am-7pm",
      "thursday": "10am-7pm", "friday": "10am-7pm",
      "saturday": "10am-7pm", "sunday": "11am-5pm"
    },
    "rating": 4.7,
    "reviewCount": 312,
    "services": { "delivery": false, "pickup": true, "storefront": true },
    "licenseNumber": "MM-PA-0012432",
    "licenseStatus": "active",
    "licenseIssuedDate": "2021-03-15",
    "licenseExpiresDate": "2026-03-14",
    "socialEquity": false,
    "categories": ["medical"],
    "amenities": ["atm", "parking", "ada_accessible"],
    "acceptsCreditCard": true,
    "deliveryOnly": false,
    "chain": null,
    "menu": {
      "totalProducts": 87,
      "onSaleCount": 5,
      "byCategory": { "flower": 22, "edibles": 18, "vapes": 15, "concentrates": 12 },
      "priceRange": { "min": 5, "max": 95 },
      "priceByCategory": {
        "flower":       { "count": 22, "min": 10, "max": 55, "avg": 28.50, "median": 27.00, "onSale": 3 },
        "edibles":      { "count": 18, "min": 5,  "max": 35, "avg": 18.20, "median": 16.50 },
        "vapes":        { "count": 15, "min": 25, "max": 75, "avg": 48.00, "median": 45.00 }
      }
    },
    "lastScraped": "2025-01-15T14:22:05.000Z"
  },
  {
    "id": "wm_55102",
    "sources": ["weedmaps", "leafly"],
    "name": "Housing Works Cannabis Co.",
    "address": { "street": "750 Broadway", "city": "New York", "state": "NY", "zip": "10003", "country": "US" },
    "coordinates": { "lat": 40.7285, "lng": -73.9920 },
    "phone": "+12125054100",
    "website": "https://hwcannabis.co",
    "rating": 4.4,
    "reviewCount": 890,
    "services": { "delivery": false, "pickup": true, "storefront": true },
    "licenseNumber": "OCM-CAURD-22-000001",
    "licenseStatus": "active",
    "socialEquity": true,
    "categories": ["recreational"],
    "amenities": null,
    "acceptsCreditCard": null,
    "deliveryOnly": false,
    "chain": null,
    "menu": null,
    "googleMapsUrl": "https://www.google.com/maps/search/Housing+Works+Cannabis+Co.+750+Broadway+New+York+NY+10003",
    "lastScraped": "2025-01-15T14:22:05.000Z"
  }
]
```

#### Data Field Reference

| Field | Type | Description |
|-------|------|-------------|
| `id` | string | Stable unique ID, source-prefixed (`wm_`, `lf_`, `jhj_`, `state_pa_`, etc.) |
| `sources` | string\[] | All sources that contributed data to this record |
| `name` | string | Dispensary display name |
| `slug` | string | URL slug (from platform sources) |
| `url` | string | Platform listing URL |
| `address` | object | `street`, `city`, `state`, `zip`, `country` |
| `coordinates` | object | `lat`, `lng` decimal degrees |
| `phone` | string | E.164 format (`+1XXXXXXXXXX`) |
| `email` | string | Public email if listed by the source, or scraped from the dispensary's website when `enrichEmail: true` |
| `website` | string | Dispensary website URL |
| `hours` | object | Keyed by day name (`monday`–`sunday`) |
| `rating` | number | Platform rating (0–5 scale) |
| `reviewCount` | number | Total review count |
| `services` | object | `delivery`, `pickup`, `storefront` booleans |
| `licenseNumber` | string | State-issued license number (from official DB when available) |
| `licenseStatus` | string | `active`, `suspended`, `expired`, `revoked`, `pending`, etc. |
| `licenseIssuedDate` | string | ISO date string (`YYYY-MM-DD`) |
| `licenseExpiresDate` | string | ISO date string, used for expiry alerts |
| `socialEquity` | boolean | `true`/`false` for licenses on a documented social-equity/justice-involved track (currently: NY's CAURD program), `null`/absent where the source doesn't publish an equity designation at all, most states don't |
| `categories` | string\[] | `medical`, `recreational`, or both |
| `amenities` | string\[] | Normalized amenity list (`atm`, `parking`, `ada_accessible`, etc.), from Weedmaps and Leafly when available |
| `acceptsCreditCard` | boolean | `true` if any source confirms credit card acceptance (iHeartJane, Weedmaps, Leafly), `null` if unknown |
| `deliveryOnly` | boolean | `true` when `services.storefront` is explicitly `false`, no public storefront, delivery/pickup only. `null` when storefront status itself is unknown |
| `chain` | string | MSO chain name if detected (`Curaleaf`, `Trulieve`, `GTI`, etc.), else `null` |
| `legalName` | string | Registered legal entity name, when distinct from the trade name in `name`, currently only populated for Illinois and Puerto Rico, whose license data separates the two |
| `menu` | object | Product counts + per-category pricing when `includeMenuData: true`. Includes `totalProducts`, `onSaleCount`, `byCategory`, `priceRange`, and `priceByCategory` (min/max/avg/median/onSale per category) |
| `socialMedia` | object | Instagram, Facebook, Twitter/X, TikTok, YouTube handles when `enrichSocialMedia: true` |
| `googleMapsUrl` | string | Direct Google Maps search link for the dispensary (computed from name + address) |
| `lastScraped` | string | ISO timestamp of this scrape |

#### Menu Items Dataset (when `includeMenuData: true`)

A separate `menu-items` dataset is pushed alongside the main dispensaries dataset. Each record represents a single product from a dispensary's menu:

```json
{
  "dispensaryId": "wm_48291",
  "dispensaryName": "Maitri Medicinals",
  "source": "weedmaps",
  "productId": "prod_9928471",
  "name": "Blue Dream (1g)",
  "brand": "Hollyweed",
  "category": "flower",
  "subcategory": "pre-roll",
  "strainType": "hybrid",
  "weightGrams": 1.0,
  "dosageMg": null,
  "priceUSD": 12.00,
  "salePriceUSD": null,
  "onSale": false,
  "priceTiers": { "gram": 12.00, "eighth": 38.00, "quarter": 70.00, "ounce": 240.00 },
  "thcPct": 22.4,
  "cbdPct": 0.1,
  "effects": ["euphoric", "creative", "energetic", "uplifted"],
  "terpenes": ["myrcene", "caryophyllene", "limonene"],
  "staffPick": false,
  "inStock": true,
  "imageUrl": "https://images.weedmaps.com/products/...",
  "description": "A classic sativa-dominant hybrid with sweet berry aromas...",
  "scrapedAt": "2025-01-15T14:22:05.000Z"
}
```

**Menu item fields:**

| Field | Description |
|-------|-------------|
| `strainType` | `indica`, `sativa`, `hybrid`, `cbd`, or `null`, sourced from all 4 platforms |
| `dosageMg` | Milligrams per serving, populated for edibles, capsules, tinctures |
| `thcPct` | THC percentage (e.g. `22.4` for 22.4%), available from Weedmaps, Leafly, iHeartJane, Dutchie |
| `cbdPct` | CBD percentage, same sources |
| `effects` | Array of up to 10 effect strings (e.g. `["euphoric","creative","relaxed"]`), from Leafly strain data, iHeartJane, Dutchie |
| `terpenes` | Array of up to 8 terpene names (e.g. `["myrcene","limonene"]`), from Leafly, iHeartJane, Dutchie |
| `staffPick` | Boolean, staff-curated picks from Dutchie and iHeartJane |
| `priceTiers` | Per-weight prices: `halfGram`, `gram`, `twoGram`, `eighth`, `quarter`, `halfOz`, `ounce`, from Weedmaps and iHeartJane |
| `brand` | Brand/manufacturer name, enables brand market share analysis across dispensaries |

This dataset is what makes the menu data genuinely valuable for market research, strain mix by dispensary, terpene preferences by region, brand distribution, effect profile trending across markets.

#### Enforcement Actions Dataset (when `includeEnforcementData: true`)

A separate `enforcement-actions` dataset, pushed when Colorado is in your `states` list. Sourced from Colorado MED's public administrative-actions list, citations, fines, license suspensions, and denials, going back to 2015:

```json
{
  "id": "co_enforcement_example-dispensary-llc_7-8-26",
  "state": "CO",
  "entityName": "Example Dispensary LLC",
  "actionType": "Stipulation, Agreement, and Order (SAO)",
  "actionDate": "2026-07-08",
  "sourceUrl": "https://drive.google.com/file/d/.../view",
  "scrapedAt": "2026-07-20T18:12:11.646Z"
}
```

**Not merged into the main dispensary dataset.** Colorado's public list only has the business entity name, no license number, so joining it onto dispensary records by name alone risks false positives (flagging the wrong business with a similar name). If you need to cross-reference, match `entityName` against dispensary `name`/`legalName` yourself with whatever confidence threshold fits your use case. Currently Colorado only; more states are being investigated (California publishes richer enforcement data with real license numbers, but its site's data-loading mechanism hasn't been reverse-engineered yet).

#### Webhook Events

Set `webhookUrl` to receive a POST request after each run. The payload is a JSON array of change events:

```json
[
  {
    "event": "new_dispensary",
    "dispensary": { "name": "Acme Cannabis", "address": { "city": "Denver", "state": "CO" }, "licenseNumber": "MED-CO-001234", ... },
    "detectedAt": "2025-01-22T10:00:00.000Z"
  },
  {
    "event": "license_suspended",
    "dispensary": { "name": "Green Leaf Denver", ... },
    "changedFields": ["licenseStatus"],
    "previousValues": { "licenseStatus": "active" },
    "currentValues":  { "licenseStatus": "suspended" },
    "detectedAt": "2025-01-22T10:00:00.000Z"
  },
  {
    "event": "dispensary_updated",
    "dispensary": { "name": "The Green Solution", ... },
    "changedFields": ["phone", "hours.monday"],
    "previousValues": { "phone": "+13035550001", "hours.monday": "9am-9pm" },
    "currentValues":  { "phone": "+13035559999", "hours.monday": "10am-8pm" },
    "detectedAt": "2025-01-22T10:00:00.000Z"
  },
  {
    "event": "license_expiring",
    "dispensary": { "name": "High Five Dispensary", "licenseExpiresDate": "2025-02-15", ... },
    "daysUntilExpiry": 24,
    "detectedAt": "2025-01-22T10:00:00.000Z"
  },
  {
    "event": "source_broken",
    "sourceId": "state-or",
    "status": "failed",
    "itemsScraped": 0,
    "lastError": "dataset.missing",
    "consecutiveZeroRuns": 1,
    "detectedAt": "2025-01-22T10:00:00.000Z"
  }
]
```

Event types: `new_dispensary`, `dispensary_closed`, `dispensary_updated`, `license_suspended`, `license_expired`, `license_expiring`, `source_broken`, `source_recovered`.

Every `updated` variant (`dispensary_updated`, `license_suspended`, `license_expired`) includes `previousValues` and `currentValues` objects with the exact before/after for each changed field. No need to store full snapshots yourself, the diff is pre-computed.

Note: `rating`-only changes are tracked in `changedFields` but do not trigger webhook delivery (too noisy at scale). All other field changes trigger delivery.

`source_broken` / `source_recovered` are a different kind of signal from the rest, not a dispensary changing, but one of the underlying government/platform sources itself going down or coming back (e.g. a state agency restructures its site and the scraper starts returning 0 results). Fires once on the transition, not on every run a source stays broken, so a source that's been down for weeks on a daily schedule doesn't re-alert every day. A source with a known, documented reason to be empty (e.g. a state that hasn't published a directory yet) never counts as "broken" here, see `RUN_HEALTH` below for the full per-source status on every run, whether or not anything changed.

#### CHANGES Key (Key-Value Store)

Every run saves a structured change report to `CHANGES` in the default Key-Value store:

```json
{
  "summary": { "new": 3, "closed": 1, "updated": 8 },
  "suppressedNew": 12,
  "changes": [
    { "changeType": "new_dispensary", "dispensary": { "name": "New Store", "address": {...} }, "detectedAt": "..." },
    { "changeType": "updated", "dispensary": { "name": "Green Leaf", ... }, "changedFields": ["hours.monday"], "previousValues": { "hours.monday": "9am-9pm" }, "currentValues": { "hours.monday": "10am-8pm" }, "detectedAt": "..." }
  ]
}
```

#### RUN\_HEALTH Key (Key-Value Store)

```json
{
  "overallStatus": "degraded",
  "totalDispensaries": 712,
  "deduplicationRate": "18%",
  "sources": {
    "weedmaps": {
      "status": "ok",
      "itemsScraped": 380,
      "errors": 0,
      "fingerprintDrift": "critical"
    },
    "leafly":   { "status": "ok", "itemsScraped": 290, "errors": 0, "fingerprintDrift": null },
    "state-ny": { "status": "ok", "itemsScraped": 88,  "errors": 0, "fingerprintDrift": null },
    "state-pa": { "status": "degraded", "itemsScraped": 42, "errors": 1, "fingerprintDrift": null }
  },
  "fingerprintDrifts": {
    "critical": 1,
    "sources": { "weedmaps": "critical" }
  },
  "changesDetected": { "new": 3, "closed": 1, "updated": 8 }
}
```

`overallStatus` values: `healthy` · `degraded` (partial data, source errors, a large market below its expected floor, or critical fingerprint drift) · `failed` (all sources returned 0 items). A `degraded` run still exits successfully, but the Console run's own status message names which sources broke, you don't have to open `RUN_HEALTH` to notice.

Per-source `status` values: `ok` · `degraded` (partial data) · `failed` (0 items) · `suspicious_zero` (zero when previous run had data, silent break) · `persistently_empty` (zero for 3+ consecutive runs) · `below_expected` (a known-large market, e.g. Oklahoma or Florida, came back far below the floor that market can possibly have, a silent break even on the first run, with no error and no prior count to compare against; the source's `expectedFloor` is included) · `not_yet_available` (documented, expected zero, e.g. a state whose regulator hasn't published a directory yet, not a bug) · `timeout` (killed by per-source timeout guard).

`fingerprintDrift` per source: `null` (no drift) · `"warning"` (non-critical field changes) · `"critical"` (required field disappeared or key null rate spiked, investigate immediately).

#### MARKET\_INTELLIGENCE Key (Key-Value Store)

Computed automatically after every run. Zero extra configuration needed.

```json
{
  "generatedAt": "2025-01-22T10:00:00.000Z",
  "national": {
    "totalDispensaries": 18247,
    "medical": 4891,
    "recreational": 9823,
    "both": 3533,
    "withDelivery": 3841,
    "withPickup": 14322,
    "chainAffiliated": 5734,
    "independent": 12513,
    "chainConcentration": 0.3143,
    "avgRating": 4.21,
    "stateCount": 44
  },
  "byState": {
    "FL": {
      "total": 2487,
      "medical": 2487,
      "recreational": 0,
      "both": 0,
      "chainAffiliated": 1823,
      "chainConcentration": 0.7329,
      "avgRating": 4.3,
      "topChains": [
        { "chain": "Trulieve", "count": 134 },
        { "chain": "Curaleaf", "count": 57 },
        { "chain": "Fluent Cannabis Care", "count": 32 }
      ],
      "withDelivery": 2100,
      "sourceCoverage": ["weedmaps", "leafly", "state-fl"]
    },
    "CO": {
      "total": 591,
      "medical": 221,
      "recreational": 370,
      "both": 154,
      "chainAffiliated": 187,
      "chainConcentration": 0.3164,
      "avgRating": 4.1,
      "topChains": [
        { "chain": "Schwazze (Star Buds)", "count": 34 },
        { "chain": "LivWell", "count": 26 },
        { "chain": "Native Roots", "count": 18 }
      ],
      "sourceCoverage": ["weedmaps", "leafly", "iheartjane", "state-co"]
    }
  },
  "topChains": [
    { "chain": "Trulieve",  "count": 187, "stateCount": 12, "states": ["FL","PA","MA","CT","MD","GA","AL","WV","OH","TX","CO","AZ"] },
    { "chain": "Curaleaf",  "count": 156, "stateCount": 23, "states": ["FL","NY","PA","NJ","CT","MA","MD","OH","MI","IL","OR","AZ","CO","MO","OK","NV","UT","WV","MN","ND","SD","MS","ME"] },
    { "chain": "Verano",    "count": 143, "stateCount": 14, "states": ["IL","OH","PA","NJ","MD","FL","MA","WV","NM","NV","AZ","AR","MO","MI"] }
  ],
  "marketConcentration": {
    "top5SharePct": 14.2,
    "top10SharePct": 21.8,
    "top25SharePct": 31.4,
    "largestChain": "Trulieve",
    "largestChainCount": 187,
    "largestChainPct": 1.03
  },
  "recentActivity": {
    "newDispensaries": 12,
    "closedDispensaries": 3,
    "updatedDispensaries": 47
  },
  "growth": {
    "vsPreviousRun": { "asOf": "2025-01-15", "totalDispensariesDelta": 63, "totalDispensariesPct": 0.35, "stateCountDelta": 0 },
    "vs30DaysAgo": { "asOf": "2024-12-23", "totalDispensariesDelta": 214, "totalDispensariesPct": 1.19, "stateCountDelta": 1 },
    "vs90DaysAgo": { "asOf": "2024-10-25", "totalDispensariesDelta": 891, "totalDispensariesPct": 5.13, "stateCountDelta": 2 }
  }
}
```

This level of market intelligence, per-state chain concentration, MSO footprint, delivery penetration rates, costs thousands of dollars per report from traditional cannabis data vendors (Headset, BDSA, New Cannabis Ventures). Here it's a free KV output on every run.

The `growth` field appears once at least one prior run exists, comparing the current run against the previous run, the closest run at least 30 days back, and the closest run at least 90 days back, each falls back to `null` until history reaches that far. Powered by `TREND_HISTORY` below.

#### TREND\_HISTORY Key (Key-Value Store)

A running time series, one compact entry appended per healthy run, the raw data behind the `growth` field above. Skipped in `testMode` and on regression-guard failures so the baseline never gets polluted with a partial or bogus data point. Capped at the most recent 260 entries (~5 years of weekly runs); older entries roll off automatically.

```json
[
  {
    "date": "2024-10-25",
    "timestamp": "2024-10-25T10:00:00.000Z",
    "totalDispensaries": 17356,
    "stateCount": 42,
    "medical": 4650,
    "recreational": 9380,
    "both": 3326,
    "chainConcentration": 0.302,
    "withDelivery": 3600,
    "newDispensaries": 18,
    "closedDispensaries": 5,
    "byState": { "FL": 2401, "CO": 580 }
  }
]
```

#### STATE\_COVERAGE Key (Key-Value Store)

Dispensary records come from two kinds of source: **official state regulators**, which carry a `licenseNumber` and `licenseStatus`, and **platforms** (Leafly, Weedmaps, Dutchie), which do not. When a state moves its licence data off its open-data portal, that state's dispensaries still appear via the platforms, but silently without licence fields, and from the raw dataset alone you can't tell "unlicensed" from "the government feed was down this run".

`STATE_COVERAGE` makes that explicit. For every requested state it reports how many dispensaries came back, how many carry official licence data, and whether that state's licence data is complete this run (and if not, why). It reuses the official source's health status, including the `below_expected` floor signal, so it never disagrees with `RUN_HEALTH`. `RUN_HEALTH` also carries a `statesWithIncompleteLicenseData` array as the one-line headline.

```json
{
  "generatedAt": "2026-08-03T19:44:38.301Z",
  "summary": {
    "statesReported": 4,
    "statesWithCompleteLicenseData": 1,
    "statesWithIncompleteLicenseData": ["OK", "AZ", "MD"]
  },
  "states": {
    "OR": {
      "total": 777, "officialSourced": 777, "withLicenseNumber": 777,
      "licenseCoverage": "100%", "officialSourceId": "state-or",
      "officialStatus": "ok", "expectedFloor": 80,
      "licenseDataComplete": true, "reason": "official_ok",
      "note": "Official state source delivered — license number and status present."
    },
    "OK": {
      "total": 34, "officialSourced": 0, "withLicenseNumber": 0,
      "licenseCoverage": "0%", "officialSourceId": "state-ok",
      "officialStatus": "below_expected", "expectedFloor": 40,
      "licenseDataComplete": false, "reason": "official_below_expected",
      "note": "Official source returned far below this market's expected size — license number and status are likely missing for most records this run."
    }
  }
}
```

`reason` is one of `official_ok` · `official_ok_on_fallback` · `no_official_directory_yet` (state hasn't published a directory yet, an expected gap, not a break) · `official_below_expected` · `official_failed` · `official_partial` · `no_official_source`. Use `licenseDataComplete` as the single boolean gate for "does this state have trustworthy licence data this run".

#### EXPIRING\_LICENSES Key (Key-Value Store)

```json
{
  "alertedAt": "2025-01-22T10:00:00.000Z",
  "windows": {
    "30": [{ "name": "High Five Dispensary", "licenseExpiresDate": "2025-02-15", "daysUntilExpiry": 24, "state": "CO" }],
    "60": [...],
    "90": [...]
  }
}
```

### Standby REST API

When run in [Apify Standby mode](https://docs.apify.com/platform/actors/development/programming-interface/standby), this Actor exposes a live REST API. The in-memory dataset is populated from the last run snapshot at startup and refreshed after each scrape completes, so your app always queries fresh data without triggering a new run.

**Base URL:** `https://<containerId>.runs.apify.net`

#### Endpoints

**`GET /dispensaries`**, Query the full dataset with filters

```
GET /dispensaries?state=CO&category=recreational&chain=Schwazze&limit=50&offset=0&q=denver
```

| Parameter | Description |
|-----------|-------------|
| `state` | 2-letter state abbreviation (e.g. `CA`, `CO`) |
| `category` | `medical` or `recreational` |
| `chain` | MSO chain name (e.g. `Trulieve`, `Curaleaf`) |
| `source` | Filter by source (`weedmaps`, `leafly`, `iheartjane`, `state-co`, etc.) |
| `q` | Full-text search on name and city |
| `limit` | Results per page, max 500 (default: 50) |
| `offset` | Pagination offset (default: 0) |

```json
{
  "total": 127,
  "limit": 50,
  "offset": 0,
  "loadedAt": "2026-04-21T08:00:00.000Z",
  "data": [ { "id": "wm_48291", "name": "L'eagle Services", ... } ]
}
```

**`GET /dispensaries/:id`**, Single dispensary by ID

```
GET /dispensaries/wm_48291
```

**`GET /health`**, Last run health report (same as `RUN_HEALTH` KV key)

**`GET /changes?since=2026-04-01T00:00:00Z&state=CO`**, Change events with optional filters

| Parameter | Description |
|-----------|-------------|
| `since` | ISO datetime, only return changes detected after this time |
| `state` | Filter to a single state |

**`GET /`**, Readiness probe + live status

```
Cannabis Dispensary Scraper, 22847 dispensaries loaded, updated 2026-04-21T08:00:00.000Z
```

#### App developer quickstart

```bash
## Get all Colorado recreational dispensaries
curl "https://<containerId>.runs.apify.net/dispensaries?state=CO&category=recreational&limit=100"

## Get change events since last week
curl "https://<containerId>.runs.apify.net/changes?since=2026-04-14T00:00:00Z"

## Look up a specific dispensary
curl "https://<containerId>.runs.apify.net/dispensaries/wm_48291"
```

The Standby API is ideal for dispensary-finder apps that need sub-second query response without re-scraping on every user request. Schedule a weekly batch scrape → serve from the REST API for the rest of the week.

### Integrations, plug it into your stack

This Actor works with the tools you already use, no glue code required.

- **Make, Zapier, and n8n**, Apify ships official connectors for all three. Add your Apify token, pick this Actor, and trigger a scenario/zap/workflow whenever a run finishes, then route the dataset straight into Google Sheets, Airtable, a CRM, Slack, or a database. See Apify's [Make](https://apify.com/integrations/make), [Zapier](https://apify.com/integrations/zapier), and [n8n](https://apify.com/integrations/n8n) integrations.
- **Webhooks (real-time change alerts)**, set the `webhookUrl` input and this Actor POSTs `new_dispensary`, `dispensary_closed`, `license_suspended`, `license_expiring_soon`, and `source_broken` events as they happen. Point it at a Make/Zapier/n8n webhook trigger, a Slack/Discord incoming webhook, or your own endpoint. Narrow the noise with `webhookStateFilter: ["CO", "CA"]`.
- **API and scheduling**, run on a schedule from the Apify Console, or call it from your backend via the [Apify API](https://docs.apify.com/api/v2) and pull the dataset as JSON, CSV, or Excel.
- **AI agents (MCP)**, this Actor is callable from LLM agents through Apify's MCP server, so an agent can pull live dispensary/license data on demand.

**Example recipe:** a new dispensary opens in a state you track → `webhookUrl` fires a `new_dispensary` event → Make/Zapier catches it → appends a row to Google Sheets and posts a Slack alert to your sales channel. No polling, no code.

### Pricing, How much does it cost?

Pay-per-event: **$0.005 per dispensary record** written to the dataset on the Free plan, plus a negligible one-time Actor-start charge. Only records in the main dataset are billed, menu items go to a separate dataset and aren't charged at all, so `includeMenuData: true` doesn't add cost.

Apify's Store discount pricing applies automatically based on your own subscription plan, no separate signup needed on this Actor:

| Your Apify plan | Price per record |
|---|---|
| Free | $0.0050 |
| Bronze | $0.0043 |
| Silver | $0.0037 |
| Gold and above | $0.0032 |

| Run scope | Approx dispensary records | Estimated cost |
|-----------|---------------|----------------|
| 1 state, 1 platform source | 50–300 | $0.25–$1.50 |
| Northeast corridor (PA MD DE NY NJ CT MA) | 1,500–2,500 | $7.50–$12.50 |
| West Coast (CA OR WA) | 2,600–3,500 | $13.00–$17.50 |
| Full default (9 states, 3 platform sources, `maxResultsPerSource: 100`) | 500–800 | $2.50–$4.00 |
| Full default + menu data | Same dispensary count, menu items are free | $2.50–$4.00 |
| Full default with `maxResultsPerSource: 0` (unlimited) | 4,000–7,000 | $20.00–$35.00 |
| All 47 states/territories, all 6 sources, unlimited | 20,000–26,000 | $100.00–$130.00 |

Costs above use the Free-plan rate ($0.005/record) as the ceiling, Bronze/Silver/Gold subscribers pay 14–36% less automatically, see the table above.

The default `maxResultsPerSource` is intentionally capped at 100 (not unlimited) so a first run completes in under two minutes, raise it or set it to `0` for a full production pull. Use `testMode: true` to try before committing to a full run, it caps at 3 results per source per state, so a full-scope smoke test costs well under $1 regardless of how many states/sources you configure.

**Running this on a schedule to monitor for changes rather than re-extract everything?** Set `onlyOutputChanges: true`. A weekly scheduled run against the full 20,000+ record dataset would otherwise re-bill the entire unchanged dataset every single week; with this on, only what's actually new or changed gets written to the dataset, typically a small fraction of the total.

Costs depend on Apify compute units. All platform sources use HTTP crawling (no browser, fast and cheap). Official state sources run 4 in parallel.

### Tips

- **Free smoke test:** `testMode: true` returns 3 results per source/state, skips the snapshot update, costs < $0.01. Run this first to verify the Actor works before a paid run
- **Weekly scheduling:** Use Apify's built-in scheduler (Monday morning recommended). The first run builds the baseline, every subsequent run reports only what changed
- **Unlimited results:** Set `maxResultsPerSource: 0` to remove the per-source cap. Needed for CA (~1,200), WA (~700), OR (~700), CO (~550), MI (~500)
- **Targeted alerts:** Combine `webhookUrl` with `webhookStateFilter: ["CO", "CA"]` to only receive webhook events for the states you care about, reduces noise for single-market operators
- **Adding a new state:** Set `suppressInitialStateChanges: false` if you want `new_dispensary` events on the first run for a freshly added state. Default (`true`) suppresses the flood
- **Debugging:** Check `RUN_HEALTH` in the Key-Value store, per-source item counts and `suspicious_zero` catch silent scraper breaks before they affect your pipeline
- **Proxy:** Enable Apify proxy for large runs (500+ dispensaries) to reduce rate-limiting from Weedmaps and Leafly. Use `RESIDENTIAL` group in `proxyConfiguration` to unlock Dutchie coverage (~40% of US dispensaries)
- **Social media:** `enrichSocialMedia: true` hits each dispensary's website and extracts Instagram, Facebook, Twitter/X, TikTok handles, great for marketing contact lists. Capped at 1,000 dispensaries per run
- **Contact emails:** `enrichEmail: true` hits each dispensary's website and extracts a contact email from `mailto:` links (falling back to page text), the field most outreach-focused buyers (data vendors, law firms, compliance consultants) ask for first. Capped at 1,000 dispensaries per run
- **Only need one state?** Single-state editions are available at a lower per-record price, search the Apify Store for "\[State] Cannabis Dispensary Data" (currently: California, New York, Florida, Michigan, Colorado)

### FAQ, Disclaimers, and Support

**Where can I get US cannabis dispensary data?**
This Actor. It merges official state license registries with Weedmaps, Leafly, iHeartJane and Dutchie into one deduplicated dataset of licensed dispensaries across 47 US states and territories plus all of Canada.

**How do I get a list of licensed dispensaries by state?**
Set the `states` input to the states you want (or run all of them). Each record includes name, address, license number, status, category, and coordinates.

**Does it include Canada?**
Yes. All 13 Canadian provinces and territories are covered alongside the 47 US jurisdictions.

**How do I track new dispensary openings, closures, and license changes?**
Turn on monitor mode and add a `webhookUrl`. Every run diffs against the last and emits `new_dispensary`, `dispensary_closed`, and `license_suspended` events with before/after values.

**How fresh is the data and how often does it update?**
Run it on any schedule (weekly is common). It reads live state open-data feeds and platform APIs each run.

**Can I get the data as CSV, JSON, or an API?**
Yes: JSON, CSV, or Excel from the Output tab, or via the Apify API. A Standby REST API is also available for live queries.

**How is this different from a Weedmaps or Leafly scraper?**
Those cover one platform and miss license data. This merges official government license registries (license numbers, status, expiry dates) with the platforms, deduplicates, and detects changes between runs.

**Is scraping dispensary data legal?**
This Actor scrapes publicly available information from dispensary listing platforms and official state government databases. It does not bypass authentication, access private data, or collect personally identifiable information beyond what is publicly posted. Users are responsible for complying with applicable platform Terms of Service and laws in their jurisdiction.

**What if a source breaks?**
The Actor degrades gracefully, one failed source doesn't stop the run. All remaining sources continue and the failure is logged in `RUN_HEALTH`. Six layers of protection catch silent degradation before it reaches your data, and, critically, before it reaches *you*:

1. **`suspicious_zero`**, source ran but returned 0 items when the previous run had data (total extraction failure, e.g. a site redesign)
2. **`persistently_empty`**, source has returned 0 items for 3+ consecutive runs, even if the *previous* run was also 0 (catches a source that's been silently dead for a while, which `suspicious_zero` alone can't see since it only compares to one prior run). States with a known, documented reason to be empty (see Known limitations) are exempt and show `not_yet_available` instead, this doesn't count as degraded.
3. **`below_expected` (expected-floor check)**, a source for a known-large market (Oklahoma, Florida, Oregon, California, and others in the hundreds-to-thousands) that finishes without an error but returns far below what that market can hold is flagged even on the *first* run, when there's no error and no prior count for the checks above to catch. This is the exact failure mode of a state migrating its license data off an open-data portal: the fetch still succeeds, but the payload is empty or a stub page. The floor is only trusted when your per-source cap could actually admit it and the source didn't simply hit that cap, so a capped run never false-flags.
4. **Structural fingerprinting**, compares each source's field structure against its stored baseline. If a key field like `name` disappears or `address.city` null rate spikes 40+ points, `RUN_HEALTH` immediately flags `overallStatus: "degraded"` with `fingerprintDrift: "critical"` on the affected source, catches *partial* silent degradation that `suspicious_zero` misses
5. **Regression guard**, refuses to overwrite the previous run's snapshot if the new run returns 80%+ fewer records, preserving your change detection baseline
6. **Active alerting**, a source flipping from working to broken (or back) fires a `source_broken`/`source_recovered` webhook event (see Webhook Events above), and a degraded run's Console status message names the affected sources directly. Detection alone used to leave you to notice a problem by opening `RUN_HEALTH` yourself; this pushes it to you instead

**What states and provinces are supported?**
47 US states/territories and 8 Canadian provinces/territories (ON, BC, AB, MB, NL, YT, NT, NU) have dedicated official license/retailer database scrapers. All 6 platform sources cover every US state (Canadian coverage currently comes from the official government/retailer sources, not the platforms). Small programs are fully supported, every active licensed dispensary is captured, whether that's Iowa (5 dispensaries), Nunavut (2 mail-order retailers), or Northwest Territories (6 stores).

**What is chain detection?**
The `chain` field is populated by pattern-matching on dispensary names to identify multi-state operators (MSOs): Curaleaf, Trulieve, Green Thumb (GTI), Verano, Acreage, Cannabist (Columbia Care), PharmaCann, Cresco Labs, MedMen, Dutchie, Planet 13, Cookies, Jars Cannabis, and 160+ others. Useful for filtering out chain locations when analyzing independent operators, or for tracking MSO expansion footprint.

**Known limitations:**

- **Alaska** currently returns 0 results, not a bug, a confirmed dead end. Alaska's open-data portal (data.alaska.gov) was decommissioned entirely (the domain no longer resolves), and the regulator's new licensing system (AMCO, migrated to Gov2Biz in 2023) sits behind DataDome bot-detection site-wide, which a standard HTTP fetch can't pass. No bulk data feed is published anywhere else. Reports `not_yet_available`; will start working automatically if AMCO ever exposes a public feed or removes the bot-wall
- **Dutchie and iHeartJane** both need a residential proxy for full data (address, phone, hours, license), their APIs are behind Cloudflare Bot Management, which blocks datacenter IPs regardless of request signature. iHeartJane returns 0 results with no proxy configured. Dutchie is better: without a proxy it now falls back to the public sitemap (not Cloudflare-protected) for partial records, name, city, and state only, no street address/phone/hours, instead of returning nothing. For full data on either: configure `proxyConfiguration` with `useApifyProxy: true, apifyProxyGroups: ["RESIDENTIAL"]` (and add `"dutchie"` to `sources` if you want that platform)
- **North Carolina, Nebraska, Guam, and the US Virgin Islands** have legal cannabis programs, but no *usable* public dispensary directory yet. Guam has licensed cultivators only (Feb 2026), no retail establishment yet. USVI has approved 10 conditional dispensary licenses (Jan 2026) but none are operating, and the published applicant list is a flat image (name/island/merit score only, no addresses) rather than structured data. All four report `not_yet_available` and return 0 by design, not a bug. Platform sources still cover them in the meantime
- **New Jersey** previously had a working license database, but the CRC restructured their site and no longer publishes a comprehensive, structured public directory, the old page 404s, and the current consumer-facing "Find a Dispensary" page has no equivalent data feed behind it (checked thoroughly: backup URLs, their Socrata catalog entry, the business-application portal, and the editorial "Roll-up" page all dead-end). Reports `not_yet_available`; platform sources still cover NJ
- **Massachusetts** also lost its old Socrata API when the CCC restructured their site, but a replacement was found: their public "Where to Buy" consumer page embeds the live dispensary list as structured data, including real coordinates the old API never had. Trade-off: no license number, status, or issue/expiry dates, it's a "where's it open" list, not the CCC's internal licensee registry, so `licenseStatus` defaults to `active` for every record
- **Potguide and AllBud** are integrated but currently return 0 results by design and are excluded from the default source list, potguide.com now redirects to an unrelated Canadian retailer, and allbud.com requires JS rendering with no accessible API. Both remain selectable (in case either comes back online) and report `not_yet_available` rather than an error; the other 4 platform sources and 47 state sources aren't affected
- **Illinois** has no structured public API, IDFPR retired their open-data feed and now publishes licenses as a PDF only. This Actor parses that PDF directly (heuristic text extraction, ~97-99% field completeness on name/street/city, ~94% on zip, ~90% on phone), which is the only public source that exists. Two things the PDF can't give us: it can't tell us which dispensaries also serve medical patients or which were awarded through the social-equity lottery (both are visual formatting cues, blue highlighting and bold text, that aren't recoverable from extracted text), so `categories` defaults to `recreational` for all IL records and `socialEquity` isn't set
- **Puerto Rico** also has no structured public API, the JRCM registry is a PDF export, last updated December 2024, so it may miss licenses issued more recently. This Actor parses it with the same heuristic approach as Illinois (~96% field completeness on name/street/city/phone)
- Coordinates are null for some state-source-only records (government databases publish addresses, not lat/lng). Enable `geocodeMissingCoordinates: true` to fill via OpenStreetMap Nominatim, uses a two-pass strategy (full address first, city+state fallback), caches results across runs so each address is geocoded only once, and marks each result with `geocodeSource` so you know which coordinates are precise vs approximate
- Menu data requires a separate pass per dispensary and can significantly increase run time for large states

**Support:** Open an issue on the [Issues tab](https://console.apify.com/actors) in Apify Console. For custom data pipelines, enterprise licensing, or state coverage requests, contact the author directly. If this Actor is useful to you, a rating or review on the [Apify Store listing](https://apify.com/registryfeeds/cannabis-dispensary-scraper) genuinely helps, it's the main way other buyers find this over a generic scraper, and it costs you a minute.

# Actor input Schema

## `testMode` (type: `boolean`):

When enabled, collects only 3 results per source per state and does NOT update the run snapshot or change detection baseline. Use this to verify the Actor works before running a full job. Costs nearly nothing.

## `sources` (type: `array`):

Which platform sources to scrape. Weedmaps and Leafly are enabled by default and cover all US states with no extra setup. iHeartJane is also enabled by default but blocks non-residential traffic — it silently returns 0 results unless you configure a residential Proxy Configuration below. Dutchie requires the same residential proxy. Potguide and AllBud are included for completeness but currently return 0 results regardless of setup (potguide.com now redirects to an unrelated retailer; allbud.com requires JS rendering with no accessible API) — left selectable in case either comes back online, but not recommended.

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

US state/territory or Canadian province/territory abbreviations to scrape. Example: \["PA", "NY", "ON"]. US official license databases available for: PA, MD, DE, NY, NJ, CT, MA, CO, WA, CA, MI, IL, OR, NV, MO, OH, AZ, FL, MN, OK, NM, MT, VA, TX, AK, HI, LA, AR, MS, ND, ME, VT, RI, DC, NH, WV, SD, UT, IA, GA, AL, KY, NC, NE, PR, GU, VI (47 total). NC, NE, NJ, GU, and VI are legal cannabis jurisdictions whose regulators don't currently publish a usable public dispensary directory (NJ's CRC retired theirs in a site restructure; GU has licensed cultivators only; VI has 10 conditional dispensary licenses but none operating) — those five return 0 state-source results until one publishes again (platform sources still cover them). IL and PR are parsed from a government PDF export (their APIs were retired) rather than a live API; MA is parsed from a public consumer directory page rather than an API (its old Socrata API was also retired) — expect slightly lower field completeness than the Socrata-backed states, though MA now includes real coordinates the old API never had. CANADA — ALL 13 jurisdictions (10 provinces + 3 territories), official regulator/retailer sources: ON (Ontario AGCO, ~1,884 stores — richest Canadian source: full address + coordinates + website), BC (British Columbia LCRB licence export + BC Cannabis Stores, ~586), AB (Alberta AGLC retailer search, ~751), MB (Manitoba LGCA store list, ~247), QC (Québec SQDC, ~113), SK (Saskatchewan SLGA permit roster, ~204), NS (Nova Scotia NSLC, ~84 — via NSLC's store API, includes coordinates; NSLC blocks datacenter IPs, so NS needs an Apify residential proxy when run in the cloud — enable proxyConfiguration; without one NS returns 0 and is flagged 'not\_yet\_available', not an error), NB (New Brunswick Cannabis NB, ~33 — note: the government store network is scheduled to close 2026-08-03, after which this returns the remaining/private retailers), NL (Newfoundland & Labrador retailer locator, ~64), PE (Prince Edward Island PEI Cannabis, 5), YT (Yukon granted licence-holders, ~10), NT (Northwest Territories, 6), NU (Nunavut mail-order retailers, 2). SK is sourced from the latest archived permit roster (SLGA blocks live automated access), so it may lag by a few weeks. Canadian codes don't collide with any US code and can be mixed freely with US states in the same run. Note: the 6 platform sources (Weedmaps/Leafly/etc.) currently only cover US states — Canadian coverage comes from the official government/retailer sources above.

## `includeStateSources` (type: `boolean`):

When enabled, also pulls from official state licensing databases. Adds license numbers, license status, issue/expiry dates, and verified addresses the platforms may not have.

## `maxResultsPerSource` (type: `integer`):

Maximum number of dispensaries to collect per source per state. Set to 0 for unlimited. Ignored when Test Mode is enabled. Default kept low (100) so a first run without a proxy stays well inside Apify's automated 5-minute reliability check — raise it for a full production run.

## `sourceTimeoutSecs` (type: `integer`):

Maximum seconds a single source is allowed to run before being cut off. Prevents one broken source from blocking the entire run and blowing up your compute budget. Default: 300 (5 minutes).

## `includeMenuData` (type: `boolean`):

When enabled, scrapes basic menu statistics per dispensary: product counts by category and price ranges. Significantly increases run time and cost.

## `includeEnforcementData` (type: `boolean`):

When enabled and Colorado is in your states list, scrapes Colorado MED's public enforcement/disciplinary action records (citations, fines, suspensions, license denials) into a separate 'enforcement-actions' dataset. Not merged into dispensary records — CO's list only has business names, no license numbers, so it can't be reliably matched by name alone. Currently Colorado only; more states planned.

## `proxyConfiguration` (type: `object`):

Optional proxy settings. Recommended for large runs to avoid rate limiting. Required for Dutchie (currently blocks direct HTTP).

## `geocodeMissingCoordinates` (type: `boolean`):

When enabled, uses the free OpenStreetMap Nominatim API to fill in lat/lng for dispensaries where state databases did not publish location data. Rate limited to 1 request/second. Adds significant run time for large datasets. Results are cached between runs.

## `maxGeocodePerRun` (type: `integer`):

Ceiling on how many missing-coordinate records to look up via Nominatim in a single run (0 = unlimited). At ~1.1 seconds per lookup, this bounds run time and cost; anything not resolved this run is attempted on the next run. Only applies when Geocode Missing Coordinates is enabled.

## `maxMenuItemsPerDispensary` (type: `integer`):

Maximum number of menu products to fetch per dispensary when 'Include Menu Stats' is enabled. Hard cap at 500. Higher values increase run time and cost significantly.

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

Optional HTTPS endpoint to receive change alerts after each run. When set, the Actor POSTs structured JSON events for new dispensary openings, closures, license suspensions, and expiring licenses. Useful for real-time monitoring without polling.

## `webhookStateFilter` (type: `array`):

If set, only send webhook events for dispensaries in these states. Leave empty to receive events for all scraped states.

## `licenseExpiryAlertDays` (type: `array`):

Alert thresholds (in days) for license expiry detection. Dispensaries whose license expires within any of these windows are included in the EXPIRING\_LICENSES KV store key and webhook events.

## `suppressInitialStateChanges` (type: `boolean`):

When enabled (default), prevents webhook floods when adding a new state to an existing run. Adding CA for the first time would otherwise fire 1,200+ new\_dispensary events — with this on, those events are suppressed on the first run and detected normally in subsequent runs.

## `onlyOutputChanges` (type: `boolean`):

When enabled, the dataset only contains dispensaries that are new or changed since the last run — not the full multi-thousand-record dataset every time. Built for recurring/scheduled monitoring: if you're running this weekly to track openings, closures, and license changes rather than re-extracting the full market each time, this can cut per-run dataset costs dramatically. MARKET\_INTELLIGENCE, TREND\_HISTORY, and the standby API still reflect the full current dataset regardless — only what's written to the default dataset changes. The very first run (no previous snapshot) always outputs everything, since there's nothing yet to diff against. Ignored in testMode.

## `enrichSocialMedia` (type: `boolean`):

When enabled, fetches each dispensary's website homepage and extracts publicly linked social media profiles (Instagram, Facebook, Twitter/X, TikTok, YouTube, LinkedIn). Results populate the socialMedia field. Adds significant run time — capped at 1,000 dispensaries per run. Disabled in testMode.

## `enrichEmail` (type: `boolean`):

When enabled, fetches each dispensary's website homepage and extracts a publicly listed contact email (mailto: links first, then visible page text as a fallback). Results populate the email field for dispensaries whose source didn't already publish one. Adds significant run time — capped at 1,000 dispensaries per run. Disabled in testMode.

## `cities` (type: `array`):

Only output dispensaries in these cities. Case-insensitive, matched against the normalized city on each record. Combine with States to disambiguate repeated city names (there is a Springfield in most states). Leave empty for every city.

## `centerLat` (type: `string`):

Latitude of the point to measure from. Used only when Radius (miles) is also set. Example: 39.7392 for Denver.

## `centerLon` (type: `string`):

Longitude of the point to measure from. Used only when Radius (miles) is also set. Example: -104.9903 for Denver.

## `radiusMiles` (type: `integer`):

Only output dispensaries within this many miles of the center point. Requires both Center latitude and Center longitude. Great-circle distance. Records with no coordinates cannot be measured and are excluded; the run log reports how many, so a coverage gap is never mistaken for a distance result. Turn on Geocode missing coordinates to resolve more of them.

## `services` (type: `array`):

Only output dispensaries offering ALL of the selected services. Leave empty for no service filtering.

## Actor input object example

```json
{
  "testMode": false,
  "sources": [
    "weedmaps",
    "leafly",
    "iheartjane"
  ],
  "states": [
    "PA",
    "MD",
    "DE",
    "NY",
    "NJ",
    "CT",
    "MA",
    "CO",
    "WA"
  ],
  "includeStateSources": true,
  "maxResultsPerSource": 100,
  "sourceTimeoutSecs": 300,
  "includeMenuData": false,
  "includeEnforcementData": false,
  "proxyConfiguration": {
    "useApifyProxy": false
  },
  "geocodeMissingCoordinates": false,
  "maxGeocodePerRun": 2000,
  "maxMenuItemsPerDispensary": 200,
  "licenseExpiryAlertDays": [
    30,
    60,
    90
  ],
  "suppressInitialStateChanges": true,
  "onlyOutputChanges": false,
  "enrichSocialMedia": false,
  "enrichEmail": false,
  "cities": [],
  "services": []
}
```

# Actor output Schema

## `dispensaries` (type: `string`):

No description

## `runHealth` (type: `string`):

No description

## `stateCoverage` (type: `string`):

No description

## `changes` (type: `string`):

No description

## `marketIntelligence` (type: `string`):

No description

## `expiringLicenses` (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 = {
    "sources": [
        "weedmaps",
        "leafly",
        "iheartjane"
    ],
    "states": [
        "PA",
        "MD",
        "DE",
        "NY",
        "NJ",
        "CT",
        "MA",
        "CO",
        "WA"
    ],
    "cities": [],
    "services": []
};

// Run the Actor and wait for it to finish
const run = await client.actor("registryfeeds/cannabis-dispensary-scraper").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 = {
    "sources": [
        "weedmaps",
        "leafly",
        "iheartjane",
    ],
    "states": [
        "PA",
        "MD",
        "DE",
        "NY",
        "NJ",
        "CT",
        "MA",
        "CO",
        "WA",
    ],
    "cities": [],
    "services": [],
}

# Run the Actor and wait for it to finish
run = client.actor("registryfeeds/cannabis-dispensary-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print("💾 Check your data here: https://console.apify.com/storage/datasets/" + run["defaultDatasetId"])
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "sources": [
    "weedmaps",
    "leafly",
    "iheartjane"
  ],
  "states": [
    "PA",
    "MD",
    "DE",
    "NY",
    "NJ",
    "CT",
    "MA",
    "CO",
    "WA"
  ],
  "cities": [],
  "services": []
}' |
apify call registryfeeds/cannabis-dispensary-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=registryfeeds/cannabis-dispensary-scraper",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/WFPyat1GSTedzd1dC/builds/eVQwQChNavkFNzv9Q/openapi.json
