# NYC OATH Hearings - Administrative Tribunal Case Records (`j0401/nyc-oath-hearings`) Actor

NYC OATH administrative tribunal cases (public open data, 22M cases): issuing agency, violation date and location, hearing / decision dates, outcome, fines and compliance status, plus up to ten charge slots. Filter by agency, result, borough or date.

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

## Pricing

from $0.03 / 1,000 nyc oath hearing 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 OATH Hearings - Administrative Tribunal Case Records

Every case filed with the New York City Office of Administrative Trials and Hearings, **22,054,337** of them, each one a summons that a City agency issued and OATH then adjudicated.

OATH is where the City's own enforcement ends up when it is contested. More than twenty agencies bring cases here - Buildings, the Fire Department, Health, Sanitation, Transportation, Environmental Protection, Parks and the Sheriff among them. The register is the tribunal's own record of what happened to each one: what was charged, when it was heard, how it came out, and what it cost.

It is one of the largest administrative-adjudication corpora published anywhere: what each agency charged, how the tribunal resolved it, and what it cost.

### Low cost

**From $0.00005 per record, down to $0.00003 at Gold** Pay-per-event: you are charged per record delivered, and nothing for the query.

### What you get

| Field | Meaning |
|---|---|
| `ticketNumber` | The case. Unique across all 22,054,337 rows |
| `agency` | Who issued it - `SANITATION OTHERS` 3,664,194 / `DOS - ENFORCEMENT AGENTS` 3,399,351 / `TAXI_TLC` 2,913,014 / `DEPT. OF BUILDINGS` 1,407,418 and 188 more |
| `violationDate` / `violationTime` | When the violation is alleged to have occurred |
| `violationDescription` / `violationDetails` | What the case is about, in the agency's own words |
| `borough` / `block` / `lot` / `houseNumber` / `street` / `zip` | Where |
| `respondentLastName` / `respondentAddressHouse` / `respondentAddressZip` | Who was summoned, and where the notice was served |
| `hearingDate` / `hearingTime` / `hearingLocation` / `hearingStatus` | The hearing |
| `decisionDate` / `hearingResult` / `complianceStatus` | How it came out, and whether the terms were met |
| `totalViolationAmount` / `penaltyImposed` / `paidAmount` / `additionalPenaltiesOrLateFees` / `balanceDue` | The money |
| `charge1Code`  `charge10Code`, each with `Section`, `Description` and `InfractionAmount` | Up to ten charges per case, each with its own code, citation section, plain description and the amount that charge carries |

### Modes

- **`rows`** (default) - cases matching your filters.
- **`aggregate`** - one count row per group: by agency, hearing result, hearing status, compliance status or borough.

### Example inputs

**Everything one agency brought** - `agency` is a substring match on the issuing agency.

```json
{ "agency": "DEPT. OF BUILDINGS", "maxResults": 100 }
```

**One respondent** - `respondent` matches the last name, so an owner entity or a surname finds its cases.

```json
{ "respondent": "REALTY", "maxResults": 100 }
```

**Cases that ended in an admission** - `hearingResult` takes the exact published value, never a substring.

```json
{ "hearingResult": "ADMIT IN-VIO", "maxResults": 100 }
```

**How one agency's cases resolved** - `mode=aggregate` returns one row per group, every group.

```json
{ "agency": "SANITATION OTHERS", "mode": "aggregate", "groupBy": "hearingResult" }
```

### Two things you need to know before you rely on a field

**The outcome column is free text typed by clerks, and it shows.** `hearingResult` holds **74 distinct values**, most of them the same few outcomes written differently:

```
IN VIOLATION    2,988,502      ADMIT IN-VIO      142,664
DEFAULTED       2,631,648      ADMIT/IN-VIO          408
WRITTEN OFF     2,428,892      POP IN-VIOL        90,490
DISMISSED       1,727,426      POP/IN-VIOL         7,436
```

`IN VIOLATION` and `ADMIT IN-VIO` and `ADMIT/IN-VIO` are three different strings, and `Dismissed Defective Service`, `Dismissed - Other Reasons` and `Dismissed No Violation/On the Merits` are three more. A filter for "dismissed" that matches on text will silently merge outcomes that the tribunal treats as distinct, so this actor matches the **exact published value** and lists all 74 for you to choose from. `'Sustained'` and `'SUSTAINED'` both exist, 275,611 and 358,241 rows apart.

**41% of cases have no outcome recorded - 9,100,169 of them.** The source does not store the key at all for these, so they come back with an empty `hearingResult`. Most are filed and not yet decided and carry no `decisionDate` either, but 1.8M of them do have a decision date with no outcome string written against it. When you filter by outcome you are asking about the subset that has one; the default listing returns the cases that carry a decision date.

### Why there is no year aggregate

Every other dimension here aggregates to a sane number of groups. The dates do not, and the reason is worth stating plainly.

Nothing validates the date columns on entry, so clerical error has accumulated for two decades:

- `violationDate` runs to **5201-05-12**
- `hearingDate` runs to **9999-10-14**
- `decisionDate` runs to **9997-05-19**
- `date_extract_y(decision_date)` yields **1,213 distinct "years"** - including 123, 196, 204 and 217

A "cases by year" chart built on this column would be mostly typos. So date dimensions are not offered as aggregates. The date *filters* are bounded instead: a range is capped at 30 days past today, so a year-5201 record can never head a "most recent" result. The record fields themselves are delivered exactly as stored - the filter is bounded, the data is not rewritten.

### Source

> **Counts below are a live snapshot** - the register is rebuilt daily, so exact figures move between reads. The order of magnitude and the ratios are stable.

New York City Office of Administrative Trials and Hearings, published on `data.cityofnewyork.us` as [OATH Hearings Division Case Status](https://data.cityofnewyork.us/d/jz4z-kudi) - public open data, no login and no key. The register is **74 columns** wide, which is mostly the ten charge slots: each carries a code, a citation section, a description and an infraction amount, and most cases use only the first one or two.

### Output

Every record carries the same key set regardless of mode - the case fields plus the aggregate columns (`groupKey`, `groupCount`, `groupBy`), which are `""` outside aggregate mode. Every record carries `sourceUpdatedAt`, the date the City last rebuilt the register.

### Related actors

- **NYC DOB Permits** - the building permits whose violations end up before this tribunal.
- **Philadelphia L\&I Violations** - the same shape of enforcement record for Philadelphia.

### Example output

**One case** - `ticketNumber=049748060H`. Every record carries the case fields plus the aggregate columns (`groupKey`, `groupCount`, `groupBy`), which are `""` outside aggregate mode:

```json
{
  "platform": "nyc-oath-hearings",
  "source": "nyc-oath-case-status",
  "mode": "rows",
  "groupKey": "", "groupCount": "", "groupBy": "",
  "ticketNumber": "049748060H",
  "violationDate": "2026-05-06T00:00:00.000",
  "violationTime": "18:50:00",
  "violationDescription": "", "violationDetails": "20260804N",
  "agency": "SANITATION POLICE",
  "respondentLastName": "1025 BOYNTON AVENUEREALTY LLC",
  "respondentAddressHouse": "1025", "respondentAddressZip": "10472", "respondentAddressBorough": "BRONX",
  "respondentFacilityNumber": "",
  "borough": "BRONX",
  "block": "03714", "lot": "0054",
  "houseNumber": "1025", "street": "BOYNTON AVENUE", "floor": "", "city": "BRONX", "zip": "10472", "state": "NEW YORK",
  "hearingStatus": "HEARING COMPLETED",
  "hearingResult": "DISMISSED",
  "hearingLocation": "SAU: BX  2",
  "hearingDate": "2026-08-04T00:00:00.000", "hearingTime": "08:30:00",
  "decisionBorough": "ONE-CLICK", "decisionDate": "2026-09-21T00:00:00.000",
  "totalViolationAmount": "50", "penaltyImposed": "0", "paidAmount": "0",
  "additionalPenaltiesOrLateFees": "0", "balanceDue": "0",
  "complianceStatus": "All Terms Met",
  "judgmentDocketedDate": "",
  "charge1Code": "AS06", "charge1CodeSection": "A.C.", "charge1CodeDescription": "DIRTY SIDEWALK", "charge1InfractionAmount": "50",
  "charge2Code": "", "charge2CodeSection": "", "charge2CodeDescription": "", "charge2InfractionAmount": "",
  "charge3Code": "", "charge3CodeSection": "", "charge3CodeDescription": "", "charge3InfractionAmount": "",
  "charge4Code": "", "charge4CodeSection": "", "charge4CodeDescription": "", "charge4InfractionAmount": "",
  "charge5Code": "", "charge5CodeSection": "", "charge5CodeDescription": "", "charge5InfractionAmount": "",
  "charge6Code": "", "charge6CodeSection": "", "charge6CodeDescription": "", "charge6InfractionAmount": "",
  "charge7Code": "", "charge7CodeSection": "", "charge7CodeDescription": "", "charge7InfractionAmount": "",
  "charge8Code": "", "charge8CodeSection": "", "charge8CodeDescription": "", "charge8InfractionAmount": "",
  "charge9Code": "", "charge9CodeSection": "", "charge9CodeDescription": "", "charge9InfractionAmount": "",
  "charge10Code": "", "charge10CodeSection": "", "charge10CodeDescription": "", "charge10InfractionAmount": "",
  "sourceUpdatedAt": "2026-09-23"
}
```

**`mode=aggregate`, `groupBy=hearingResult`** - one row per outcome, exact string for exact string:

```
IN VIOLATION    2,988,502
DEFAULTED       2,631,648
WRITTEN OFF     2,428,892
DISMISSED       1,727,426
```

**Cases that ended in an admission** - `hearingResult=ADMIT IN-VIO`:

```
H2   11.29  See Attached NOV
```

**What the tribunal decided this year** - `mode=aggregate`, `groupBy=agency`:

```
(blank)                       3,759,126
SANITATION OTHERS             3,664,194
DOS - ENFORCEMENT AGENTS      3,399,351
TAXI_TLC                      2,913,014
DEPT. OF BUILDINGS            1,407,418
FIRE DEPARTMENT OF NYC          955,594
```

**Compliance follow-through** - `mode=aggregate`, `groupBy=complianceStatus`:

```
(blank)          9,945,943
All Terms Met    9,834,617
Penalty Due      1,529,535
Compliance Due     549,711
Both Due           194,531
```

# Actor input Schema

## `agency` (type: `string`):

Substring match on the issuing agency, e.g. 'BUILDINGS', 'SANITATION', 'TAXI'. Blank = any.

## `respondent` (type: `string`):

Substring match on the respondent's last name. Blank = any.

## `street` (type: `string`):

Substring match on the violation street name. Blank = any.

## `chargeCode` (type: `string`):

Substring match on the first charge's code, e.g. 'H2'. Blank = any.

## `ticketNumber` (type: `string`):

Substring match on the ticket number. Blank = any.

## `hearingResult` (type: `string`):

Exact outcome. The tribunal's own clerks typed these, so it is a 74-value list with many near-duplicates - 'IN VIOLATION', 'ADMIT IN-VIO' and 'POP IN-VIOL' are separate values, matched exactly and never as substrings. Blank = any. 41% of cases have no result recorded yet and come back with an empty field.

## `hearingStatus` (type: `string`):

Exact hearing status, e.g. 'PAID IN FULL', 'HEARING COMPLETED', 'DOCKETED'. Blank = any.

## `complianceStatus` (type: `string`):

Exact compliance status: 'All Terms Met', 'Penalty Due', 'Compliance Due', 'Both Due'. Blank = any.

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

Borough where the violation occurred. 'NOT NYC' covers summonses issued outside the five boroughs. Blank = any.

## `violationFrom` (type: `string`):

Earliest violation date, YYYY-MM-DD. Blank = any.

## `violationTo` (type: `string`):

Latest violation date, YYYY-MM-DD. Blank = any.

## `hearingFrom` (type: `string`):

Earliest hearing date, YYYY-MM-DD. Blank = any.

## `hearingTo` (type: `string`):

Latest hearing date, YYYY-MM-DD. Blank = any.

## `decisionFrom` (type: `string`):

Earliest decision date, YYYY-MM-DD. Note 34% of cases have no decision yet; a date range still returns them. Blank = any.

## `decisionTo` (type: `string`):

Latest decision date, YYYY-MM-DD. Blank = any.

## `recentDays` (type: `integer`):

Only cases whose violation date falls within the last N days. Ignored when 'Violation date from' is set. 0 = off.

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

rows = hearing cases matching your filters (default). aggregate = one count row per group (see groupBy).

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

Which dimension to aggregate over (mode=aggregate). Blank = agency. Every group is returned - aggregate mode is not cut off by maxResults. Date dimensions are deliberately absent: clerical typos split the decision column into 1,213 distinct 'years'.

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

Cap the number of records pushed in rows mode (0 = default 50; up to 2,000 per run). Each record is metered individually, so there is no per-run charge cap. Aggregate mode returns every group.

## Actor input object example

```json
{
  "agency": "",
  "respondent": "",
  "street": "",
  "chargeCode": "",
  "ticketNumber": "",
  "hearingResult": "",
  "hearingStatus": "",
  "complianceStatus": "",
  "borough": "",
  "violationFrom": "",
  "violationTo": "",
  "hearingFrom": "",
  "hearingTo": "",
  "decisionFrom": "",
  "decisionTo": "",
  "recentDays": 0,
  "mode": "rows",
  "groupBy": "",
  "maxResults": 50
}
```

# Actor output Schema

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

NYC OATH hearing cases - 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-oath-hearings").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-oath-hearings").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-oath-hearings --silent --output-dataset

```

## MCP server setup

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

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/FfUVW1qMmzfMPlmqR/builds/efDJhlSgQjvHNpmxT/openapi.json
