What this guide covers
What openFDA is (and the device endpoints)
openFDA is the FDA's public API programme. It exposes several device datasets as JSON over HTTPS, no authentication required to start. The one that maps to MAUDE is device/event; the others are useful for cross-referencing:
| Endpoint | Dataset |
|---|---|
/device/event.json | MAUDE adverse-event reports (MDRs) |
/device/recall.json | Device recalls |
/device/enforcement.json | Recall enforcement reports |
/device/510k.json | 510(k) clearances |
/device/classification.json | Product-code classifications |
/device/udi.json | GUDID / UDI device records |
/device/registrationlisting.json | Establishment registrations & listings |
For the rest of this guide we focus on device/event, but the syntax below is identical across all of them.
Anatomy of a request
Every request is a single GET URL. The base is:
https://api.fda.gov/device/event.json
You shape it with four query parameters:
search— the query (field:value expressions). Omit it to match everything.count— return aggregated counts of a field instead of raw records.limit— how many records to return (max 1000; default 1).skip— offset for pagination (max 25000).
A minimal real query — one manufacturer's reports, first record only:
https://api.fda.gov/device/event.json?search=
device.manufacturer_d_name:"ACME+MEDICAL"&limit=1
The response has two parts: a meta object (including meta.results.total — the full match count, even when you only pulled one record) and a results array. Reading meta.results.total first is how you size a query before paginating it.
Search syntax
The search value is a Lucene-style expression. The building blocks:
| Goal | Syntax |
|---|---|
| Field equals term | event_type:Malfunction |
| Exact phrase (spaces) | device.generic_name:"INFUSION+PUMP" |
| AND / OR | a:x+AND+b:y · a:x+OR+a:z |
| Grouping | event_type:(Death+OR+Injury) |
| Date / numeric range | date_received:[20250101+TO+20251231] |
| Field exists | _exists_:device.device_report_product_code |
| Wildcard | device.brand_name:acme* |
Two rules save a lot of confusion: spaces in a phrase become + (URL-encoding), and dates use YYYYMMDD with no separators. A query combining a manufacturer, an event type and a date window:
https://api.fda.gov/device/event.json?search=
device.manufacturer_d_name:"ACME+MEDICAL"
+AND+event_type:(Death+OR+Injury)
+AND+date_received:[20250101+TO+20251231]&limit=100
The fields that matter
MAUDE records are deeply nested. The fields you'll actually query for device monitoring:
| Field | What it is |
|---|---|
device.brand_name | Trade name on the report |
device.generic_name | Generic/common device name |
device.manufacturer_d_name | Manufacturer as reported |
device.device_report_product_code | Three-letter FDA product code — the most reliable axis |
event_type | Death · Injury · Malfunction · (No answer/Other) |
date_received | Date FDA received the report |
date_of_event | Date of the incident (often blank) |
report_number | Report identifier (used to de-duplicate) |
product_problems | Coded problem descriptions |
device.openfda.* | Harmonised fields: device_class, regulation_number, medical_specialty_description |
The device.openfda object is worth knowing — it enriches each record with the harmonised classification data, so you can filter by device class or medical specialty without a second lookup.
Counting & aggregation (the .exact rule)
The count parameter turns the API into an analytics engine — instead of records, you get the frequency of each value of a field. This is how you get a breakdown by event type, or the top product codes for a manufacturer, in a single call. But there's a rule that catches nearly everyone:
.exact when counting text fields. count=event_type returns a SERVER_ERROR ("Text fields are not optimised for … aggregations"). You must write count=event_type.exact. The .exact suffix counts the whole field value verbatim rather than tokenised words — which is also what you want, or "infusion pump" would be counted as "infusion" + "pump" separately.Working count queries:
# Breakdown of a manufacturer's reports by event type
...?search=device.manufacturer_d_name:Medtronic&count=event_type.exact
# Top product codes a manufacturer is reported under
...?search=device.manufacturer_d_name:Medtronic
&count=device.device_report_product_code.exact
# Device-class split (openfda numeric field — no .exact needed)
...?search=device.manufacturer_d_name:Medtronic&count=device.openfda.device_class
Note the last one: device.openfda.device_class is not a free-text field, so it counts without .exact. Rule of thumb: text → .exact; keyword/numeric → plain.
Pagination & limits
Two hard ceilings shape how you pull large result sets:
limitmaxes at 1000 per request. Ask for more and you getBAD_REQUEST.skipmaxes at 25000. Soskip-based paging can only reach the first 25,000 records of a result set (26,000 total with a finallimit=1000page).
For result sets larger than that — and MAUDE queries routinely are; a broad event_type:Malfunction search returns millions — you have two options:
search_after— openFDA's cursor pagination. Each response'smetagives you a token to pass assearch_afteron the next call, walking past the 25k wall.- Date-window chunking — split the query into monthly or quarterly
date_receivedranges, each small enough to page normally. This is often simpler for scheduled monitoring, since you're pulling "what's new since last run" anyway.
meta.results.total first. Under ~26,000 → page with skip. Over it → chunk by date_received, or use search_after.Rate limits & API keys
Without a key, openFDA allows 1,000 requests/day and 240/minute per IP. A free API key (register at open.fda.gov) raises the daily limit to 120,000 requests/day, same 240/minute. For anything scheduled, get the key — then append it:
...?search=event_type:Malfunction&limit=1000&api_key=YOUR_KEY
Keep the key server-side (it's tied to your rate budget, and a key in front-end code is a key anyone can spend). Handle 429 Too Many Requests with backoff, and cache aggressively — the data only refreshes periodically, so re-pulling the same window minutes later is wasted budget.
Gotchas that waste an afternoon
- Filter on
date_received, notdate_of_event. The event date is frequently blank, so date-of-event ranges silently drop most records. Received-date is populated and is what you want for "new reports" monitoring anyway. .exactfor counting/sorting text fields — as above, the number-one source ofSERVER_ERROR.- De-duplicate by
report_number. One event generates an initial report plus follow-ups (supplements); count events, not rows, by grouping on the report number. - No native model-number field. There's no clean "model" query — filter by manufacturer and product code, then narrow in your own output.
- Redactions. Narrative text may contain
(b)(4)/(b)(6)where content was withheld — expect and handle it. - Freshness lag. The dataset refreshes on the FDA's schedule (roughly weekly), and manufacturer MDRs arrive on reporting timelines, so the most recent weeks are always incomplete.
These are API mechanics. The deeper interpretive limits of MAUDE — that it can't prove causation or support rate calculations — are covered in our guide to searching and monitoring the MAUDE database, which this article is the technical companion to.
A worked monitoring query
Putting it together — a weekly job watching a product code for new death/injury reports would, each run: read the total for the new window, then page it.
# 1. How many new serious reports this week for product code DZE?
https://api.fda.gov/device/event.json?search=
device.device_report_product_code:DZE
+AND+event_type:(Death+OR+Injury)
+AND+date_received:[20260717+TO+20260724]
&limit=1&api_key=YOUR_KEY
# -> read meta.results.total
# 2. Pull them (page with skip if total > 1000)
...&limit=1000&skip=0
...&limit=1000&skip=1000 # next page, etc.
De-duplicate the results by report_number, triage by event_type, read the narrative for context, and log what you found. Run it on a schedule keyed to date_received and you have a monitoring loop.
That's one product code, one endpoint, one regulator. The engineering reality of doing it for a whole portfolio — dozens of codes and brand-name variants, de-duplicated, cross-referenced against your devices, and reconciled with the other five regulators who don't offer anything as clean as openFDA — is where a hand-rolled script stops scaling.
The openFDA API is one source of six
MedFortis runs this pipeline continuously — openFDA plus MHRA, Health Canada, EU Safety Gate, Swissmedic and EUDAMED — cross-referenced to your device lines, de-duplicated and logged. See it on your own devices with a free Gap Scan™.
Request your free Gap Scan →Key takeaways
- MAUDE lives at
api.fda.gov/device/event.json; the same syntax covers recalls, 510(k)s, classification and UDI. - Read
meta.results.totalbefore paginating;limitcaps at 1000,skipat 25000 — beyond that, usesearch_afteror chunk bydate_received. - Count text fields with
.exact(keyword/numeric fields don't need it) — the top source of errors. - Filter on
date_receivednotdate_of_event, de-duplicate byreport_number, and get a free API key for scheduled use (120k/day).