# Connecticut Payroll & Pension - State Employee and Retiree Pay (`j0401/ct-payroll-pension`) Actor

Connecticut state employee payroll (29.9M rows, 2015-today) and open pension (8.15M rows, 2014-today) as public open data: agency, department, job, union, hire and termination dates, overtime and gross pay; plus tier, retirement type and pension wage. Filter by name, agency, union, year or amount.

- **URL**: https://apify.com/j0401/ct-payroll-pension.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 connecticut payroll or pension 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

## Connecticut Payroll & Pension - State Employee and Retiree Pay

Two registers of Connecticut state pay, straight from the State's open data:

- **Payroll** - 29.9 million rows, calendar 2010 to today (the source's own
  title says 2015; the file opens in 2010). One row per employee per pay period.
- **Pension** - 8.15 million rows, 2014 to today. One row per retiree per
  monthly pension payment.

### Low cost

**From $0.00005 per record, down to $0.00003 at Gold.** Pay only for the
records you actually receive - both registers stay queryable, you are never
billed for the scan behind it.

### What you get

**Payroll** (`corpus="payroll"`, the default):

- The agency, the sub-department and the State's own job code
- The union and the full-time / part-time split
- Original hire date and termination date
- The whole money breakdown: annual rate, bi-weekly rate, salaries and wages,
  **overtime**, fringe, other pay, and **total gross**
- The employee's home city, ZIP and state

**Pension** (`corpus="pension"`):

- The retirement plan and tier - SERS, Teachers, MERS, Judges
- **Why** they are drawing - service, hazardous duty, disability, vested rights
- The bargaining unit, the retirement type code
- **The pension wage paid that period**
- The retiree's residence state (5.9M still in Connecticut; 805K in Florida)

### Filter it

- **By name** - `lastName="Smith"` works on both registers.
- **By agency** - `agency="University of Connecticut"` (5.2M payroll rows),
  `"Correction"` (2.5M), `"Transportation"`.
- **By job** - `jobCode="CorrectionOfficer"`.
- **By union** - on either register.
- **By retirement type** - `retireTypeDescription="Hazardous Duty"`.
- **Where retirees live** - `retirementState="FL"`.
- **By year**, **pay-date range**, **last-N-days** and **amount range**.

### Or roll it up

Set `mode=aggregate` for a server-side count by **agency**, **department**,
**job indicator**, **check status** or **year** on payroll; by **tier**,
**bargaining unit**, **retirement type** or **residence state** on pension.
Every group comes back - this mode is not cut off by `maxResults`.

### Example inputs

**Overtime at the Department of Correction** - payroll rows for one agency.

```json
{ "corpus": "payroll", "agency": "Correction", "year": 2025, "maxResults": 50 }
```

**A retiree's pension payments** - name filters work on both registers.

```json
{ "corpus": "pension", "lastName": "DUGAN", "maxResults": 50 }
```

**Pensions drawn on hazardous duty**

```json
{ "corpus": "pension", "retireTypeDescription": "Hazardous Duty", "maxResults": 50 }
```

**Where retirees live** - one row per state.

```json
{ "mode": "aggregate", "corpus": "pension", "groupBy": "retirementState" }
```

### Then answer real questions

- What does overtime cost the Department of Correction in a year?
- Which agency grew headcount, and which shrank?
- What is the age of the State's pension payroll by tier?
- How many Connecticut retirees draw a pension while living in another state?
- Who is a named individual state employee, and what are they paid?

### Notes on the data, from the source

- **The two registers cannot be joined to each other.** Both carry a hashed
  employee id, and they are different hash spaces - sampling five ids from each
  and cross-querying both ways gives **0 of 10 matches**, verified. They are
  offered side by side because they describe the same workforce at two ends of a
  career, not because they link.
- **Department is only populated from 2024 onward** (5.0M of 29.9M payroll
  rows, 17%). For 2023 and earlier it is entirely blank while `agency` is
  complete - use `agency` for a filter that spans the whole register.
- **Refunds and adjustments are in here.** 12,793 payroll rows are negative -
  the deepest is -$4,705,475.86, and in calendar 2026 alone they run to
  -$124,657.73 across 339 rows. Pension wage reaches -$404,282.72 (4,959
  negative rows). Leave the minimum amount blank to include them, or set 0.01
  for payments only.
- **`checkStatus="R"`** marks 11,010 payroll rows as reversals - payments that
  were taken back.
- **The termination date is free text in two shapes** (`2025-05-10` and
  `2023-08-11 00:00:00.0`), exactly as the State holds it.
- Some retirees are listed as `UNDISCLOSED` - that is the State's own redaction,
  not a missing value.

### Example output

**One pension payment** - `corpus=pension`, `lastName=DUGAN` returns payments
like the one below:

```json
{
  "platform": "ct-payroll-pension",
  "source": "ct-open-pension",
  "mode": "rows",
  "groupKey": "",
  "groupCount": "",
  "groupBy": "",
  "firstName": "CARIANN",
  "middleInitial": "P",
  "lastName": "DUGAN",
  "nameSuffix": "",
  "employerAgency": "",
  "employerDepartment": "",
  "deptId": "",
  "jobCode": "",
  "employeeClass": "",
  "jobIndicator": "",
  "fullPart": "",
  "unionDescription": "Correctional Officers (NP-4)",
  "annualRate": "",
  "biWeeklyRate": "",
  "salariesWages": "",
  "overtime": "",
  "fringe": "",
  "otherPay": "",
  "totalGross": "",
  "sersAmount": "",
  "arpAmount": "",
  "teachersAmount": "",
  "judgesAmount": "",
  "checkNumber": "",
  "checkDate": "2026-08-31T00:00:00.000",
  "checkOption": "",
  "checkStatus": "",
  "originalHireDate": "",
  "terminationDate": "",
  "city": "",
  "postal": "",
  "state": "NC",
  "tierDescription": "SERS - TIER2",
  "bargainingUnit": "08",
  "retireType": "7",
  "retireTypeDescription": "Hazardous Duty",
  "pensionWage": "6491.77",
  "payEndDate": "2026-08-31T00:00:00.000",
  "corpus": "pension",
  "calendarYear": "",
  "fiscalYear": "",
  "balanceYear": "2026",
  "sourceUpdatedAt": "2026-09-02"
}
```

**`mode=aggregate`, `corpus=payroll`, `groupBy=agency`** - the largest employers,
largest first:

```
University of Connecticut     5.2M
Correction                    2.5M
```

# Actor input Schema

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

payroll = state employee pay records (default, 29.9M rows). pension = retiree pension payments (8.15M rows). 🔴 The two cannot be joined to each other: their hashed employee ids are different hash spaces (0 of 10 cross-register matches, verified). Descriptions below mark which register each filter belongs to.

## `firstName` (type: `string`):

Substring match on the first name. Both registers. Blank = any.

## `lastName` (type: `string`):

Substring match on the last name. Both registers. Blank = any.

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

Payroll only. Substring match on the employing agency, e.g. 'University of Connecticut', 'Correction', 'Transportation'. 124 agencies, fully populated across all years. Blank = any.

## `department` (type: `string`):

Payroll only. Substring match on the sub-department. 🔴 Only populated from calendar 2024 onward (5.0M of 29.9M rows, 17%) - for 2023 and earlier it is entirely blank. Use 'agency' for a filter that spans the whole register. Blank = any.

## `jobCode` (type: `string`):

Payroll only. Substring match on the State's own job code description, e.g. 'CorrectionOfficer', 'JudicialEmployee', 'UCHC UniversityHealthProf'. 4,332 distinct values. Blank = any.

## `unionDescription` (type: `string`):

Substring match on the bargaining unit's description. Both registers (87 unions on payroll, 81 on pension). Blank = any.

## `tierDescription` (type: `string`):

Pension only. The retirement plan and tier, matched exactly. 15 values plus a blank tier (39,862 rows). Note 'SERS-HYBRID TIER 2' and 'SERS - HYBRID TIER 3' differ only in spacing, and 'SERS - TIER2' is a substring of 'SERS - TIER2A' - both are separate values here. Blank = any.

## `bargainingUnit` (type: `string`):

Pension only. Substring match on the bargaining unit code (e.g. '06', '07', '10'). 82 values. Blank = any.

## `retireTypeDescription` (type: `string`):

Pension only. Why the person is drawing a pension - 'Service (25 Years or More)' (2.6M), 'Hazardous Duty' (991K), 'Disability', 'Vested Rights' and others. 29 values in total, matched exactly. Blank = any.

## `retirementState` (type: `string`):

Pension only. Two-letter state where the retiree lives, e.g. 'CT' (5.9M), 'FL' (805K), 'MA', 'NC'. 77 values. Blank = any.

## `city` (type: `string`):

Payroll only. Substring match on the employee's home city, e.g. 'Hartford', 'Stamford'. Blank = any.

## `fullPart` (type: `string`):

Payroll only. 'F' = full-time (23.7M rows), 'P' = part-time (6.2M). Blank = any.

## `jobIndicator` (type: `string`):

Payroll only. The State's own job-indicator codes: 'P' (27.9M), 'S' (2.0M), 'N' (71 rows). Blank = any.

## `checkStatus` (type: `string`):

Payroll only. 'F' = final (29.9M rows), 'R' = reversal (11,010 rows, i.e. a payment that was clawed back). Blank = any.

## `year` (type: `integer`):

Calendar year on payroll (2010-2026; the source's own title says 2015 but the file opens in 2010), balance year on pension (2014-2026). Same value, both registers - the actor writes it in each register's own type. Blank = any.

## `checkFrom` (type: `string`):

Earliest check (pay) date, YYYY-MM-DD. Both registers. Blank = any.

## `checkTo` (type: `string`):

Latest check (pay) date, YYYY-MM-DD. Both registers. Blank = any.

## `minAmount` (type: `number`):

Smallest gross pay (payroll) or pension wage (pension) in dollars. Leave blank to include refunds and adjustments, which are negative (339 payroll rows in 2026 alone, down to -$124,657.73; 4,959 pension rows). Set 0.01 for payments only.

## `maxAmount` (type: `number`):

Largest gross pay or pension wage in dollars. Blank = no upper bound.

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

Only rows whose pay date falls within the last N days. Ignored when 'Pay date from' is set. 0 = off.

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

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

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

Which dimension to aggregate over (mode=aggregate). Blank = agency on payroll, tierDescription on pension. Every group is returned - aggregate mode is not cut off by maxResults. 🔴 Payroll and pension dimensions do not overlap, and a mismatched pair is refused rather than silently answered over the wrong register; 'year' works on both.

## `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
{
  "corpus": "payroll",
  "firstName": "",
  "lastName": "",
  "agency": "",
  "department": "",
  "jobCode": "",
  "unionDescription": "",
  "tierDescription": "",
  "bargainingUnit": "",
  "retireTypeDescription": "",
  "retirementState": "",
  "city": "",
  "fullPart": "",
  "jobIndicator": "",
  "checkStatus": "",
  "checkFrom": "",
  "checkTo": "",
  "recentDays": 0,
  "mode": "rows",
  "groupBy": "",
  "maxResults": 50
}
```

# Actor output Schema

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

Connecticut payroll and pension 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/ct-payroll-pension").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/ct-payroll-pension").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/ct-payroll-pension --silent --output-dataset

```

## MCP server setup

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

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/jIhcPiQUbefvFmCeZ/builds/a2bmrPdnhoM0faQ88/openapi.json
