GET /medication/lookup

Turns a medication name as an agent holds it - a brand from a prescription, a US product name, a French or international substance name - into the identifiers the /medication/* routes require: a French CIS (one presentation in the BDPM register), a US product NDC, or the canonical identifier of an active substance. The answer comes from local stores already ingested (BDPM, openFDA, RxNorm), with no network call and no secret at request time.

Start here when the agent holds a name. If it already holds a CIS or an NDC, call /medication/resolve directly. See the live /catalog for the authoritative listing and price.

ambiguous is the normal answer here

Read this before the examples: a brand name routinely covers several strengths, forms and presentations, and each presentation is its own CIS. “Doliprane” is 18 CIS in the current BDPM stock - tablets, suppositories, oral powders, from 100 mg to 1000 mg. Returning “the” CIS for Doliprane would be wrong in most cases, so this route does not: ambiguous is the nominal case, and the list is the answer.

verdictMeaningcandidates
exactOne candidate answersexactly one element
ambiguousSeveral do - all served, up to 50, with total exact. The usual casetwo or more elements
noneThe referentials publish nothing for this name, at the stock date[]

candidates is always present - empty on none, a one-element list on exact. Each candidate carries what tells it apart from the others (country, strength, form, marketing-authorisation holder, French marketing status), so the caller can choose.

none is a paid answer

A none is served as a 200 and billed - never a 404. “This name is not in the referential, as of the stock date” is exactly what stops an agent from inventing a CIS. Only a malformed request - q missing, empty, without any letter or digit, or a filter outside its domain - leaves the 200 range, and it is not billed.

What a resolution does not attest

Every answer carries the same fixed sentence in data.limits, served verbatim:

A resolution names what the referential publishes for the text you sent, as of the date carried by the provenance; it does not certify that the entity resolved is the one you had in mind. A brand name routinely covers several strengths, forms and presentations, hence several CIS: choosing between them belongs to the caller, and no candidate served here is medical advice. Substance candidates fold salt and hydrate forms onto their base through a closed curated list (the fold is visible in normalization.substance_key): a form that list does not cover keeps its suffix and yields no substance candidate rather than a guessed one. Matching is exact on the published label or on its leading words, never fuzzy and never scored; candidates are ordered substance first, then by CIS, then by NDC — a reading order, not a relevance ranking.

This route names what three public registers publish. It does not say which presentation suits anyone, and nothing in a candidate is a recommendation - the order is a reading order, and the choice between presentations belongs to the caller.

Three natures of identifier

Each candidate’s id is the key downstream accepts as is, and id_kind says which kind it is:

id_kindidWhat it designatesWhere it goes
cis8 digits (62080626)One French presentation in the BDPMcis= on resolve, generics, availability, report; a cis item of interactions
ndclabeler-product (50580-108)One US product (openFDA / RxNorm)ndc= on resolve and report; an ndc item of interactions
substanceINN form (acetylsalicylic acid)An active substance, whatever the countrydci= on resolve, generics, availability; a dci item of interactions

The substance is the bridge between France and the US: the same substance id resolves on both sides, which is how a French CIS and a US NDC meet in one interaction check. It is also the only nature that is not a presentation - it carries no strength, no form, no holder. A substance id goes into the dci key of an interactions item, never into its cis key; the report route takes a presentation (cis or ndc) as its subject, not a substance.

A single name often returns a substance and presentations - “aspirine” returns the substance acetylsalicylic acid and eight French presentations whose published name starts with “Aspirine”. That is a normal ambiguous, not an anomaly: the caller decides whether it is asking about the molecule or about a box.

How the name is matched

Two keys are computed from your text, and both are served back in data.normalization - without them, a none would be inexplicable:

  • name_key - matched against published product names (BDPM denominations, US brand and generic names): lower case, French accents folded, all punctuation, hyphens included, turned into a word boundary, spaces collapsed. DOLIPRANE, doliprane, Dolipranê and sachet-dose / sachet dose land on the same key.
  • substance_key - matched against active substances: the same folding, plus the folding of known salt and hydrate forms onto their base (esomeprazole magnesium → esomeprazole, sertraline hydrochloride → sertraline).

Matching is an equality on word boundaries: a published label matches when it is the key, or starts with the key followed by a word boundary. So “Tylenol” returns TYLENOL EXTRA STRENGTH - the label that carries the match is served, the difference is visible, and a partial match is never exact unless it is the only one. There is no edit distance, no similarity score, no typo tolerance.

What is deliberately not folded:

You sendKeyResult
parace-tamolparace tamolnone - words are never glued back together, because the indexed column is not either
Efferalgan 1gefferalgan 1gnone - the BDPM publishes EFFERALGAN 1000 mg, ...: 1g is not a word of any label. Send Efferalgan and filter with strength=1000 mg

Salt and hydrate forms: a closed list, on purpose

The salt fold runs on a closed, curated list of salt, counter-ion and hydrate tokens (hydrochloride, chlorhydrate, besilate, sodium, succinate, dihydrate…). Outside that list, the suffix stays, and no substance candidate is returned rather than a guessed one: hydroxyzine pamoate keeps its key hydroxyzine pamoate, and comes back with the US products whose label carries those words - but no substance candidate.

That restriction is the guarantee, not a gap. A generic “strip whatever looks like a salt suffix” rule was measured fabricating false interactions on the interactions route: it reduced both potassium chloride and sodium chloride to the same key, and handed a potassium-only product the interaction the ANSM thesaurus publishes for sodium salts. Every token on the list is one that was checked against the referentials; a form it does not cover is served as unresolved rather than mapped to a substance it may not be.

Parameters

ParameterTypeRequiredDescription
qstringyesThe name as you hold it - brand, product name, substance. At least one letter or digit, up to 1,024 characters
countrystringnoFR or US - limits the registers consulted
formstringnoA pharmaceutical form as published (comprimé, suppositoire, TABLET)
strengthstringnoA strength as published (1000 mg, 500 mg/1)
GET /medication/lookup?q=Doliprane&form=suppositoire
  • Filters reduce, they never reshape. Each one keeps the candidates whose published value contains the filter on word boundaries: form=comprimé keeps comprimé effervescent(e) sécable, strength=500 mg keeps 500 mg + 30 mg - but never 1500 mg.
  • A substance candidate leaves the set as soon as form or strength is set: a substance publishes neither, so nothing attests that it matches. Likewise a presentation whose register does not publish the filtered field is dropped. The service does not fall back silently on what it cannot attest.
  • country limits the registers: country=US does not open the BDPM, so Doliprane becomes a none; a substance stays servable under US, because the substance table is the bridge and belongs to neither country.
  • No pagination. The list is capped at 50 (the item cap of /medication/interactions); total counts every candidate before the cap and truncated says whether the list was cut - “paracetamol” finds 116 candidates. A second page would be a second paid call for the same question: narrow with a filter instead. A truncated list is never exact.

Every filter applied is echoed in data.filters.

200 response - UnifiedResponse

{
  "data": { ... },
  "provenance": {
    "source": "BDPM + openFDA + RxNorm",
    "fetched_at": "2026-10-15T15:37:33.138077267Z",
    "freshness": { "kind": "snapshot", "as_of": "2026-10-08T00:00:00Z" }
  }
}
  • provenance.source: the registers actually consulted, joined by +.
  • freshness.as_of: the oldest snapshot among them - an answer is never dated fresher than its oldest data.

Fields of data

FieldTypeDescription
querystringThe text you sent, echoed
verdictstringexact, ambiguous or none
candidatesobject[]Always served. Substance first, then French presentations by CIS, then US products by NDC - a reading order, not a relevance ranking. At most 50
totalnumberCandidates found before the cap; the verdict is derived from it
truncatedboolWhether the served list is cut
normalizationobject{ name_key, substance_key } - the two keys actually searched
filtersobjectThe filters applied, echoed ({} when none)
limitsstringThe fixed sentence quoted above

Each candidate:

FieldTypeDescription
idstringCIS, NDC or substance identifier - pass it downstream as is
id_kindstringcis, ndc or substance
labelstringThe name as the register publishes it - never recased, never re-accented
sourcestringThe register the candidate comes from (BDPM, openFDA + RxNorm, or the DCI/INN substance table)
countrystringFR or US - presentations only
strengthstringPublished strength(s) of the active substance(s), joined by + for a combination
formstringPublished pharmaceutical form
holderstringMarketing-authorisation holder (FR) or labeler (US)
marketing_statusstringFrench marketing status as published (Commercialisée, Non commercialisée)

A field the register does not publish is absent, never null and never filled in.

Example - exact

GET /medication/lookup?q=Levothyrox%2075:

{
  "data": {
    "query": "Levothyrox 75",
    "verdict": "exact",
    "candidates": [
      {
        "id": "62080626",
        "id_kind": "cis",
        "label": "LEVOTHYROX 75 microgrammes, comprimé sécable",
        "country": "FR",
        "strength": "75 microgrammes",
        "form": "comprimé sécable",
        "holder": "MERCK SANTE",
        "marketing_status": "Commercialisée",
        "source": "BDPM"
      }
    ],
    "total": 1,
    "truncated": false,
    "normalization": { "name_key": "levothyrox 75", "substance_key": "levothyrox 75" },
    "filters": {},
    "limits": "A resolution names what the referential publishes for the text you sent, as of the date carried by the provenance; it does not certify that the entity resolved is the one you had in mind. A brand name routinely covers several strengths, forms and presentations, hence several CIS: choosing between them belongs to the caller, and no candidate served here is medical advice. Substance candidates fold salt and hydrate forms onto their base through a closed curated list (the fold is visible in normalization.substance_key): a form that list does not cover keeps its suffix and yields no substance candidate rather than a guessed one. Matching is exact on the published label or on its leading words, never fuzzy and never scored; candidates are ordered substance first, then by CIS, then by NDC — a reading order, not a relevance ranking."
  },
  "provenance": {
    "source": "BDPM + openFDA + RxNorm",
    "fetched_at": "2026-10-15T15:37:33.138077267Z",
    "freshness": { "kind": "snapshot", "as_of": "2026-10-08T00:00:00Z" }
  }
}

“Levothyrox” alone is 13 presentations; with the strength in the text, one published label starts with those words. The id goes straight into /medication/resolve?cis=62080626.

Example - ambiguous, a substance and its presentations

GET /medication/lookup?q=aspirine (limits and provenance as above):

{
  "data": {
    "query": "aspirine",
    "verdict": "ambiguous",
    "candidates": [
      { "id": "acetylsalicylic acid", "id_kind": "substance", "label": "ACIDE ACÉTYLSALICYLIQUE", "source": "BDPM" },
      { "id": "61172160", "id_kind": "cis", "label": "ASPIRINE UPSA TAMPONNEE EFFERVESCENTE 1000 mg, comprimé effervescent", "country": "FR", "strength": "1000 mg", "form": "comprimé effervescent(e)", "holder": "UPSA", "marketing_status": "Commercialisée", "source": "BDPM" },
      { "id": "61464022", "id_kind": "cis", "label": "ASPIRINE PROTECT 100 mg, comprimé gastro-résistant", "country": "FR", "strength": "100 mg", "form": "comprimé gastro-résistant(e)", "holder": "BAYER HEALTHCARE", "marketing_status": "Commercialisée", "source": "BDPM" },
      { "id": "61946403", "id_kind": "cis", "label": "ASPIRINE ARROW 100 mg, comprimé gastro-résistant", "country": "FR", "strength": "100 mg", "form": "comprimé gastro-résistant(e)", "holder": "ARROW GENERIQUES", "marketing_status": "Commercialisée", "source": "BDPM" },
      { "id": "65265765", "id_kind": "cis", "label": "ASPIRINE UPSA 500 mg, comprimé effervescent", "country": "FR", "strength": "500 mg", "form": "comprimé effervescent(e)", "holder": "UPSA", "marketing_status": "Commercialisée", "source": "BDPM" },
      { "id": "66598317", "id_kind": "cis", "label": "ASPIRINE ARROW 75 mg, comprimé gastro-résistant", "country": "FR", "strength": "75 mg", "form": "comprimé gastro-résistant(e)", "holder": "ARROW GENERIQUES", "marketing_status": "Commercialisée", "source": "BDPM" },
      { "id": "67831952", "id_kind": "cis", "label": "ASPIRINE UPSA VITAMINEE C TAMPONNEE EFFERVESCENTE, comprimé effervescent", "country": "FR", "strength": "0,330 g + 0,200 g", "form": "comprimé effervescent(e)", "holder": "UPSA", "marketing_status": "Commercialisée", "source": "BDPM" },
      { "id": "68799614", "id_kind": "cis", "label": "ASPIRINE DU RHONE 500 mg, comprimé", "country": "FR", "strength": "500 mg", "form": "comprimé", "holder": "BAYER HEALTHCARE", "marketing_status": "Commercialisée", "source": "BDPM" },
      { "id": "69029253", "id_kind": "cis", "label": "ASPIRINE DU RHONE 500 mg, comprimé à croquer", "country": "FR", "strength": "500 mg", "form": "comprimé à croquer", "holder": "BAYER HEALTHCARE", "marketing_status": "Commercialisée", "source": "BDPM" }
    ],
    "total": 9,
    "truncated": false,
    "normalization": { "name_key": "aspirine", "substance_key": "aspirine" },
    "filters": {},
    "limits": "A resolution names what the referential publishes for the text you sent, …"
  },
  "provenance": { "…": "…" }
}

The substance comes first and carries the INN as its id (acetylsalicylic acid) next to the label the BDPM publishes (ACIDE ACÉTYLSALICYLIQUE): that id is what a dci item of /medication/interactions accepts as is. The eight presentations are the French products whose published name starts with “Aspirine”; telling them apart is what strength, form and holder are for.

Example - none

GET /medication/lookup?q=Efferalgan%201g:

{
  "data": {
    "query": "Efferalgan 1g",
    "verdict": "none",
    "candidates": [],
    "total": 0,
    "truncated": false,
    "normalization": { "name_key": "efferalgan 1g", "substance_key": "efferalgan 1g" },
    "filters": {},
    "limits": "A resolution names what the referential publishes for the text you sent, as of the date carried by the provenance; it does not certify that the entity resolved is the one you had in mind. A brand name routinely covers several strengths, forms and presentations, hence several CIS: choosing between them belongs to the caller, and no candidate served here is medical advice. Substance candidates fold salt and hydrate forms onto their base through a closed curated list (the fold is visible in normalization.substance_key): a form that list does not cover keeps its suffix and yields no substance candidate rather than a guessed one. Matching is exact on the published label or on its leading words, never fuzzy and never scored; candidates are ordered substance first, then by CIS, then by NDC — a reading order, not a relevance ranking."
  },
  "provenance": {
    "source": "BDPM + openFDA + RxNorm",
    "fetched_at": "2026-10-15T15:37:33.227079665Z",
    "freshness": { "kind": "snapshot", "as_of": "2026-10-08T00:00:00Z" }
  }
}

A 200, billed. No published label starts with the words efferalgan 1g - the BDPM writes the strength 1000 mg. The normalization echo shows exactly what was searched, which is what makes the none explainable: q=Efferalgan&strength=1000 mg is the call that finds the presentations.

Errors

Only requests the service cannot answer leave the 200 range - none of them are billed. There is no 404 for a name the registers do not publish: that is a none.

StatuscodeCase
400MISSING_PARAMETERq absent, empty or blank
400INVALID_INPUTq without any letter or digit, or longer than 1,024 characters; an empty form/strength; a country other than FR/US; a repeated parameter
405METHOD_NOT_ALLOWEDAny method other than GET
503DATA_UNAVAILABLEA register the request needs has not been ingested yet - rather than a none that would pass a coverage gap off as an absence
500INTERNALInternal error (detail logged, not exposed)
{ "code": "MISSING_PARAMETER", "error": "missing or empty required parameter: q" }
{ "code": "INVALID_INPUT", "error": "invalid country: expected FR or US, got \"DE\"" }

Attribution

French medication data is derived from the BDPM (Base de données publique des médicaments), made freely available under article L.161-40-1 of the French Social Security Code (source and date required). US medication data is derived from openFDA and RxNorm, in the public domain (NLM/FDA).

See also