DEV Community

Amaury
Amaury

Posted on Edited on

Look up French companies by SIREN in JavaScript

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"
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.

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

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

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

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

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

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

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

Top comments (0)