Skip to content
firmus.shdocs

Every page's text, headings and title. Code samples are not indexed.

Conventions

Base URL, versioning, country codes, record fields and pagination.

Everything is served from https://api.firmus.sh, under /v1. Every operation takes its country first, and answers JSON, except a file job's result, which is a CSV, and a deleted watchlist or segment, a 204 with no body. Reading is a GET; resolving, enriching and submitting a job are a POST, and the watchlists are created with POST, changed with PATCH and deleted with DELETE. A segment is run with POST, run again with GET, exported with POST and deleted with DELETE.

/v1/{country}/legal-entities/{registry_number}
/v1/{country}/legal-entities/{registry_number}/establishments
/v1/{country}/legal-entities/{registry_number}/events
/v1/{country}/establishments/{establishment_number}
/v1/{country}/suggest
/v1/{country}/resolve
/v1/{country}/enrich
/v1/{country}/jobs
/v1/{country}/jobs/{id}
/v1/{country}/jobs/{id}/result
/v1/{country}/watchlists
/v1/{country}/watchlists/{id}
/v1/{country}/watchlists/{id}/watches
/v1/{country}/events
/v1/{country}/segments
/v1/{country}/segments/{id}
/v1/{country}/segments/{id}/export

Treat unknown fields as something to ignore, not as an error — new ones appear inside v1. A change that cannot be made compatibly becomes /v2 instead.

This distinction decides which endpoint answers your question.

  • A legal entity is the company as the register knows it: the thing that has a name, a legal form, a registration date and one identifier for its whole life.
  • An establishment is one place where it operates: an office, a factory, a shop. It has its own identifier, its own address, its own activity and its own opening and closing dates.

One legal entity has one or more establishments, exactly one of which is the head office at any time. In France the two identifiers are visibly related — an establishment number begins with its company's registry number — but that is a French convention and not a rule this API relies on. Use the endpoints to move between them.

A company that has moved its head office keeps its registry number and gets a new establishment. For "the current address of this company", read the legal entity, whose registered_address is its head office's.

Countries and identifiers

The country is a two-letter, lower-case ISO 3166-1 code — fr today; see coverage. It selects which national data set answers rather than filtering a result, which is why it comes first in the path.

Identifiers are the register's own, published as strings and never as numbers: they have a fixed width and they can carry leading zeros. For France, registry_number is the nine-digit SIREN and establishment_number is the fourteen-digit SIRET. Do not derive one from the other — an establishment number beginning with its company's registry number is a French convention, not a rule of this API.

An identifier that no record carries is a 404. A country not served yet is also a 404, not a validation error, so your handler needs no change when it starts answering.

What a record looks like

Codes travel with their scheme

Classifications are published as a code and the scheme that resolves it:

"activity": { "code": "70.10Z", "scheme": "naf2008" }

The same code exists in several revisions with different meanings, and a register can change which one it publishes, so the pair is what keeps a stored value resolvable. legal_form, activity, workforce and size_category all follow it; workforce and size_category add the year the figure describes, which is not the year it was published.

/suggest is the exception. There a code travels with a label instead of a scheme — the label is denormalized into the search index, so a result needs no join. Read legal_form.label, not legal_form.scheme, on a suggestion.

Vocabularies

kind is one of company, sole_proprietor, non_profit, public_body or other, derived from the register's legal-form code, which stays in legal_form.code. A legal entity's status is active or ceased; an establishment's is active or closed.

Nulls mean "not published"

Every field is always present. A field the register does not publish for this record is null rather than absent, so a client can tell "the register does not have it" from "the response was truncated". Nested objects follow the same rule: activity is always there, and both of its members can be null.

country_data

Each canonical field exists because more than one country publishes it. Everything a register publishes that has no such column is handed through in country_data, under its original name, unchanged:

Five of the ten fields SIREN 552032534 carries, as the register publishes them:

"country_data": {
  "sigle": null,
  "denomination_usuelle1": null,
  "economie_sociale_solidaire": false,
  "activite_principale_naf25": "70.10Y",
  "unite_purgee": false
}

The keys are the source's, so they are French — the one place a response is not in English. Most of them are null here, which is the rule above holding rather than a truncated block: the register publishes no trading name for this company. activite_principale_naf25 is the same activity under the register's 2025 nomenclature, which is why it reads 70.10Y where activity.code reads 70.10Z — two vocabularies, one fact, and only the canonical one is promised to be comparable across countries.

country_data is null when a record has no such block, and on a list item it is always null: read a single establishment to get it.

data_updated_at

The register's own timestamp for that record, in UTC, to the second — not the time it was imported. It is how old the fact is.

It can be null for a record whose source carries no such date, and it does not move when an unchanged record is re-imported. Sources & coverage covers the lag between a register publishing a change and this API serving it.

partially_disclosed

true when the register publishes the record in reduced form and several fields are withheld by law. The record still exists and still answers 200; the fields it may not carry are null. See masked records before deciding what your code does about a missing name.

Pagination

The establishments list and a watchlist's numbers take page and per_page:

ParameterDefaultRange
page11 and up — no ceiling
per_page201 to 100

Both are clamped, never rejected. per_page=1000 returns 100 results, page=0 returns page 1, and a value that is not a number falls back to the default. A request does not fail over page or per_page.

Their responses carry the counts:

"pagination": { "page": 2, "per_page": 1, "total_count": 3, "total_pages": 3 }

total_pages is 0 for an empty collection, and a page past the end is an empty list rather than a 404. The order is stable, so two pages can never overlap or skip a row.

The events list is the exception: events keep being recorded while you read it, so it pages with a cursor and carries no counts. Pass pagination.next_cursor back as cursor until it is null; per_page is clamped the same way. Its filters are not clamped: one it cannot read is refused with 400 invalid_request.

The watched-events feed, GET /v1/{country}/events, pages with a cursor too, and differs on one point: it is a feed you keep reading, so next_cursor is never null once it holds an event. Stop on has_more: false, keep next_cursor, and poll with it later; see read watched events.

Next

  • Errors — the error object and every code a client can meet.
  • Rate limits — the four allowances, and the headers that report them.