FDA Wholesale Drug Distributor & 3PL Reporting Monitor avatar

FDA Wholesale Drug Distributor & 3PL Reporting Monitor

Pricing

from $10.00 / 1,000 resolved target checks

Go to Apify Store
FDA Wholesale Drug Distributor & 3PL Reporting Monitor

FDA Wholesale Drug Distributor & 3PL Reporting Monitor

Look up FDA wholesale drug distributor (WDD) and 3PL annual reporting records by license or facility name. Monitor reported expiration dates, DBAs and reporting changes across supplier lists. DSCSA reporting data only; verify state licensure separately.

Pricing

from $10.00 / 1,000 resolved target checks

Rating

0.0

(0)

Developer

Vadim Bezrukov

Vadim Bezrukov

Maintained by Community

Actor stats

0

Bookmarked

2

Total users

1

Monthly active users

a day ago

Last modified

Share

Screen and monitor wholesale drug distributors (WDD) and third-party logistics providers (3PL) against FDA's official DSCSA annual-reporting database. Give it a watchlist of licence numbers or facility names, and it tells you what FDA reports today and exactly what changed since your last check.

This is federal reporting data, not an Authorized Trading Partner check. This Actor monitors public FDA WDD/3PL annual-reporting records. Presence in, or absence from, this database is not by itself proof of current state licensure, FDA approval, regulatory compliance, or complete DSCSA Authorized Trading Partner status. FDA recommends checking applicable state licensure separately. Read Limitations before you use this in an onboarding decision.

What problem this solves

Under DSCSA, wholesale drug distributors and third-party logistics providers must meet applicable licensure and reporting requirements, and FDA republishes their annual reporting database every business day. If you buy, store or move prescription drugs, you need to know when a facility you depend on stops appearing, reports a different licence expiration, changes its trading name, or files under a new reporting year.

Doing that by hand means re-searching FDA's web form facility by facility and eyeballing the difference. This Actor turns that into a scheduled job: one watchlist in, typed changes out.

Built for pharmaceutical manufacturers and repackagers, wholesale distributors, 3PLs, pharmacies and dispensers, procurement and supplier-onboarding teams, and DSCSA compliance vendors and data teams.

What the FDA WDD/3PL reporting database is

FDA's Drug Supply Chain Security Act annual-reporting database holds what wholesale drug distributors and third-party logistics providers report to FDA each year. Each row is one licence for one facility for one reporting year, so a single facility commonly appears many times: once per state licence, and again when it files for a new year.

The Actor reads FDA's own published bulk file, so it sees exactly what the search page sees, all 38,000-odd rows at once, in one request.

Use from AI agents with MCP

With access to this Actor, connect it directly at https://mcp.apify.com?tools=automa-flow/fda-wdd-3pl-reporting-monitor. Ask your agent to screen a supplier list and report MATCHED, NOT_FOUND, AMBIGUOUS or a source failure. The output is worded so an agent cannot turn a reporting record into a licensure or compliance claim: nothing in it says "approved", "licensed" or "compliant".

Execution requires the caller's own Apify OAuth or token. Authenticated MCP tool discovery and direct API execution with limited permissions were verified on 2026-09-07. Execution through agentic payment providers is not separately tested.

Quickstart

Watch two facilities, one by licence and one by name:

{
"targets": [
{"licenseState": "US-OH", "licenseNumber": "WHO.0008724", "reference": "supplier-001"},
{"facilityName": "AdiraMedica LLC", "facilityState": "PA", "reference": "supplier-002"}
],
"monitorKey": "pharma-suppliers-prod"
}

The first run stores a baseline and publishes what FDA reports today. Every later run with the same monitorKey publishes only what changed.

The Console demo preselects snapshotAndChanges, so repeated demo runs still show current records. Select changesOnly for a scheduled watchlist. API calls that omit mode default to changesOnly.

Sample output

A matched licence, on a first run:

{
"record_type": "BASELINE",
"status": "SUCCESS",
"reference": "supplier-3pl-001",
"target_key": "LICENSE|US-IN|94000227A|*",
"target_type": "LICENSE",
"match_status": "MATCHED",
"record_key": "0f1c...",
"facility_name": "AdiraMedica LLC",
"dba_names": [],
"facility_type": "3PL",
"license_number": "94000227A",
"license_state": "US-IN",
"license_expiration_date": "2026-09-30",
"license_expired": false,
"days_until_license_expiration": 24,
"reporting_year": 2026,
"facility_street": "585 Turner Industrial Way",
"facility_city": "Aston",
"facility_state": "PA",
"change_types": [],
"changes": null,
"source": "fda_wdd_3pl_reporting",
"source_snapshot_date": "2026-09-04",
"scraped_at": "2026-09-06T20:27:13Z",
"schema_version": 1,
"fingerprint": "8a3f..."
}

A licence renewal on a later run:

{
"record_type": "CHANGE",
"status": "SUCCESS",
"reference": "supplier-3pl-001",
"change_types": ["LICENSE_EXPIRATION_CHANGED"],
"changes": {
"licenseExpirationDate": {"previous": "2026-09-30", "current": "2027-09-30"}
},
"license_expiration_date": "2027-09-30",
"source_snapshot_date": "2026-09-11"
}

examples/sample_input.json and examples/sample_output.json hold a complete run covering every outcome, including not-found, ambiguous and invalid targets.

Watching exact licences

The precise way to watch a supplier. licenseState accepts US-OH or OH; licenseNumber must be exactly as FDA publishes it.

{
"targets": [
{"licenseState": "US-OH", "licenseNumber": "WHO.0008724", "reference": "ohio"},
{"licenseState": "US-CO", "licenseNumber": "TPLP.0000067", "reference": "colorado"}
],
"monitorKey": "pharma-suppliers-prod"
}

Case and surrounding whitespace are normalized. Punctuation is not stripped, because WHO.0008724 and WHO0008724 are different licences.

One thing the live data will surprise you with: a licence number is not globally unique. 182 (state, number) pairs in the current file are reported under more than one facility name, and 227 span both WDD and 3PL. A licence target returns all of its rows and tells you how many distinct facilities that is, in matched_facility_count. It never picks one for you.

Watching facility names

Use this when you have a name but no licence number.

{
"targets": [
{"facilityName": "AdiraMedica LLC", "reference": "adira"},
{"facilityName": "Frontier Scientific Solutions, LLC", "facilityState": "NC", "reference": "frontier"}
],
"monitorKey": "pharma-suppliers-prod"
}

Names match exactly after Unicode, case, punctuation and whitespace normalization. There is no fuzzy matching and no entity resolution, so AdiraMedical LLC will not match AdiraMedica LLC; it returns NOT_FOUND.

If one name is reported by several distinct facilities, the result is AMBIGUOUS, not a guess. The row lists the candidate addresses so you can pick one, and ambiguous targets are never charged. In the current file, 1,281 of 1,463 distinct names resolve to exactly one facility; facilityState narrows most of the rest.

A facility with many state licences is not ambiguous. It returns one row per licence and is charged once.

Change monitoring

Reuse the same monitorKey on a schedule. Each run compares against the last successful observation for that key.

{
"targets": [{"licenseState": "US-OH", "licenseNumber": "WHO.0008724", "reference": "supplier-001"}],
"monitorKey": "pharma-suppliers-prod",
"mode": "changesOnly",
"firstRunBehavior": "emitCurrent"
}
Change typeMeans
REPORTING_RECORD_ADDEDA reporting row appeared that was not in the previous validated snapshot
REPORTING_RECORD_REMOVEDA reporting row that was in the previous validated snapshot is absent from the current one
LICENSE_EXPIRATION_CHANGEDThe reported expiration date on an existing row changed
DBA_CHANGEDThe reported doing-business-as names changed
REPORTED_LICENSE_EXPIRATION_REACHEDA reported expiration date moved into the past since your last run
REPORTING_YEAR_CHANGEDThe latest reporting year for this target advanced
FACILITY_NAME_CHANGEDThe facility name reported under this target changed
FACILITY_ADDRESS_CHANGEDThe reported facility address changed
FACILITY_TYPE_CHANGEDThe facility is now reported under a different type
BECAME_FOUND / BECAME_NOT_FOUNDThe target appeared in, or disappeared from, the reporting database

Read REPORTING_RECORD_REMOVED literally. It means the row was in the previous successful FDA snapshot and is not in the current successfully validated one. It does not mean the licence was revoked, that the facility is unlicensed or non-compliant, or that it is no longer an Authorized Trading Partner. Likewise REPORTED_LICENSE_EXPIRATION_REACHED is arithmetic on a date the facility reported to FDA; it does not replace a state-board licence check.

There is no disciplinary-action change type, because FDA publishes no disciplinary-action field in this database, in either the bulk file or the search UI. The Actor does not invent one.

Schedules, webhooks, API and MCP

Schedule. Create an Apify schedule with cron 0 6 * * 1-5 (weekdays 06:00 UTC) so it runs shortly after FDA's business-day update, on a Task holding your watchlist and monitorKey. In changesOnly mode a quiet day writes no rows, which is a successful run, not a failure.

Webhook. Point a ACTOR.RUN.SUCCEEDED webhook at your endpoint and read the changes view; every row is already an alert payload with stable ids, previous and current values, and the snapshot date it came from.

https://api.apify.com/v2/datasets/{datasetId}/items?view=changes&clean=true

API. Run it synchronously and read the Dataset:

curl -X POST "https://api.apify.com/v2/acts/automa-flow~fda-wdd-3pl-reporting-monitor/run-sync-get-dataset-items?token=$APIFY_TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"targets": [{"licenseState": "US-OH", "licenseNumber": "WHO.0008724", "reference": "supplier-001"}],
"monitorKey": "pharma-suppliers-prod",
"mode": "snapshotAndChanges"
}'

MCP. https://mcp.apify.com?tools=automa-flow/fda-wdd-3pl-reporting-monitor

Pricing

Pay per event:

EventPriceWhen
monitor-run$0.025Once per run, after the FDA bulk file downloads and passes validation
target-checked$0.010Once per unique target resolved conclusively: MATCHED or NOT_FOUND

You are charged for the check, not for the row. A target that matches 40 state licences costs the same as one that matches one, and an unchanged recheck that publishes nothing is still one completed check.

Never charged: ambiguous names, invalid targets, duplicate targets, retries, source failures, rejected snapshots, and targets beyond the run's charge limit.

WatchlistWeeklyMonthly
100 suppliers$1.025~$4.10
1,000 suppliers$10.025~$40.10
10,000 suppliers$100.025~$400.10

Measured locally on 2026-09-07 by replaying a saved full snapshot (38,030 rows): 1.79 s to parse and 0.44 s to match 10,000 targets, with 33.1 MB of peak traced Python allocations. This is not total process memory. Replay makes no HTTP requests; normal execution downloads the FDA file once. See examples/benchmark.json. These measurements exclude Actor boot, storage and monitor-lock API calls. Five hosted checks on 2026-09-07 completed in 5.69–17.74 seconds. Platform usage was $0.00054 for one target and $0.00210 for 100 targets. These are measured validation workloads, not guarantees for every input; final usage settles after a run and should be read from the Apify run API.

Source freshness

Every row carries source_snapshot_date, taken from the bulk file's Last-Modified header, so you always know which FDA publication an observation came from. FDA updates the file every business day. The run summary warns with SNAPSHOT_STALE if the file is more than seven days old, without treating age alone as a withdrawal. A publication older than the last accepted Last-Modified is rejected with SNAPSHOT_DATE_REGRESSED, including regressions within the same day. A missing or invalid timestamp cannot replace a previously dated snapshot.

Comparison scope and overlapping runs

Each monitorKey and normalized facilityTypes selection has its own baseline. Omitting facilityTypes or passing [] selects both types. Changing a filter starts that filter's baseline; returning to a previously used filter resumes its history. Configuration changes therefore do not produce supplier removals.

The corrected version uses a new state namespace because earlier state did not record the matching filter. The first run after upgrading creates a baseline according to firstRunBehavior; old state is retained but is not compared.

Runs sharing a comparison scope execute exclusively. A concurrent attempt returns MONITOR_BUSY without reading shared state, downloading FDA data or charging events. Retry after the active run finishes. In Apify, this uses one unhandled semaphore request in a named request queue and the platform's atomic request lock. The semaphore URL is never fetched. Local runs use an OS file lock in their configured storage directory.

Work inside the lock has a 15-minute deadline, inside a one-hour cloud lease. Normal completion releases the lock; after a crash or a failed unlock, another run may need to wait until the lease expires. Do not manually clear a lock while its owner is running. Lock API failure aborts execution without an unlocked fallback. Interrupted delivery keeps the existing checkpoint safeguards. Cloud coordination adds four unbilled queue API calls per completed run; these are separate from the one FDA download and are excluded from the local benchmark.

Failure semantics

NOT_FOUND is never SOURCE_FAILED. A target is reported NOT_FOUND only when the FDA file downloaded, decoded, matched the expected 13-column schema, cleared the row-count and licence-coverage floors, ended with a complete line terminator, and was matched in full.

Anything else is a FAILED row with a machine-readable reason and a failed run: timeouts, 429/5xx after the retry budget, an HTML page served instead of data, a truncated download, missing or renamed required columns, an implausible row-count collapse, an unknown facility type, conflicting values for one record identity, an older publication, or a row the schema cannot interpret. Identical semantic duplicate rows are collapsed before matching; conflicting duplicates are rejected regardless of their order in the file.

When the source fails:

  • no target is reported as absent;
  • no change event is produced;
  • the last good state for every target is left untouched;
  • nothing is charged.

A truncated or collapsed snapshot therefore cannot produce thousands of false REPORTING_RECORD_REMOVED events. That is the guarantee this Actor is built around, and it is covered by tests.

Privacy and the fields you get

FDA publishes a facility contact name, phone number and email on every row. This Actor does not need them to answer "did this supplier's reported licensing information change", so they are dropped at parse time: never stored, never published, never logged, and replaced with placeholders in the test fixtures.

You get: facility name, DBA names, facility type, licence number, licence state, reported licence expiration, reporting year, and the facility's public business address. Street is null for about 9% of live rows, where FDA publishes none.

Limitations

  • Not an ATP verifier. FDA tells you to check both applicable state licensure and this reporting database. This Actor does one of those two. It does not join state-board licence sources, and it does not handle ATP digital credentials.
  • Reported, not verified. Every value is what the facility reported to FDA. A reported expiration date is not a state board's current licence status, and the source contains placeholder dates such as 1901-01-01 and 2049-12-31.
  • A submission log, not a registry. Rows are per reporting year, so a facility that filed in 2025 and again in 2026 has rows for both.
  • Exact matching only. No fuzzy matching, no entity resolution, no aliases beyond FDA's own DBA field. A misspelled name returns NOT_FOUND.
  • US only, and only the two facility types FDA publishes here: WDD and 3PL.
  • Watchlist size. Up to 10,000 targets per run. Monitoring state is stored per monitorKey; a watchlist whose stored state grows beyond the per-key limit fails loudly with STATE_TOO_LARGE rather than silently losing history. Split it across several monitorKey values if you hit that.
  • Old targets you stop watching keep their stored state under that monitorKey. Use a fresh key to start clean.

Technical source details

Source filehttps://www.accessdata.fda.gov/cder/wdd_3pl_facilities_report.txt
FormatTab-separated, CRLF, Windows-1252, 13 columns, no quoting
Size6.2 MB raw, 536 KB gzipped, 38,030 rows (2026-09-06)
Requests per run1
AuthenticationNone
ProxyNone
BrowserNone
Update cadenceEvery business day

Record identity is a SHA-256 over facility type, licence state, licence number, facility name, street, city, facility state and reporting year. Those eight fields were verified unique across all 38,030 live rows; the three-field identity that looks natural (licence state, licence number, type) collides on 290 keys. Row position is never used as identity.

Every row is an append-only observation with scraped_at, schema_version and a fingerprint over the normalized reported fields only, so rows captured months apart stay comparable.

Support

Open an issue on the Actor's Apify page. For a source-caused bug, include the RUN_SUMMARY record from the run's key-value store: it carries the snapshot date, row count, schema signature, file hash and any warnings.

Saved workflow examples

Each example has a $0.10 event-charge cap, 512 MB memory and a five-minute timeout. They create no schedule automatically. Lookup examples publish current records on repeat runs; the monitor emits the first baseline and later changes or unresolved targets. A verified unchanged monitor can have an empty Dataset; check RUN_SUMMARY for completed and charged checks.