# FMCSA Carrier Contact Takeover Monitor (`automa-flow/fmcsa-carrier-contact-takeover-monitor`) Actor

Monitor a USDOT watchlist for FMCSA phone, email, officer, legal-name and address changes. Normalized before/after events come from the official public Company Census, with formatting-only noise suppressed.

- **URL**: https://apify.com/automa-flow/fmcsa-carrier-contact-takeover-monitor.md
- **Developed by:** [Vadim Bezrukov](https://apify.com/automa-flow) (community)
- **Categories:** Business, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

$2.50 / 1,000 carrier checks

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

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

## FMCSA Carrier Contact Takeover Monitor

Monitor a USDOT watchlist for FMCSA phone, email, officer, legal-name and address changes. Paste the approved roster, run it, and use the before/after rows as a re-verification list before the next load is tendered.

The source is the official public FMCSA Company Census on data.transportation.gov (resource `az4n-8mr2`, also called the Company Census File or MCMIS census). The Actor does not open SAFER pages, does not score safety, and does not decide that fraud occurred. Attention level indicates workflow priority, not fraud probability.

An agent can call `https://mcp.apify.com?tools=automa-flow/fmcsa-carrier-contact-takeover-monitor` and ask: "Check these USDOT numbers and tell me which phone, email, officer, legal name or address fields changed since the baseline."

### Why carrier contact changes matter

Brokers, 3PLs, shippers and carrier-compliance teams keep an approved roster. A new dispatch phone, email, company officer or legal name is a reason to re-check the carrier before tendering, even when authority and insurance monitors stay quiet. This Actor watches that contact slice and returns the old value and the new value.

### What this Actor monitors

Each USDOT is read from the daily Company Census file. The compared fields are:

| Field | Census column |
| --- | --- |
| Legal name | `legal_name` |
| DBA | `dba_name` |
| Phone | `phone` |
| Cell phone | `cell_phone` |
| Email | `email_address` |
| Physical address | `phy_street`, `phy_city`, `phy_state`, `phy_zip`, `phy_country` |
| Mailing address | `carrier_mailing_street`, `carrier_mailing_city`, `carrier_mailing_state`, `carrier_mailing_zip`, `carrier_mailing_country` |
| Officers | `company_officer_1`, `company_officer_2` |

Officer rows have a name and the source field. The census does not publish a reliable role, so none is invented. Empty census values stay null. The Actor does not add authority, insurance, safety or cargo fields.

### Quick start

The form is prefilled with three active public carriers: Schneider National Carriers (`264184`), Swift Transportation Co. of Arizona (`54283`) and Werner Enterprises (`53467`).

1. Replace that list with your approved USDOT numbers.
2. Keep `includeBaseline` on.
3. Run. Each found carrier is a `BASELINE_CREATED` row. A number that is not in the file is `NO_RESULTS`.
4. Schedule the same input daily. The default repeat still writes one row per carrier. A changes-only task sets `emitUnchanged` to false and then returns material changes only.

```json
{
  "usdotNumbers": ["264184", "54283", "53467"],
  "snapshotKey": "approved-carriers",
  "emitUnchanged": false,
  "includeBaseline": true
}
```

### Input

`usdotNumbers` is required. One to 10,000 values. Each value is 1 to 8 digits. Spaces are trimmed, leading zeros are removed, and repeats are checked once. `0` is rejected before the run starts.

`snapshotKey` names an independent roster. Use a different key for a different list so the baselines do not mix. Allowed characters are letters, numbers, `_` and `-`, up to 80 characters. The stored key is a hash of that label.

`includeBaseline` defaults to true. `emitUnchanged` defaults to true, so a repeat of the prefilled roster still writes `OK_NO_CHANGE` rows. Set `emitUnchanged` to false only on a scheduled task that should list changes. The watchlist length is capped at 10,000 by the input form. A longer list is rejected before the run starts.

### Output

One dataset row is one carrier observation. `status` and `recordType` are the same.

| Status | Meaning |
| --- | --- |
| `BASELINE_CREATED` | First successful census record for this USDOT and snapshot. No historical change is invented. |
| `OK_NO_CHANGE` | Normalized contact fields match the last successful check. Written by default. Hidden when `emitUnchanged` is false. |
| `CHANGE` | At least one field changed in a way the normalizer treats as material. |
| `AMBIGUOUS_CHANGE` | The only difference is an officer name whose tokens match in a different order. The row is emitted so it is not dropped. |
| `NO_RESULTS` | The census request succeeded and this USDOT is not in the file. |
| `SOURCE_FAILED` | This USDOT could not be checked. Its baseline is left as it was. |
| `UNPROCESSED` | The check was not charged, the run time budget ran out, or the saved baseline schema is not one this build can read. The baseline is left as it was. |

`changes` keeps the raw before/after values and the normalized forms. `current` is the raw census contact when the carrier was found. `attentionLevel` is `high` or `medium`, or null when nothing material changed.

High means phone, cell phone, email, officer, legal name, or two or more identity fields together. Medium means only a DBA or a single address. Attention level indicates workflow priority, not fraud probability.

`source.datasetUpdatedAt` is the census file time, not the carrier's MCS-150 filing date. A null timestamp means the catalog time was unavailable for that run. The rows are still the census records that were returned.

The run summary is `RUN_SUMMARY` in the run key-value store. It counts unchanged carriers even when those rows are omitted. `outcome` is `ok`, `partial`, `no_results`, `source_failed` or `internal_error`.

Baselines are not stored in the run store. They live in the named store `fmcsa-carrier-contact-takeover-monitor-state`, one record per snapshot and USDOT. That record keeps the last census contacts, including a business email and a cell phone, until you delete it. In Apify Storage, open that key-value store and delete the baseline keys for one roster, or delete the store. Deleting the store removes every snapshot. It does not delete dataset rows that were already delivered.

### Change types

Field codes stay specific. A phone change is never collapsed into a generic alert.

- `LEGAL_NAME_CHANGED`
- `DBA_CHANGED`
- `PHONE_CHANGED`
- `CELL_PHONE_CHANGED`
- `EMAIL_CHANGED`
- `PHYSICAL_ADDRESS_CHANGED`
- `MAILING_ADDRESS_CHANGED`
- `OFFICER_CHANGED`
- `MULTIPLE_IDENTITY_FIELDS_CHANGED` when two or more of those fields change together
- `CENSUS_RECORD_REMOVED` when a previously seen USDOT leaves the file
- `CENSUS_RECORD_ADDED` when a USDOT that was confirmed absent appears for the first time
- `CENSUS_RECORD_RESTORED` when a previously seen USDOT returns

`changeClasses` adds `CONTACT_CHANGE`, `NAME_CHANGE`, `ADDRESS_CHANGE` or `OFFICER_CHANGE` as a convenience. Filter on `changeTypes` when the field matters.

When a previously seen USDOT disappears from the file, the row is `NO_RESULTS`, `previouslyObserved` is true, and the only change type is `CENSUS_RECORD_REMOVED`. The row does not invent a change for every field. If that USDOT comes back with the same contacts, the row is `CHANGE` with only `CENSUS_RECORD_RESTORED`. If it comes back with different contacts, the row lists those field changes and `CENSUS_RECORD_RESTORED`. A USDOT that was confirmed absent before it ever had a census row, and later appears, is `CENSUS_RECORD_ADDED` with `previouslyObserved` false. Repeated absence stays `NO_RESULTS` with an empty `changes` object.

### Normalization

Formatting noise is suppressed. Semantic changes are not. Raw text is always what you see in `before` and `after`.

Phone: trim, keep digits, and drop a leading `1` only when the number has 11 digits. `(555) 123-4567`, `555-123-4567`, `5551234567` and `+1 555 123 4567` match. `5551234567` and `5559990000` do not.

Email: Unicode normalization, trim, and lowercasing of the whole address. Plus tags stay. `Ops@Example.com` matches `ops@example.com`. `ops+dispatch@example.com` does not match `ops@example.com`.

Names: case, whitespace and decorative punctuation such as commas and periods. Dotted `L.L.C.` matches `LLC`. The words LLC, INC, CORP, CO and LP are not removed. Hyphens stay, so `Smith-Jones` does not match `Smith Jones`.

Addresses use the same text rules, plus ZIP punctuation only. There is no geocoding, USPS rewrite or fuzzy street match. `123 Main St.` matches `123 main st`. A different street number does not.

Officers use the same text rules. The same names in a different slot order are not a change. A different set of names is `OFFICER_CHANGED`. `SMITH, JOHN` and `JOHN SMITH` in the same slot are `AMBIGUOUS_CHANGE`, not a silent match and not a clean replacement.

### First-run baseline

The first successful run creates the comparison baseline. Starting from the next successful run, the Actor emits material changes.

With `includeBaseline` true, that first run writes a `BASELINE_CREATED` row for every USDOT that is in the file, so the dataset is not empty. It does not invent a change that happened before you started watching.

An immediate second run with the same census values produces no `CHANGE` rows. By default it writes an `OK_NO_CHANGE` row for each carrier, so the dataset is not empty. Set `emitUnchanged` to false when you want that second run to stay empty unless something material changed. `RUN_SUMMARY.unchanged` still counts the carriers that were checked.

A failed check does not replace a good baseline. The next successful check compares against the last good one.

### Scheduling

Save the input, schedule it daily, and send the dataset to the place you already review carriers.

1. In Apify, open the run and create a schedule for the saved input. Daily is the cadence that matches the census file.
2. Add a webhook on successful runs if you only need the dataset when the run finishes.
3. Read the `changes` view. It includes `CHANGE`, `AMBIGUOUS_CHANGE`, and a `NO_RESULTS` row for a USDOT that was seen before and then left the file.

Use one `snapshotKey` per roster. Two schedules with the same key can block each other: the second run stops and asks you to wait, so the baselines are not written by two runs at once.

### n8n, Make and API

Start the Actor with the Apify API using the same JSON input. In n8n or Make, use the Apify node or an HTTP request to run the Actor, then read the default dataset. Map `usdotNumber`, `status`, `attentionLevel`, `changeTypes` and `changes` into the review queue.

Do not build a fraud rule on `attentionLevel`. Route `high` rows to a person who can re-verify the carrier. Route `NO_RESULTS` to someone who can confirm the USDOT. Leave `SOURCE_FAILED` for a retry. Those rows were not billed and the baseline did not move.

The direct MCP tool is `https://mcp.apify.com?tools=automa-flow/fmcsa-carrier-contact-takeover-monitor`. Hosted MCP search and a run on your own Apify account are separate steps. This README does not start a run for you.

### Related automa-flow tools

- **Send only the changes on:** [Dataset Cleaner](https://apify.com/automa-flow/dataset-filter-transform) filters this run's Dataset to rows whose `status` is `CHANGE` or `AMBIGUOUS_CHANGE`, renames columns and dedupes, before an alert or CRM export. Pass the Dataset ID as `datasetId`.

Each Actor runs and charges separately. This Actor does not call them.

### Pricing

A completed carrier check is one `carrier-check` event at $0.0025. One thousand successful checks are $2.50. The event covers a baseline, a no-change check, a change, an ambiguous change, or a confirmed absence.

`SOURCE_FAILED`, invalid input and `UNPROCESSED` are not charged. Duplicate USDOT numbers in one input are checked once. Retries against the census are not a separate event.

`RUN_SUMMARY.carrierChecksBilled` is the number of events charged on a pay-per-event run. On a run that is not pay-per-event the field stays 0, while `checked` still counts the work.

This price is the launch hypothesis for a later pricing review. It is not a live Store price until the Actor is published. Platform usage for an ordinary run is intended to be included in the event. Measure a 10,000-carrier day on staging before publication. Do not treat this README as a bill.

### Data source

Requests go to `https://data.transportation.gov/resource/az4n-8mr2.json` in batches of 75 USDOT numbers, with at most 4 batches in flight. The client retries 408, 425, 429, 500, 502, 503, 504, timeouts and connection failures, at most 3 attempts, with exponential backoff and `Retry-After` when the server sends it. After the first HTTP 429 the run sends one batch at a time and waits 5 seconds, then 10, or the `Retry-After` when it is a minute or less. No proxy and no browser.

The watchlist is processed in slices of 450 USDOT numbers. Each slice is checked, saved, charged and written to the dataset before the next one starts, so a 10,000-carrier roster fits the default 256 MB, and a run that reaches its time limit keeps every slice it finished. The format-change and absence checks below look at one slice at a time.

The catalog is read from `https://api.us.socrata.com/api/catalog/v1?ids=az4n-8mr2` for the file timestamp. A missing catalog does not fail the census read. Checked on 29 September 2026, the id was still `az4n-8mr2`, the file had been updated the day before, and the public API returned legal name, phone, email, both officer fields and both addresses without a token.

Email and officer coverage is high on active rows and is not 100 percent. A null email means the census file has no email, not that the lookup failed.

### Privacy and limitations

The census is a public FMCSA business file. Officer names and business emails in that file are part of the carrier record the agency publishes. The Actor does not add personal data from other sites, does not infer fraud, and does not call a carrier safe or unsafe.

Logs contain the USDOT and the names of fields that changed. They do not contain phone numbers, emails, addresses or officer names. Those values are only in the dataset.

Limits you should plan for:

- The census can lag a filing. `datasetUpdatedAt` tells you which file you read.
- Null contact fields are real gaps in the file.
- Formatting rules are conservative. An unusual abbreviation can still alert. That is preferred to hiding a new phone number.
- This is not an authority, insurance, safety, new-carrier or discovery product.
- Two runs of the same snapshot key cannot update baselines together. The second run stops while the first is still running. A later run takes the lock about three minutes after the last heartbeat. If this run cannot refresh its own lock, it stops without saving or charging.
- If a run stops after it has saved a new observation and before that row is delivered, the next run delivers it when the census still matches. It does not charge that observation again. If the census has moved on, the comparison uses the last delivered baseline.
- Delivery is at least once. If a run is killed after the dataset row is written and before the baseline is marked delivered, the next run writes that same row again. Deduplicate on `usdotNumber` plus `fingerprint`.
- If the census keeps failing, the run stops requesting later batches. Those USDOT numbers are `SOURCE_FAILED` and their baselines stay as they were.
- If the account charge limit is reached, the rest of the list is `UNPROCESSED`. Those carriers are not billed and their baselines stay as they were.
- After a census rate limit the run slows to one batch at a time. A `Retry-After` longer than 60 seconds on a rate limit, or longer than 20 seconds on a server error, fails that batch instead of pausing the run for the full delay. That batch is `SOURCE_FAILED`, not billed, and its baselines stay as they were.

### NO_RESULTS vs SOURCE_FAILED

`NO_RESULTS` means the request succeeded, the payload matched the census shape, and that USDOT was not in the rows. It is a completed check.

`SOURCE_FAILED` means this USDOT was not verified. Typical causes are a timeout, a rate limit that survived retries, HTTP 401, 403, 404 or 5xx, invalid JSON, a row with no legal name, or a row that is not tied to one USDOT. If one census batch drops too many carriers that were present on the last good check, or the same field changes for more than half of a roster of 20 or more and for at least 10 carriers, or a phone or physical address is filled on fewer than 20 percent of at least 10 present rows, that batch is `SOURCE_FAILED` and those baselines stay. One failed batch does not erase results from batches that succeeded. Failed USDOTs do not update their baselines, and they are not charged.

If every USDOT fails, the run itself fails. A mix of successes and failures finishes as a partial success so you still get the rows that were checked.

### FAQ

**Does this decide that a carrier was taken over?** No. It reports contact-field evidence from the official census. A person or a downstream process decides what to re-verify.

**Why is the second run empty?** The default repeat writes `OK_NO_CHANGE` rows. The dataset is empty on a repeat only when `emitUnchanged` is false and nothing material changed. Read `RUN_SUMMARY` for the unchanged count.

**Why did a phone format change not alert?** The digits match, including a leading US `1` on an 11-digit number. The raw strings can differ.

**What if the officer was stored as `LAST, FIRST` and later as `FIRST LAST`?** That is `AMBIGUOUS_CHANGE`. It is not treated as the same person and it is not dropped.

**Can I watch two rosters?** Yes. Use two `snapshotKey` values. They do not share baselines.

**Does the default run need my carriers?** The prefilled USDOTs are public active carriers so a first run returns baselines. Replace them with your roster before you schedule it.

If a run fails or the rows look wrong, open an Issue on this Actor and include the run URL. Apify shares that run automatically. Do not paste phone numbers, emails, addresses or officer names into the Issue. `RUN_SUMMARY` has the counts, the census time and the error family.

Update history is in the [changelog](https://apify.com/automa-flow/fmcsa-carrier-contact-takeover-monitor/changelog).

# Changelog

This Actor's version history is a separate document: https://apify.com/automa-flow/fmcsa-carrier-contact-takeover-monitor/changelog.md

# Actor input Schema

## `usdotNumbers` (type: `array`):

Watchlist of USDOT numbers, 1 to 10000. Each value is 1 to 8 digits. Surrounding spaces are ignored and leading zeros are dropped, so 00264184 is 264184. Repeats are checked once. A value of 0 is rejected.

## `snapshotKey` (type: `string`):

Separate baseline namespace for one roster. 1 to 80 characters: letters, numbers, underscore or hyphen. The default is default. The raw text is not used as a storage key.

## `emitUnchanged` (type: `boolean`):

When true, carriers with no material change are written to the dataset. The default is true so a repeat of the same roster still returns rows. Set false on a scheduled changes-only task. The first baseline is controlled by Include baseline, not by this switch.

## `includeBaseline` (type: `boolean`):

When true, the first successful observation of a USDOT is written as BASELINE_CREATED so the dataset is not empty. Later runs compare against that baseline. Set false only if you already stored a baseline and want changes only.

## Actor input object example

```json
{
  "usdotNumbers": [
    "264184",
    "54283",
    "53467"
  ],
  "snapshotKey": "default",
  "emitUnchanged": true,
  "includeBaseline": true
}
```

# Actor output Schema

## `results` (type: `string`):

Baselines, material changes, ambiguous changes, confirmed absences and source failures.

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

CHANGE and AMBIGUOUS_CHANGE rows with before and after values.

## `problems` (type: `string`):

NO_RESULTS, SOURCE_FAILED and UNPROCESSED rows.

## `runSummary` (type: `string`):

Counts for the watchlist, including unchanged carriers that were not written to the dataset.

# 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 = {
    "usdotNumbers": [
        "264184",
        "54283",
        "53467"
    ],
    "snapshotKey": "default",
    "emitUnchanged": true,
    "includeBaseline": true
};

// Run the Actor and wait for it to finish
const run = await client.actor("automa-flow/fmcsa-carrier-contact-takeover-monitor").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 = {
    "usdotNumbers": [
        "264184",
        "54283",
        "53467",
    ],
    "snapshotKey": "default",
    "emitUnchanged": True,
    "includeBaseline": True,
}

# Run the Actor and wait for it to finish
run = client.actor("automa-flow/fmcsa-carrier-contact-takeover-monitor").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 '{
  "usdotNumbers": [
    "264184",
    "54283",
    "53467"
  ],
  "snapshotKey": "default",
  "emitUnchanged": true,
  "includeBaseline": true
}' |
apify call automa-flow/fmcsa-carrier-contact-takeover-monitor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,automa-flow/fmcsa-carrier-contact-takeover-monitor"
        }
    }
}
```

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/g0ODhVFcJwRA7nieg/builds/3tW36Vj5wfVEviP6L/openapi.json
