Which generics can substitute for this medication?
A BDPM generic group is a regulatory decision, not a computed equivalence. /medication/generics returns every member, its official role and its published price.
By Matthias Begot ·
An agent handling a prescription, a purchase order or a supplier catalogue
eventually asks the sourcing question: what else can stand in for this
product? GET /medication/generics answers it
from the French BDPM generic-groups register — the reference brand
(princeps), every equivalent, each member’s official BDPM role, and its
boxes with their published price and reimbursement rate. The reason this needs
an endpoint rather than a string comparison is the first thing to get straight:
a generic group is a decision published by a regulator, not a set computed
from molecule and dose. Members that a naive match would exclude are in the
group, and members a naive match would include are not.
The group is a register entry, not a computed set
In France, substitution is bounded by law. Under article L. 5125-23 of the code de la santé publique, a pharmacist may dispense a different specialty within the same generic group, provided the prescriber has not excluded it with an express, justified mention on the prescription. The groups themselves are established by the ANSM under article R. 5121-5 and published in the répertoire des groupes génériques. Membership of that register is what makes substitution lawful — not chemical similarity.
The BDPM publishes the same groups as a data file, and gives each member one of four roles. The endpoint relays that role verbatim as a closed set:
type | What the register says |
|---|---|
princeps | The reference specialty of the group (letter R in the répertoire) |
generique | A generic of that reference (letter G) |
complement_posologique | A generic admitted by complémentarité posologique — a different strength |
substituable | A substitutable specialty under R. 5121-5-2 (letter S) — a different modified-release oral form |
Those last two rows are the whole argument for reading the register instead of reimplementing it.
Two roles a heuristic gets wrong
A different strength, in the group. Group 33 is labelled
METFORMINE (CHLORHYDRATE DE) 1000 mg - GLUCOPHAGE 1000 mg, comprimé pelliculé sécable. — a 1000 mg group. It contains two 500 mg members:
{ "cis": "69448283", "name": "METFORMINE ARROW LAB 500 mg, comprimé pelliculé",
"type": "complement_posologique" },
{ "cis": "61182628", "name": "METFORMINE CRISTERS PHARMA 500 mg, comprimé pelliculé",
"type": "complement_posologique" }
Any filter that requires the strengths to match drops them. The register keeps them, and labels why they are there.
A different pharmaceutical form, in the group. Group 698 is labelled
TRAMADOL (CHLORHYDRATE DE) 100 mg - CONTRAMAL L.P. 100 mg, comprimé à libération prolongée - TOPALGIC L.P. 100 mg, comprimé à libération prolongée.
Among its members:
{ "cis": "69934919", "name": "TRAMADOL ARROW L.P. 100 mg, comprimé à libération prolongée",
"type": "substituable" },
{ "cis": "69701943", "name": "TRAMADOL ARROW L.P. 100 mg, gélule à libération prolongée",
"type": "substituable" }
A capsule sitting in a group of tablets. That is exactly what R. 5121-5-2 provides for: a specialty presented in a modified-release oral form different from the reference can be entered in the group by a decision of the ANSM director general, and is then substitutable like any other member. No attribute of the product tells you this. Only the register does.
The same group also shows that a group can carry more than one princeps —
CONTRAMAL LP and TOPALGIC LP are both references of group 698, and the group
label names both.
Membership, price and availability are three different questions
Take the atorvastatin 10 mg group (913), reached from the brand:
GET /medication/generics?name=TAHOR%2010%20mg%2C%20comprim%C3%A9%20pellicul%C3%A9
It returns 19 members. Reading them as “19 things I can order” is wrong in three separate ways, and each way is visible in the response.
Prices are per box, and boxes are not the same size. The princeps sells a
28-tablet box at price_cents: 351; the generics sell 30-tablet boxes at
price_cents: 318. Comparing 351 to 318 gives a ~9 % gap; per tablet the gap is
closer to 15 %. The endpoint publishes the box label next to the price
precisely so that the caller does not compare across pack sizes by accident. It
does not compute a unit price — that is arithmetic on a relayed fact, and it
belongs to the caller.
Between generics there is often nothing to compare. Every 30-tablet generic
in this group is priced at exactly 318, and every 90-tablet one at exactly
904. Price shopping inside a French generic group is largely a non-event;
the value of the call is identifying the legally substitutable set, not
finding a bargain in it.
Three money fields, and never a fourth. price_cents is the medicine,
price_with_dispensing_fee_cents is what is paid at the counter, and
dispensing_fee_cents is the pharmacist’s fee — three separate BDPM columns, in
integer euro cents, never a monetary float and never recomputed from one
another. A box whose price the register does not publish carries none of the
three rather than a guessed 0. No patient out-of-pocket amount is served: it
would depend on coverage the register knows nothing about.
Being in the group does not mean it is on the market. Presentations carry their BDPM marketing declaration:
{
"cip13": "3400949458783",
"label": "plaquette(s) polyamide aluminium PVC-Aluminium de 30 comprimé(s)",
"marketing": "Déclaration d'arrêt de commercialisation",
"declared_at": "2022-06-17",
"reimbursement_rate": "65%",
"price_cents": 373
}
In group 913, three members have every box flagged as discontinued —
including one of the two princeps entries, TAHOR 10 mg comprimé à croquer.
Add the two members with no identity at all (below) and 5 of the 19 are not
something an agent should propose sourcing today. Marketing declarations answer
“was this box declared as marketed?”; whether a supply problem has been reported
is a different register and a different call —
GET /medication/availability.
A member with no identity says so
The BDPM publishes generic groups and specialties in two separate files, and
the groups file references CIS codes the specialties file no longer carries —
authorisations withdrawn and not carried over. That is roughly 24 % of all
group members. Such a member arrives with its cis, its role, and nothing
else:
{
"cis": "65785748",
"identity": "withdrawn_from_bdpm_specialities",
"type": "generique",
"is_query": false
}
No name is guessed from the CIS, and the member is not silently dropped — it belongs to the group, and removing it would misstate the group’s composition. The answer also counts the gap at the top level:
"coverage": {
"members_without_identity": 2,
"note": "2 group member(s) have no name or presentations: the BDPM generic-groups file references CIS codes absent from the BDPM specialities file (marketing authorisation withdrawn); nothing is inferred"
}
A group whose members are all known carries no coverage at all. This 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.
Three keys, and what is_query marks
The endpoint takes exactly one of cis, name, dci or ndc. Unlike
/medication/resolve, a name here takes no
country — generic groups are a French notion, so there is only one register
to ask.
dci goes through the same canonical substance key as the rest of the family,
so an English INN reaches the French groups of the same molecule and can
aggregate several of them: dci=acetaminophen returns 7 groups and 36
members, combination products included. On every answer, is_query: true marks
the member(s) that matched the key you sent — one member for a precise name,
and potentially most of a group for a dci, since every member of a paracetamol
group contains paracetamol.
Scope, stated rather than implied
Generic groups are a French (BDPM) construct, and the endpoint does not
pretend otherwise. A US ndc key returns a billed 200 with an honest note
instead of an error:
{
"data": {
"found": false,
"groups": [],
"coverage": {
"scope": "FR",
"note": "generic groups are a French (BDPM) notion; NDC keys are out of scope in v1"
}
},
"provenance": {
"source": "medication-grid generics coverage (BDPM scope)",
"freshness": { "kind": "live" }
}
}
That branch consults no store, so it can never return a 503. Two further
bounds are worth stating plainly:
- Hybrid groups are a different register. L. 5125-23 also allows substitution within a groupe hybride, but the ANSM publishes those in a separate registre des groupes hybrides (letters R/H). This endpoint serves the BDPM generic-groups file and its four roles — a hybrid relationship is not among them.
- The response carries no excipients with notable effect. So it cannot tell
you whether the
CIFexclusion of the arrêté of 12 November 2019 — one of the three medical situations (MTE,EFG,CIF) in which a prescriber may exclude substitution — applies to a given patient.
Which is the general line: the endpoint relays sourced, dated facts — roles, names, prices, rates as published. It is never a substitution recommendation, clinical advice or a patient dosage. That boundary bounds the whole medication family.
What you pay for
- Discover the endpoint in the live catalog, call it, receive the
402. - Pay — sign the chosen rail and replay the request.
- Read
type,marketingandcoveragetogether, never the member list alone. - Branch — propose the group, check a candidate’s availability, or escalate to a human.
A well-formed key carrying no generic group is a billed 200 with
found: false and groups: [] — the agent asked “is this medication part of a
generic group?”, and “no” is the answer it paid for. Only requests the service
cannot answer leave the 200 range, and none of them are billed:
{ "error": "provide exactly one of: cis, name, dci, ndc", "code": "INVALID_INPUT" }
Zero or several keys, a CIS that is not eight digits
(invalid cis: expected exactly 8 digits), a package-level NDC, an empty
name/dci — all 400, none billed. Answers come from a monthly snapshot
of the BDPM, served with no network call and no secret at request time;
freshness.as_of names the image the answer was built against. A missed refresh
keeps serving the last good snapshot, honestly dated — staleness is never
turned into a 5xx. 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
generics is a middle step. GET /medication/resolve
turns the CIS or NDC you hold into an identity card — status, canonical
substances, presentations — and its generic_groups field gives the group
{ id, label } this endpoint then expands into members.
GET /medication/availability says whether
the ANSM has published a shortage report for a candidate before it is proposed,
and POST /medication/interactions checks
that candidate against the rest of a patient’s list using the ANSM thesaurus.
For identity, generic group, reported shortages and interactions on 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 GET /medication/generics documentation;
for how agents discover and call Invoket endpoints, see
For agents.
BDPM data is reused under
article L. 161-40-1 of the French Social Security Code,
which requires citing the source and its update date — carried in provenance
on every answer.