GET /company/peppol

Answers one question: is this company reachable on the Peppol network - under which participant identifiers, and for which document types? The answer is served offline from the ingested complete public export of the Peppol directory: a dated photo (coverage.directory_as_of), never a live SMP lookup, with zero network at call time.

Use it as the pre-action check before an agent sends an electronic invoice - a hook that matters with the French e-invoicing reform (first deadline September 2026) approaching: before emitting an e-invoice, check that the counterparty can actually receive it on Peppol, and in which formats. It pairs naturally with POST /invoice/generate and POST /invoice/validate (the document), and with GET /company/resolve and GET /company/vat (the counterparty). See the live /catalog for the authoritative endpoint listing and price.

x402 golden rule: the agent pays for a directory lookup that ran, not for a “positive” result. A well-formed identifier that is absent from the directory is a 200 reachable: false, billed - the absence is the answer, dated by the export (see the “not registered” answer below). Requests the service cannot answer - zero or two parameters, a malformed identifier, a failed Luhn check, or a siret= that does not exist in the Sirene register (see the one 404)

  • leave the 200 range and are not billed.

Parameters

Provide exactly one of siren, siret or participant - zero or two is a 400 INVALID_INPUT. There is no search by name.

ParameterTypeRequiredDescription
sirenstringone of9-digit French SIREN (Luhn-checked); queries the French index
siretstringone of14-digit French SIRET (Luhn-checked); its existence is checked in the Sirene register first (unknown → 404, not billed), then matched by its SIREN prefix
participantstringone ofExplicit Peppol participant identifier <icd>:<value> (4-digit ISO 6523 ICD scheme + value as-is), any country
GET /company/peppol?siren=333285633
GET /company/peppol?siret=33328563300033
GET /company/peppol?participant=0002:000126010
  • siren= / siret= interrogate the French index: one SIREN returns every identifier the company appears under in the directory (see below). The SIRET form is matched by its SIREN prefix.
  • participant= is an exact lookup on the identifier (leading zeros count), for any country. It is also the only way to query an identifier whose embedded number would fail a Luhn check - a real case: the Monegasque SIREN 000126010 (ENERTECH MONACO) is only reachable through participant=0002:000126010.

The French index - one SIREN, all its identifiers

A French company can be registered in the Peppol directory under several identifiers at once, each with its own declared document types:

  • the head-office SIRET (ICD 0009),
  • the bare SIREN (ICD 0002 or 0225),
  • the CTC routing sub-addresses of the 2026 French e-invoicing reform (ICD 0225, FR:CTC) - in the current export, this scheme carries the vast majority of French reachability.

A siren= query returns them all, so the agent sees the complete routing picture in one call - which identifier to address, and which document types each one accepts.

200 response - UnifiedResponse

{
  "data": { ... },
  "provenance": {
    "source": "peppol-directory",
    "fetched_at": "2026-07-12T20:44:13Z",
    "freshness": { "kind": "snapshot", "as_of": "2026-07-12T00:00:00Z" }
  }
}
  • freshness.kind: snapshot - as_of is the date of the ingested directory export, the same date as coverage.directory_as_of.
  • provenance.source: peppol-directory - the public Peppol directory export, nothing else was consulted.

Fields of data

FieldTypeDescription
reachableboolAt least one directory record was found for the queried identifier(s)
participantsarrayOne entry per directory record - empty when reachable is false
companyobjectMinimal Sirene identity echo: { name, natural_person, status }; present for siren=/siret= input only - see below
coverageobjectdirectory_as_of (date of the export behind the answer) + note (“not a live SMP lookup”) - on every response; identity when the Sirene echo is unavailable

Each entry of participants[]:

FieldTypeDescription
idstringFull Peppol participant identifier, scheme:value
schemestring4-digit ISO 6523 ICD scheme (0009, 0002, 0225, …)
valuestringIdentifier value under that scheme
countrystringCountry declared in the directory
namesarrayNames as declared by the participant in the export - relayed as-is, never corrected
registered_onstringRegistration date as declared - a 0001-01-01 is the participant’s own declaration, relayed as-is
document_typesarrayDeclared document types for this identifier: identifier (full Peppol document type identifier), display_name (as declared), label (short label derived at ingestion - invoice, credit-note, … - null when it cannot be derived, never invented)

A dated photo of the directory, not a live lookup

The central point of this endpoint’s contract: the answer reflects the public Peppol directory export ingested into the local store - millions of participants, tens of millions of declared document types - at the date of that export, not the state of the network at call time. Every response says so twice:

  • coverage.directory_as_of dates the export behind the answer;
  • coverage.note spells it out: “reachability reflects the public Peppol directory export dated directory_as_of, not a live SMP lookup”.

A registration or deregistration that happened after the export is not visible. The answer is never presented as real-time - the agent always knows exactly how fresh the photo is.

”Not registered” is a paid answer

A well-formed identifier that is absent from the directory gets a 200 reachable: false, billed - never a 404: not being registered on Peppol is not the same thing as not existing. Real example (production store, export of 2026-07-12):

GET /company/peppol?siren=902120096 - a well-formed SIREN (Luhn passes), a real sole trader, absent from the Peppol directory:

{
  "data": {
    "reachable": false,
    "participants": [],
    "company": {
      "name": null,
      "natural_person": { "last_name": "DEMAILLY", "first_name": "LOLITA" },
      "status": "active"
    },
    "coverage": {
      "directory_as_of": "2026-07-12",
      "note": "reachability reflects the public Peppol directory export dated directory_as_of, not a live SMP lookup"
    }
  },
  "provenance": {
    "source": "peppol-directory",
    "fetched_at": "2026-07-12T20:44:29.010422662Z",
    "freshness": { "kind": "snapshot", "as_of": "2026-07-12T00:00:00Z" }
  }
}

The question “is this company reachable on Peppol?” received its answer: no, as of the export date (coverage.directory_as_of). The agent knows it cannot send a Peppol invoice to this counterparty today. And the Sirene echo shows the company does exist (a natural person, active): “not registered on Peppol” is not “nonexistent” - which is exactly why an absence from the directory is never a 404. The 404 below answers the other question entirely: an identifier that does not exist at all.

The one 404: a SIRET that does not exist

Absence from the directory is a paid answer. Absence from reality is not. A siret= is checked against the Sirene register before the directory is queried: a 14-digit, Luhn-valid SIRET whose establishment was never assigned by INSEE is a 404 NOT_FOUND, not billed, with the very same message as GET /company/resolve:

{ "error": "siret not found in the Sirene register", "code": "NOT_FOUND" }

The existence of an identifier is one single truth across the service: one route cannot answer “unknown” while another bills an answer for the same input - least of all the one that charges. The two SIRETs of the same company, on the production store:

RequestAnswer
?siret=39440693800008 - Luhn-valid, NIC never assigned by INSEE404 NOT_FOUND, not billed - word for word the answer of /company/resolve
?siret=39440693800149 - the real head office200 reachable: true, billed - 0225:394406938_0073_replyto, strictly identical to ?siren=394406938

Two boundaries keep this rule narrow:

  • Only siret= can 404. siren= and participant= have no establishment existence to verify: a SIREN unknown to the Sirene stock is stated in coverage.identity (see the Sirene identity echo) and still gets a billed 200, and a participant= is an explicit directory key rather than a French register entry.
  • A 404 is never invented. Once the SIRET exists, the lookup is the one described above, unchanged (matched by its SIREN prefix, same response as the equivalent siren=). And if the service cannot consult the Sirene stock at all, reachability is served as before with its stated coverage - the answer is never downgraded to a 404 out of ignorance.

Example - reachable, multi-identifier French index

GET /company/peppol?siren=333285633 (EQUIPTEC - the French index returns two identifiers: the head-office SIRET under ICD 0009, and the bare SIREN under FR:CTC 0225 from the French reform, each with its own document types; production store, export of 2026-07-12, document types abridged):

{
  "data": {
    "reachable": true,
    "participants": [
      {
        "id": "0009:33328563300033",
        "scheme": "0009",
        "value": "33328563300033",
        "country": "FR",
        "names": ["EQUIPTEC"],
        "registered_on": "2023-03-27",
        "document_types": [
          {
            "identifier": "urn:oasis:names:specification:ubl:schema:xsd:ApplicationResponse-2::ApplicationResponse##urn:fdc:peppol.eu:poacc:trns:invoice_response:3::2.1",
            "display_name": "Peppol Invoice Response transaction 3.0",
            "label": "application-response"
          }
        ]
      },
      {
        "id": "0225:333285633",
        "scheme": "0225",
        "value": "333285633",
        "country": "FR",
        "names": ["equiptec_Sage_Company_FUll_Name"],
        "registered_on": "0001-01-01",
        "document_types": [
          {
            "identifier": "urn:oasis:names:specification:ubl:schema:xsd:Invoice-2::Invoice##urn:cen.eu:en16931:2017#compliant#urn:peppol:france:billing:cius:1.0::2.1",
            "display_name": "France UBL Invoice CIUS",
            "label": "invoice"
          },
          {
            "identifier": "urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100::CrossIndustryInvoice##urn:cen.eu:en16931:2017#compliant#urn:peppol:france:billing:cius:1.0::D22B",
            "display_name": "France CII Invoice CIUS",
            "label": "cross-industry-invoice"
          },
          {
            "identifier": "urn:oasis:names:specification:ubl:schema:xsd:CreditNote-2::CreditNote##urn:cen.eu:en16931:2017#compliant#urn:peppol:france:billing:cius:1.0::2.1",
            "display_name": "France UBL Credit Note CIUS",
            "label": "credit-note"
          }
        ]
      }
    ],
    "company": {
      "name": "EQUIP'TEC",
      "natural_person": null,
      "status": "active"
    },
    "coverage": {
      "directory_as_of": "2026-07-12",
      "note": "reachability reflects the public Peppol directory export dated directory_as_of, not a live SMP lookup"
    }
  },
  "provenance": {
    "source": "peppol-directory",
    "fetched_at": "2026-07-12T20:44:13.847400075Z",
    "freshness": { "kind": "snapshot", "as_of": "2026-07-12T00:00:00Z" }
  }
}

Note the fidelity to the export: the two records carry different declared names ("EQUIPTEC" vs "equiptec_Sage_Company_FUll_Name") and the second declares registered_on: "0001-01-01" - relayed as-is, never corrected. The Sirene echo company, by contrast, carries the official legal name ("EQUIP'TEC"). Each identifier lists its own document types: here the reform-era 0225 identifier is the one accepting invoices and credit notes.

Example - explicit participant identifier

GET /company/peppol?participant=0002:000126010 (ENERTECH MONACO - a SIREN with leading zeros whose Luhn check does not pass: only queryable through the explicit Peppol identifier; no Sirene echo is attempted on this input form; production store, export of 2026-07-12):

{
  "data": {
    "reachable": true,
    "participants": [
      {
        "id": "0002:000126010",
        "scheme": "0002",
        "value": "000126010",
        "country": "FR",
        "names": ["ENERTECH MONACO"],
        "registered_on": "2021-11-10",
        "document_types": [
          {
            "identifier": "urn:oasis:names:specification:ubl:schema:xsd:Invoice-2::Invoice##urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0::2.1",
            "display_name": "Peppol BIS Billing UBL Invoice V3",
            "label": "invoice"
          },
          {
            "identifier": "urn:oasis:names:specification:ubl:schema:xsd:CreditNote-2::CreditNote##urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0::2.1",
            "display_name": "Peppol BIS Billing UBL Credit Note V3",
            "label": "credit-note"
          },
          {
            "identifier": "urn:oasis:names:specification:ubl:schema:xsd:OrderResponse-2::OrderResponse##urn:fdc:peppol.eu:poacc:trns:order_response:3::2.1",
            "display_name": "Peppol Order Response transaction 3.0",
            "label": "order-response"
          }
        ]
      }
    ],
    "coverage": {
      "directory_as_of": "2026-07-12",
      "note": "reachability reflects the public Peppol directory export dated directory_as_of, not a live SMP lookup"
    }
  },
  "provenance": {
    "source": "peppol-directory",
    "fetched_at": "2026-07-12T20:44:20.679729822Z",
    "freshness": { "kind": "snapshot", "as_of": "2026-07-12T00:00:00Z" }
  }
}

Sirene identity echo

For siren= / siret= input, data.company echoes the counterparty’s Sirene identity - { name, natural_person, status } - so the agent can cross-check who it is addressing without a second call (for the full identity card, use GET /company/resolve).

  • The echo is never attempted for participant= input: an explicit Peppol identifier can belong to any country.
  • A SIREN unknown to the Sirene register is never an error: coverage.identity says why the echo is missing ("siren_unknown_to_sirene", or "sirene_dataset_unavailable" when the Sirene store itself cannot be read).

Freshness

Answers are served from a snapshot: freshness.as_of is the date of the ingested Peppol directory export, the same date carried by coverage.directory_as_of on every response. If a refresh is missed, the service keeps serving the last good export - honestly dated - and never turns staleness into a 5xx.

Limits

Stated plainly, so the agent knows what the answer does and does not claim:

  • The answer is a photo at the export date - a registration or deregistration after the export is not visible. There is no live SMP lookup in v1.
  • names and registered_on are the participants’ own declarations in the public export - quality varies, and they are relayed as-is, never corrected.
  • Reachable does not mean compliant or obliged: the endpoint states a directory fact, not a regulatory opinion - being absent from the directory says nothing about a company’s legal e-invoicing obligations.
  • An identifier whose embedded SIREN fails the Luhn check (real case: leading-zero Monegasque SIREN 000126010) is only queryable through an explicit participant= identifier.
  • The Sirene identity echo covers French companies present in the Sirene stock.

Errors

Only requests the service cannot answer leave the 200 range - none of them are billed. “Absent from the directory” is not one of them: it is a paid 200 reachable: false. The only business 404 is a siret= that does not exist in the Sirene register - there, what is missing is the input, not the answer.

StatuscodeCase
400INVALID_INPUTZero or two of siren/siret/participant; malformed SIREN/SIRET (wrong length, non-digits); participant not of the form <icd>:<value>
400INVALID_CHECKSUMsiren=/siret= fails its Luhn check - a probable typo the agent should fix
404NOT_FOUNDA well-formed siret= absent from the Sirene register (an establishment number never assigned - same message as /company/resolve); also unknown routes. Never on siren=/participant=, and never for an absence from the Peppol directory
503DATA_UNAVAILABLEThe Peppol directory dataset has not been ingested yet - there is no last good export to serve
500INTERNALInternal error (detail logged, not exposed)
{ "error": "provide exactly one of 'siren', 'siret' or 'participant'", "code": "INVALID_INPUT" }

{ "error": "siret not found in the Sirene register", "code": "NOT_FOUND" }

Attribution

Reachability is answered from the Peppol directory (Peppol Directory, directory.peppol.eu) - data published by the network’s participants, attribution OpenPeppol. The identity echo comes from the Sirene register (INSEE).

See also

  • GET /company/resolve - the official identity card behind a SIREN/SIRET: legal name, status, legal form, NAF activity, head office.
  • GET /company/vat - live intra-EU VAT number validation against VIES: the other pre-invoicing check on the same counterparty.
  • POST /invoice/generate - generate the EN 16931 e-invoice once the counterparty’s reachability is confirmed.
  • POST /invoice/validate - deterministic EN 16931 validation of the invoice document itself, including the ?ruleset=peppol pack (Peppol BIS Billing 3.0): validate the BIS invoice, then check the Peppol participant here - the verdict judges the document, this route judges the participant’s reachability.
  • For agents - discovery surfaces, the live /catalog and how settlement works.