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 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."
}
| Field | Type | Meaning |
|---|---|---|
found | boolean | Whether the participant resolved on the network |
participant_id | string | The scheme:value you asked about, echoed back |
name | string or null | Business name, if the SMP returned one |
country | string or null | Country code, if the SMP returned one |
capabilities | array of string | Document type identifiers the participant can receive |
message | string or null | Explanation 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.