How to Look Up Any French Company by SIRET in Seconds
Antoine5 min read·Just now--
France has a public registry of every business and establishment in the country — over 40 million records. If you’re doing KYC checks, CRM enrichment, invoice validation, or just building a form where users enter a company number, you’ll need to query this data. The catch is that the official API is designed for bulk data consumers, not app developers who just want to look up a single company. Here’s a faster path.
SIREN vs SIRET: the basics
Every French business has a SIREN number: 9 digits that identify the legal entity. Think of it like a company ID.
Each SIREN can have one or more SIRET numbers. A SIRET is 14 digits: the 9-digit SIREN plus a 5-digit NIC (Numero Interne de Classement) that identifies a specific establishment (office, warehouse, store). A company with 3 offices has 1 SIREN and 3 SIRETs.
SIRET: 802 954 785 00028
|_________| |___|
SIREN NICBoth numbers use a Luhn checksum, just like credit card numbers. Here’s a quick validator:
def luhn_check(number: str) -> bool:
"""Validate a SIREN or SIRET using the Luhn algorithm."""
digits = [int(d) for d in number]
odd_digits = digits[-1::-2]
even_digits = digits[-2::-2]
total = sum(odd_digits) + sum(sum(divmod(d * 2, 10)) for d in even_digits)
return total % 10 == 0
# Validate a SIRET
siret = "80295478500028"
assert len(siret) == 14 and siret.isdigit()
print(luhn_check(siret)) # True or FalseThere’s one famous exception: La Poste (the French postal service, SIREN 356000000) fails the Luhn check but is perfectly valid. Edge cases are fun.
The official INSEE API
The French statistics institute (INSEE) maintains the SIRENE database and offers an API at `api.insee.fr`. It works, but:
- You need to register for OAuth2 credentials and manage token refresh
- Rate limits are strict (30 requests/minute for free accounts)
- The response format is deeply nested JSON with French field names
- Search is limited and doesn’t support fuzzy matching well
- Bulk data is distributed as massive CSV files (14+ GB)
For a production app that needs fast lookups, the overhead of managing INSEE credentials and dealing with their rate limits adds friction you don’t need.
Looking up a company by SIRET
The European Business API indexes the full SIRENE database and exposes it through simple REST endpoints. Here’s a direct SIRET lookup:
import httpx
API_KEY = "your-api-key"
BASE_URL = "https://frenchbusinessapi.com"
def lookup_siret(siret: str) -> dict:
"""Look up a French establishment by SIRET number."""
resp = httpx.get(
f"{BASE_URL}/sirene/siret/{siret}",
headers={"X-API-Key": API_KEY},
)
resp.raise_for_status()
return resp.json()
company = lookup_siret("80295478500028")
print(company)The response gives you everything about that establishment:
{
"siret": "80295478500028",
"siren": "802954785",
"nic": "00028",
"denomination": "ALAN",
"denomination_usuelle": null,
"enseigne": null,
"code_naf": "65.12Z",
"est_siege": true,
"etat_administratif": "A",
"date_creation": "2016-01-01",
"tranche_effectifs": "42",
"tranche_effectifs_libelle": "1 000-1 999 salaries",
"adresse": "40 RUE DE RICHELIEU",
"geo_adresse": "40 Rue de Richelieu 75001 Paris",
"code_postal": "75001",
"commune": "PARIS 1ER ARRONDISSEMENT",
"code_commune": "75101",
"longitude": 2.337,
"latitude": 48.866
}A few useful fields to note:
- `etat_administratif`: `”A”` means active, `”F”` means closed. Essential for KYC.
- `est_siege`: `true` if this is the company headquarters.
- `tranche_effectifs`: Workforce size range, with a human-readable label.
- `longitude`/`latitude`: GPS coordinates of the establishment.
- `code_naf`: The French activity classification code (like SIC codes in the US).
Finding all establishments for a company
A SIREN lookup returns every establishment (office, branch, store) for a company:
def lookup_siren(siren: str, actif_only: bool = True) -> dict:
"""Get all establishments for a SIREN."""
resp = httpx.get(
f"{BASE_URL}/sirene/siren/{siren}",
params={"actif_only": actif_only},
headers={"X-API-Key": API_KEY},
)
resp.raise_for_status()
return resp.json()
result = lookup_siren("802954785")
print(f"Found {result['count']} establishments")
for etab in result["results"]:
hq = " (HQ)" if etab["est_siege"] else ""
print(f" {etab['siret']} - {etab['commune']}{hq}")Set `actif_only=False` to include closed establishments — useful for audit trails or historical analysis.
Searching by name
When you don’t have a SIRET but know the company name, you can search:
def search_companies(
name: str = None,
postal_code: str = None,
city: str = None,
naf_code: str = None,
hq_only: bool = False,
limit: int = 25,
) -> dict:
"""Search French companies by name, location, or activity."""
params = {"limit": limit, "actif_only": True}
if name:
params["q"] = name
if postal_code:
params["code_postal"] = postal_code
if city:
params["commune"] = city
if naf_code:
params["code_naf"] = naf_code
if hq_only:
params["siege_only"] = True
resp = httpx.get(
f"{BASE_URL}/sirene/search",
params=params,
headers={"X-API-Key": API_KEY},
)
resp.raise_for_status()
return resp.json()
# Find all bakeries in Lyon
bakeries = search_companies(city="LYON", naf_code="10.71C", hq_only=True, limit=10)
for b in bakeries["results"]:
print(f"{b['denomination']} - {b['adresse']} {b['code_postal']}")You can combine filters: name + postal code, NAF code + city, etc. The search is case-insensitive.
Real-world use case: auto-fill from SIRET
Here’s a practical pattern for a B2B form where a user enters their SIRET and you auto-fill the rest:
def autofill_company_form(siret: str) -> dict:
"""
Given a SIRET, return pre-filled form data.
Returns an error dict if the SIRET is invalid or the company is closed.
"""
# Quick local validation first
clean = siret.replace(" ", "").replace("-", "")
if not clean.isdigit() or len(clean) != 14:
return {"error": "SIRET must be exactly 14 digits"}
try:
company = lookup_siret(clean)
except httpx.HTTPStatusError as e:
if e.response.status_code == 404:
return {"error": "SIRET not found in the registry"}
if e.response.status_code == 400:
return {"error": "Invalid SIRET format"}
raise
if company["etat_administratif"] == "F":
return {
"error": "This establishment is closed (ferme)",
"closed_since": company.get("date_creation"),
}
return {
"company_name": company["denomination"],
"trade_name": company["enseigne"] or company["denomination_usuelle"],
"address": company["geo_adresse"],
"postal_code": company["code_postal"],
"city": company["commune"],
"is_headquarters": company["est_siege"],
"activity_code": company["code_naf"],
"workforce": company["tranche_effectifs_libelle"],
"siren": company["siren"],
"siret": company["siret"],
}
# User enters their SIRET in a registration form
form = autofill_company_form("802 954 785 00028")
if "error" not in form:
print(f"Welcome, {form['company_name']}!")
print(f"Address: {form['address']}")
print(f"Workforce: {form['workforce']}")This saves the user from manually typing their company name, address, and other details. It also lets you verify that the SIRET they entered actually exists and corresponds to an active business — a basic but important KYC step.
Tips for working with SIRENE data
- SIRET changes when a company moves. The SIREN stays the same, but the NIC changes. Always store the SIREN as the stable company identifier.
- NAF codes (Nomenclature d’Activites Francaise) classify what a company does. `62.01Z` is software development, `56.10A` is restaurants, `65.12Z` is insurance. The full list has thousands of codes.
- Workforce ranges are updated annually and can lag reality. Use them as estimates, not exact counts.
- La Poste (SIREN 356000000) is the only entity that fails the Luhn checksum but is still valid. Any validation code should handle this edge case.
- Communes in Paris, Lyon, and Marseille are split by arrondissement. “PARIS 1ER ARRONDISSEMENT” is a commune in SIRENE, not just “PARIS.”
Whether you’re building an onboarding flow, a CRM integration, or a compliance check, the SIRENE database is the authoritative source for French company data. How you access it is up to you.
—
I’m building European Business API — a toolkit of 7 APIs for European B2B operations. Free tier available.