FDA Wholesale Drug Distributor & 3PL Reporting Monitor
Pricing
from $10.00 / 1,000 resolved target checks
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
Maintained by CommunityActor stats
0
Bookmarked
2
Total users
1
Monthly active users
a day ago
Last modified
Categories
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.
- Official search page: https://www.accessdata.fda.gov/scripts/cder/wdd3plreporting/
- About the programme: https://www.fda.gov/drugs/drug-supply-chain-security-act-dscsa/annual-reporting-prescription-drug-wholesale-distributors-and-third-party-logistics-providers
- FDA guidance on checking licensure: https://www.fda.gov/drugs/drug-supply-chain-integrity/check-licensure-wholesale-drug-distributors-and-third-party-logistics-providers
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 type | Means |
|---|---|
REPORTING_RECORD_ADDED | A reporting row appeared that was not in the previous validated snapshot |
REPORTING_RECORD_REMOVED | A reporting row that was in the previous validated snapshot is absent from the current one |
LICENSE_EXPIRATION_CHANGED | The reported expiration date on an existing row changed |
DBA_CHANGED | The reported doing-business-as names changed |
REPORTED_LICENSE_EXPIRATION_REACHED | A reported expiration date moved into the past since your last run |
REPORTING_YEAR_CHANGED | The latest reporting year for this target advanced |
FACILITY_NAME_CHANGED | The facility name reported under this target changed |
FACILITY_ADDRESS_CHANGED | The reported facility address changed |
FACILITY_TYPE_CHANGED | The facility is now reported under a different type |
BECAME_FOUND / BECAME_NOT_FOUND | The 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:
| Event | Price | When |
|---|---|---|
monitor-run | $0.025 | Once per run, after the FDA bulk file downloads and passes validation |
target-checked | $0.010 | Once 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.
| Watchlist | Weekly | Monthly |
|---|---|---|
| 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 withSTATE_TOO_LARGErather than silently losing history. Split it across severalmonitorKeyvalues 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 file | https://www.accessdata.fda.gov/cder/wdd_3pl_facilities_report.txt |
| Format | Tab-separated, CRLF, Windows-1252, 13 columns, no quoting |
| Size | 6.2 MB raw, 536 KB gzipped, 38,030 rows (2026-09-06) |
| Requests per run | 1 |
| Authentication | None |
| Proxy | None |
| Browser | None |
| Update cadence | Every 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
- Look up a WDD by license: replace the sample state and license number.
- Look up a 3PL by facility name: replace the exact reported facility name.
- Monitor supplier reporting changes: replace the watchlist and reuse its monitor key.
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.