DEV Community

Amaury
Amaury

Posted on Edited on

Look up French companies by SIREN in Python

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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)}")
Enter fullscreen mode Exit fullscreen mode

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")]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"])
Enter fullscreen mode Exit fullscreen mode

Errors you will meet

  • 400 means the number is not a valid SIREN or SIRET.
  • 401 means the RapidAPI key or host is wrong, or your subscription is not active.
  • 404 means 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.
  • 429 means you are over your plan's rate or quota. Wait, then retry.
  • 502 or 503 means 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)