Skip to content
firmus.shdocs

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

Changelog

Every change to the API and to the data behind it, newest first.

Newest first. Every entry names the endpoints it touches and says whether it is additive or breaking.

2026-11-02 — watch a saved segment: the companies that enter it, and those that leave

Additive for the operations, and one change to the feed's items to read before you deploy. Three new operations, two new MCP tools, a new field on every item of the feed, a new kind of webhook body and header, and a fourth meaning for 402 quota_exceeded.

  • PUT /v1/{country}/watchlists/{id}/segments/{segment_id}: a list follows a saved segment (201; 200 when sent again, which changes include_leavers). After each daily update of the segment index, the companies that entered it, and with include_leavers: true those that left it, are delivered through the list's webhooks, digest and feed. Nothing already in the segment is delivered. Not counted as a call. See follow a segment.
  • GET /v1/{country}/watchlists/{id}/segments and DELETE /v1/{country}/watchlists/{id}/segments/{segment_id}: what a list follows, with the allowance, and stopping (204, or 404 if the list does not follow it). Not counted as calls. Each followed segment carries segment.watch (paused_at, failures, last_failed_at): a segment whose daily update fails three times in a row is paused, and delivers nothing until it is followed again or resumed in the console, from a new baseline.
  • GET /v1/{country}/events: every item now carries type, event on the items you already read, and segment.entered or segment.left on the new ones, which hold segment_change instead of event. A reader that assumes event on every item breaks on the first segment item: branch on type. Nothing changes for a list that follows no segment. See read watched events.
  • Webhooks: segment bodies have type segment.entered or segment.left and the header Firmus-Segment-Change-Id instead of Firmus-Event-Id. A partially disclosed company, a sole trader of a segment saved without the acknowledgement, or a company no longer in the registry is never sent; its delivery is settled withheld in the console's delivery log. A company that could not be read in full is not withheld: the delivery stays pending and is retried, and the feed ends its page before it with has_more: true.
  • Segment items are kept 90 days; older ones leave the feed.
  • 402 quota_exceeded now also answers a follow beyond the plan's watched segments (1 on Free, 10 on Pro, 100 on Scale), without Retry-After.
  • MCP: watch_segment and unwatch_segment; watched_events returns the segment items too. See Use Firmus from Claude.

2026-11-02 — segments: count, sample and export companies by explicit filters

Additive. Five new operations, two new MCP tools, three new fields on a job, a new error code, and two more meanings for 402 quota_exceeded. No existing answer changes.

  • POST /v1/{country}/segments: the exact count of the active companies matching explicit filters, four facets and a sample of at most ten, with the filters as interpreted. save: true stores the segment (201). One call. See run a segment.
  • GET /v1/{country}/segments, GET|DELETE /v1/{country}/segments/{id}: the saved segments and the allowance (not a call), a saved segment run again (one call), deleted (not a call).
  • POST /v1/{country}/segments/{id}/export: every company the segment may list, or the first limit in SIREN order, as a file job of operation segment_export (202). Partially disclosed units are never listed, and sole traders only with the acknowledgement sole-traders-2026-11-01.
  • GET /v1/{country}/jobs/{id} gains counts.segment_export, exported_rows and segment, all null on a file job.
  • 402 quota_exceeded now also answers a save beyond the plan's saved segments (3 on Free, 100 on Pro, 1 000 on Scale), without Retry-After, and an export beyond the month's exported rows (1 000, 25 000, 250 000), with it.
  • 503 segment_index_unavailable: a server that cannot count segments yet.
  • MCP: segment and segment_export. See Use Firmus from Claude.

2026-11-02 — watch named companies: watchlists, webhooks, digests and a feed

Additive. Nine new operations, three new MCP tools, and a third meaning for 402 quota_exceeded. No existing answer changes.

  • GET|POST /v1/{country}/watchlists and GET|PATCH|DELETE /v1/{country}/watchlists/{id}: lists of SIRENs and SIRETs to follow, each with its filters (severity_floor, include_kinds, exclude_kinds, include_establishments, include_bulk) and an email digest, daily or weekly. Not counted as calls. See watch companies.
  • GET|POST|DELETE /v1/{country}/watchlists/{id}/watches: the numbers of a list, added up to 1 000 at a time from numbers or from a finished file job's job_id, each answered added, already_watched, not_found or invalid. Not counted as calls.
  • GET /v1/{country}/events: the watched events of every list, oldest first, read from a cursor. One call per page.
  • Webhooks, set up in the console: each watched event POSTed to your endpoints, signed in Firmus-Signature (t=…,v1=…, HMAC-SHA256), retried for 24 hours, then kept and replayable from the console. A legal notice first recorded without its values is delivered again with the same event id and trigger: "retyped" once it is readable.
  • 402 quota_exceeded now also answers an add that would take the organization past its watched companies: 5 on Free, 1 000 on Pro, 10 000 on Scale. It carries no Retry-After, unlike the two monthly allowances. See errors.
  • MCP: watch, unwatch and watched_events. See Use Firmus from Claude.

2026-11-01 — the source of each field group, in the API and in the file

Additive, with one changed answer. A new key in sources, two new columns at the end of a file job that enriches, and partially_disclosed now also says true for a masked head office.

  • POST /v1/{country}/enrich: every enriched row's sources gains legal, { "registry": "bodacc", "published_on": "2026-06-02" }, when at least one legal field was selected, and null otherwise. published_on is the bulletin date of the latest notice behind the selected legal fields, null when there is none. registry: "insee_sirene" is the source of every value but the events and the legal fields.
  • POST /v1/{country}/enrich: a row's partially_disclosed is now true when the company, or its head office when one was read (a head-office field or events selected), is partially disclosed, so it says whether something on that row is masked. A disclosed company whose head office is masked used to answer false and now answers true; no other row's value changes, and the legal fields still follow the company's status alone. The full card's partially_disclosed is unchanged and describes the company. See enrich.
  • File jobs that enrich gain two columns after firmus_partial, at the end of the line: firmus_partially_disclosed, the row's partially_disclosed as true or false, and firmus_sources, insee_sirene or insee_sirene; bodacc 2026-06-02 when a legal field was selected (insee_sirene; bodacc with no notice behind it). Both are empty on a row that was not enriched. Every earlier column keeps its position, so firmus_partial is no longer last on these jobs: a program that finds it by counting from the end must count two further. A job submitted before this change keeps the columns it was created with, without the new two. See submit a job.
  • MCP: enrich names the BODACC beside INSEE Sirene in each row's sources.

2026-11-01 — inconclusive job rows, and the timeline of a removed company

Additive, with one changed answer. Two new fields and a new last column; a removed company's timeline is now served instead of a 404.

  • File jobs search a row whose search ran out of time partway once more, with a fresh budget, before writing it. The result gains a last column, firmus_partial, always present and after every other firmus_ column, the legal ones included: true on a row still not conclusive after that, whose not_found or ambiguous is worth submitting again. GET /v1/{country}/jobs/{id} gains progress.partial_rows, the count of those rows. A program that reads the result by column position from the end must account for the new column. A job submitted before this change keeps the columns it was created with, without the new one. See submit a job.
  • GET /v1/{country}/legal-entities/{registry_number}/events gains removed, true when the legal entity is no longer in the register and its removal is recorded. Its timeline used to answer 404 not_found once the entity was removed; it now answers 200 with removed: true, every event's values withheld and a BODACC event's source_ref null. A registry number with no record that the log knows through other events is served the same way, with removed: false. 404 now means neither the register nor the log knows the number, and is still answered before the filters are read. See list events.
  • MCP: job_status reports the inconclusive rows, and timeline says when a company left the registry.

Additive. New event kinds, a new source and three new enrich fields; no existing field moves. A client that ignores kinds it does not recognise, as list events asks, needs no change.

  • GET /v1/{country}/legal-entities/{registry_number}/events now also lists the notices the BODACC published about a company in the trade register, with source: "bodacc" and the notice's id as source_ref: insolvency proceedings opened, converted, under a plan, closed or reopened, conciliation, personal recovery, a business sold or acquired, removal from the trade register, registration, and every other notice as legal_entity.legal_notice_published. Each kind has a .corrected and a .cancelled variant, whose payload.corrects.notice_id names the notice it corrects; a court retracting its opening is a legal_entity.insolvency.opened.cancelled that names none. The list is the raw log: a cancelled event stays beside its cancellation. Every payload carries bulletin, published_on and url, except on a company that was partially disclosed or not yet in the register when the notice was read, whose payload is {}, or corrects alone.
  • legal_entity.accounts_filed is off by default: the list leaves it out unless kind names it. The gazette's history since January 2008 is bulk, so it is listed only with include_bulk=true.
  • POST /v1/{country}/enrich has a legal theme: collective_proceeding, last_accounts_filed and last_business_transfer, read from those notices with corrections applied, each with its source and notice_url. The default event_kinds is every legal-entity kind but filed accounts. See enrich.
  • POST /v1/{country}/jobs writes the legal fields as eight new firmus_ columns when they are selected.
  • MCP: timeline and enrich describe the new kinds and fields to an assistant.

2026-11-01 — file jobs

Additive. Three new operations and a new error code; nothing existing moves.

  • POST /v1/{country}/jobs takes a whole CSV — a multipart file, a JSON csv string or a JSON rows array — and resolves it, enriches it, or both, in the background. It answers 202 with a job. The columns are detected from the headers unless mapping names them. A job holds up to 1 000 rows and 2 MB on Free, 25 000 rows and 20 MB on Pro, 100 000 rows and 50 MB on Scale, and is checked against the month's agent rows for the whole file before it is admitted. See submit a job.
  • GET /v1/{country}/jobs/{id} reads its status, progress and counts per step, and once it has finished gives result_url and a download_url that needs no key.
  • GET /v1/{country}/jobs/{id}/result downloads the result: your file, with its delimiter and byte-order mark, and the firmus_ columns appended. Neither read is counted as a call. A job and its files are kept seven days.
  • 409 job_not_finished is new, answered by the result while the job is queued or running. A failed job serves the rows it answered before it stopped. invalid_request, quota_exceeded, public_key_forbidden and not_found now also answer /jobs.
  • MCP: submit_job and job_status give an assistant the same jobs, and resolve and enrich now send it to them for a whole file.

Additive. A new operation; nothing existing moves.

  • GET /v1/{country}/legal-entities/{registry_number}/events lists what the register recorded about a legal entity and its establishments — ceased, renamed, head office moved, an establishment opened or closed — newest first, paged with a cursor, filterable by kind, severity and date. See list events.
  • invalid_request now also answers an /events filter or cursor that cannot be read.

2026-08-13 — a discovery root, and documentation_url on errors

Additive. Neither change removes or renames anything, and no existing field moves.

  • GET /v1 answers a discovery document: what this API is, where the contract and this documentation live, how to authenticate, what the free tier allows, and every endpoint with its summary and its parameters. It needs no key, for the reason GET /v1/openapi does not. Every field of it is derived from the contract, so the two cannot disagree.
  • documentation_url joins the error object for every code this page explains, pointing at the section that explains it. It is absent from any other refusal, so treat it as optional — and keep branching on code, which is the field that carries the meaning.

v1

The initial contract. Four operations, all GET:

  • GET /v1/{country}/legal-entities/{registry_number}
  • GET /v1/{country}/legal-entities/{registry_number}/establishments
  • GET /v1/{country}/establishments/{establishment_number}
  • GET /v1/{country}/suggest

plus GET /v1/openapi, which serves the contract itself and needs no key. GET /v1 arrived later; see the entry above.

France (fr) is the country served. See sources & coverage.

How changes are made

Additive changes happen inside v1, without notice. A new field on a response, a new optional query parameter, a new country code, a new value in a vocabulary like kind. Write clients that ignore fields they do not recognise, and none of these will reach you as a problem.

Breaking changes get a new version in the path. Removing or renaming a field, changing the type or meaning of one, removing an endpoint, or making an optional parameter required. v1 does not change under you: it becomes v2 beside it.

Two things sit in between, and both are announced here rather than assumed:

  • Data changes. A register changing what it publishes is not an API change, but it can be indistinguishable from one — a nomenclature moving to a new revision is the clearest example. Those get an entry naming the field and the effective date.
  • New status codes. A new refusal reason arrives as a new code in the same error object. Handle an unrecognised code by its HTTP status, and this stays additive.

The contract at GET /v1/openapi is the machine-readable version of all of this.