# faers.mobi signals API For LLM / machine clients. Human UI is https://faers.mobi/. OpenAPI 3 schema: https://faers.mobi/openapi.json Machine index: https://faers.mobi/llms.txt FAERS disproportionality is a **hypothesis**, not causation. Always read `n`, `eb05`, and `trend`. ## MCP Same tools over this REST API. No new statistics. Paste-ready: https://faers.mobi/mcp.md - Hosted Streamable HTTP: https://faers.mobi/mcp - Stdio server: https://faers.mobi/mcp/server.py - Stdio config: https://faers.mobi/mcp/claude_desktop.example.json | tool | REST | |------|------| | `search_drug` | `GET /signals?drug=` | | `search_event` | `GET /signals?event=` | | `get_signal` | `GET /signals?drug=&event=` | | `get_signal_history` | `…&format=series` | | `get_profile` | `…&format=profile` | | `compare_class` | `…&format=class` | | `find_novel_signals` | `novel=` + optional `trend=` / thresholds | | `explain_signal` | templates live JSON (no new numbers) | | `rwe_brief` | `…&format=brief` | | `get_label_status` | `…&format=label` | Claude Desktop / Cursor — hosted URL, or download `server.py` and merge the example JSON (set `args` to the absolute path you saved): ```json { "mcpServers": { "faers-mobi": { "url": "https://faers.mobi/mcp" } } } ``` ```json { "mcpServers": { "faers-mobi": { "command": "python3", "args": ["/path/to/server.py"], "env": { "FAERS_MOBI_API": "https://faers.mobi" } } } } ``` Smoke: ``` python3 mcp/server.py --call get_signal drug=tzield event=Nausea python3 mcp/server.py --call compare_class drug=semaglutide event=Nausea python3 mcp/server.py --call explain_signal drug=tzield event=Nausea python3 mcp/server.py --call rwe_brief drug=tzield event=Nausea python3 mcp/server.py --call rwe_brief drug=tzield curl -sS 'https://faers.mobi/signals?drug=tzield&event=Nausea&format=brief' curl -sS 'https://faers.mobi/openapi.json' ``` ## ChatGPT Actions / Custom GPT Paste-ready import: https://faers.mobi/chatgpt.md 1. Create a GPT → Configure → Actions → Import from URL 2. Schema: `https://faers.mobi/openapi.json` (auth: none) 3. Paste the Instructions block from `/chatgpt.md` (when to use `series` / `profile` / `class` / `brief`; always hypothesis, not causation) No new compute. Same public `/signals` as curl / MCP. Cheap JSON/csv/label is a **separate worker** from `format=brief` / `pdf` / `profile` / `class` / `series`. A brief crawler must not leave `GET /signals?drug=&event=` at 0 bytes. Heavy formats may 503/504 under load; retry. No new query parameters beyond `indication=hide` / `low_info=hide` (added 2026-09-17; opt-in, default order unchanged). ## explain_signal (client template) No new `/signals` format and no new EB05 math. Template live JSON (`n`, `eb05`, `trend`, `novel`; optional `format=class` `class_wide_hint`) into 3–6 sentences. Always end with the hypothesis disclaimer. Prompt / helper contract: 1. Isolated pair (drug × event; fuzzy near-matches omitted). 2. n observed in flagged quarters; peak EWMA EB05 (max of the smoothed quarterly EB05 series; EB05 = 5th percentile of the dual-Gamma GPS/EBGM posterior). The brief also names the peak quarterly EB05. 3. Trend (last-4 vs prior-4 EWMA EB05) and Novel vs cached FDA label. 4. If class payload present: class-wide vs drug-specific hint. 5. **This is a statistical hypothesis from spontaneous reports, not evidence that the drug causes the event.** R: `.explain_signal_text()` in `app/view/signal_timeline.R` (UI “Hypothesis” on the selected pair). Python: `explain_text()` / MCP tool `explain_signal` (templates GET `/signals` + optional `format=class`). ## `GET /signals?drug=` Isolated (drug, event) pairs for one brand or generic. Same ranking as the live search: exact / prefix / substring / same-substance family. Fuzzy near-matches (ziprasidone for a Tzield query) are **not** included. ``` GET https://faers.mobi/signals?drug=teplizumab GET https://faers.mobi/signals?drug=tzield ``` ### Response `200` with a JSON array. Empty array if nothing isolates. Each object: | field | meaning | |-------|---------| | `drug` | FAERS-reported drug string | | `event` | MedDRA PT | | `n` | summed `observed` over flagged quarters | | `eb05` | peak **EWMA** EB05: max of `ewma_eb05` over the pair's quarters (EB05 = 5th percentile of the dual-Gamma posterior). The raw quarterly peak is `max(eb05)` in `format=series` | | `novel` | `known` = the PT or one of its MedDRA synonyms was matched in the cached FDA label; `novel` = a real label is cached and nothing matched, i.e. **not matched in the cached label text**, NOT "previously unknown risk"; `?` = no usable cached label (no row, or a placeholder row with no text) | | `trend` | `Accelerating` / `Rising` / `Stable` / `Declining` / `New` from last-4 vs prior-4 mean EWMA EB05 | | `eb05_recent` | mean EWMA EB05 in the last up-to-4 quarters of the pair | | `eb05_prior` | mean EWMA EB05 in the preceding up-to-4 quarters | | `indication` | `true` when the PT reads as the drug's cached-label indication text. "Limitations of Use" spans and negated sentences are excluded, so a warning such as semaglutide × Pancreatitis or empagliflozin × Diabetic ketoacidosis is **not** flagged | | `low_info` | `true` when MedDRA marks the PT low-information (procedures, generic investigations) | | `reported_drug` | the raw string the FAERS report carried, unchanged | | `ingredient` | the active it resolves to; `a;b` for a combination; omitted when none could be identified | | `product_type` | `single`, `combination`, or `trial_uncertain` | | `medication_error` | `true` when the PT is a medication error, dose omission, or device/product-quality issue | `400` if neither `drug` nor `event` is present. When **both** `drug=` and `event=` are set, isolation is AND: drug names from the drug query (exact / prefix / substring / same-substance family) crossed with event names from the event query (exact / prefix / substring). Fuzzy near-matches are omitted on both sides. Empty if either side isolates to nothing. ``` GET https://faers.mobi/signals?drug=tzield&event=Nausea GET https://faers.mobi/signals?drug=teplizumab&event=rash ``` Spaces in `drug=` / `event=` may be written `%20` or `+`; both decode to a space. ### Optional filters | param | values | effect | |-------|--------|--------| | `trend=` | `Accelerating`, `Rising`, `New`, `Stable`, `Declining` (comma-separated) | keep those Trend labels | | `novel=` | `novel`, `known`, `?` (aliases: `undetermined`, `unknown`) | keep those Novel labels | | `indication=` | `hide` | drop indication-confounded rows (`indication: true`) | | `low_info=` | `hide` | drop MedDRA low-information rows (`low_info: true`) | | `product_type=` | `single`, `combination`, `trial_uncertain` (comma-separated) | keep only rows whose reported drug string names that kind of product | | `medication_error=` | `hide` | drop rows whose PT describes how the product was used or supplied rather than what it did | | `min_eb05=` | non-negative number | keep rows with `eb05` ≥ that value | | `min_n=` | non-negative number | keep rows with `n` ≥ that value | | `limit=` | 1–1000 (default 500) | page size | | `offset=` | ≥ 0 (default 0) | skip this many rows after filters | | `sort=` | `eb05`, `n`, `trend`, `novel` | rank the isolated set before paging | | `order=` | `desc` (default) or `asc` | sort direction (`trend` desc = Accelerating first) | | `format=series` | (needs both `drug=` and `event=`) | quarterly time-course for isolated pairs | | `format=profile` | (needs `drug=` or `event=`) | compact RWE snapshot for one isolated drug or event | | `format=class` | (needs `drug=`) | ATC4 peers for a drug or drug×event | | `format=brief` | (needs `drug=` or `event=`) | copyable RWE markdown (served as `text/plain`) | | `format=pdf` | (needs `drug=` or `event=`) | same brief as PDF (Typst) | An unrecognised `format=` value returns **400** with `error`, `allowed` and `usage`, rather than falling back to the pair list. `format=brief` is Markdown served as `text/plain` by design; that is not an error. **Ranking or "top signals" asks (LLM clients):** the default list keeps its documented order and may lead with an indication PT (tzield → Insulin therapy). Add `indication=hide&low_info=hide` (optionally `sort=eb05`), or use `format=profile` / `format=brief`, whose tops already exclude both. Never silently drop rows on the caller's behalf: the flags are on every row. ``` GET https://faers.mobi/signals?drug=tzield&indication=hide&low_info=hide&sort=eb05&order=desc&limit=5 ``` | `format=label` | (needs both `drug=` and `event=`) | cached FDA-label Novel status for that pair | ``` GET https://faers.mobi/signals?event=rash&trend=Accelerating GET https://faers.mobi/signals?event=rash&novel=novel GET https://faers.mobi/signals?event=rash&min_eb05=5&min_n=10 GET https://faers.mobi/signals?event=rash&limit=20 GET https://faers.mobi/signals?event=rash&min_eb05=5&limit=50&offset=50 GET https://faers.mobi/signals?event=rash&sort=n&order=desc&limit=20 GET https://faers.mobi/signals?drug=tzield&trend=Rising&novel=novel ``` Filters apply to the isolated set, then `limit`/`offset` page the result. `X-Total-Count` is the number of rows after filters (scan ceiling 5000). `X-Limit` and `X-Offset` echo the page. JSON body stays an array. `X-FAERS-Through` is the latest FAERS quarter in the live extract (max `quarter` in the signal store; no new compute). `format=profile` also has `faers_through`; `format=brief` prints `Data through YYYYQn`. Default rank is isolation priority then peak EWMA EB05. `trend` / `novel` on the JSON page are computed for the returned rows. `trend=` / `novel=` filters compute those columns on the scan set first (slower on broad events). ## CSV Same isolation as JSON. Header: `drug,event,n,eb05,novel,trend,eb05_recent,eb05_prior`. Empty isolation returns that header only (no splash dump). ``` GET https://faers.mobi/signals?drug=tzield&format=csv GET https://faers.mobi/signals?event=ischemic%20stroke&format=csv GET https://faers.mobi/signals?drug=tzield&event=Nausea&format=csv ``` `Accept: text/csv` also works. Filename is `signals-.csv`. The faers.mobi search UI “Download CSV” control hits this endpoint for the current query. It is hidden (message only) on the splash so the top-N list is not downloaded. ## `format=series` Quarterly time-course for an AND-isolated pair. Same isolation as `GET /signals?drug=&event=`. Requires both parameters (`400` if either is missing). JSON array of `{drug, event, points}` where each point is a flagged quarter: | field | meaning | |-------|---------| | `quarter` | YYYYQn | | `n` | `observed` that quarter | | `eb05`, `eb50`, `eb95` | GPS/EBGM interval (same as the UI plot) | | `ewma_eb05` | EWMA of EB05 (same series Trend uses) | ``` GET https://faers.mobi/signals?drug=tzield&event=Nausea&format=series ``` `X-Total-Count` is the number of isolated **pairs** (not quarters). Tzield × Nausea is one pair. Pagination / filters apply to pairs before the series is attached. The copyable RWE brief (UI textarea, MCP `rwe_brief`, and `format=brief`) folds a **compact** last-quarters stanza from this payload when a pair is selected — span, sum `n`, and the last up-to-4 quarters (`n` / `eb05` / `ewma_eb05`). Not a JSON dump. Drug-only `?drug=tzield` has no series stanza. The search UI also shows a quarterly **n / EB05 / EWMA** table for the selected pair (same live payload). Event-only search skips it. ## `format=brief` Same markdown as the UI copyable RWE brief and MCP `rwe_brief`. No new compute: stitches live `format=profile`, and when **drug and** `event=` are set `format=class` + compact `format=series` last-quarters + pair explain. `Content-Type: text/plain; charset=utf-8` (body is Markdown; served as text/plain so restricted LLM page readers accept it). Requires `drug=` **or** `event=` (`400` if neither). Event-only has no class/series (those need a drug). ``` GET https://faers.mobi/signals?drug=tzield&format=brief GET https://faers.mobi/signals?drug=tzield&event=Nausea&format=brief GET https://faers.mobi/signals?event=Nausea&format=brief ``` Drug-only has profile + disclaimer, no Series stanza. Pair includes Series last-quarters matching `format=series`. Event-only is the event profile (n_drugs / n_pairs, top drugs) + disclaimer. Always includes the hypothesis-not-causation disclaimer. Event search UI (`?event=` / `?q=` of an event) shows the same profile, copyable brief, and Download PDF. ## `format=pdf` Same content as `format=brief`, rendered with Typst (`application/pdf`). Requires `drug=` **or** `event=` (`400` if neither). UI **Download PDF** hits this for drug, pair, and event search. ``` GET https://faers.mobi/signals?drug=tzield&format=pdf GET https://faers.mobi/signals?drug=tzield&event=Nausea&format=pdf GET https://faers.mobi/signals?event=Nausea&format=pdf ``` Includes the hypothesis disclaimer and `Data through` quarter. ## `format=label` Cached FDA-label Novel status for one AND-isolated pair. Same `novel` value as the pair JSON (`known` / `novel` / `?`). **`novel` means the PT and its MedDRA synonyms were not matched in the cached label text, not that the risk is previously unknown**: a narrow PT can miss a broader warning (nilotinib × Acute myocardial infarction, whose label says "myocardial infarction" and "ischemic heart disease"). `label_status` says whether there was any text to match. No live OpenFDA scrape. Requires `drug=` **and** `event=` (`400` if either is missing). JSON object, not an array. | field | meaning | |-------|---------| | `novel` | `known` if the event string appears in the cached label; `novel` if a label is cached and it does not; `?` if no label is cached | | `label_status` | `cached` (matched against real label text), `empty` (a placeholder row with no text), `missing` (no row for this drug). `novel` is `?` unless this is `cached` | | `label_cached` | true only when `label_status` is `cached` | | `n`, `eb05` | same pair stats as `GET /signals?drug=&event=` | ``` GET https://faers.mobi/signals?drug=tzield&event=Nausea&format=label ``` MCP: `get_label_status`. ## `format=profile` One-shot snapshot for an isolated drug or event. Same isolation as `GET /signals?drug=` / `?event=`. Requires `drug=` **or** `event=` (`400` if neither). When both are set, isolation is AND and the snapshot is still the drug-focused pair set (unchanged). JSON object, not an array: | field | meaning | |-------|---------| | `query` | drug (or `drug + event`) | | `drugs` | isolated FAERS drug strings (no fuzzy near-matches) | | `n_pairs` | isolated pair count after min filters (scan ceiling 5000) | | `n_profiled` | pairs Trend/Novel were attached on (cap 500) | | `trend` | counts by Accelerating / Rising / New / Stable / Declining | | `novel` | counts by `novel` / `known` / `?` | | `top_eb05` | up to 10 pairs by peak EWMA EB05, excluding MedDRA low-information PTs and indication-confounded PTs (each row carries `low_info` / `indication` booleans; `excluded_from_tops` counts what was dropped) | | `top_n` | up to 10 pairs by n | | `novel_rising` | up to 20 novel + Accelerating/Rising pairs | ``` GET https://faers.mobi/signals?drug=tzield&format=profile GET https://faers.mobi/signals?drug=tzield&event=Nausea&format=profile GET https://faers.mobi/signals?event=Nausea&format=profile ``` Event-only: `query` is the event; `drugs` / `n_drugs` are isolated drugs that flag it; `top_eb05` / `top_n` / `novel_rising` are those pair rows (top drugs for the event). Event search UI, `format=brief`, and `format=pdf` use this snapshot. `X-Total-Count` is `n_pairs`. Pair list / CSV / `format=series` unchanged. ## `format=class` ATC4 chemical-subgroup peers from the existing DiAna class table. **Does not recompute** disproportionality. Requires `drug=` (`400` without it). With `event=`, peers are same-class drugs that also flag that isolated event. Without `event=`, peers are same-class drugs on the query drug’s top-10 events. `class_wide` is true when ≥5 class drugs flag the event (same cutoff as the UI class-effect filter). ``` GET https://faers.mobi/signals?drug=tzield&event=Nausea&format=class GET https://faers.mobi/signals?drug=tzield&format=class ``` JSON object: `substance`, `atc_code`, `atc_class4`, `class_wide`, `class_wide_hint`, `focal`, `peers` (drug, n, eb05, novel, trend, `indication`, `low_info` — the same booleans as list rows). `n_peer_drugs` counts flagging **drug strings**, `n_class_substances` counts distinct substances; the two differ because brands, generics and combination products are separate FAERS strings. A class with fewer than 3 substances gets `class_wide: null`, no `class_wide_hint`, and a `class_note` saying no class-wide comparison is possible. Isolation of the query drug still omits fuzzy near-matches (no ziprasidone on a Tzield query). Peers are class members, not isolation hits. ## `GET /signals?event=` Same isolation for an event term (spelling-normalized: `ischemic stroke` finds MedDRA `Ischaemic stroke`). Fuzzy near-match events are omitted. ``` GET https://faers.mobi/signals?event=ischaemic%20stroke GET https://faers.mobi/signals?event=ischemic%20stroke ``` ### Related UI - Search: `https://faers.mobi/?q=teplizumab` - Named-drug profile (same isolation): `https://faers.mobi/?drug=teplizumab` - Event search: `https://faers.mobi/?q=ischaemic%20stroke` or `https://faers.mobi/?event=Nausea` (event profile, copyable RWE brief, Download PDF; no Class / Series) - Pair: `https://faers.mobi/?drug=tzield&event=Nausea` (selected pair shows a quarterly n / EB05 / EWMA table from `format=series`) - Isolated search: type a drug, then **AND event** (e.g. Nausea). Event search + **AND drug** is the reverse. Same AND as `GET /signals?drug=&event=`. ## Ingredient grouping and product type `drug=` substring-matches the raw strings FAERS reports, so a search returns blinded-trial and compounded strings alongside the plain ingredient. Measured 2026-09-26: six of the top eight `semaglutide` rows by EB05 were `semaglutide b ml pds290` (a blinded trial), `methylcobalamin semaglutide` (compounded) or a pen string, every one of them `novel: "?"` because no FDA label is filed under the raw string. Every row now carries `reported_drug`, `ingredient` and `product_type`. `product_type=single` keeps only single-ingredient rows. It is an opt-in **view, not a merge**: unfiltered, every row is still returned and still carries its `product_type`, so nothing is hidden by default. Novelty is looked up on the **ingredient**, not the raw string, which is why a trial or compounded string no longer reports `novel: "?"` merely because the exact string has no label of its own. `format=label` still reports which label was actually matched. Counts and EB05 are per reported string and are **never summed or averaged across strings**. An ingredient-level recomputation from deduplicated cases is a separate offline step and is not part of this release. ## Medication errors and product issues `trulicity` Accidental underdose (n=8850) and Extra dose administered (n=11598) rank as signals while describing how the product was used, not what it did. Every row now carries `medication_error`, and `medication_error=hide` drops them. They stay in the unfiltered API. The flag is two MedDRA groups: the HLGT *Medication errors and other product use errors and issues*, and the whole *Product issues* SOC, which covers device and manufacturing terms. Together 1,652 of 82,124 PTs. Chosen by looking up where the named PTs actually sit, not by matching words on the PT text. ## What a brief discloses `format=brief` and `format=pdf` now carry a **Filters** section naming every filter applied and the number of rows each removed, so a small result can be told from a filtered one. Counts are over surviving rows, and are never summed or averaged across reported drug strings. ## Class peers are counted by ingredient `format=class` counts distinct **ingredients**, excluding the focal drug's own. The class-wide threshold scales with the class: half its substances, floored at 2 and **capped at 5**, so it never demands more than the old flat bar and relaxes only for small classes. A flat 5 was set when peers were counted as drug strings, where one substance could contribute several; counting ingredients made it far harder to reach and it began reading known class effects as drug-specific. Counting raw drug strings made `ozempic` and `rybelsus` peers of `semaglutide` -- one substance under three names, reported as three drugs flagging the event, which inflated the class-wide verdict for any drug sold under several names.