# FDA Drug Labels Scraper (openFDA SPL Data) (`steadfetch/openfda-drug-labels-extractor`) Actor

Extract US drug label records (brand, generic, manufacturer, route, ingredients, indications, dosage, warnings) from the official openFDA SPL API. Filter by date, keyword or manufacturer. For pharmacovigilance, drug data pipelines and formulary research. Schema-validated JSON; no browser, no login.

- **URL**: https://apify.com/steadfetch/openfda-drug-labels-extractor.md
- **Developed by:** [Steadfetch](https://apify.com/steadfetch) (community)
- **Categories:** Business, Other
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.00 / 1,000 drug label delivereds

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

## steadfetch/openfda-drug-labels-extractor

Extract structured US drug label records from the official [openFDA drug label API](https://api.fda.gov/drug/label.json). Use native openFDA search syntax or an effective date, and get validated JSON with label ids, versions, brand and generic names, manufacturers, indications and warnings. No browser, no login, no CAPTCHA.

### What it does

The Actor calls the official, keyless openFDA API and returns one dataset item per normalized drug label (SPL document).

- **Native openFDA search:** pass any query supported by the API, for example `openfda.brand_name:"*aspirin*"` or `openfda.generic_name:"SILICEA"`.
- **Effective date filter:** `effective_since` keeps labels whose `effective_time` is on or after the given ISO date. The source metadata field `meta.results.last_updated` shows when openFDA last refreshed the dataset.
- **Pagination:** 1,000 records per request with `limit`/`skip`, newest effective date first, up to 5,000 results per run.
- **Billing-safe deduplication:** one item per `label_id`+`version` and only the first record of each `set_id` per run, so the same label version is never billed twice.
- **Schema validation:** every item is validated against `.actor/dataset_schema.json` before it reaches the dataset. Invalid items fail the run; nothing unvalidated is ever pushed.
- **Rate-limit friendly:** 429 responses are retried with exponential backoff. If the limit persists, the Actor stops pagination gracefully and returns the labels collected so far; a rate limit is never charged or reported as a run error.

openFDA is a public, keyless API released as public domain (CC0): see the [openFDA terms](https://open.fda.gov/terms/) and [license](https://open.fda.gov/license/). The drug label endpoint contains no personally identifiable information. This Actor maps product-level label metadata only: it never collects patient, prescriber or contact data, and it does not scrape the openFDA website.

`effective_time` is normalized from the openFDA `YYYYMMDD` value to ISO 8601 (`YYYY-MM-DD`, JSON Schema `date`). Multi-part source text is joined into one string; list fields stay arrays; unknown optional values are `null` or an empty array.

### Input

Minimal run, the fixture used during development:

```json
{
    "search": "openfda.generic_name:\"SILICEA\"",
    "maxResults": 10
}
```

Incremental run since a given effective date:

```json
{
    "effective_since": "2026-01-01",
    "maxResults": 1000
}
```

| Field | Type | Description |
| --- | --- | --- |
| `search` | string | Optional openFDA search syntax. Example: `openfda.brand_name:"*aspirin*"`. See the [search parameter reference](https://open.fda.gov/apis/drug/label/search-parameters/). |
| `effective_since` | string (`YYYY-MM-DD`) | Optional lower bound on `effective_time`. |
| `maxResults` | integer | Default 100, maximum 5000. Every delivered label is one billable event. |

All fields are optional. With no filters the Actor pages through all labels, newest effective date first.

### Example output

```json
{
    "url": "https://api.fda.gov/drug/label.json?search=id:%22ca7bbcc8-2354-375c-e053-2995a90a72a0%22",
    "title": "SILICEA",
    "scrapedAt": "2026-10-20T13:00:00.000Z",
    "label_id": "ca7bbcc8-2354-375c-e053-2995a90a72a0",
    "set_id": "0000025c-6dbf-4af7-a741-5cbacaed519a",
    "version": "2",
    "effective_time": "2021-09-02",
    "brand_names": ["SILICEA"],
    "generic_names": ["SILICEA"],
    "manufacturers": ["Rxhomeo Private Limited d.b.a. Rxhomeo, Inc"],
    "indications": "INDICATIONS Condition listed above or as directed by the physician",
    "warnings": "WARNINGS This product is to be used for self-limiting conditions If symptoms do not improve in 4 days, or worsen, discontinue use and seek assistance of a health care professional."
}
```

The `warnings` text above is shortened for readability. The committed fixture in `test/fixtures/valid-items.json` is the exact, complete output for the real records in `test/fixtures/search-silicea.json`.

### Output fields

| Field | Type | Notes |
| --- | --- | --- |
| `label_id` | string | openFDA label id (SPL document id). Required. Deduplication key together with `version`. |
| `set_id` | string | openFDA set id shared by all versions of the same label. Required. Only the first record of each set_id is delivered per run. |
| `version` | string or null | Label version as recorded by openFDA. |
| `effective_time` | string (`YYYY-MM-DD`) | Label effective date, normalized from openFDA `YYYYMMDD`. Required. |
| `brand_names` | array of strings | Brand names from the label's openFDA block. |
| `generic_names` | array of strings | Generic names from the label's openFDA block. |
| `manufacturers` | array of strings | Labeler/manufacturer names. |
| `indications` | string | Indications and usage text, joined into one string. Empty when the source has none. |
| `warnings` | string | Warnings text, joined into one string. Empty when the source has none. |
| `url` | string | Official openFDA API URL for this label record (Steadfetch common envelope). |
| `title` | string or null | First brand name, otherwise the first generic name. |
| `scrapedAt` | string | ISO 8601 UTC timestamp of the extraction. |

### Billing and deduplication

The Actor uses Apify pay-per-event pricing with the two synthetic platform events:

| Event | Charged when | Price |
| --- | --- | --- |
| `apify-actor-start` | a run starts (synthetic, automatic) | 0.00005 USD per run (Apify-recommended default; includes 5 seconds of free compute) |
| `apify-default-dataset-item` | a normalized, schema-validated label is saved to the dataset (synthetic, automatic) | 0.003 USD per label = **3.00 USD per 1,000 labels** |

There is no custom per-item event and no `Actor.charge()` call: the platform already charges `apify-default-dataset-item` per pushed item, so adding another event would bill users twice.

Duplicate handling follows the billing contract:

- **Same label version:** records with the same `label_id`+`version` are emitted once per run.
- **Same label, multiple versions:** only the first record of each `set_id` is emitted per run. The API is queried newest first, so the newest version is the one delivered; older versions are not billed again.
- **Never charged:** errors, retries, 429 rate limits, empty pages, malformed records and duplicates do not push an item.

### Pricing

The result price is **3.00 USD per 1,000 labels** (`apify-default-dataset-item` at 0.003 USD). It sits inside the direct-competitor band for openFDA drug label extractors on the Apify Store (2-50 USD per 1,000) and inside Steadfetch's 1-10 USD per 1,000 band.

**Measured cost:** a local benchmark returned 5,000 labels in 6.5 seconds using 5 API requests, with 188 MB peak RSS and a 256 MB memory setting. At Apify's 0.20 USD per compute unit (1 CU = 1 GB of RAM for 1 hour), that run costs about 0.00009 USD, or roughly 0.00002 USD per 1,000 labels. Adding the run-start event, cost stays below 0.0001 USD per 1,000 labels; even with 10x platform overhead it remains below 0.001 USD per 1,000, far under the 0.15 USD per 1,000 estimate in the spec. No paid proxy is used. Margin is above 99 %.

**Works on the Apify free plan** — about 1,666 drug labels for the $5 monthly credit ((5.00 USD − 0.0002 USD run-start at the default 4 GB memory) ÷ 0.003 USD per label).

### Development

```bash
npm install
npm test                # lint + typecheck + offline Vitest suites (no credit)
npm run publish:check   # Steadfetch publish gate
npm run build && npm run verify:build
npm run schemas:validate
npm run smoke           # local Apify run (no platform credit)
```

Fixtures are real openFDA responses saved under `test/fixtures/`. The default test suite never touches the network and never spends Apify credit. `npm run smoke` executes the Actor locally; a platform run is only needed once, with the minimal input above.

### Project structure

```
.actor/actor.json           Actor definition (payPerEvent, dataset schema, output schema)
.actor/input_schema.json    INPUT: search, effective_since, maxResults
.actor/dataset_schema.json  Item JSON Schema (fields) + table view for the Store
.actor/pay_per_event.json   PPE pricing (two synthetic platform events)
src/fetch.ts                openFDA client: query builder, pagination, retry/backoff
src/extract.ts              Pure normalization and deduplication (fixture-tested, no network)
src/main.ts                 Entry point: fetch, validate, push
src/validate-output.ts      Ajv validator shared by the run and the tests
test/                       Offline fixtures and Vitest suites
```

### FAQ

**Do I need an API key?**
No. openFDA is keyless. There is no secret to configure and no key is accepted through the input.

**Does it need a browser?**
No. openFDA returns structured JSON, so plain HTTP is roughly 10x cheaper than a browser-based Actor.

**Does it return personal data?**
No. Only product-level label metadata is mapped. Patient, prescriber and contact fields are never read or emitted.

**What does `effective_since` do?**
It adds `effective_time:[YYYYMMDD TO 99991231]` to the openFDA query, so only labels effective on or after that date are returned. Use it for incremental extractions; `meta.results.last_updated` tells you when the source dataset was last refreshed.

**How does pagination work?**
The Actor requests 1,000 records per call with `limit`/`skip`, sorted by `effective_time:desc`, until `maxResults` is reached or the result set is exhausted.

**What happens with duplicate labels?**
The same `label_id`+`version` is emitted once per run, and only the first record of each `set_id` is kept. Duplicates are counted in the run log and are never charged.

**What happens when openFDA rate-limits the Actor?**
429 responses are retried with exponential backoff (honoring `Retry-After`). If the limit persists after all retries, pagination stops gracefully and the labels already collected are returned. No rate-limited request produces a billable item.

**What happens when openFDA has no matches?**
The API returns a 404 "No matches found"; the Actor treats it as an empty result set, not as a failure, and emits nothing.

**Is the output schema stable?**
Yes. New fields are additive and the required fields in `.actor/dataset_schema.json` do not change. `effective_time` is always an ISO 8601 date.

### Maintenance

This Actor is **tested daily**: automated runs query the official openFDA API with a minimal input and check that the output still matches the published schema. Failures are reported as issues in the Steadfetch Actors repository.

# Actor input Schema

## `search` (type: `string`):

Optional openFDA search syntax. Example: openfda.brand_name:"*aspirin*" or openfda.generic_name:"SILICEA". See https://open.fda.gov/apis/drug/label/search-parameters/ for the available fields.

## `effective_since` (type: `string`):

Optional. Keep labels whose effective_time is on or after this date (YYYY-MM-DD). Use it for incremental extractions; the source metadata field meta.results.last_updated shows when openFDA last refreshed the dataset.

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

Maximum number of normalized labels to return. Default 100 keeps the first run quick and cheap on the Apify free plan; maximum 5,000. Each delivered label is one billable event.

## Actor input object example

```json
{
  "search": "openfda.generic_name:\"SILICEA\"",
  "maxResults": 10
}
```

# Actor output Schema

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

Dataset items validated against the item schema in dataset_schema.json (fields).

# 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 = {
    "search": "openfda.generic_name:\"SILICEA\"",
    "maxResults": 10
};

// Run the Actor and wait for it to finish
const run = await client.actor("steadfetch/openfda-drug-labels-extractor").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 = {
    "search": "openfda.generic_name:\"SILICEA\"",
    "maxResults": 10,
}

# Run the Actor and wait for it to finish
run = client.actor("steadfetch/openfda-drug-labels-extractor").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 '{
  "search": "openfda.generic_name:\\"SILICEA\\"",
  "maxResults": 10
}' |
apify call steadfetch/openfda-drug-labels-extractor --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,steadfetch/openfda-drug-labels-extractor"
        }
    }
}
```

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/cIgpZrtXq0RnvDRSc/builds/G86V8FCoxALKRvfS8/openapi.json
