Contrôler un numéro d’identification d’entreprise
Vérifier qu’un SIREN ou un SIRET saisi dans un formulaire est bien formé, avant de l’enregistrer.
Pas besoin d’IA
GratuitInstantanéVos données ne sortent pas
Le verdict
N0, parce que le contrôle est une identité arithmétique : il n’y a pas d’approximation à améliorer, seulement une exception à connaître, et la bibliothèque la porte — sur 35600000009075, l’établissement de Rennes de La Poste, une clé de Luhn écrite à la main répond faux là où la bibliothèque répond vrai. Les trois niveaux au-dessus sont fermés pour la même raison : on n’apprend pas une fonction qu’on sait calculer. La sortie de cette fiche ne se fait donc pas par le haut de l’échelle ; elle se fait par le répertoire Sirene, qui répond à la question que la clé ne pose pas.
Clé de contrôle, par une bibliothèque de numéros normalisés
Pas besoin d’IA
Règle et algorithme classiqueN0GratuitInstantané
Python
"""
Contrôler un numéro d'identification d'entreprise française, hors ligne et
exactement.
Niveau N0. Un SIREN fait neuf chiffres, un SIRET quatorze, et le dernier
chiffre de chacun se calcule à partir des précédents. Le contrôler est de
l'arithmétique : la réponse est exacte, elle est la même à chaque fois, et
elle ne coûte rien.
Cette arithmétique n'est pas ce qu'on écrit. Une bibliothèque de numéros
normalisés la porte — `python-stdnum` ici, `stdnum-js` en JavaScript — et
avec elle l'exception qu'une clé de Luhn écrite à la main rate : tous les
établissements de La Poste partagent le SIREN 356000000, et leurs SIRET ne
satisfont pas Luhn. La règle que l'INSEE donne pour eux est que la somme
simple des quatorze chiffres soit un multiple de cinq. 35600000009075,
l'établissement de Rennes, échoue à Luhn et est valide.
Ce que la clé prouve est étroit, et le rapport dit laquelle a été appliquée
pour que l'appelant puisse le savoir. Un numéro peut être bien formé et
n'appartenir à personne : 000000000 passe. Savoir si l'entreprise existe, et
si elle est encore en activité, est une consultation du répertoire Sirene,
pas un calcul plus difficile.
"""
from __future__ import annotations
from stdnum.fr import siren, siret
# The separators people paste around these numbers: the ordinary space, the
# non-breaking ones a spreadsheet inserts, the full stop, the hyphens. Anything
# else surviving the clean-up is refused rather than quietly dropped, which is
# what turns a mistyped number into a different valid one.
SEPARATORS = " . -‐‑–"
# Nine digits, or fourteen. Sixty-four characters leaves room for every
# separator anyone has pasted and refuses a pasted paragraph.
MAX_CHARACTERS = 64
VALIDATORS = {"SIREN": (9, siren.is_valid), "SIRET": (14, siret.is_valid)}
BY_LENGTH = {9: "SIREN", 14: "SIRET"}
def check_company_number(raw, *, expected: str | None = None) -> dict:
"""
Say whether `raw` carries a well-formed SIREN or SIRET.
`expected` is the kind the caller is asking for, "SIREN" or "SIRET". Left
out, the length decides — the only reading there is, since the two lengths
do not overlap — and `kind` in the report says which one was checked.
Nothing here raises on bad input: a validator that throws in a request path
leaves the form with no answer to show. Every refusal comes back as
`valid: False` with a `reason` in plain words.
"""
if not isinstance(raw, str):
return _report(False, None, "", f"a company number is text, not {type(raw).__name__}")
if len(raw) > MAX_CHARACTERS:
return _report(False, None, "", f"longer than {MAX_CHARACTERS} characters")
compact = "".join(character for character in raw if character not in SEPARATORS)
# `str.isdigit` is true of Arabic-Indic and other decimal digits, which the
# checksum cannot be computed on; ASCII is what the register uses.
if not compact or not compact.isascii() or not compact.isdigit():
return _report(False, None, compact, "not digits and separators only")
kind = expected or BY_LENGTH.get(len(compact))
if kind not in VALIDATORS:
return _report(False, None, compact, f"{len(compact)} digits: expected nine or fourteen")
length, is_valid = VALIDATORS[kind]
if len(compact) != length:
return _report(False, kind, compact, f"{kind} is {length} digits, got {len(compact)}")
if not is_valid(compact):
return _report(False, kind, compact, f"the check digit of this {kind} does not match")
return _report(True, kind, compact, None)
def _report(valid: bool, kind: str | None, compact: str, reason: str | None) -> dict:
return {"valid": valid, "kind": kind, "compact": compact, "reason": reason}JavaScript
/**
* Contrôler un numéro d'identification d'entreprise française, hors ligne et
* exactement.
*
* Niveau N0. Un SIREN fait neuf chiffres, un SIRET quatorze, et le dernier
* chiffre de chacun se calcule à partir des précédents. Le contrôler est de
* l'arithmétique : la réponse est exacte, elle est la même à chaque fois, et
* elle ne coûte rien.
*
* Cette arithmétique n'est pas ce qu'on écrit. Une bibliothèque de numéros
* normalisés la porte — `stdnum-js` ici, `python-stdnum` en Python — et avec
* elle l'exception qu'une clé de Luhn écrite à la main rate : tous les
* établissements de La Poste partagent le SIREN 356000000, et leurs SIRET ne
* satisfont pas Luhn. La règle que l'INSEE donne pour eux est que la somme
* simple des quatorze chiffres soit un multiple de cinq. 35600000009075,
* l'établissement de Rennes, échoue à Luhn et est valide.
*
* Ce que la clé prouve est étroit, et le rapport dit laquelle a été appliquée
* pour que l'appelant puisse le savoir. Un numéro peut être bien formé et
* n'appartenir à personne : 000000000 passe. Savoir si l'entreprise existe, et
* si elle est encore en activité, est une consultation du répertoire Sirene,
* pas un calcul plus difficile.
*/
import { stdnum } from 'stdnum';
const { siren, siret } = stdnum.FR;
// The separators people paste around these numbers: the ordinary space, the
// non-breaking ones a spreadsheet inserts, the full stop, the hyphens.
// Anything else surviving the clean-up is refused rather than quietly dropped,
// which is what turns a mistyped number into a different valid one.
export const SEPARATORS = ' . -‐‑–';
// Nine digits, or fourteen. Sixty-four characters leaves room for every
// separator anyone has pasted and refuses a pasted paragraph.
export const MAX_CHARACTERS = 64;
const VALIDATORS = {
SIREN: [9, (value) => siren.validate(value).isValid],
SIRET: [14, (value) => siret.validate(value).isValid],
};
const BY_LENGTH = { 9: 'SIREN', 14: 'SIRET' };
/**
* Say whether `raw` carries a well-formed SIREN or SIRET.
*
* `expected` is the kind the caller is asking for, 'SIREN' or 'SIRET'. Left
* out, the length decides — the only reading there is, since the two lengths
* do not overlap — and `kind` in the report says which one was checked.
*
* Nothing here throws on bad input: a validator that throws in a request path
* leaves the form with no answer to show. Every refusal comes back as
* `valid: false` with a `reason` in plain words.
*
* @param {string} raw
* @param {{expected?: 'SIREN'|'SIRET'}} [options]
*/
export function checkCompanyNumber(raw, { expected } = {}) {
if (typeof raw !== 'string') {
return report(false, null, '', `a company number is text, not ${kindOf(raw)}`);
}
if (raw.length > MAX_CHARACTERS) {
return report(false, null, '', `longer than ${MAX_CHARACTERS} characters`);
}
const compact = [...raw].filter((character) => !SEPARATORS.includes(character)).join('');
// Only ASCII digits: the checksum cannot be computed on Arabic-Indic or
// other decimal digits, and the register uses ASCII.
if (!/^[0-9]+$/.test(compact)) {
return report(false, null, compact, 'not digits and separators only');
}
const kind = expected ?? BY_LENGTH[compact.length];
if (!(kind in VALIDATORS)) {
return report(false, null, compact, `${compact.length} digits: expected nine or fourteen`);
}
const [length, isValid] = VALIDATORS[kind];
if (compact.length !== length) {
return report(false, kind, compact, `${kind} is ${length} digits, got ${compact.length}`);
}
if (!isValid(compact)) {
return report(false, kind, compact, `the check digit of this ${kind} does not match`);
}
return report(true, kind, compact, null);
}
const kindOf = (value) => (value === null ? 'null' : typeof value);
const report = (valid, kind, compact, reason) => ({ valid, kind, compact, reason });Essayer
Ce code ne peut pas tourner dans un navigateur. Les sorties ci-dessous sont calculées à la construction du site, en exécutant le vrai code.
Chaque sortie est celle de l’extrait, exécutée à la construction du site. Rien n’est écrit à la main, et rien ne sort de la machine : le contrôle est local.
Un SIREN collé d’un extrait Kbis
Donné
732 829 320
Ce qu’il renvoie
{ "valid": true, "kind": "SIREN", "compact": "732829320", "reason": null }
Ce qu’il en conclut
SIREN bien formé — 732829320
Neuf chiffres, clé comprise : la vérification est une identité arithmétique.
La clé ne dit rien de plus : elle ne sait pas si ce numéro a été attribué.
Le SIRET du même établissement, séparé par des tirets
Donné
732-829-320-00074
Ce qu’il renvoie
{ "valid": true, "kind": "SIRET", "compact": "73282932000074", "reason": null }
Ce qu’il en conclut
SIRET bien formé — 73282932000074
Quatorze chiffres, clé comprise : la vérification est une identité arithmétique.
La clé ne dit rien de plus : elle ne sait pas si ce numéro a été attribué.
Un SIRET de La Poste, que la clé de Luhn seule refuserait
Donné
35600000009075
Ce qu’il renvoie
{ "valid": true, "kind": "SIRET", "compact": "35600000009075", "reason": null }
Ce qu’il en conclut
SIRET bien formé — 35600000009075
Quatorze chiffres, clé comprise : la vérification est une identité arithmétique.
La clé ne dit rien de plus : elle ne sait pas si ce numéro a été attribué.
Un numéro de TVA intracommunautaire collé dans le champ
Donné
FR44732829320
Ce qu’il renvoie
{ "valid": false, "kind": null, "compact": "FR44732829320", "reason": "not digits and separators only" }
Ce qu’il en conclut
Refusé
not digits and separators only
Neuf zéros
Donné
000000000
Ce qu’il renvoie
{ "valid": true, "kind": "SIREN", "compact": "000000000", "reason": null }
Ce qu’il en conclut
SIREN bien formé — 000000000
Neuf chiffres, clé comprise : la vérification est une identité arithmétique.
La clé ne dit rien de plus : elle ne sait pas si ce numéro a été attribué.
Là, ça casseLa clé accepte : 000000000 est bien formé. Elle ne dit pas qu’une entreprise porte ce numéro, parce qu’elle ne consulte rien — c’est le répertoire Sirene qui répond à cette question-là.
Un SIREN saisi avec deux chiffres inversés
Donné
382 290 401
Ce qu’il renvoie
{ "valid": true, "kind": "SIREN", "compact": "382290401", "reason": null }
Ce qu’il en conclut
SIREN bien formé — 382290401
Neuf chiffres, clé comprise : la vérification est une identité arithmétique.
La clé ne dit rien de plus : elle ne sait pas si ce numéro a été attribué.
Là, ça casseLe numéro voulu était 382 209 401 ; la saisie a inversé « 09 » en « 90 », et la clé accepte quand même. Luhn attrape toutes les fautes d’un seul chiffre, et toutes les inversions sauf celle-là.
Ce que ça implique
- Vos données ne sortent pas
- Résultat toujours identique
- Se teste unitairement, cas par cas
- Dépend d’une bibliothèque
- Empreinte négligeable
Ce que cela ajoute à votre périmètre réglementaire
- Aucun périmètre spécifique ajouté : le numéro est contrôlé dans votre processus et n’est transmis à personne
- Le SIREN d’une entreprise individuelle identifie une personne physique : le contrôler localement ne le communique pas, l’interroger au répertoire le communique
Point de rupture
La clé dit qu’un numéro est bien formé, pas qu’il désigne une entreprise : 000000000 passe, et 382 209 401 saisi « 382 290 401 » passe aussi, parce que Luhn attrape toutes les fautes d’un seul chiffre mais pas l’inversion « 09 » → « 90 ». Le témoin est dans le même test : sur ces mêmes numéros, aucune faute d’un seul chiffre ne passe.
Quand passer au niveau suivant
Il n’y a pas de niveau au-dessus. Le jour où il vous faut le nom, l’adresse ou l’état — active ou cessée — de l’entreprise derrière le numéro, la réponse est une consultation du répertoire Sirene, ouverte et sans clé, et elle fait sortir le numéro de votre infrastructure.
Les quatre options
- RecommandéN0Clé de contrôle, par une bibliothèque de numéros normalisésGratuit · Instantané
- N1Niveau sans objet ici
- N2Niveau sans objet ici
- N3Niveau sans objet ici
Les autres options
N1Niveau sans objet ici
Niveau sans objet ici
Modèle classique légerN1
Il n’y a rien à apprendre. Le dernier chiffre est une fonction des huit ou des treize autres, et cette fonction est publiée ; un classifieur entraîné sur des numéros étiquetés approcherait une identité arithmétique qu’un modulo calcule exactement. Il manquerait de surcroît le cas que la bibliothèque traite — les SIRET de La Poste échouent à la clé de Luhn et sont valides —, parce qu’ils sont trop rares dans un échantillon pour qu’une règle s’en dégage.
N2Niveau sans objet ici
Niveau sans objet ici
Petit modèle spécialisé auto-hébergéN2
Même argument, avec un service à tenir en plus. Charger un modèle spécialisé pour calculer une somme pondérée de neuf chiffres coûte un démarrage, de la mémoire et une version à suivre, pour une sortie moins sûre que celle d’une bibliothèque de quelques kilooctets. Et ce qu’on voudrait réellement au-dessus de la clé — l’existence de l’entreprise — ne se trouve pas dans un modèle, mais dans un répertoire.
N3Niveau sans objet ici
Niveau sans objet ici
API de LLM généralisteN3
Un modèle généraliste rend une phrase sur un calcul qu’il n’exécute pas, et sa réponse ne se rejoue ni ne se justifie : elle n’expose aucune retenue qu’on pourrait relire. La question qui se pose au-dessus de la clé a une réponse publique et datée, le répertoire Sirene, qu’un modèle remplacerait par un souvenir d’entraînement dont il ne connaît pas la date de figement. Il resterait à lui confier la recherche d’un numéro noyé dans un contrat ou une mention légale, ce qui est un autre besoin que celui de cette fiche.
Pour aller plus loin
- INSEE — définition du numéro Siren
- INSEE — définition du numéro Siret
- API Recherche d’entreprises — documentation du point d’accès public
- python-stdnum — le module des numéros français
- stdnum-js — le portage JavaScript, dépôt du projet
- Wikipédia — formule de Luhn, et les erreurs qu’elle ne détecte pas
Source des chiffres
- python-stdnum sur PyPI (consulté) : version 2.2, publiée le 4 janvier 2026, licence LGPL v2 ou ultérieure
- python-stdnum, module stdnum.fr.siret (consulté) : « The Luhn checksum is used to validate the numbers (except for La Poste) »
- stdnum-js, dépôt du projet (consulté) : version 1.12.6 publiée le 1er août 2026, licence MIT, modules fr/siren et fr/siret, exception La Poste comprise
- API Recherche d’entreprises sur data.gouv.fr (consulté) : « L’API étant totalement ouverte d’accès », sans clé, limite de sept appels par seconde
- INSEE, définition du numéro Siren (consulté) : « Le numéro SIREN est un identifiant de neuf chiffres attribué à chaque unité légale »
- Wikipédia, système d’identification du répertoire des établissements (consulté) : l’exception de La Poste, dont les SIRET ont pour règle que la somme des quatorze chiffres soit un multiple de cinq
Métadonnées
Information générale, tenue à jour au mieux. Ce n’est pas un avis juridique. Faites valider votre situation par un professionnel.