Quick answer
openFDA's docs tell you to join query terms with +AND+. Do that from any normal HTTP client and you get HTTP 500, because the transport layer percent-encodes your + into %2B and the API has no idea what you meant. The fix is to join with a plain space and let the encoder do its job.
That is one of three traps we had already paid for on this API family before building the openFDA Drug Labels Scraper. Here they all are, so you can skip the tuition.
Trap 1 — the + that becomes %2B ➕
openFDA's search syntax genuinely uses +AND+ in the documented URL examples. But those examples are already-encoded URLs. If you pass field:value+AND+other:value as a query parameter to requests, curl-cffi, httpx or anything else, the encoder correctly escapes + to %2B — and the server now sees a literal plus sign inside your search term.
search=openfda.brand_name:"ibuprofen"+AND+... -> %2BAND%2B -> HTTP 500
search=openfda.brand_name:"ibuprofen" AND ... -> +AND+ -> HTTP 200
A space is what you want, because a space is what encodes to +.
Trap 2 — a prefill that ANDs itself into nothing 🕳️
This one cost us a failed cloud smoke test on a sibling Actor over openFDA's device endpoint. The scaffolding had helpfully given every filter a default value, so the demo run asked for: this applicant, AND this device name, AND this product code, AND this advisory committee, AND this device class, AND this date range.
That query is perfectly valid. It matches nothing. The run finished SUCCEEDED with zero rows, which every dashboard reads as 100% healthy.
The lesson generalises well past openFDA: a demo input is a product surface, not a form. Its job is to return data on the first click. Prefill one realistic filter, then check that exact payload against the live API — a schema validator will confirm your payload is well-formed and can never tell you it matches nothing.
Trap 3 — the pagination ceiling nobody mentions 🧱
limit maxes at 1000 and skip maxes at 25,000. Past that the API stops, and if your paginator assumes it can walk forever it will simply stop returning rows without saying why. We verified the ceiling directly against drug/label.json rather than inheriting the number from the sibling endpoint — worth doing, because these endpoints do not all behave identically.
Beyond 25,000 you narrow the query or window it by effective_time. There is no deeper offset to reach for.
Why SPL labels, when you already have adverse events 💊
Structured Product Labeling is the FDA's record of what a drug's label actually says — the legal text on the box and the insert:
- active ingredient and strength, purpose, indications and usage
- warnings, "do not use", "ask a doctor", "stop use", pregnancy/breastfeeding
- dosage and administration, storage, inactive ingredients
- brand name, generic name, manufacturer, NDC product code, application number, route, product type
That is a different corpus from adverse-event reports (what happened after someone took it) and different again from device clearances or recalls. Same API family, no overlap in output rows.
The egress decision we made on your behalf 💸
Full SPL documents are big. Some labels run to tens of kilobytes of legal prose, and if you are pulling thousands of them you are paying — in time and in bandwidth — for text you may never read.
So field selection is a first-class input, and the default is lean: identity and openFDA metadata always, content sections only when you ask for them. Ask for warnings and you get warnings; don't, and you don't carry them.
This is not an abstract concern for us. External data transfer is roughly two-thirds of what our platform bill actually consists of, which makes "request only what you need" a design constraint rather than a nicety.
Nullable everywhere, on purpose 🕳️
Almost every SPL section is optional. A CVS store-brand ibuprofen label might carry active_ingredient, purpose, indications_and_usage and warnings and simply not have ask_doctor_or_pharmacist at all.
If your dataset schema declares those fields as bare strings, the first sparse record kills the write with a schema validation error — and a fully-populated test sample hides the bug perfectly. Every optional field here is nullable, and the row shape never changes between records: missing means null, never a missing key.
Who this is for
-
Regulatory affairs and pharmacovigilance — track label changes by
versionandeffective_time. - Pharmacy and formulary tooling — ingredient, route and NDC data as a table.
- Health-tech and research — a keyless corpus of what manufacturers legally claim.
Pricing
$0.20 per run plus $0.003 per label — $3.20 per 1,000. Pay for rows that land.
→ openFDA Drug Labels Scraper on Apify
Built by Devil Scrapes. We handle the encoding quirks, the pagination ceilings and the fields that are legitimately missing, so you get a clean table instead of a 500.
Top comments (0)