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 looks up companies from Node.js with the built-in fetch, checks numbers locally, handles errors, and runs batches of up to 100. You need Node.js 20+ and a RapidAPI account for the French Company & Establishment API. The free plan gives 100 requests a month; check the plan table on the listing, because quotas can change.
Subscribe to the free plan, then copy your key from the listing's code snippet. The host is french-company-establishment-api.p.rapidapi.com. Keep the key in an environment variable:
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.
export function luhnOk(number) {
if (!/^\d+$/.test(number) || ![9, 14].includes(number.length)) return false;
let total = 0;
for (let i = 0; i < number.length; i++) {
let digit = Number(number[number.length - 1 - i]);
if (i % 2 === 1) {
digit *= 2;
if (digit > 9) digit -= 9;
}
total += digit;
}
return total % 10 === 0;
}
luhnOk("110014016") returns true. luhnOk("123456789") returns false, because its check digit is wrong.
Step 2: Make a request
This helper sends a GET with the two RapidAPI headers and retries transient errors:
const HOST = "french-company-establishment-api.p.rapidapi.com";
const BASE = `https://${HOST}`;
const HEADERS = { "X-RapidAPI-Key": process.env.RAPIDAPI_KEY, "X-RapidAPI-Host": HOST };
async function getJson(path, params = {}, retries = 2) {
const url = new URL(BASE + path);
for (const [key, value] of Object.entries(params)) url.searchParams.set(key, value);
for (let attempt = 0; ; attempt++) {
const response = await fetch(url, { headers: HEADERS, signal: AbortSignal.timeout(20000) });
if ([429, 502, 503].includes(response.status) && attempt < retries) {
await new Promise(resolve => setTimeout(resolve, 2 ** attempt * 1000));
continue;
}
const text = await response.text();
let body;
try { body = JSON.parse(text); } catch { body = { error: text.slice(0, 200) }; }
return { status: response.status, body };
}
}
Retry 429, 502 and 503 after a wait; not 400 or 404, which would fail the same way.
Step 3: Look up a company
async function companyBySiren(siren) {
if (!luhnOk(siren)) throw new Error(`${siren} fails the check digit; fix the number first`);
const { status, body } = await getJson(`/v1/companies/${siren}`);
if (status === 200) return body.company;
if (status === 404) return null; // not in the register; body.status says "not_found"
throw new Error(`HTTP ${status}: ${body.error ?? JSON.stringify(body)}`);
}
A 200 response carries 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 withheld.
Step 4: Search by name
The search takes a name and an optional postcode and returns rows, each with its own status and company. It does not pick a winner among several matches, so you decide:
async function searchByName(name, postcode) {
const params = { q: name };
if (postcode) params.postcode = postcode;
const { status, body } = await getJson("/v1/search", params);
if (status !== 200) throw new Error(`HTTP ${status}: ${body.error ?? JSON.stringify(body)}`);
return body.results.map(row => row.company).filter(Boolean);
}
A postcode ranks nearby companies higher but does not filter them out.
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:
async function companiesBatch(sirens) {
const valid = sirens.filter(luhnOk);
const invalid = sirens.filter(s => !luhnOk(s));
const rows = [];
for (let start = 0; start < valid.length; start += 100) {
const chunk = valid.slice(start, start + 100);
const response = await fetch(`${BASE}/v1/companies/batch`, {
method: "POST",
headers: { ...HEADERS, "Content-Type": "application/json" },
body: JSON.stringify({ ids: chunk }),
signal: AbortSignal.timeout(60000),
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
rows.push(...(await response.json()).results);
}
for (const s of invalid) rows.push({ input: { query: s }, ok: false, status: "invalid_input" });
return rows;
}
Each row has its own status, so one bad number never fails the batch.
Putting it together
const sirens = ["110014016", "794598813", "356000000"];
for (const siren of sirens.slice(0, 2)) {
const company = await companyBySiren(siren);
if (!company) {
console.log(siren, "not found");
continue;
}
const address = company.hq_address?.line ?? "address withheld";
console.log(siren, company.legal_name, company.status, address);
}
for (const row of await companiesBatch(["110014016", "123456789", "356000000"])) {
console.log(row.input.query, row.status);
}
Save it as .mjs and run it with node, because the code uses top-level await.
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 establishment was not confirmed; the body then still includes the company. -
429means you are over your plan's rate or quota. Wait, then retry. -
502or503means the upstream source or the service is temporarily unavailable.
Rotate the key 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)