A SIREN is the nine-digit number that identifies a French company, and a SIRET adds five digits for one of its establishments. This tutorial shows how to look up companies from Python with requests, check the numbers locally before you spend a request, handle the errors the API returns, and run a batch of up to 100 numbers in one call.
You need Python 3.10+, pip install requests, and a RapidAPI account. The API is listed as French Company & Establishment API on RapidAPI. The free plan gives 100 requests a month. Check the plan table on the listing, because quotas can change.
Open the listing, subscribe to the free plan, and copy your key from the code snippet. The host is french-company-establishment-api.p.rapidapi.com. Store the key in an environment variable rather than in your script:
export RAPIDAPI_KEY="your-key-here"
Step 1: Check the number before you send it
Every SIREN and SIRET ends with a check digit computed with the Luhn algorithm. A failed check is usually a typo, and a local check costs nothing.
The function below works for both SIREN (9 digits) and SIRET (14 digits):
def luhn_ok(number: str) -> bool:
if not number.isdigit() or len(number) not in (9, 14):
return False
total = 0
for position, char in enumerate(reversed(number)):
digit = int(char)
if position % 2 == 1:
digit *= 2
if digit > 9:
digit -= 9
total += digit
return total % 10 == 0
luhn_ok("110014016") returns True. luhn_ok("123456789") returns False, because its check digit is wrong.
Step 2: Make a request
Every request needs the two RapidAPI headers. This helper sends a GET, retries transient errors, and returns the status and parsed body:
import os
import time
import requests
HOST = "french-company-establishment-api.p.rapidapi.com"
BASE = f"https://{HOST}"
HEADERS = {"X-RapidAPI-Key": os.environ["RAPIDAPI_KEY"], "X-RapidAPI-Host": HOST}
def get_json(path, params=None, retries=2):
for attempt in range(retries + 1):
response = requests.get(BASE + path, params=params, headers=HEADERS, timeout=20)
if response.status_code in (429, 502, 503) and attempt < retries:
time.sleep(2 ** attempt)
continue
try:
body = response.json()
except ValueError:
body = {"error": response.text[:200]}
return response.status_code, body
Retry 429, 502 and 503 after a short wait; do not retry 400 or 404, which return the same answer again.
Step 3: Look up a company
def company_by_siren(siren: str):
if not luhn_ok(siren):
raise ValueError(f"{siren} fails the check digit; fix the number first")
status, body = get_json(f"/v1/companies/{siren}")
if status == 200:
return body["company"]
if status == 404:
return None # not in the register; body["status"] says "not_found"
raise RuntimeError(f"HTTP {status}: {body.get('error', body)}")
A 200 response has ok, a status of found, and a company object. The fields you will use most are legal_name, status, hq_siret, hq_address.line, naf and creation_date. The address is null when the source withholds it.
Step 4: Search by name
The search endpoint takes a name and an optional postcode. It returns a list of rows, each with its own status and company. It does not pick a single winner when several companies match, so you decide what to do with the list:
def search_by_name(name: str, postcode: str | None = None):
params = {"q": name}
if postcode:
params["postcode"] = postcode
status, body = get_json("/v1/search", params)
if status != 200:
raise RuntimeError(f"HTTP {status}: {body.get('error', body)}")
return [row["company"] for row in body["results"] if row.get("company")]
A postcode ranks nearby companies higher but is not a filter, so distant ones can still appear.
Step 5: Look up many numbers in one call
The batch endpoint takes up to 100 SIRENs in a single request, and the gateway counts it as one request. Check the numbers locally first, then send only the valid ones:
def companies_batch(sirens):
valid = [s for s in sirens if luhn_ok(s)]
invalid = [s for s in sirens if not luhn_ok(s)]
rows = []
for start in range(0, len(valid), 100):
chunk = valid[start:start + 100]
response = requests.post(BASE + "/v1/companies/batch", json={"ids": chunk}, headers=HEADERS, timeout=60)
if response.status_code != 200:
raise RuntimeError(f"HTTP {response.status_code}")
rows.extend(response.json()["results"])
rows.extend({"input": {"query": s}, "ok": False, "status": "invalid_input"} for s in invalid)
return rows
Each row in the result has its own ok and status, so one bad number never fails the whole batch.
Putting it together
if __name__ == "__main__":
for siren in ["110014016", "794598813"]:
company = company_by_siren(siren)
if company is None:
print(siren, "not found")
continue
address = (company.get("hq_address") or {}).get("line", "address withheld")
print(siren, company["legal_name"], company["status"], address)
for row in companies_batch(["110014016", "123456789", "356000000"]):
print(row["input"]["query"], row["status"])
Errors you will meet
-
400means the number is not a valid SIREN or SIRET. -
401means the RapidAPI key or host is wrong, or your subscription is not active. -
404means the company is not in the register. On the SIRET endpoint it can also mean the company exists but the establishment was not confirmed; the body then includes the company, so read it before deciding. -
429means you are over your plan's rate or quota. Wait, then retry. -
502or503means the upstream source or the service is temporarily unavailable.
Keep the key out of code you share, and rotate it in RapidAPI if it leaks.
Disclosure: I maintain this API. It reuses public data from the French company register through the government search API; it is not an official government service. This article was prepared with AI assistance.
Listing: https://rapidapi.com/amaury1729/api/french-company-establishment-api
Top comments (0)