# Connecticut Business Registry Scraper — SOTS (`ai-lead-gen-tools/us-ct-sots`) Actor

Search Connecticut's full business registry by name or account number, or stream new registrations since a date. Normalized records from data.ct.gov: status, account number, registration date, mailing address, ownership certifications. Complete registry, not only active companies. No proxy needed.

- **URL**: https://apify.com/ai-lead-gen-tools/us-ct-sots.md
- **Developed by:** [Vladimir Chernykh](https://apify.com/ai-lead-gen-tools) (community)
- **Categories:** Lead generation, Business
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.80 / 1,000 company scrapeds

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 Business Registry Scraper — SOTS

Search Connecticut's complete business registry by name or account number, filter
by status, or stream new registrations since a date.

The source is Connecticut's own public Socrata publication of the Secretary of the
State business registry
([`data.ct.gov`](https://data.ct.gov/resource/n7gp-d28j.json), dataset `n7gp-d28j`):
**1 305 785 rows**, a plain JSON API, no cookies, no captcha, no proxy.

### What you get

One row per **company**, in a stable shape — every field is always present, and
what Connecticut does not publish is `null` rather than missing.

| Field | Source | Notes |
|---|---|---|
| `name`, `legalName` | `name` | as registered |
| `sourceId` | `accountnumber`, falling back to `id` | Connecticut's own business account number |
| `documentNumber` | `accountnumber` | same value, `null` when no account exists |
| `status` | `status` | 27 real statuses, see the caveat below |
| `formationDate` | `date_registration` | ISO 8601; `null` for the 361 064 rows carrying a placeholder |
| `mailingAddress` | `mailing_address` | split into `street1`, `street2`, `city`, `state`, `zip`, `country`; `null` for the 16.2 % of rows with no address |
| `ownershipFlags` | five certification columns | `womanOwned`, `minorityOwned`, `veteranOwned`, `disabilityOwned`, `lgbtqiOwned` |
| `filingHistory` | `date_registration` | a single "registered with the SOTS" event; no document history |

Always `null` / empty because Connecticut does not publish them in this dataset:
`ein`, `dbaName`, `statusDate`, `jurisdiction`, `dissolutionDate`,
`principalAddress`, `registeredAgent`, `officers`, `annualReports`, `lastEvent`.

`entityType` is derived from the name (LLC / Corp / Trust …). Connecticut publishes
no entity-type column, so this is a **guess, not a published value** — it is not
used for anything, and you should not treat it as authoritative.

### Ownership certifications — unique to Connecticut

The registry carries five self-reported ownership certifications. Each is a real
boolean on every record, measured on the full dataset:

| Flag | `true` rows |
|---|---|
| `womanOwned` | 107 005 |
| `minorityOwned` | 84 476 |
| `veteranOwned` | 12 810 |
| `disabilityOwned` | 8 017 |
| `lgbtqiOwned` | 6 322 |

This is the field that makes Connecticut different from Texas, New York and
Oregon: a supplier-diversity list straight from the state, without a separate
third-party vendor.

### Three modes

```json
{ "mode": "search", "searchTerms": ["TESLA"], "maxResults": 100 }
```

```json
{ "mode": "search", "accountNumbers": ["0123456"] }
```

```json
{ "mode": "new", "sinceDays": 7, "maxResults": 500 }
```

`mode: "new"` is the one worth scheduling: it returns only companies registered
after the previous run and skips everything already delivered, using a cursor in
the key-value store. Connecticut published **4 808 registrations in the last 30
days** when measured, so a daily run is a real feed, not an empty one. Put the
actor on a daily schedule and you stop paying for the same companies every day.

### Caveat: this is the full registry, not a list of active businesses

Connecticut keeps closed entities in the same dataset, so the status filter is not
cosmetic. Of 1 305 785 rows:

| Status | Rows |
|---|---|
| Active | 464 136 (35 %) |
| Forfeited | 359 318 |
| Dissolved | 320 733 |
| Withdrawn | 54 113 |
| Revoked | 46 636 |
| Cancelled | 18 823 |
| Merged | 12 716 |
| Expired Reservation | 12 279 |
| Rejected | 9 652 |

19 further statuses make up the rest. Set `"status": "Active"` to get only
currently registered companies; leave it empty and you also get every entity that
was ever registered and later dissolved or forfeited. The value is matched
**exactly and case-sensitively**, because the API is — a guessed spelling returns
zero rows rather than a wrong answer.

### Caveat: one address, and not for every company

Connecticut publishes a single composed `mailing_address` string, six positions:
`[street, line2, city, state, country, zip]`.

The **separator is a space followed by a comma** (`" ,"`), not a comma on its own.
Commas that belong to the street line itself are written without the preceding
space, so splitting on `","` shifts every field after the street one position to
the left and turns the country column into a state:

```
"80 N. MAIN ST., APT 5B3 , ,KENT ,CT ,United States ,06757"
```

The comma after `ST.` is part of the street; the fields are separated by `" ,"`.
This actor splits on `" ,"`, so the row above yields
`street1: "80 N. MAIN ST., APT 5B3"`, `city: "KENT"`, `state: "Connecticut"`,
`zip: "06757"`. Measured on 600 sampled rows: splitting on `","` produced 25
broken rows out of 600, splitting on `" ,"` produced none.

```
"621 Terryville Ave , ,Bristol ,CT ,United States ,06010-4078"
"47 Stephen St ,2 ,Stamford ,CT ,United States ,06902-4803"   ← 2nd position = apartment
"15 E. PUTNAM AVE. PMB 119 , ,GREENWICH ,CT , ,06830"         ← country empty
"916 Southern Blvd , ,Bronx ,NY ,United States ,10459-4545"   ← out-of-state organisation
```

The actor splits it **positionally**, not by guessing: a naive split would take
`",2,"` for the city. `street2` therefore holds an apartment or care-of line, never
a city. A state position is only accepted when it holds a two-letter code, a ZIP
only when it matches `NNNNN` or `NNNNN-NNNN`. ZIP+4 is trimmed to the 5-digit ZIP.
A row whose city, state and ZIP are all blank is dropped rather than kept as a
street name with three empty fields.

Three things follow from the source format, and all three are reported honestly:

- **16.2 % of rows have no address at all** (211 214 of 1 305 785). There are two
  encodings, and the second one does not look empty:

  | Published value | Rows |
  |---|---|
  | `", , , , ,"` | 116 791 |
  | `"No information provided , , , , ,"` | 94 423 |

  Neither is `null` in the source, so a filter on "not empty" in the state's own
  API cannot find either one. Both come back with `mailingAddress: null` here.
- **In about 6 of 600 sampled rows the ZIP is missing while the street, city and
  state are real** (`"106 MAIN STREET , ,DERBY ,CT , ,"`). Those are kept with
  `zip: null` — the rest of the address is genuine.
- **A company physically located in another state** shows that state in
  `mailingAddress.state` — this is a *mailing* address, not necessarily a
  Connecticut place of business. Measured over a 3 000-row sample: 1 553 addresses
  in Connecticut, then New York (72), Massachusetts (30), New Jersey (22),
  California (18), Florida (16), Texas (10).

`principalAddress` is left `null` rather than copied from the mailing address:
one address is one fact, and inventing a second is worse than admitting it is
missing.

### Caveat: no officers, no filing history

The registry splits principals and filed documents into separate datasets. This
actor reads only the companies dataset, so `officers` is always `[]` and
`filingHistory` holds only the registration event. If you need directors or annual
reports, this actor does not answer that question yet.

### Cost and speed

**$2.00 per 1 000 companies** ($0.002 per company record). You are charged per
company saved to the dataset — not per request, not per page, not per source row.

A source page is 1000 rows and **236 bytes per row** on the Apify platform, which
is less than half of the Oregon dataset. There is no proxy cost: `data.ct.gov` is
a public JSON endpoint that answers directly from Apify. For `mode: "new"` a
scheduled run typically costs **one request and nothing at all** when no company
was registered since the last run — the run summary reports `charged` per run.

### Input reference

| Input | Default | Meaning |
|---|---|---|
| `mode` | `search` | `search` uses the terms/account numbers; `new` returns the incremental feed |
| `searchTerms` | — | case-insensitive substring match on the business name, min 2 chars |
| `accountNumbers` | — | exact Connecticut account numbers, one request each |
| `status` | — | exact, case-sensitive status filter (`Active`, `Forfeited`, …) |
| `maxResults` | 100 | cap on saved companies (this is what you pay for) |
| `sinceDays` | 7 | first-run lookback for `mode: "new"` |
| `sinceDate` | — | explicit `YYYY-MM-DD` start for `mode: "new"` |
| `maxPagesPerTerm` | 10 | caps paging per term (1000 rows per page) |
| `includeRaw` | false | attach the untouched source row, including the composed address string |
| `maxConcurrency` | 3 | parallel requests |
| `requestTimeoutSecs` | 30 | per-request timeout |
| `proxyConfiguration` | off | not needed; kept for unusual network setups |

### Output

Company records go to the default dataset (see the **Companies** tab). Run
counters — `pushed`, `skippedSeen`, `companies`, `sourceRows`, `duplicateRows`,
`rowsWithoutId`, `status`, `mode`, `sinceDate`, `cursor`, `sourceTotalRows`,
`requestFailures`, `truncated`, `charged` — are stored in the key-value store
under `SUMMARY`.

`sourceRows` against `companies` shows what the de-duplication did; `duplicateRows`
counts source rows folded into an existing company, and `rowsWithoutId` counts rows
that carried neither an account number nor a row id and could not be identified.

# Actor input Schema

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

"search" looks companies up by the terms and account numbers you provide. "new" ignores them and returns companies registered after the last run — put the actor on a schedule and you get a new-registrations feed.

## `searchTerms` (type: `array`):

Company names or fragments. Matching is case-insensitive and finds the term anywhere in the name, so "TESLA" also returns "TESLA MOTORS CT LLC". Minimum 2 characters. Every matched company is de-duplicated, so paying once per company.

## `accountNumbers` (type: `array`):

Exact Connecticut business account numbers, e.g. "0123456". Fastest path: one request per number, no name matching. This is the same value returned in the sourceId and documentNumber fields.

## `status` (type: `string`):

Restrict results to one registry status, matched exactly and case-sensitively: "Active", "Forfeited", "Dissolved", "Withdrawn", "Revoked", "Cancelled", "Merged", "Expired Reservation", "Rejected" and 18 other values. Leave empty for the full registry — only about 35% of Connecticut rows are Active.

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

Hard cap on the number of company records saved to the dataset. Each saved record is charged.

## `sinceDays` (type: `integer`):

Used in "new" mode when no explicit start date is set: how many days to look back on the first run. Later runs continue from the cursor saved in the key-value store. Connecticut published 4 808 registrations in the last 30 days when measured.

## `sinceDate` (type: `string`):

Explicit start date in "new" mode, format YYYY-MM-DD. Leave empty to use the saved cursor, or the window above on the first run.

## `maxPagesPerTerm` (type: `integer`):

The source returns at most 1000 rows per request. This caps how many pages a single term may page through, so a very broad term cannot run away.

## `includeRaw` (type: `boolean`):

Attach the untouched source row to every record under the `raw` field, including the composer `mailing_address` string and the export timestamp create_dt. Useful for auditing the address split.

## `maxConcurrency` (type: `integer`):

Parallel requests to the state's API. Keep it modest to stay polite to a public dataset.

## `requestTimeoutSecs` (type: `integer`):

Per-request timeout before a retry is attempted.

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

Not needed: the dataset is a public JSON API and answers directly from Apify. Enable only if you have a specific reason to route the traffic.

## `runBudgetSecs` (type: `integer`):

How long this run may take before it stops and returns what it has. Leave it empty (recommended) and the run uses the whole timeout you set for it. Set a number only to stop earlier. Stopping early is not a failure: the run summary reports it and you get the records found so far.

## Actor input object example

```json
{
  "mode": "search",
  "searchTerms": [
    "TESLA"
  ],
  "maxResults": 100,
  "sinceDays": 7,
  "maxPagesPerTerm": 10,
  "includeRaw": false,
  "maxConcurrency": 3,
  "requestTimeoutSecs": 30,
  "proxyConfiguration": {
    "useApifyProxy": false
  },
  "runBudgetSecs": 205
}
```

# Actor output Schema

## `dataset` (type: `string`):

One row per company. Every field is always present; values Connecticut does not publish are null, never omitted, so the column set is stable between runs. The mailing address is split into street, city, state and ZIP, and the five ownership certifications are exposed in ownershipFlags. See the Companies tab for the full table.

## `summary` (type: `string`):

Counters for the run: records pushed, source rows read, duplicate rows collapsed, rows skipped for having no id, status filter applied, records skipped as already seen, API requests and failures, whether the result was truncated, the incremental cursor, the total row count reported by the source, and how many charge events were emitted.

# 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 = {
    "searchTerms": [
        "TESLA"
    ],
    "runBudgetSecs": 205
};

// Run the Actor and wait for it to finish
const run = await client.actor("ai-lead-gen-tools/us-ct-sots").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 = {
    "searchTerms": ["TESLA"],
    "runBudgetSecs": 205,
}

# Run the Actor and wait for it to finish
run = client.actor("ai-lead-gen-tools/us-ct-sots").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 '{
  "searchTerms": [
    "TESLA"
  ],
  "runBudgetSecs": 205
}' |
apify call ai-lead-gen-tools/us-ct-sots --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,ai-lead-gen-tools/us-ct-sots"
        }
    }
}
```

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/cVF3BDucSN3Bu9qK4/builds/wB65IXwEskp7ZIGfY/openapi.json
