# Check drug-drug interactions from the French ANSM thesaurus > Drug-drug interactions from the official French ANSM thesaurus, relayed verbatim — with an explicit coverage block, so "no interaction" never quietly means "not checked". Published 2026-08-10 · Updated 2026-08-15 · HTML version: https://invoket.com/blog/check-a-prescription-for-interactions-before-an-agent-acts --- An agent that is about to dispense, substitute, order or summarise a list of medications needs to know whether an authority has published an interaction between them — and it cannot get that from a language model's memory or from a drug label's prose. [`POST /medication/interactions`](/docs/api/medication-interactions) compares every pair in a list against the official **ANSM interactions thesaurus** and returns, per interacting pair, the constraint level, the management and the mechanism **exactly as the ANSM published them**. Just as importantly, it returns an explicit coverage block naming everything it could *not* compare. One x402 settlement per answer, no account. ## The authoritative source that went missing Until January 2024 there was an obvious answer to "which free API gives me structured drug-drug interactions": the US National Library of Medicine's RxNav Drug Interaction API. The NLM [discontinued it on 2 January 2024](https://lhncbc.nlm.nih.gov/RxNav/APIs/InteractionAPIs.html), along with RxNav's Interactions tab. The rest of the RxNav family — RxNorm, RxClass, RxTerms — kept running, so drug *normalisation* survived and drug *interaction* did not. What is left in the open is not equivalent: - **openFDA drug labels** expose a `drug_interactions` field, but that field is the label's own interactions **section** — one manufacturer's narrative about one product. It is not a comparable pair with a graded severity, and it does not tell you what happens between *your* two products. - **Drug ontologies** (RxNorm, ATC) normalise names. They assert nothing about what happens when two substances meet. The French **ANSM interactions thesaurus** (*Thésaurus des interactions médicamenteuses*) is a genuine authority-published list of interacting couples, each with a severity level and a prescribed management, produced by a dedicated working group. It is also published as a **PDF**, which is exactly why no one serves it as an API. ## One call, one prescription The endpoint takes 2 to 50 items and evaluates **every inter-item pair**. Each item carries exactly one key — a French `cis` code, a US `ndc`, an INN/DCI `dci` substance, or an ANSM `class` label — and is auto-typed by that key. ```http POST /medication/interactions Content-Type: application/json ``` ```json { "items": [ { "dci": "warfarine" }, { "dci": "amiodarone" }, { "dci": "simvastatine" }, { "dci": "millepertuis" } ] } ``` Four items make six pairs. Five of them are published couples: | Level | Pair | ANSM protagonists | |----------------------------|-------------------------------|-----------------------------------------------------| | `contre_indication` | warfarin × St John's wort | `ANTIVITAMINES K` × `MILLEPERTUIS` | | `association_deconseillee` | simvastatin × St John's wort | `SIMVASTATINE` × `MILLEPERTUIS` | | `precaution_emploi` | warfarin × amiodarone | `ANTIVITAMINES K` × `AMIODARONE` | | `precaution_emploi` | warfarin × simvastatin | `ANTIVITAMINES K` × `INHIBITEURS DE L'HMG-COA RÉDUCTASE (STATINES)` | | `precaution_emploi` | amiodarone × simvastatin | `AMIODARONE` × `SIMVASTATINE` | `interactions[]` is sorted by decreasing severity, so an agent that only has budget for one branch reads index `0`. The full entry carries the ANSM's own French text, untouched: ```json { "a": { "item": 0, "matched": "warfarin", "ansm_protagonist": "ANTIVITAMINES K" }, "b": { "item": 3, "matched": "millepertuis", "ansm_protagonist": "MILLEPERTUIS" }, "level": "contre_indication", "level_label": "contraindication", "management": "En cas d'association fortuite, ne pas interrompre brutalement la prise de millepertuis mais contrôler l'INR avant puis après l'arrêt du millepertuis.", "mechanism": "Diminution des concentrations plasmatiques de l'antivitamine K, en raison de son effet inducteur enzymatique, avec risque de baisse d'efficacité voire d'annulation de l'effet dont les conséquences peuvent être éventuellement graves (évènement thrombotique).", "source": "Thésaurus ANSM" } ``` Note the two protagonist fields, because they answer different questions. `matched` is the substance the service actually compared (`warfarin`); `ansm_protagonist` is the label the ANSM published — here the **class** `ANTIVITAMINES K`, of which warfarin is a member. The interaction is a class-level statement, and the response says so rather than pretending the agency named your molecule. The four levels are a closed set, each with an English `level_label`: `contre_indication` (contraindication), `association_deconseillee` (advised against), `precaution_emploi` (precaution for use), `a_prendre_en_compte` (to be taken into account). ## "No interaction" is firm only for the pairs actually compared This is the veracity line of the endpoint, and the reason it is safe to wire into an agent at all. An empty `interactions[]` is meaningless on its own — it must be read together with `coverage`. ```json { "interactions": [], "evaluated_pairs": 1, "coverage": { "complete": true, "unresolved_items": [], "notes": "Absence of interactions is asserted only for the evaluated pairs; unresolved items and unevaluated substances were not compared." } } ``` That is a real all-clear: both items resolved, the pair was compared, nothing is published. Now the same shape with one item the service could not resolve — a well-formed but unknown CIS code: ```json { "interactions": [], "evaluated_pairs": 0, "coverage": { "complete": false, "unresolved_items": [ { "item": 0, "input": "99999999", "reason": "unknown_cis" } ] } } ``` Identical `interactions: []`, opposite meaning. Nothing was compared. An agent that branches on the array alone would read a typo as a safety clearance — which is precisely the failure mode a checking endpoint exists to prevent. Two lists feed `coverage`, and both are binding: - **`unresolved_items`** — an identifier *you sent* that resolved to nothing, with a typed `reason` (`unknown_cis`, `unknown_ndc`, `unknown_dci`, `unknown_class`). Whatever key type you used, it lands in this one list. - **`unevaluated_substances`** — a substance found *inside a resolved medication* that the thesaurus does not cover (the common case: one ingredient of a multi-substance product). Omitted when empty. `complete` is `true` only when every item resolved **and** every pair was compared. The rule for an agent is one line: **treat `complete: false` as "not checked", never as "clear"**. ## Where the ANSM splits a couple, the service does not choose for you Some couples are published on several lines with **different levels**, each restricted to a class member or to a dose. Colchicine against a strong CYP3A4 inhibitor is the textbook case. Asking for colchicine and clarithromycin returns **three** lines for that single pair: ```json [ { "level": "contre_indication", "applies_to": "avec les macrolides", "b": { "ansm_protagonist": "INHIBITEURS PUISSANTS DU CYP3A4" } }, { "level": "contre_indication", "b": { "ansm_protagonist": "MACROLIDES (SAUF SPIRAMYCINE)" } }, { "level": "association_deconseillee", "applies_to": "avec les antifongiques azolés, les inhibiteurs de protéases boostés par ritonavir et le cobicistat", "b": { "ansm_protagonist": "INHIBITEURS PUISSANTS DU CYP3A4" } } ] ``` `applies_to` is the scope of the line, verbatim. Clarithromycin is a macrolide, so lines one and two apply to it and the third describes *other* members of the same inhibitor class. The service returns all three and lets the caller read the scope, because filtering would mean resolving wording like *"les macrolides"* into a substance list — and getting that wrong means silently dropping a contraindication that did apply. When any returned line is scoped, `coverage.notes` says so explicitly. The same holds for couples the ANSM splits **by dose**: aspirin against oral anticoagulants comes back on three levels, and only the scope tells you which one a 75 mg antiplatelet dose falls under. Lines are still ranked by severity, so here the applicable line is the *last* one — a reminder that `interactions[0]` is the most severe published line, not automatically the one that applies. Read `applies_to` **before** acting on `level`. ## US medications join through the INN substance bridge An `ndc` item is resolved to its US product, then to its **INN substance** — the same canonical `substance.id` the whole medication family uses. That substance is then compared against the French thesaurus, so a US NDC and a French CIS carrying the same molecule are compared on equal footing: ```json { "items": [ { "ndc": "43353-021" }, { "dci": "millepertuis" } ], "country": "US" } ``` A generic warfarin sodium NDC comes back with the same contraindication as the French `dci` above, `matched` at `warfarin`, `ansm_protagonist` at `ANTIVITAMINES K`. A US ingredient with no INN bridge and no thesaurus entry goes to `coverage.unevaluated_substances[]` — never silently dropped. And an NDC that resolves to nothing is an `unresolved_items` entry with `reason: "unknown_ndc"`, which is why the coverage read is not optional on cross-border lists. ## A frozen source, honestly dated The ANSM [announced](https://ansm.sante.fr/documents/reference/thesaurus-des-interactions-medicamenteuses-1) that the thesaurus published in **September 2023** is its final version and that it will not update the document further, redirecting practitioners to the product characteristics summaries. So this is a reference that is authoritative and **frozen** — which changes what an honest API has to tell you about it. Every response carries the provenance: ```json "provenance": { "source": "Thésaurus ANSM", "freshness": { "kind": "snapshot", "as_of": "..." } } ``` `kind` is `snapshot`, and `as_of` names the edition the answer was built from — not the ingestion time. An agent that needs to reason about how current its evidence is reads `as_of` and decides. Staleness is **never** turned into a `5xx`: a missed refresh keeps serving the last good snapshot, honestly dated. A thesaurus not ingested at all is a `503` — because there is nothing to serve, not because the data is old. The whole evaluation runs against that local snapshot, so there is **no network call and no secret** at request time. ## What this endpoint is not Every response carries a `disclaimer` field stating the boundary, and it is a field rather than a footer because agents read fields: > Factual relay of the French ANSM interactions thesaurus (level and management > as published); not personalized medical advice. Concretely: - **Not a clinical opinion.** No dosage, no personalised recommendation, no risk score. The endpoint relays what an agency published about a couple. - **Not translated.** `management` and `mechanism` are the ANSM's original French. Translating a contraindication's wording would make the service the author of a clinical instruction, which it declines to be. A field the ANSM did not publish is **omitted**, never served `null` or filled in. - **Not exhaustive of world pharmacology.** It is the ANSM thesaurus, and `coverage` marks its edges on every call. - **Inter-item pairs only.** A marketed combination never flags its own internal association. ## Where it sits in the x402 loop There is no signup before the call: 1. **Discover** the endpoint in the live catalog, call it, receive the `402`. 2. **Pay** — sign the chosen rail and replay the request. 3. **Read `coverage.complete` first**, then `interactions[]`. 4. **Branch** — escalate to a human on a contraindication, look for an alternative, or proceed. Per the x402 golden rule, the agent pays for the answer to its question — so a well-formed list with **no** published interaction is a billed `200`, because "nothing published between these evaluated pairs" *is* the information. Only requests the service cannot answer leave the 200 range and are **not billed**: ```json { "code": "INVALID_INPUT", "error": "provide at least 2 items" } { "code": "INVALID_INPUT", "error": "item 0: provide exactly one of: cis, ndc, dci, class" } ``` Fewer than 2 items, more than 50, an item with zero or several keys, a malformed CIS or NDC, an invalid `country`, a non-JSON body — all `400`, none billed. [Quickstart](/docs/quickstart) walks the full discover → `402` → pay → replay cycle; price and accepted rails are served live by the [catalog](https://api.invoket.com/catalog), never pinned in an article. ## Chain it An interaction check is rarely the last call. [`GET /medication/resolve`](/docs/api/medication-resolve) gives the identity card and the canonical substances behind each item you are about to send here. [`GET /medication/generics`](/docs/api/medication-generics) finds equivalents of a medication a contraindication just knocked out, and [`GET /medication/availability`](/docs/api/medication-availability) tells you whether the alternative is actually in stock before the agent commits to it. For identity, generics, shortage status and interactions against one medication in a single settlement, use the composite [`GET /medication/report`](/docs/api/medication-report). For the full field reference, the `coverage` semantics and the error codes, see the [`POST /medication/interactions` documentation](/docs/api/medication-interactions); for how agents discover and call Invoket endpoints, see [For agents](/docs/for-agents).