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".
By Matthias Begot · · Updated
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
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, 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_interactionsfield, 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.
POST /medication/interactions
Content-Type: application/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:
{
"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.
{
"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:
{
"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 typedreason(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:
[
{ "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:
{ "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 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:
"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.
managementandmechanismare 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 servednullor filled in. - Not exhaustive of world pharmacology. It is the ANSM thesaurus, and
coveragemarks 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:
- Discover the endpoint in the live catalog, call it, receive the
402. - Pay — sign the chosen rail and replay the request.
- Read
coverage.completefirst, theninteractions[]. - 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:
{ "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 walks the full discover → 402 → pay → replay
cycle; price and accepted rails are served live by the
catalog, never pinned in an article.
Chain it
An interaction check is rarely the last call.
GET /medication/resolve gives the identity card
and the canonical substances behind each item you are about to send here.
GET /medication/generics finds equivalents of
a medication a contraindication just knocked out, and
GET /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.
For the full field reference, the coverage semantics and the error codes, see
the POST /medication/interactions documentation;
for how agents discover and call Invoket endpoints, see
For agents.