How to scrape salary ranges from Ashby job boards with Python
Pull startup job postings with pay ranges from Ashby boards, annualised to yearly amounts, with currency and remote filters and an only-new feed.
datagrit › Guides › French Company Finder - Sirene Financials
GuideBuild lists of French companies from the official Sirene register, filtered by net result, revenue, NAF code and location, using Python or cURL.
Published 2026-10-01 · uses the French Company Finder - Sirene Financials Actor
Say you sell accounting software to French SMEs in the Lyon area, or you are an investor looking for loss-making bakery chains that might be open to a deal, or a procurement team checking whether a supplier's SIREN still belongs to an active company. All three questions have the same shape: give me French companies that match an activity, a place, a size and a financial profile, as a table.
France publishes the raw material for this as open data, including revenue and net result for companies that file public accounts. What it does not give you is a ready-made margin, or a location filter you can trust without checking every row. That gap is what this post walks through: where the data lives, the traps in it, and how to get a clean, filtered company list into Python.
The Sirene register identifies French companies by a 9-digit SIREN number and each of their establishments by a 14-digit SIRET number. The state exposes it through a free public search API at recherche-entreprises.api.gouv.fr, and every company has a public page on annuaire-entreprises.data.gouv.fr. For companies that file their accounts publicly, the API also carries the latest revenue and net result, and it accepts revenue and net result ranges as search parameters.
Working with that API directly, you run into these issues:
Retry-After.The French Company Finder Actor on Apify wraps the API and handles these. It requests up to 100 matching establishments per company, checks the head office first, and re-checks every filter on every record. With Only active companies on (the default), a company is returned only if it has an open establishment in your location. It derives isEmployer, exposes the revenue and net result ranges as input fields and computes the net margin on each row.
Software companies with an open establishment in the Rhône department, revenue above one million euros, profitable only:
{
"queries": ["logiciel"],
"departments": ["69"],
"activityCodes": ["62.01Z", "62.02A"],
"minRevenue": 1000000,
"minNetResult": 1,
"onlyActive": true,
"employersOnly": true,
"includeDirectors": false,
"maxItems": 100
}
Notes on the fields:
queries are searched separately; leave them out to search on filters alone.sirens accepts SIREN or SIRET numbers. Number lookups ignore the filters and also return closed companies, which is what you want for KYB checks.minNetResult: 1 keeps profitable companies; maxNetResult: 0 finds loss-making ones.companySizes takes PME, ETI or GE; employeeBands and legalForms take INSEE codes (for example 5710 for SAS).headOfficeOnly: true requires the head office itself to be in the location.includeDirectors is off by default because names of directors are personal data.
from apify_client import ApifyClient
client = ApifyClient("YOUR_APIFY_TOKEN")
run_input = {
"queries": ["logiciel"],
"departments": ["69"],
"minRevenue": 1000000,
"minNetResult": 1,
"maxItems": 100,
}
run = client.actor("datagrit/french-company-finder").call(run_input=run_input)
for c in client.dataset(run["defaultDatasetId"]).iterate_items():
if c.get("found"):
print(c["siren"], c["name"], c["revenue"], c["netMarginPct"], c["matchedEstablishmentCity"])
A KYB check on one company by SIREN:
curl -X POST \
"https://api.apify.com/v2/acts/datagrit~french-company-finder/run-sync-get-dataset-items?token=YOUR_APIFY_TOKEN" \
-H "Content-Type: application/json" \
-d '{"sirens": ["380059113"]}'
If a number is not in the public register, the run adds a status row for it (not charged) and names it in the status message. If the input has only numbers and none of them exists, the run fails, so a scheduled check cannot pass silently on a typo.
One row per company (or per establishment for SIRET lookups). The fields you will use most:
| Field | Example | Meaning |
|---|---|---|
siren | 380059113 | Company identifier |
name | SERVICES LOGICIELS D'INTEGRATION BOURSIERE - SLIB (SLIB) | Display name |
activityCode | 62.02A | Main NAF activity code |
companySize | GE | PME, ETI or GE |
employeeBand | 100-199 employees | INSEE band label |
revenue / revenueYear | 25256877 / 2025 | Latest published revenue in EUR |
netResult | 897017 | Latest net result in EUR |
netMarginPct | 3.6 | Net result divided by revenue |
city | PARIS | Head office city |
matchedEstablishmentCity | LYON | The open establishment that matched your location |
The example shows why the matched establishment matters: the company is headquartered in Paris but was returned for a Rhône search because it runs an open establishment in Lyon. Rows also include vatNumber, coordinates of the head office, establishmentsOpen, creationDate, isActive, isSocialEconomy, isEmployer and a sourceUrl to the public company page.
Schedule a monthly run per target region and NAF code, export as CSV and upsert into your CRM using siren as the key. Because every run reads the live register, companies that cease activity drop out of the next list when onlyActive is on, and new ones appear.
Pass the supplier's SIREN through the cURL call above in your onboarding flow. Check isActive, closureDate, legalForm and vatNumber against what the supplier gave you. A SIRET lookup returns exactly that establishment, including whether it is still open.
import pandas as pd
rows = [c for c in client.dataset(run["defaultDatasetId"]).iterate_items() if c.get("found")]
df = pd.DataFrame(rows)
with_accounts = df.dropna(subset=["revenue", "netResult"])
print(f"{len(with_accounts)} of {len(df)} companies publish accounts")
by_band = (
with_accounts.groupby("employeeBand")
.agg(companies=("siren", "count"), median_margin=("netMarginPct", "median"))
.sort_values("companies", ascending=False)
)
print(by_band)
revenue and netResult are empty for them, and revenue or net result filters exclude them. The status message reports how many returned companies have revenue.matchedEstablishmentsListed of 100 means 100 or more.includeDirectors on, and their use is your responsibility under data protection law.The Actor reads only the open data the French state publishes through its public API; it does not log in or bypass access controls.
Profit filters and location checks you can trust turn Sirene into a usable prospecting and KYB source; pricing is pay per result, and the Actor is on Apify with the full field list in the documentation.
Pull startup job postings with pay ranges from Ashby boards, annualised to yearly amounts, with currency and remote filters and an only-new feed.
Monitor open roles at a list of companies across 8 applicant tracking systems in one schema, with numeric salaries and a daily only-new feed.
Collect Greenhouse job postings with published pay ranges per location zone, annualised and filtered by currency, using Python, cURL or pandas.