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"Confirmed: {result['participant_id']}")
print(f"Country: {result['country']}")
print(f"Document types: {len(result['capabilities'])}")
else:
# found: false means "not confirmed" โ see the caution below. It is not
# proof that the participant is absent from the network.
print(f"Not confirmed: {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 a message. Code that treats a missing participant as an HTTP failure
will never see the case it is trying to handle.
found: false means "not confirmed", not "not registered"Two different things produce exactly the same answer:
- the participant genuinely is not published on the Peppol network, and
- no registry answered โ a directory returned a server error, timed out, or refused the connection.
In both cases the body is identical: found is false, and message is the same fixed
sentence, Participant not found on the Peppol network. Nothing in the response tells you which
of the two happened, so you cannot tell them apart from this endpoint.
Two consequences for your code. Treat message as fixed text rather than a diagnosis, and never
show it to an end user as the reason their invoice was stopped. And treat an unconfirmed
receiver as "try again later" rather than "this identifier is wrong" โ when a tax authority's
test directory was unavailable for a morning, every lookup against it looked exactly like an
unregistered customer, and the people who were told to correct their data had nothing to
correct.
Found:
{
"found": true,
"participant_id": "0106:12345678",
"name": null,
"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
}
name is empty in every responsename is shown as null above because that is what this endpoint returns, for every
participant, however complete the entry in the directory is. The cause is on our side, not the
network's: the route reads a field that the lookup service does not have, so the field falls
back to its default every time. It is a platform defect, reported to our engineers as
issue #734.
The business name is available โ from a different call. GET /api/v1/lookup/participant,
described under Business name, access points and cache state
below, returns it as business_name. Do not build a screen around name from this endpoint.
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 | true only when the network confirmed the participant. false covers both "not published" and "no registry answered" |
participant_id | string | The scheme:value you asked about, echoed back |
name | string or null | Always null โ a defect on our side, see the warning above. Use business_name from GET /api/v1/lookup/participant instead |
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 | Fixed text when found is false, identical for both causes. Not a diagnosis |
The response is flat. There is no nested object wrapping the participant's details, and no
access point, registration date or timing block. Read country and capabilities 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"]:
# False here also covers "no registry answered", which is reported in
# exactly the same way as "not registered". A False is a good reason
# not to send; it is not evidence that the identifier is wrong.
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 not confirmed for invoices โ hold, and retry later")
Business name, access points and cache stateโ
GET /api/v1/participants/lookup answers one question โ can this participant receive this
document type โ and returns a deliberately small, flat object. Three questions it cannot answer
have their own calls, on a separate family of routes:
- what the participant's business name is (the
namefield above never carries it), - who actually runs the receiver's access point, and
- whether the answer you just got came from GoRoute's cache or from the network a moment ago.
| Call | What it is for |
|---|---|
GET /api/v1/lookup/participant | The full result: business name, SMP address, every document type, and the access point behind each one |
GET /api/v1/lookup/verify/{scheme}/{identifier} | A short answer: does it exist, what is it called, how long did it take |
Both need an API key whose permissions include invoices:read. That is a different
permission from the one GET /api/v1/participants/lookup needs, which is participants:read.
If your key is scoped to only one of the two, give it both before using this section.
A found: false (or exists: false) from either call carries the same caution as the one
earlier on this page: it means the network did not confirm the participant, which is not the
same as proving the participant absent.
Full lookupโ
GET /api/v1/lookup/participant takes the scheme and the identifier as two separate query
parameters. This is not how GET /api/v1/participants/lookup works, which takes one
peppol_id containing a colon โ the two calls are not interchangeable.
| Parameter | Required | Meaning |
|---|---|---|
scheme | yes | Peppol scheme, for example 0106 |
identifier | yes | The identifier within that scheme, without the scheme prefix |
document_type | no | A full document type identifier. When it matches one the participant publishes, that document type's first access point is copied into the top-level endpoint field |
fresh | no | true skips GoRoute's cached copy and asks the network now. Default false |
import requests
API_BASE = "https://app.goroute.ai/peppol-api"
response = requests.get(
f"{API_BASE}/api/v1/lookup/participant",
params={"scheme": "0106", "identifier": "12345678"},
headers={"X-API-Key": "your_api_key"},
)
response.raise_for_status()
participant = response.json()
if participant["found"]:
print(participant["business_name"], participant["country"])
for document_type in participant["document_types"]:
for endpoint in document_type["endpoints"]:
print(document_type["document_type_id"], "->", endpoint["endpoint_uri"])
A response looks like this. The values below are illustrative, not a real participant:
{
"found": true,
"participant_id": "0106:12345678",
"scheme": "0106",
"identifier": "12345678",
"business_name": "Example Company BV",
"country": "NL",
"smp_url": "https://smp.example.com",
"document_types": [
{
"document_type_id": "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",
"process_ids": [
"urn:fdc:peppol.eu:2017:poacc:billing:01:1.0"
],
"endpoints": [
{
"transport_profile": "peppol-transport-as4-v2_0",
"endpoint_uri": "https://ap.example.com/as4",
"certificate": "MIIFxzCCA6+gAwIBAgIQ...",
"service_activation_date": null,
"service_expiration_date": null,
"service_description": "Example Access Point B.V.",
"technical_contact_url": "https://example.com/peppol-support"
}
],
"scheme": "busdox-docid-qns"
}
],
"endpoint": null,
"cached": false,
"cached_at": null,
"cache_expires_at": null,
"lookup_time_ms": 245
}
Top-level fields:
| Field | Type | Meaning |
|---|---|---|
found | boolean | true only when the network confirmed the participant |
participant_id | string | scheme:identifier, joined back together |
scheme | string | The scheme you asked about |
identifier | string | The identifier you asked about |
business_name | string or null | The participant's business name, from the SMP's business card where there is one, otherwise from our own records |
country | string or null | Country code |
smp_url | string or null | Address of the SMP that holds this participant's registration |
document_types | array | One entry per document type the participant publishes โ see below |
endpoint | object or null | Populated only when you passed document_type and it matched. Same shape as an entry in endpoints |
cached | boolean | true when the answer came from GoRoute's cache rather than the network |
cached_at | string or null | When the cached copy was taken. null on an uncached answer |
cache_expires_at | string or null | When the cached copy stops being used. null on an uncached answer |
lookup_time_ms | integer or null | How long this call took |
Each entry in document_types:
| Field | Type | Meaning |
|---|---|---|
document_type_id | string | The full Peppol document type identifier |
process_ids | array of string | The processes the receiver supports for that document type |
endpoints | array | The access points that receive it โ see below |
scheme | string or null | The identifier scheme the SMP published it under: busdox-docid-qns for an exact registration, or peppol-doctype-wildcard. A send must ask for the document under the scheme the receiver actually publishes |
Each entry in endpoints:
| Field | Type | Meaning |
|---|---|---|
transport_profile | string | The AS4 transport profile, for example peppol-transport-as4-v2_0 |
endpoint_uri | string | Where documents are delivered |
certificate | string or null | The access point's certificate as the SMP publishes it, base64. Its subject is how you learn which provider holds an identifier that is not ours |
service_activation_date | string or null | Part of the response shape, but always null today: we do not read this element from the SMP |
service_expiration_date | string or null | The same โ always null today |
service_description | string or null | Free text the SMP publishes about who runs the endpoint |
technical_contact_url | string or null | Technical contact the SMP publishes |
POST /api/v1/lookup/participant is the same call with scheme, identifier and
document_type in a JSON body instead of the query string, and the same response. It has no
fresh parameter, so it always accepts a cached answer.
Quick verificationโ
GET /api/v1/lookup/verify/{scheme}/{identifier} puts the scheme and identifier in the path,
separated by a slash rather than a colon, and returns a short answer. It accepts fresh as a
query parameter, like the call above.
scheme, identifier = "0106", "12345678"
response = requests.get(
f"{API_BASE}/api/v1/lookup/verify/{scheme}/{identifier}",
headers={"X-API-Key": "your_api_key"},
)
{
"exists": true,
"participant_id": "0106:12345678",
"business_name": "Example Company BV",
"country": "NL",
"smp_url": "https://smp.example.com",
"lookup_time_ms": 245,
"cached": false
}
exists, not foundThis call reports success in exists. GET /api/v1/participants/lookup and
GET /api/v1/lookup/participant both use found. Reading found from a verify response gets
you None, which is falsy โ so the mistake reads as "not on the network" rather than failing
loudly.
One more path, which you probably do not wantโ
GET /api/v1/lookup/{scheme}/{identifier} also exists, and returns a third shape again: it was
assembled for the GoRoute portal's own screens. It reports success in exists, returns
capabilities as a list of nested objects rather than the document_types above, and includes
a business_card field that is null for every participant, always.
Build on GET /api/v1/lookup/participant or GET /api/v1/lookup/verify/{scheme}/{identifier}
instead. Those two return the lookup service's own response model, so their fields follow the
service; the portal-shaped one is hand-assembled for a screen and promises a business card it
never delivers.
The one lookup that needs no API keyโ
GET /api/v1/public/lookup/{scheme}/{identifier} is the last member of the family, and the
only call on this site that returns a participant's business name without a credential.
It takes no API key, no permission and no organisation. It was built so a public web page can
render a lookup result inline, and it reads the same registry data as the keyed calls, through
the same service and the same cache.
It returns only public SMP and SML data โ the same information anyone can already read straight from the participant's SMP. It does not expose anything about your account, your documents or your organisation.
The scheme and the identifier go in the path, separated by a slash:
curl "https://app.goroute.ai/peppol-api/api/v1/public/lookup/0208/0000000196"
{
"exists": true,
"participant_id": "0208:0000000196",
"business_name": "VME FORUM PARKING",
"country": "BE",
"smp_url": "https://smp.peppol.storecove.com",
"capabilities": [
{
"document_type": {
"value": "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"
},
"processes": [
{"value": "urn:fdc:peppol.eu:2017:poacc:billing:01:1.0"}
]
}
],
"lookup_time_ms": 2233,
"cached": true
}
A participant the network did not confirm answers 200 as well, with exists false and
everything else null or empty:
{
"exists": false,
"participant_id": "9999:test-nonexistent-xyz",
"business_name": null,
"country": null,
"smp_url": null,
"capabilities": [],
"lookup_time_ms": 0,
"cached": false
}
| Field | Type | Meaning |
|---|---|---|
exists | boolean | true only when the network confirmed the participant. Note the name: this call reports success in exists, not found |
participant_id | string | scheme:identifier, joined back together |
business_name | string or null | The participant's business name, from the SMP's business card where there is one, otherwise from our own records. null when neither has it |
country | string or null | Country code |
smp_url | string or null | Address of the SMP holding the registration |
capabilities | array | One entry per document type, each an object with a nested document_type.value and a list of processes โ not the flat strings GET /api/v1/participants/lookup returns, and not the document_types shape above |
lookup_time_ms | integer or null | How long the lookup took |
cached | boolean | true when the answer came from our cache rather than the network |
There is no business_card field here. That field belongs to the portal-shaped route in the
previous section, which never fills it.
fresh=true works exactly as it does on the keyed calls โ it skips the cached copy and asks
the network now. The cache is the same one: a successful lookup is held for one hour, an
unsuccessful one is never cached. See When to pass fresh below.
curl "https://app.goroute.ai/peppol-api/api/v1/public/lookup/0208/0000000196?fresh=true"
Because it carries no credential, this call is counted against an anonymous, per-IP budget that you share with every other unauthenticated caller from the same address. The service defaults are 60 requests a minute and 10,000 a day; a deployment can be configured differently, so treat those as defaults rather than a guarantee. Going over returns 429:
{
"detail": {
"error": "rate_limit_exceeded",
"error_code": "EDGE_RATE_LIMIT_MINUTE",
"message": "Too many requests. Limit: 60/minute. Retry after 23 seconds.",
"retry_after": 23
}
}
error_code is EDGE_RATE_LIMIT_MINUTE or EDGE_RATE_LIMIT_DAY depending on which window
you hit, and the response carries a Retry-After header. Use it for a public page or a
one-off check. For anything a customer's sending depends on, use the keyed calls above,
where the budget is yours.
When to pass freshโ
GoRoute caches a successful lookup for one hour. Within that hour, a change the participant
made to their registration โ a new document type, or a move to a different access point โ can
be invisible to you. fresh=true skips the cached copy and asks the network, which is what you
want immediately after someone tells you they have just registered or just changed provider.
You do not need it after an outage. An unsuccessful lookup is never cached, so a retry always asks the network again.
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.
Before you build one, two facts about what GoRoute already does. A successful lookup is
cached on our side for one hour, under a key that ignores letter case โ so 0248:OM1100099003
and 0248:om1100099003 are one entry, not two. An unsuccessful lookup is never cached, so
retrying after a registry outage really does ask the network again rather than replaying the
same negative answer. That makes a client-side cache of your own useful mainly for saving
requests against your rate limit, not for saving time on the network.
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 ReceiverNotConfirmed(Exception):
"""The network did not confirm the receiver. Retryable โ not proof of a bad ID."""
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"]:
# Refuse the send: nothing should go to an unconfirmed recipient.
# But do not say the receiver is unregistered โ this answer also
# appears when no registry could be reached, and the check is
# worth retrying before anyone is asked to correct an identifier.
raise ReceiverNotConfirmed(
f"Receiver {receiver_id} could not be confirmed on the Peppol "
f"network. This may be a registration that does not exist, or "
f"a registry that did not answer. Retry before treating the "
f"identifier as wrong."
)
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 four outcomes worth distinguishing:
- A participant that is not registered โ 200 with
found: false. - A registry that did not answer โ also 200 with
found: false, and reported in exactly the same words as the first. You cannot tell it apart from the response, so any code that acts onfound: falseis acting on both. - An identifier that is not in
scheme:valueform โ 400. - A transport problem between you and GoRoute โ a timeout or a connection error.
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",
}
safe_lookup never has to invent a result for the first two outcomes: the endpoint already
answers 200 with found: false for both. What it cannot do is tell them apart โ so treat
everything it returns with found: false as "not confirmed, hold and retry", and reserve "this
identifier is wrong" for the 400, which really does mean the value you sent was malformed.
What do we know about this recipient before I send?โ
The lookup above asks the Peppol network a single question: is this participant registered, and for what. Three further calls answer a wider one โ will this invoice actually arrive, and what happened the last time we sent to them? All three are live on production and on test, and all three need an API key.
Two of them are called "Analyze Recipient" and they are not the same call. They take different inputs, need different permissions, and answer different questions. Pick with this table before you read any further.
| You want to know | Use | Permission |
|---|---|---|
| Is this Peppol ID registered to receive invoices at all, and what does its country require? | POST /api/v1/invoices/recipients/analyze | invoices:send |
| How have our sends to this recipient gone, and what fields should we include? | POST /api/v1/intelligence/recipients/analyze | reports:read |
| The full history behind that second answer | GET /api/v1/intelligence/recipients/{recipient_id}/history | reports:read |
Permissions are as each route declares them, read from the API source on 21 September 2026.
Can this recipient receive an invoice?โ
POST /api/v1/invoices/recipients/analyze โ requires the invoices:send permission.
This one is a live registry check. It takes the Peppol ID as a query parameter, not in a
JSON body, which is easy to get wrong:
curl -X POST "https://app.goroute.ai/peppol-api/api/v1/invoices/recipients/analyze?peppol_id=0106:12345678" \
-H "X-API-Key: your_api_key"
The response:
{
"peppol_id": "0106:12345678",
"exists": true,
"can_receive_invoices": true,
"message": "Recipient is registered on the Peppol network",
"lookup_status": "registered",
"country_code": "NL",
"supported_document_types": [
"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"
],
"requirements": [
"KVK number recommended",
"SI-UBL format supported"
],
"smp_endpoint": "https://ap.example.com/as4",
"last_updated": null
}
can_receive_invoices: true does not mean they can take your specificationThis field answers a weaker question than it appears to: it says the recipient is registered for some invoice document type, not that it can accept the one you are about to send.
So a buyer registered only for Peppol BIS Billing 3.0 comes back true โ and then cannot receive
the PINT OM invoice an Omani seller sends, or a PINT A-NZ invoice for an Australian or New Zealand
buyer. You get true, you send, and the delivery fails.
To answer the question you actually have, read supported_document_types in the same response. It
holds the recipient's full Peppol document type identifiers, so look in it for the identifier you
intend to send. Allow for a wildcard registration too: a receiver may publish a prefix such as
urn:peppol:pint:billing-1* that covers your document without listing it exactly โ see the
scheme field under Full lookup for how the two kinds of registration differ.
lookup_status is the field to branch on. It takes one of four values:
lookup_status | What it means | What to do |
|---|---|---|
registered | Found, and it accepts invoices | Send |
no_invoice_capability | Found, but it is not registered to receive invoices | Do not send over Peppol; another channel is needed |
not_registered | No registration exists for this ID | Check the ID with the customer |
unavailable | No registry answered. Nothing is known either way | Wait and retry. Do not ask anyone to correct the ID |
That last value is the reason to prefer this call when you are deciding whether to send. It
has a name for "we could not find out", and its message says so to the customer in plain
words: "The Peppol registry for this participant is not responding, so it could not be
checked. This is not a problem with the Peppol ID โ try again shortly."
requirements carries GoRoute's country-specific notes. They exist for eleven countries
today โ AU, BE, DE, DK, FI, GB, IT, NL, NO, SE and SG โ and the list is empty for every other
country. smp_endpoint is only filled when the registry returned one.
How have our sends to this recipient gone?โ
POST /api/v1/intelligence/recipients/analyze โ requires the reports:read permission.
An API key with read or admin scope holds reports:read; a key scoped only to sending
does not.
This call is about your own sending history, not about the registry. It takes a JSON body:
curl -X POST https://app.goroute.ai/peppol-api/api/v1/intelligence/recipients/analyze \
-H "X-API-Key: your_api_key" \
-H "Content-Type: application/json" \
-d '{"recipient_id": "0106:12345678", "document_type": "invoice"}'
recipient_id is required. document_type defaults to "invoice".
{
"recipient_id": "0106:12345678",
"country": "NL",
"is_known": true,
"capabilities": {
"can_receive": true,
"accepts": ["Invoice", "Credit Note"],
"transport": "AS4 v2",
"endpoint": null
},
"statistics": {
"total_invoices": 42,
"successful": 40,
"failed": 2,
"success_rate": 0.952,
"avg_delivery_seconds": 4.1
},
"intelligence": {
"common_failures": [],
"required_fields": ["BuyerReference"],
"recommended_fields": ["PaymentTerms"]
},
"warnings": [],
"recommendations": ["Include these fields for best results: BuyerReference"]
}
is_known tells you whether GoRoute has ever sent to this recipient for your organisation.
When it is false, statistics and intelligence are both null and recommendations
holds one line: "First invoice to this recipient - include all recommended fields".
endpoint is always null โ access point addresses are deliberately not exposed here.
capabilities.accepts holds short names from the Peppol code list table, such as Invoice,
Credit Note or Order โ not document type identifiers. Do not compare it against a full
identifier like
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;
the comparison will never match. If you need identifiers, use
the invoices call above and read supported_document_types. A
document type that is not in our code list table falls back to whatever name the code list gives
it, so treat these as labels for a human rather than values to branch on.
A warning appears when the success rate falls below 80%, and it names the rate.
can_receive: false from this call does not mean the recipient cannot receiveInside the response above, capabilities.can_receive is a plain true/false. When the registry
lookup behind it fails โ a timeout, an unreachable SMP, a malformed identifier โ this call
returns can_receive: false with an empty accepts list. That is the same answer it
gives for a participant that genuinely cannot receive invoices, and there is no field that
tells the two apart.
So never put can_receive: false from this endpoint in front of a user as "this recipient
cannot receive invoices". The likely result is someone hunting for a typo in a Peppol ID that
is perfectly correct.
If the answer has to be shown to a person, or acted on automatically, use
the invoices call above instead. It has lookup_status: "unavailable" for exactly this case.
Read from the API source on 21 September 2026. We have reported this to engineering as a product defect; this page will be updated when the behaviour changes.
The full historyโ
GET /api/v1/intelligence/recipients/{recipient_id}/history โ requires the reports:read
permission. The recipient's Peppol ID goes in the path.
curl "https://app.goroute.ai/peppol-api/api/v1/intelligence/recipients/0106:12345678/history" \
-H "X-API-Key: your_api_key"
It returns one of two different shapes, so check is_known before reading anything else.
A recipient GoRoute has no record of:
{
"recipient_id": "0106:12345678",
"is_known": false,
"message": "No historical data for this recipient"
}
A recipient it does have a record of:
{
"recipient_id": "0106:12345678",
"is_known": true,
"country": "NL",
"statistics": {
"total_invoices": 42,
"successful": 40,
"failed": 2,
"success_rate": 0.952
},
"performance": {
"avg_delivery_time_ms": 4100,
"avg_delivery_seconds": 4.1
},
"intelligence": {
"common_failures": [],
"required_fields": ["BuyerReference"],
"recommended_fields": ["PaymentTerms"]
},
"capabilities": [],
"timeline": {
"first_seen": "2026-03-04T09:12:00+00:00",
"last_invoice": "2026-09-18T14:02:00+00:00",
"last_success": "2026-09-18T14:02:00+00:00",
"last_failure": "2026-07-22T11:40:00+00:00"
}
}
One caveat worth stating plainly: this endpoint's response is not described by a named schema in the published API description โ it is typed only as a free-form object. The shape above is what the code returns, read on 21 September 2026. Treat every field as optional and read it defensively.