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

  • 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 establishment was not confirmed; the body then still includes the company.
  • 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.

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