Skip to main content

Participant Lookup

Before sending a document, check that the receiver is registered on the Peppol network and can receive your document type.

GET /api/v1/participants/lookup answers that question. It takes one identifier, and it returns a flat object describing what the network knows about that participant.

Quick Lookupโ€‹

Pass the Peppol identifier as a single value in scheme:value form โ€” for example 0106:12345678. The parameter is called peppol_id; identifier is accepted as an alias for the same value.

import requests

API_BASE = "https://app.goroute.ai/peppol-api"


def lookup_participant(peppol_id: str) -> dict:
"""Look a participant up on the Peppol network.

peppol_id is a single string in scheme:value form, e.g. "0106:12345678".
"""
response = requests.get(
f"{API_BASE}/api/v1/participants/lookup",
params={"peppol_id": peppol_id},
headers={"X-API-Key": "your_api_key"},
)
response.raise_for_status()
return response.json()


# Look up a Dutch company
result = lookup_participant("0106:12345678")

if result["found"]:
print(f"Found: {result['name']}")
print(f"Country: {result['country']}")
print(f"Document types: {len(result['capabilities'])}")
else:
print(f"Not found: {result['message']}")
The value must contain a colon

The scheme and the identifier are one parameter, not two. A value without a colon is rejected with 400 and the message Provide peppol_id as scheme:value, e.g. 0248:OM1100099003. Sending the scheme and the identifier as two separate query parameters does not work.

Lookup Responseโ€‹

A participant that is not on the network is not an error. The endpoint answers 200 with found set to false and an explanation in message. Code that treats a missing participant as an HTTP failure will never see the case it is trying to handle.

Found:

{
"found": true,
"participant_id": "0106:12345678",
"name": "Example Company BV",
"country": "NL",
"capabilities": [
"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",
"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"
],
"message": null
}

Not found:

{
"found": false,
"participant_id": "0106:99999999",
"name": null,
"country": null,
"capabilities": [],
"message": "Participant not found on the Peppol network."
}
FieldTypeMeaning
foundbooleanWhether the participant resolved on the network
participant_idstringThe scheme:value you asked about, echoed back
namestring or nullBusiness name, if the SMP returned one
countrystring or nullCountry code, if the SMP returned one
capabilitiesarray of stringDocument type identifiers the participant can receive
messagestring or nullExplanation when found is false

The response is flat. There is no nested object wrapping the participant's details, and no access point, registration date or timing block. Read name directly from the top level.

Check Document Type Supportโ€‹

capabilities is a list of document type identifier strings โ€” not objects. Each entry is the full Peppol document type identifier, so test it with a substring match rather than by reading a field off it.

def can_receive(peppol_id: str, document_type: str) -> bool:
"""Check whether a participant can receive a given document type.

document_type is matched against the document type identifier strings the
network returns, e.g. "Invoice-2" or "CreditNote-2".
"""
result = lookup_participant(peppol_id)

if not result["found"]:
return False

return any(document_type in capability for capability in result["capabilities"])


# Check before sending an invoice
if can_receive("0106:12345678", "Invoice-2"):
print("Receiver can accept invoices")
send_invoice(invoice)
else:
print("Receiver cannot accept invoices")

Looking Up Multiple Participantsโ€‹

There is no batch lookup endpoint. GET /api/v1/participants/lookup accepts a single participant per request, so look up multiple participants by calling it once per participant.

def lookup_many(peppol_ids: list[str]) -> list[dict]:
"""Look several participants up by calling the lookup endpoint once each."""
return [
{"peppol_id": peppol_id, "result": lookup_participant(peppol_id)}
for peppol_id in peppol_ids
]


# Look up multiple receivers
for entry in lookup_many([
"0106:12345678",
"0106:87654321",
"0208:0123456789",
]):
state = "found" if entry["result"]["found"] else "not found"
print(f"{entry['peppol_id']}: {state}")

Each lookup is a separate rate-limited request, so keep your request rate within your plan's limit when iterating over long lists.

Caching Lookupsโ€‹

Cache lookup results to reduce API calls. A participant's registration changes rarely, so a short time-to-live is usually enough.

import redis
import json


class ParticipantCache:
def __init__(self, redis_client: redis.Redis, ttl: int = 3600):
self.redis = redis_client
self.ttl = ttl # Cache for 1 hour

def lookup(self, peppol_id: str) -> dict:
"""Look a participant up, using the cache when possible."""
cache_key = f"peppol:participant:{peppol_id}"

cached = self.redis.get(cache_key)
if cached:
return json.loads(cached)

result = lookup_participant(peppol_id)

# Cache positive results only. A participant that is not registered today
# may be registered tomorrow, and caching "not found" delays you noticing.
if result["found"]:
self.redis.setex(cache_key, self.ttl, json.dumps(result))

return result

def invalidate(self, peppol_id: str):
"""Drop a cached participant."""
self.redis.delete(f"peppol:participant:{peppol_id}")


# Usage
cache = ParticipantCache(redis.Redis())
participant = cache.lookup("0106:12345678")

Look receivers up as part of your own business process โ€” when a supplier or customer record is created or edited โ€” rather than in the moment you send. That keeps the send path fast and gives you somewhere to report a bad identifier to a human.

Validation Before Sendingโ€‹

Fold the lookup into your sending workflow so an undeliverable document fails before it is submitted.

class InvoiceSender:
def __init__(self, api_key: str):
self.api_key = api_key

def send(self, invoice_xml: str) -> dict:
"""Send an invoice, checking the receiver first."""

# Extract the receiver's Peppol ID from the invoice, in scheme:value form
receiver_id = self.extract_receiver(invoice_xml)

lookup = lookup_participant(receiver_id)

if not lookup["found"]:
raise ValueError(
f"Receiver {receiver_id} is not registered on the Peppol network: "
f"{lookup['message']}"
)

if not any("Invoice-2" in capability for capability in lookup["capabilities"]):
raise ValueError(
f"Receiver {receiver_id} does not accept invoices. "
f"Registered document types: {lookup['capabilities']}"
)

return self.do_send(invoice_xml)

Error Handlingโ€‹

There are three outcomes worth distinguishing: a participant that is not registered (200 with found: false), an identifier that is not in scheme:value form (400), and a transport problem.

def safe_lookup(peppol_id: str) -> dict:
"""Look up, and turn every outcome into a result you can act on."""
try:
return lookup_participant(peppol_id)

except requests.HTTPError as e:
if e.response is not None and e.response.status_code == 400:
return {
"found": False,
"participant_id": peppol_id,
"capabilities": [],
"message": (
"Identifier must be in scheme:value form, e.g. 0106:12345678"
),
}
raise

except requests.Timeout:
return {
"found": False,
"participant_id": peppol_id,
"capabilities": [],
"message": "Lookup timed out, please retry",
}

Note that safe_lookup never has to invent a "not registered" result: the endpoint already returns one, with its own explanation in message.

Next Stepsโ€‹