You have a spreadsheet of French customers or suppliers with their SIREN numbers, and you want to know which ones are valid, which companies are still active, and which have ceased. Checking them one by one in a browser takes an afternoon. This tutorial gives you a short Python script that does it for you, using the State's free search API, while respecting its rate limit. We ran it on real numbers on 10 October 2026; the output below is what it returned.
The API's limits come from its documentation, read on 10 October 2026.
What you'll learn
- What the State's search API allows, and its limits
- A complete, tested Python script (standard library only)
- How it validates numbers before calling
- How it handles the rate limit and HTTP 429
- How to read the results, and what "not found" means
The API you'll use
API Recherche d'entreprises is the State's free company search API, which powers the Annuaire des Entreprises. According to its documentation:
- No key and no account are needed.
- It accepts at most 7 requests per second per IP address, and 30 per second per network (ASN). Above that it answers HTTP 429 with a
Retry-Afterheader. - The rate is a maximum, not a guarantee, and access can be restricted under heavy load.
- It recommends sending an explicit User-Agent.
- It doesn't return non-diffusible companies.
For the background on this API and the other official option, see free SIREN APIs: the official options.
The input
A CSV file with one SIREN per line, spaces allowed:
441639465
794598813
4416 3946
552100554
The script
Save it as check_sirens.py. It uses only Python's standard library (Python 3.10 or later).
import csv
import json
import sys
import time
import urllib.error
import urllib.request
from datetime import datetime, timezone
API = "https://recherche-entreprises.api.gouv.fr/search?q={}"
HEADERS = {"User-Agent": "my-company-siren-check/1.0 (+https://example.com/contact)"}
def luhn_ok(siren: str) -> bool:
if len(siren) != 9 or not siren.isdigit():
return False
total = 0
for i, ch in enumerate(reversed(siren)):
d = int(ch) * (2 if i % 2 else 1)
total += d - 9 if d > 9 else d
return total % 10 == 0
def lookup(siren: str) -> dict | None:
req = urllib.request.Request(API.format(siren), headers=HEADERS)
for attempt in range(5):
try:
with urllib.request.urlopen(req, timeout=20) as r:
data = json.load(r)
return next((c for c in data["results"] if c["siren"] == siren), None)
except urllib.error.HTTPError as e:
if e.code == 429:
time.sleep(int(e.headers.get("Retry-After", "1")))
continue
raise
raise RuntimeError(f"{siren}: still rate-limited after 5 tries")
def main(src: str, dst: str) -> None:
with open(src, newline="", encoding="utf-8") as f:
sirens = [row[0].replace(" ", "") for row in csv.reader(f) if row]
with open(dst, "w", newline="", encoding="utf-8") as f:
out = csv.writer(f)
out.writerow(["siren", "result", "name", "status", "closed_on", "checked_at"])
for siren in sirens:
checked = datetime.now(timezone.utc).isoformat(timespec="seconds")
if not luhn_ok(siren):
out.writerow([siren, "invalid_number", "", "", "", checked])
continue
company = lookup(siren)
time.sleep(0.25) # stay well under 7 requests per second
if company is None:
out.writerow([siren, "not_found_in_public_data", "", "", "", checked])
continue
status = {"A": "active", "C": "ceased"}.get(company["etat_administratif"], company["etat_administratif"])
out.writerow([siren, "found", company["nom_complet"], status, company.get("date_fermeture") or "", checked])
if __name__ == "__main__":
main(sys.argv[1], sys.argv[2])
Run it:
python check_sirens.py sirens.csv results.csv
Replace the contact address in the User-Agent with your own site.
What it does, step by step
- Cleans each number: removes spaces.
- Validates it locally: a SIREN has nine digits, and according to INSEE its ninth digit is a Luhn check digit. A number that fails is marked
invalid_numberand never sent to the API, which saves a call and catches typos. The check is explained in SIREN, SIRET and VAT formats. - Searches the API by number and keeps only the result whose
sirenmatches exactly, so a partial match can't slip in. - Waits 0.25 seconds between calls, about 4 requests per second, well under the documented 7.
- Handles HTTP 429: waits the number of seconds in
Retry-After, then tries again, up to five times. - Writes one row per number, with the time of the check.
The output
This is what the script returned on 10 October 2026:
| siren | result | name | status | closed_on | checked_at |
|---|---|---|---|---|---|
| 441639465 | found | RENAULT (RENAULT) | active | 10 Oct 2026, 12:57:01 UTC | |
| 794598813 | found | DOCTOLIB | active | 10 Oct 2026, 12:57:02 UTC | |
| 44163946 | invalid_number | 10 Oct 2026, 12:57:02 UTC | |||
| 552100554 | found | PEUGEOT SA | ceased | 16 Jan 2021 | 10 Oct 2026, 12:57:02 UTC |
(The CSV writes the dates in ISO format; they're shown here in words.)
Three things to notice:
- PEUGEOT SA is ceased, since 16 January 2021. If it were in your active-supplier list, this is the row that matters.
- 4416 3946 is invalid: Renault's SIREN with a digit missing has eight digits, so it never reached the API. A changed digit would be caught by the check digit in the same way.
- Every row has its check time. A status without a date is an opinion; with a date, it's a fact you can show later.
Reading "not found"
not_found_in_public_data means the API returned no company with that exact SIREN. It doesn't prove the business doesn't exist: the API leaves out non-diffusible businesses, typically individual entrepreneurs who opted out of publication. Send these rows to a person, who can ask the company for proof. See why some French companies don't appear in public searches.
Before you run it on your real file
A few practical checks that save time:
- Clean the column first. Numbers exported from spreadsheets sometimes lose leading zeros or gain decimals. A SIREN is nine digits, always; the script marks anything else as invalid.
- Deduplicate. The same SIREN often appears many times in a customer file. Check each number once and join the result back.
- Separate SIRET columns. If your file holds SIRETs, take the first nine digits as the SIREN, and keep the SIRET for site-level checks.
- Start small. Run 20 rows, read the output, then run the rest.
- Budget the time. At about four requests per second, 1,000 numbers take a little over four minutes, plus any waits after a 429.
Reading the results with your team
The output is a decision list, not just data:
foundandactive: nothing to do, but keep the check date.foundandceased: close or review the account, and find out whether a successor company exists.invalid_number: a typo in your file. Ask the customer for the right number.not_found_in_public_data: a person reviews it and asks for proof.
Keep the input file, the output file and the date of the run together: the three of them are your evidence of what you checked. If someone later asks why an account was closed or kept open, that evidence answers in seconds.
Taking it further
- Add columns you need from the result, such as the head-office address (
siege) or the activity code. Print one result as JSON to see the fields. - Run it on a schedule, monthly or before renewals, and compare with the previous run to spot companies that changed status.
- Keep the output files: they're your record of what you checked and when.
- Expect slower runs on shared cloud IPs, where the per-network limit applies.
For what to do with the ceased ones, see your customer or supplier is "radiée" or "cessée", and for cleaning the rest of your file, cleaning your CRM with official registers.
Fuentio's API will answer the same question for France and Spain in one format, with the source and check time on every row. It isn't open yet: join the waitlist below to hear the day it opens.
See what we cover in France and Spain.
Limits. The API's limits come from its documentation, read on 10 October 2026; they can change, so check them before running large batches. The output shown is what the script returned on 10 October 2026. The script is an example: test it on your own data and respect the API's terms. Not legal advice.
Frequently asked questions
Is there a free way to check many SIRENs at once?
Yes. The State's search API is free and needs no key; the script above checks a CSV within its rate limit.
How fast can I call the API?
At most 7 requests per second per IP address, and 30 per second per network, according to its documentation. The script makes about 4 per second.
Why does a valid SIREN return "not found"?
The company may be non-diffusible: the API doesn't return those. Ask the company for proof.
Do I need to validate SIRENs before calling?
It's not required, but the Luhn check catches most typos for free and saves calls.
Sources
- API Recherche d'entreprises, documentation (read on 10 October 2026): recherche-entreprises.api.gouv.fr
- INSEE, definition of the SIREN number: insee.fr
- Annuaire des Entreprises: annuaire-entreprises.data.gouv.fr
