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

