# What is behind a CIS code or a US NDC? > One paid call turns a French CIS or a US NDC into an official identity card — status, prices, reimbursement rates — with the INN as the FR↔US bridge. Published 2026-08-10 · HTML version: https://invoket.com/blog/what-is-behind-a-cis-code-or-a-us-ndc --- An agent reading a prescription, an order line or a supplier catalogue holds an opaque code: eight digits of a French **CIS**, or a US **NDC** like `58151-155`. [`GET /medication/resolve`](/docs/api/medication-resolve) turns that code into the official identity card behind it — marketing-authorisation status, active substances normalised to their **INN**, boxes with their published price and reimbursement rate, generic group — served from the French **BDPM** register or from **openFDA + RxNorm**, on the same route. The one design decision worth an article is the join key: every active substance is normalised to its INN, so a French CIS and a US NDC for the same molecule carry the **same** `substances[].id`. That id is what makes a US medication checkable against a French thesaurus. ## Two registers, one route, four keys The endpoint takes exactly **one** key, and the key chosen decides which register answers and what provenance the response carries. | Key | Register consulted | `provenance.freshness.kind` | |-----------|-------------------------------------------|-----------------------------| | `cis` | BDPM (France) | `snapshot` — monthly image | | `ndc` | openFDA + RxNorm (US) | `snapshot` — dump date | | `name` + `country=FR\|US` | the single register named by `country` | `snapshot` | | `dci` | the committed DCI↔INN substance table | `live` — pure computation | A **name without a country** is a `400`, and the reason is provenance rather than parsing: ```json { "code": "MISSING_PARAMETER", "error": "missing or empty required parameter: country" } ``` A bare `ELIQUIS` exists in both registers. Answering it from both would produce one response with two sources and one `as_of` — a blended provenance that is false about both. So the caller says which register it is asking, and the answer names exactly one source. Zero or several keys is the other `400`: ```json { "code": "INVALID_INPUT", "error": "provide exactly one of: cis, ndc, name, dci" } ``` ## The identity card France publishes France is the side that carries the most, because the BDPM publishes more than a product listing. `GET /medication/resolve?cis=60424338`, against the 2026-08-08 snapshot: ```json { "found": true, "medications": [ { "country": "FR", "identifiers": { "cis": "60424338" }, "name": "AMOXICILLINE VIATRIS 500 mg, gélule", "form": "gélule", "routes": ["orale"], "holder": "VIATRIS SANTE", "status": "Autorisation active", "marketing_status": "Commercialisée", "authorization_date": "1998-12-10", "enhanced_surveillance": false, "dispensing": ["liste I"], "substances": [ { "id": "amoxicillin", "labels_fr": ["amoxicilline", "amoxicilline trihydratée"], "labels_en": ["amoxicillin"], "raw_label": "AMOXICILLINE TRIHYDRATÉE", "source_code": "28165", "mapped": true } ], "presentations": [ { "cip7": "3491317", "cip13": "3400934913174", "label": "plaquette(s) thermoformée(s) PVC-aluminium PVDC de 12 gélule(s)", "collectivities_approved": true, "reimbursement_rate": "65%", "price_cents": 153, "price_with_dispensing_fee_cents": 255, "dispensing_fee_cents": 102 } ], "generic_groups": [ { "id": 148, "label": "AMOXICILLINE 500 mg - CLAMOXYL 500 mg, gélule" } ] } ] } ``` Three details in there are the ones agents get wrong when they scrape this data themselves. - **The three money fields are three separate BDPM columns**, in euro cents, never a monetary float and never recomputed from one another. `price_cents` is the medicine; `price_with_dispensing_fee_cents` is what is paid at the counter; `dispensing_fee_cents` is the pharmacist's fee. A presentation whose price the register does not publish carries **none** of the three — never a guessed `0`. The same ELIQUIS answer shows both cases side by side: the box of 60 tablets carries all three, the hospital pack of 100 single-tablet blisters carries no price at all. - **No patient out-of-pocket amount is served.** It would be an arithmetic invention layered on a relayed fact, and it depends on coverage the register does not know about. - **`generic_groups` carries the group, not its members** — `{ id, label }` only. Listing the equivalents is the job of [`GET /medication/generics`](/docs/api/medication-generics), which returns the reference brand and each member with its own presentations. A field the source does not carry is **omitted**, never served `null`: a US result has no `status` or `presentations`, a French result has no `generic_name`. ## The substance is the bridge Atorvastatin makes the mechanism concrete across the two registers. On the US side, `name=LIPITOR&country=US` resolves to four NDCs from the openFDA/RxNorm store: ```json { "country": "US", "identifiers": { "ndc": "58151-155" }, "name": "Lipitor", "generic_name": "atorvastatin calcium", "form": "TABLET, FILM COATED", "holder": "Viatris Specialty LLC", "dispensing": ["prescription"], "substances": [ { "id": "atorvastatin", "labels_fr": ["atorvastatine", "atorvastatine calcique trihydratée"], "labels_en": ["atorvastatin"], "raw_label": "ATORVASTATIN CALCIUM TRIHYDRATE", "source_code": "83367", "mapped": true } ] } ``` On the French side, the same molecule arrives under a different published label — `ATORVASTATINE CALCIQUE TRIHYDRATÉE` on one CIS, `ATORVASTATINE CALCIQUE` on another — and both normalise to the same `id: "atorvastatin"`. Registers publish **salt forms**; the INN names the active moiety. That is the whole reason the normalisation exists: string matching `ATORVASTATIN CALCIUM TRIHYDRATE` against `ATORVASTATINE CALCIQUE` gets you nothing, and neither does asking a language model to guess. The [WHO INN Programme](https://www.who.int/teams/health-product-and-policy-standards/inn), running since 1950, exists precisely to give each pharmaceutical substance a single globally accepted name. Using it as the primary key is not an Invoket convention — it is the identifier the sources themselves are anchored to. What that id then unlocks is the point: [`POST /medication/interactions`](/docs/api/medication-interactions) is indexed by the same canonical substance, so a list assembled from US NDCs can be checked against the French **ANSM interactions thesaurus** — an authoritative source with no US-published equivalent since the NLM retired its interaction API. The bridge is not a convenience; it is what makes a cross-border check possible at all. ## When normalisation fails, the response says so The service never guesses a substance it cannot place. ELIQUIS shows the failure mode, in production, today: ```json { "substances": [ { "id": "apixaban", "labels_fr": [], "labels_en": [], "raw_label": "APIXABAN", "source_code": "61321", "mapped": false } ], "coverage": { "unmapped_substances": ["APIXABAN"] } } ``` Read it strictly. `mapped: false` means the published label matched no entry in the DCI↔INN table, so the `id` is a normalisation of that label rather than a confirmed INN, and `coverage.unmapped_substances` names it at the top level so a caller does not have to walk every substance array to find out. Ids can still coincide across registers when both publish the same label — the US ELIQUIS resolves to `apixaban` too — but a coincidence is not a guarantee, and the response refuses to present it as one. **`mapped: true` is the assertion; the rest is a relayed label with a flag on it.** When every substance normalised, `coverage` is absent entirely. That is the same discipline the rest of the family holds: a bound the service knows about is published as a field, not left for the caller to discover in production. ## Facts as published, never a verdict `status` and `marketing_status` are relayed verbatim from the register, and the uncomfortable values are the ones that matter. Asking for `TAHOR 10 mg, comprimé pelliculé` returns several CIS codes carrying `"status": "Autorisation abrogée"` and `"marketing_status": "Non commercialisée"` — revoked authorisations, still in the register, still resolvable. That is a **billed 200**, not a `404`: an agent handed a stale code from an old catalogue needs to be told the authorisation is revoked, and "revoked" is an answer worth paying for. The same logic runs through the platform — [an abrogated article of French law](/docs/api/legal-article) is a `200` too. The US side needs its own caution, stated by the source: inclusion in the **NDC Directory** does not mean the FDA has verified the submitted information or approved the product. So `found: true` on an `ndc` asserts *this product is listed in the directory*, nothing more. The endpoint relays labels, dates, prices and rates exactly as published. It is **never clinical advice, a patient dosage, or a risk score** — that line bounds the whole medication family. ## A well-formed but unknown key is a paid answer `GET /medication/resolve?cis=99999999` returns a **billed 200**: ```json { "data": { "found": false }, "provenance": { "source": "BDPM", "freshness": { "kind": "snapshot", "as_of": "2026-08-08T00:00:00Z" } } } ``` The agent asked "is this identifier a real medication?" and got the answer. The absence **is** the information — and `as_of` tells you which image of the register that absence is asserted against, which matters more here than anywhere: NDCs are delisted, French authorisations are revoked, and a code that resolved last quarter may not resolve today. French answers come from a **monthly** BDPM snapshot, US answers from the dated openFDA dump, both served with no network call and no secret at request time. A missed refresh keeps serving the last good snapshot, honestly dated — **staleness is never turned into a `5xx`**. A register never ingested at all is a `503`, because there is nothing to serve. A `dci` key consults no store and therefore can never return `503`. ## Why a French register is worth an endpoint US drug data is abundant and free: openFDA is dedicated to the public domain under [CC0](https://open.fda.gov/license/), RxNorm is NLM-published, and a sizeable population of near-identical wrappers already sits on top of them. France publishes its register just as freely — under [article L. 161-40-1 of the Social Security Code](https://base-donnees-publique.medicaments.gouv.fr/telechargement), the BDPM is available for download and reuse, on the condition that the source and its update date are cited. But it publishes it as **tab-separated text files refreshed monthly**, with no official API in front of them. Everything between those files and a resolvable identifier — parsing, joining specialities to presentations to prices to generic groups, normalising salt forms to the INN — is work every consumer would otherwise redo. That is the gap this endpoint fills, and it is why the required attribution (source and date) is carried in `provenance` on every single answer rather than buried in a footer. Lookup is by **identifier or exact name only**. There is no search by symptom, indication or fuzzy name in v1 — the endpoint serves agents that already hold a code. ## What you pay for 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 `found`, `status` and `coverage` together**, never one alone. 4. **Branch** — proceed, resolve the generic group, or escalate to a human. A well-formed identifier the register does not carry is a billed `200`. Only requests the service cannot answer leave the 200 range, and none of them are billed: zero or several keys, a CIS that is not eight digits, a package-level NDC (`58151-155-30` is malformed — the endpoint takes a **product** NDC), a name without a country. [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 `resolve` is the entry point of the family, and each of the others takes the identifier it hands back. [`GET /medication/generics`](/docs/api/medication-generics) expands the `generic_groups` label into its members and their public prices. [`GET /medication/availability`](/docs/api/medication-availability) says whether the ANSM has published a shortage report for the product. [`POST /medication/interactions`](/docs/api/medication-interactions) compares the normalised substances against the ANSM thesaurus. For identity, generic group, reported shortages and interactions on 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 [`GET /medication/resolve` documentation](/docs/api/medication-resolve); for how agents discover and call Invoket endpoints, see [For agents](/docs/for-agents).