# NYC Business Licenses - Inspections, Violations, Complaints (`j0401/nyc-dcwp-licenses`) Actor

NYC DCWP's licensing ecosystem (public open data): 72.5K licences, 65.9K applications, 278K field inspections, 137K violations with their Admin Code charge and hearing outcome, and 76.5K consumer complaints - all linked by business id, so one lookup returns a business's whole regulatory record.

- **URL**: https://apify.com/j0401/nyc-dcwp-licenses.md
- **Developed by:** [Wenhao Yang](https://apify.com/j0401) (community)
- **Categories:** Business
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.06 / 1,000 nyc dcwp records

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

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

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## NYC Business Licenses - Inspections, Violations, Complaints

### Low cost

**From $0.0001 per record, down to $0.00006 at Gold** - pay per record delivered, and nothing for the query. Cost scales with what you pull, not with the size of the register.

New York City's Department of Consumer and Worker Protection licensing ecosystem. Not just a licence list: the applications behind the licence, the inspections of the premises, the charges that came out of those inspections, and the consumer complaints filed against the business - five tables, all resolved on one business id.

### What you get

| Table | Rows | What it holds |
|---|---|---|
| licenses | 72,452 | current licences: status, category, licence type, address with BIN/BBL/community board |
| applications | 65,932 | new and renewal applications: intake channel, submission and closure, temporary-operation letters |
| inspections | 277,976 | field inspections: type (patrol, tobacco sale-to-minor, scale, sweep), status, outcome |
| violations | 137,316 | the charge itself - the Admin Code or Public Health Law section cited - with the hearing outcome and disposition flags |
| complaints | 76,527 | consumer complaints: 311 channel, result, and any refund or contract-cancellation amount |

### Modes

- **licenses** (default) / **applications** / **inspections** / **violations** / **complaints**
- **business** - everything above for one business id, in one response
- **aggregate** - counts by status, category, charge, outcome

A `business` query turns five separate datasets into one answer: this business holds these licences, applied on these dates, was inspected here, was charged under these sections, and had these complaints - with the outcome of each.

### Example inputs

**One business, all five tables** - `mode=business` resolves the id across licences, applications, inspections, violations and complaints in one response.

```json
{ "mode": "business", "businessId": "BA-1786461-2026" }
```

**Active tobacco licences in one borough** - `category` and `borough` are substring matches; `licenseStatus` is the exact published value.

```json
{ "licenseStatus": "Active", "category": "Tobacco",
  "borough": "Brooklyn", "maxResults": 100 }
```

**Charges that were pleaded** - `mode=violations` with the exact hearing outcome.

```json
{ "mode": "violations", "outcome": "Pleaded", "maxResults": 100 }
```

**What the city inspects most** - `mode=aggregate` counts one row per group.

```json
{ "mode": "aggregate", "corpus": "inspections", "groupBy": "inspectionType" }
```

### Why this is hard

The five tables spell the same facts differently. The building number is `address_building` on one, `building_number` on the next, `building_no` on the third and `building_nbr` on the fourth; ZIP is `address_zip` / `zip` / `zip_code` / `postcode`. Nothing joins unless you know each table's spelling. Expiry dates carry sentinel rows dated 2100 - and because an expiry date in the *future* is the normal state of an active licence (44,099 of them), a naive date ceiling deletes every valid licence in the register while keeping the junk. The complaint table is the only one missing the business id on some rows (2,497 of them), so the join has to tolerate gaps rather than drop them.

### Output

One schema across all five tables - every record carries the full key set, empty where the table has nothing to say. Dates arrive as `YYYY-MM-DD`.

### Notes

- Public open data from NYC Open Data. No login, no scraping.
- DCWP publishes this because it is the city's consumer-protection record - who is licensed, and what they did with the licence.
- Charges are metered per record delivered, so pulling one business's record costs a fraction of a cent.

### Example output

**One licence** - `businessId=BA-1786461-2026` on the default `licenses` mode. Every record carries the full key set; the columns the table has nothing to say about come back `""`, and the aggregate columns (`groupKey`, `groupCount`, `groupBy`) are `""` outside aggregate mode:

```json
{
  "platform": "nyc-dcwp",
  "source": "nyc-dcwp-licenses",
  "corpus": "licenses",
  "recordKind": "license",
  "mode": "licenses",
  "groupKey": "", "groupCount": "", "groupBy": "",
  "businessId": "BA-1786461-2026",
  "businessName": "Paceline Consults Inc",
  "dbaTradeName": "",
  "businessCategory": "Debt Collection Agency",
  "licenseNumber": "2136952-DCWP",
  "licenseType": "Premises",
  "licenseStatus": "Active",
  "licenseCreationDate": "2026-08-19",
  "licenseExpirationDate": "2027-01-31",
  "detail": "",
  "contactPhone": "(212) 812-2995",
  "addressType": "Complete Address",
  "addressBuilding": "225",
  "addressStreet": "W 34TH ST",
  "addressStreet2": "",
  "addressCity": "NEW YORK",
  "addressState": "NY",
  "addressZip": "10122",
  "addressBorough": "Manhattan",
  "communityBoard": "105",
  "councilDistrict": "03",
  "bin": "1014402",
  "bbl": "1007840019",
  "nta": "MN17",
  "censusTract": "109",
  "latitude": "40.751483",
  "longitude": "-73.991764",
  "applicationId": "", "applicationType": "", "intakeChannel": "", "submissionDate": "", "applicationStatus": "", "dateClosed": "",
  "tempOpLetterIssued": "", "tempOpLetterExpiration": "", "certificateOfInspection": "",
  "inspectionNumber": "", "dateOfOccurrence": "", "dcwpLicenseNumber": "", "inspectionType": "", "inspectionStatus": "",
  "nohNumber": "", "recordId": "", "relatedRecordType": "", "violationDate": "", "charge": "", "chargeCount": "",
  "cureEligible": "", "cured": "", "outcome": "", "guilty": "", "notGuilty": "", "dismissed": "",
  "intakeDate": "", "serviceRequestNumber": "", "complaintCode": "", "resultDate": "", "result": "", "referredTo": "",
  "contractCancelledAmount": "", "refundAmount": "",
  "sourceUpdatedAt": "2026-08-20"
}
```

# Actor input Schema

## `mode` (type: `string`):

licenses = current licences (default). applications = licence applications. inspections = field inspections. violations = charges from those inspections (with hearing outcome). complaints = consumer complaints. business = EVERYTHING for one business id, in one response. aggregate = one count row per group.

## `corpus` (type: `string`):

Which table to aggregate - only used when mode=aggregate. Ignored otherwise.

## `groupBy` (type: `string`):

Aggregate dimension, only used when mode=aggregate. Valid per table: licenses -> licenseStatus, licenseType, businessCategory, borough; applications -> applicationStatus, applicationType, intakeChannel, businessCategory; inspections -> inspectionType, inspectionStatus, borough; violations -> outcome, charge, cureEligible, businessCategory; complaints -> result, complaintCode, intakeChannel, businessCategory. Blank = a sensible default per table. Blank = a sensible default for the selected table - do not leave it set when switching corpus.

## `businessId` (type: `string`):

Exact DCWP business id, e.g. 'BA-1305489-2022'. One id links the licence, applications, inspections, violations and complaints tables - this is the through-key. Blank = any. Use it with mode=business to pull one business's whole record.

## `name` (type: `string`):

Business name substring, e.g. 'GROCERY'. Blank = any.

## `dba` (type: `string`):

Doing-business-as name substring. Blank = any.

## `category` (type: `string`):

Category substring, e.g. 'Tobacco', 'Home Improvement'. Blank = any.

## `borough` (type: `string`):

Borough substring, e.g. 'Brooklyn'. Blank = any. Not available on the violations table.

## `zip` (type: `string`):

ZIP prefix, e.g. '11216'. Blank = any. Not available on the violations table.

## `licenseStatus` (type: `string`):

Exact licence status (licenses table only). Blank = any.

## `licenseType` (type: `string`):

Licence-type substring (licenses / applications). Blank = any.

## `applicationStatus` (type: `string`):

Exact application status (applications table only). Blank = any.

## `applicationType` (type: `string`):

Exact application type (applications table only). Blank = any.

## `intakeChannel` (type: `string`):

How the case arrived - 'Online' / 'Walk-In' / '311' (applications / complaints). Blank = any.

## `inspectionType` (type: `string`):

Inspection-type substring (inspections table only), e.g. 'Tobacco'. Blank = any.

## `inspectionStatus` (type: `string`):

Exact inspection status (inspections table only). Blank = any.

## `charge` (type: `string`):

Charge-text substring (violations table only), e.g. 'FLAVORED', 'UNLICENSED'. Blank = any.

## `outcome` (type: `string`):

Exact hearing outcome (violations table only). Blank = any (many charges have no outcome yet).

## `complaintResult` (type: `string`):

Result-text substring (complaints table only), e.g. 'Referred'. Blank = any.

## `issuedFrom` (type: `string`):

Earliest licence creation date, YYYY-MM-DD (licenses only). Blank = any.

## `issuedTo` (type: `string`):

Latest licence creation date, YYYY-MM-DD (licenses only). Blank = any.

## `submittedFrom` (type: `string`):

Earliest submission date, YYYY-MM-DD (applications only). Blank = any.

## `submittedTo` (type: `string`):

Latest submission date, YYYY-MM-DD (applications only). Blank = any.

## `inspectedFrom` (type: `string`):

Earliest inspection date, YYYY-MM-DD (inspections only). Blank = any.

## `inspectedTo` (type: `string`):

Latest inspection date, YYYY-MM-DD (inspections only). Blank = any.

## `violatedFrom` (type: `string`):

Earliest violation date, YYYY-MM-DD (violations only). Blank = any.

## `violatedTo` (type: `string`):

Latest violation date, YYYY-MM-DD (violations only). Blank = any.

## `receivedFrom` (type: `string`):

Earliest complaint intake date, YYYY-MM-DD (complaints only). Blank = any.

## `receivedTo` (type: `string`):

Latest complaint intake date, YYYY-MM-DD (complaints only). Blank = any.

## `maxResults` (type: `integer`):

Cap records returned for this run. Default 50 keeps the daily auto-test fast. 0 = no cap (the full matching slice, up to 10,000).

## Actor input object example

```json
{
  "mode": "licenses",
  "corpus": "licenses",
  "groupBy": "",
  "businessId": "",
  "name": "",
  "dba": "",
  "category": "",
  "borough": "",
  "zip": "",
  "licenseStatus": "",
  "licenseType": "",
  "applicationStatus": "",
  "applicationType": "",
  "intakeChannel": "",
  "inspectionType": "",
  "inspectionStatus": "",
  "charge": "",
  "outcome": "",
  "complaintResult": "",
  "issuedFrom": "",
  "issuedTo": "",
  "submittedFrom": "",
  "submittedTo": "",
  "inspectedFrom": "",
  "inspectedTo": "",
  "violatedFrom": "",
  "violatedTo": "",
  "receivedFrom": "",
  "receivedTo": "",
  "maxResults": 50
}
```

# Actor output Schema

## `recordsUrl` (type: `string`):

NYC DCWP records - as JSON

## `datasetUrl` (type: `string`):

No description

## `runUrl` (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 = {};

// Run the Actor and wait for it to finish
const run = await client.actor("j0401/nyc-dcwp-licenses").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 = {}

# Run the Actor and wait for it to finish
run = client.actor("j0401/nyc-dcwp-licenses").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 '{}' |
apify call j0401/nyc-dcwp-licenses --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,j0401/nyc-dcwp-licenses"
        }
    }
}
```

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/RHN9i45MXFHBEcWFP/builds/j04oCJreFb0O3uTiL/openapi.json
