# FCC ULS License Scraper - Business Radio & Microwave Licenses (`groundplanes/fcc-uls-business-radio-licenses`) Actor

Scrape FCC Universal Licensing System (ULS) business/industrial land-mobile radio (IG, YG) and point-to-point microwave (CF, MG) licenses by state, status, and expiration window from FCC bulk files.

- **URL**: https://apify.com/groundplanes/fcc-uls-business-radio-licenses.md
- **Developed by:** [Groundplane Labs](https://apify.com/groundplanes) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.10 / 1,000 licenses

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

## FCC ULS License Scraper - Business Radio & Microwave Licenses

Scrape commercial land-mobile business radio (`IG`, `YG`, `GJ`, `YO`, `YB`, `YI`, etc.) and point-to-point microwave (`CF`, `MG`, `NN`, `TP`, etc.) licenses directly from the Federal Communications Commission (FCC) Universal Licensing System (ULS) weekly bulk license archives (`l_LMpriv.zip` and `l_micro.zip`). Filter by radio service code, licensee state, license status, or upcoming expiration window to identify organizations operating commercial radio and microwave infrastructure.

> **Disclaimer:** This Actor is an independent tool and is not affiliated with or endorsed by the Federal Communications Commission (FCC).

***

### What it does

- **Streams FCC ULS Bulk Archives via HTTP Range Requests:** Queries the official FCC ULS complete license archives (`l_LMpriv.zip` and `l_micro.zip` on `data.fcc.gov`) using ZIP Central Directory HTTP `Range` requests to stream and decompress only the License Header (`HD.dat`) and Entity (`EN.dat`) tables.
- **Commercial Business Radio & Microwave Coverage:** Supports Private Land Mobile Industrial/Business Pool (`IG`, `YG`, `YB`, `YI`, `YO`, `GJ`, `GO`, etc.) and Microwave (`CF`, `MG`, `NN`, `TP`, etc.) radio service codes.
- **Expiring License Detection:** In `EXPIRING` mode, filters active (`A`) licenses expiring between today and `today + expiringWithinDays` (default 180 days) and calculates exact `daysUntilExpiration`.
- **Privacy Controls Built In:** Individual licensees are excluded by default: any row with `applicant_type_code == 'I'` is individual unconditionally, a row whose entity name is in `LAST, FIRST` form built from its last/first name fields is individual, a row with a personal first/last name and no business entity name is individual, and a partnership (`applicant_type_code == 'P'`) row whose entity name has no business marker (such as LLC, LP, LLP, INC, CO, or CORP) is filtered the same way. Contact fields (`contactName`, `contactEmail`, `contactPhone`) are also excluded by default (`includeIndividualLicensees: false`, `includeContacts: false`).
- **Actionable Ordering:** In `EXPIRING` mode, results are sorted by `expirationDate` ascending (soonest expiring first) so urgent renewals appear first; in all other modes (`ACTIVE`, `EXPIRED`, `CANCELLED`), results are sorted by `grantDate` descending (newest first) before applying `maxItems`.

***

### Who it's for

- **Two-Way Radio & Land Mobile Dealers:** Find commercial facilities, farms, warehouses, construction firms, hotels, utilities, and school districts whose FCC Part 90 business radio (`IG` / `YG`) licenses are approaching their 10-year renewal window.
- **Telecom & Backhaul Providers:** Track common carrier (`CF`) and private operational fixed (`MG`) point-to-point microwave links by state and expiration date.
- **FCC Licensing & Compliance Consultants:** Monitor upcoming license expirations and recently cancelled or expired licenses requiring renewal or re-coordination.
- **Infrastructure & M\&A Analysts:** Audit active spectrum holdings and wireless infrastructure footprints across corporate entities (by FRN and call sign).

***

### Input

All parameters have working defaults/prefills that return results in under 2 minutes.

| Field | Type | Default / Prefill | Description |
|---|---|---|---|
| `radioServiceCodes` | `array<string>` | `["IG", "YG"]` | FCC ULS radio service codes. Land Mobile uses `l_LMpriv.zip` (`IG`, `YG`, `YB`, `YI`, `YO`, `GJ`, `GO`, etc.); Microwave uses `l_micro.zip` (`CF`, `MG`, `NN`, `TP`, etc.). |
| `states` | `array<string>` | `["AZ"]` | Two-letter US state or territory postal codes for the licensee address (e.g. `["AZ"]`, `["AZ", "NV"]`). Leave empty for all states. |
| `status` | `string` | `"EXPIRING"` | `EXPIRING` (Active licenses expiring within `expiringWithinDays`), `ACTIVE`, `EXPIRED`, or `CANCELLED` (includes Cancelled `C` and Terminated `T`). |
| `expiringWithinDays` | `integer` | `180` | Lookahead window in days when `status` is `"EXPIRING"`. |
| `includeContacts` | `boolean` | `false` | Include a named attention contact, email, and telephone number (`contactName`, `contactEmail`, `contactPhone`) from `EN.dat`. Defaults to `false` for privacy compliance. |
| `includeIndividualLicensees` | `boolean` | `false` | Include licenses held by natural persons. A licensee is a natural person when `applicant_type_code == 'I'` (unconditionally), when `EN.dat` has a first/last name and no business entity name, or when the entity name is in `LAST, FIRST` form. Partnership rows (`applicant_type_code == 'P'`) with no business marker in the entity name are treated the same way. Defaults to `false` so only organizational/business licensees are returned. |
| `maxItems` | `integer` | `200` | Maximum number of license records to output. |

#### Example input JSON

```json
{
  "radioServiceCodes": ["IG", "YG"],
  "states": ["AZ"],
  "status": "EXPIRING",
  "expiringWithinDays": 180,
  "includeContacts": false,
  "includeIndividualLicensees": false,
  "maxItems": 200
}
```

#### Example run options (with default $50 max charge protection)

When invoking via the Apify API or CLI, set a default maximum charge of **$50** (`"maxTotalChargeUsd": 50`):

```json
{
  "maxTotalChargeUsd": 50
}
```

The Actor checks `calculate_max_event_charge_count_within_limit("apify-default-dataset-item")` before every dataset push and stops cleanly when your spending limit is reached.

***

### Output

Results are saved to the default dataset. Deduplication is enforced by `callSign + uniqueSystemId`.

#### Output fields

| Field | Type | Description |
|---|---|---|
| `callSign` | `string` | FCC license call sign (`HD.dat` column 5). |
| `uniqueSystemId` | `string` | FCC ULS `unique_system_identifier` (`HD.dat` column 2). |
| `frn` | `string \| null` | 10-digit FCC Registration Number (FRN). |
| `licenseeName` | `string \| null` | Business or institutional licensee name (`EN.dat` entity name). |
| `radioService` | `string` | 2-letter FCC radio service code (e.g. `IG`, `YG`, `CF`, `MG`). |
| `licenseStatus` | `string` | Normalized status: `Active`, `Expired`, `Cancelled`, or `Terminated`. |
| `grantDate` | `string \| null` | License grant date in `YYYY-MM-DD` format. |
| `expirationDate` | `string \| null` | License expiration date in `YYYY-MM-DD` format. |
| `daysUntilExpiration` | `integer \| null` | Calendar days from today (UTC) until `expirationDate` (negative if already expired). |
| `street` | `string \| null` | Licensee street address or P.O. Box. |
| `city` | `string \| null` | Licensee city. |
| `state` | `string \| null` | Two-letter state or territory code. |
| `zip` | `string \| null` | Licensee postal ZIP code (formatted `XXXXX` or `XXXXX-XXXX`). |
| `ulsUrl` | `string` | Direct link to the FCC ULS license page (`https://wireless2.fcc.gov/UlsApp/UlsSearch/license.jsp?licKey=<uniqueSystemId>`). |
| `contactName` | `string \| null` | Included **only** when `includeContacts` is `true`. |
| `contactEmail` | `string \| null` | Included **only** when `includeContacts` is `true`. |
| `contactPhone` | `string \| null` | Included **only** when `includeContacts` is `true`. |

#### Sample output row (synthetic example)

```json
{
  "callSign": "WQXM901",
  "uniqueSystemId": "3891042",
  "frn": "0025918402",
  "licenseeName": "DESERT RIDGE LOGISTICS LLC",
  "radioService": "IG",
  "licenseStatus": "Active",
  "grantDate": "2016-11-18",
  "expirationDate": "2026-11-18",
  "daysUntilExpiration": 18,
  "street": "4200 W BUCKEYE RD",
  "city": "PHOENIX",
  "state": "AZ",
  "zip": "85009",
  "ulsUrl": "https://wireless2.fcc.gov/UlsApp/UlsSearch/license.jsp?licKey=3891042"
}
```

*(If `includeContacts: true` is enabled, optional contact fields `"contactName": "FLEET OPERATIONS DEPT"`, `"contactEmail": "radio-compliance@example.com"`, and `"contactPhone": "602-555-0199"` are also included.)*

***

### How pricing works

Pay per result. You only pay for rows returned.

***

### Data source and terms

- **Official Source:** Federal Communications Commission (FCC) — [Universal Licensing System (ULS) Public Access Files / Database Downloads](https://www.fcc.gov/wireless/data/public-access-files-database-downloads)
- **Bulk Archive Endpoints:**
  - Private Land Mobile (`l_LMpriv.zip`): `https://data.fcc.gov/download/pub/uls/complete/l_LMpriv.zip`
  - Microwave (`l_micro.zip`): `https://data.fcc.gov/download/pub/uls/complete/l_micro.zip`
- **Update Cadence:** Complete license archives are generated weekly (typically Sunday morning) by the FCC.
- **Terms of Use:** FCC ULS public access files are U.S. Government public records published under [FCC Privacy & Data Policy](https://www.fcc.gov/general/privacy-policy).

***

### Compliance

This Actor retrieves public licensing records published by the Federal Communications Commission (FCC). Personal contact fields (`includeContacts` and `includeIndividualLicensees`) are turned **off by default**.

If you enable contact fields or use organization address data for outreach:

- **Regulatory Compliance:** Buyers are solely responsible for complying with the Telephone Consumer Protection Act (**TCPA**), the **National Do-Not-Call (DNC) Registry**, the **CAN-SPAM Act**, and all applicable state and international privacy and telemarketing laws before conducting any outreach.
- **No Consumer / Individual Targeting:** This tool is designed for B2B commercial infrastructure research (e.g., corporate, industrial, utility, and institutional radio licensees) and must not be used for consumer or individual targeting.

***

### Limits and FAQ

- **Why does the Actor use 1 GB memory (`1024 MB`)?**
  The FCC's complete Land Mobile Private archive (`l_LMpriv.zip`) is ~424 MB compressed (~2.5 GB uncompressed across all tables). This Actor uses HTTP `Range` requests against the ZIP Central Directory to stream and decompress only `HD.dat` (~26.7 MB compressed) and `EN.dat` (~75.3 MB compressed) in 256 KB chunks, filtering rows in memory in ~14 seconds. If a network intermediary ignores `Range` requests, the Actor downloads the full archive once and extracts the same two tables locally rather than misreading offsets. Measured peak memory on the worst case (all states, `ACTIVE`, `IG`+`YG`+`CF`+`MG`) is about 616 MB, so 1024 MB is enough even when querying multiple states and both Land Mobile and Microwave archives in a single run.
- **Why does the Actor keep the stock `python-httpx` User-Agent?**
  Documented exception: `data.fcc.gov` is Akamai-fronted. Retested 2026-11-01 via httpx, the identified neutral UA `GroundplaneLabs-Apify/1.0` (no email) passes GET `Range` requests (206) but fails HEAD requests (403 on every trial), and this Actor relies on HEAD to inspect each archive. The stock library UA passes both HEAD (200) and GET `Range` (206) and does not impersonate a browser, so it is kept deliberately.
- **What happens when 0 rows match?**
  Zero matches on a healthy source is a successful run: the status reads `Complete: 0 licenses matched your filters` and states how many records were scanned, plus which filters (`states`, `expiringWithinDays`, `status`, or `radioServiceCodes`) to widen. A run only fails loudly when the source itself is broken (empty or restructured files, failed downloads), so upstream data changes never fail silently.
- **Can I query both Land Mobile (`IG`, `YG`) and Microwave (`CF`, `MG`) in one run?**
  Yes. The Actor automatically maps each requested radio service code to `l_LMpriv.zip` or `l_micro.zip` and downloads only the required archive(s).

***

### Troubleshooting

- **Canary input:** to smoke-test the Actor, run the default input above with `"maxItems": 5`. It returns rows in well under a minute on a healthy source and ends with a `Complete:` status.
- **Status starts with `PARTIAL:`:** the run finished but something material was withheld, and the status message says exactly why (for example a stale weekly archive, a failed source archive, or rows skipped by validation). Rows already collected are still delivered.
- **Status starts with `Complete: 0 licenses matched`:** the source is healthy and records were scanned, but no license fits your filters. Widen `expiringWithinDays`, add `states` or `radioServiceCodes`, or switch `status` to `ACTIVE`.
- **Run failed with a layout or download message:** the FCC changed or failed to publish a bulk file. Retry once; if it persists, report it as below.

Found a problem or need a field? Open an issue on the Issues tab; we respond quickly.

***

### Changelog

#### v0.1.3 (2026-11-01)

- Privacy fix (Blocker): `applicant_type_code == 'I'` rows are now individual unconditionally, entity names in `LAST, FIRST` form are treated as individual, and partnership (`P`) rows with no business marker in the entity name are filtered by default. Added a unit test for the individual test.
- Retry policy aligned to the fleet standard: 6 tries, jittered backoff from 2 to 32 seconds capped at 120 seconds, with `Retry-After` honored as seconds or as an HTTP date.
- Default memory reduced from 4096 MB to 1024 MB (measured worst case about 616 MB). Strict boolean parsing for `includeContacts` and `includeIndividualLicensees`. Non-US state codes are pre-filtered and counted as filtered instead of failing validation. Listing wording uses singular contact naming and the description starts with "Scrape". User-Agent: the identified neutral UA was retested via httpx and fails HEAD, so the stock UA is kept as a documented exception (see Limits and FAQ).

#### v0.1.2 (2026-10-31)

- Red Team fix round (download integrity): the ZIP central-directory CRC32 is now carried through the download path; every 206 response has its Content-Range verified against the requested range; and decompression verifies the end-of-stream marker, the decompressed byte count, and the CRC32 against the central directory, so a truncated or corrupted stream fails loudly instead of yielding a partial table.

#### v0.1.1 (2026-10-31)

- Fixed run status contract: runs now end with `Complete:` or `PARTIAL:` with honest scanned / matched / emitted counts; zero matches on a healthy source is a `Complete: 0` success instead of a failure.
- Added per-row output validation before push and a core-column sentinel on `HD.dat` / `EN.dat` that fails loudly if the FCC restructures the files.
- Added per-archive isolation (one failed archive no longer discards the other's rows), a staleness check on the weekly archives, and a full-download fallback when a network intermediary ignores HTTP `Range` requests.
- Store categories corrected to Business only; default run options now include the $50 max charge; an explicit empty `states` list now means all states, as documented.

#### v0.1 (2026-10-31)

- Initial release supporting FCC ULS Land Mobile Private (`l_LMpriv.zip`) and Microwave (`l_micro.zip`) bulk archives via HTTP Range streaming.
- Added `EXPIRING`, `ACTIVE`, `EXPIRED`, and `CANCELLED` filtering, state filtering, and privacy-first contact flags (`includeContacts`, `includeIndividualLicensees` default `false`).

# Actor input Schema

## `expiringWithinDays` (type: `integer`):

When status is EXPIRING, keeps active licenses whose expirationDate is between today and today + expiringWithinDays.

## `includeContacts` (type: `boolean`):

Defaults to false for privacy compliance because FCC ULS EN.dat contains a named attention contact, email, and telephone number. Enable only when needed for authorized compliance or renewal workflows.

## `includeIndividualLicensees` (type: `boolean`):

Defaults to false for privacy compliance; drops rows where the licensee is an individual person (has first/last name and no business/entity name).

## `maxItems` (type: `integer`):

Maximum number of license rows to return.

## `radioServiceCodes` (type: `array`):

FCC ULS radio service codes to include, as 2-letter codes. Common choices: IG (Industrial/Business Pool Conventional), YG (Industrial/Business Pool Trunked), CF (Common Carrier Fixed Point-to-Point Microwave), MG (Microwave Industrial/Business Pool). All codes present in the two bulk archives are supported: land mobile (l_LMpriv.zip) GB, GI, GJ, GO, GP, GU, IG, IK, LN, NC, PW, QD, QO, QQ, QT, RS, SG, SL, SP, SY, WP, WX, YB, YG, YI, YK, YM, YO, YU, YW, YX; microwave (l_micro.zip) CE, CF, CJ, CT, MG, MW, NN, PC, PE, TI, TN, TP, TS, TT, WA, WM, WR. Unsupported codes fail the run with a clear message.

## `states` (type: `array`):

Two-letter US state or territory postal codes for the licensee address (e.g. \["AZ"] or \["AZ", "NV"]). Leave empty to match all states.

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

Filter by license status: EXPIRING (active licenses expiring within expiringWithinDays), ACTIVE (all active licenses), EXPIRED, or CANCELLED.

## Actor input object example

```json
{
  "expiringWithinDays": 180,
  "includeContacts": false,
  "includeIndividualLicensees": false,
  "maxItems": 200,
  "radioServiceCodes": [
    "IG",
    "YG"
  ],
  "states": [
    "AZ"
  ],
  "status": "EXPIRING"
}
```

# Actor output Schema

## `results` (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 = {
    "expiringWithinDays": 180,
    "includeContacts": false,
    "includeIndividualLicensees": false,
    "maxItems": 200,
    "radioServiceCodes": [
        "IG",
        "YG"
    ],
    "states": [
        "AZ"
    ],
    "status": "EXPIRING"
};

// Run the Actor and wait for it to finish
const run = await client.actor("groundplanes/fcc-uls-business-radio-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 = {
    "expiringWithinDays": 180,
    "includeContacts": False,
    "includeIndividualLicensees": False,
    "maxItems": 200,
    "radioServiceCodes": [
        "IG",
        "YG",
    ],
    "states": ["AZ"],
    "status": "EXPIRING",
}

# Run the Actor and wait for it to finish
run = client.actor("groundplanes/fcc-uls-business-radio-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 '{
  "expiringWithinDays": 180,
  "includeContacts": false,
  "includeIndividualLicensees": false,
  "maxItems": 200,
  "radioServiceCodes": [
    "IG",
    "YG"
  ],
  "states": [
    "AZ"
  ],
  "status": "EXPIRING"
}' |
apify call groundplanes/fcc-uls-business-radio-licenses --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,groundplanes/fcc-uls-business-radio-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/ekesE8Peyv5iPSI3m/builds/OKizNDWqecw9AfYlg/openapi.json
