Partner Verification
Two different things share the word "verification" on the Peppol network, and they are not the same feature. Read this first, because the rest of the page depends on which one you want.
| You want to know | Feature | Where |
|---|---|---|
| "Is my integration correct, and can I display the GoRoute Verified badge?" | GoRoute Verified โ self-service integration verification | This page |
| "Is my trading partner registered on Peppol and can they receive my document type?" | Participant lookup | Participant Lookup and the section below |
GoRoute Verified integration verification is built and available today:
POST /api/v1/partners/verify, GET /api/v1/partners/verification and
GET /api/v1/partners/verified/{org_id}.
GoRoute Verified โ integration verificationโ
You verify your own integration. You send test documents to five sandbox scenarios, then ask GoRoute to check that you handled each one correctly. Pass all five and your organisation is marked verified for 365 days.
The five required scenariosโ
All five must pass. There are no other scenarios and no partial badge.
| Scenario | What you must do | How GoRoute checks it |
|---|---|---|
test-success | Send a document to 9999:test-success | A transaction to that receiver with status delivered in the last 30 days |
test-recipient-not-found | Send a document to 9999:test-recipient-not-found and handle the failure | A transaction to that receiver with status failed |
test-timeout | Send a document to 9999:test-timeout and handle the timeout | A transaction to that receiver with status failed |
test-duplicate-event | Send to 9999:test-duplicate-event and deduplicate the repeated webhook | A transaction to that receiver with status delivered |
webhook-signature | Receive a webhook and verify its HMAC-SHA256 signature | A webhook delivery to your registered endpoint with a valid signature |
Transactions are matched within a 30-day window, so run the scenarios and request verification in the same month.
Request verificationโ
POST /api/v1/partners/verify โ requires the org:manage permission.
Both body fields are optional. If you send none of them, GoRoute looks at your recent sandbox transactions.
| Field | Type | Required | Meaning |
|---|---|---|---|
webhook_url | string | No | The webhook URL to use for the webhook-signature scenario. Must match a registered webhook endpoint. Max 2048 characters. |
transaction_ids | array of string | No | Restrict the check to specific transactions. Maximum 5. |
import requests
response = requests.post(
"https://app.goroute.ai/peppol-api/api/v1/partners/verify",
headers={
"X-API-Key": "your_api_key",
"Content-Type": "application/json",
},
json={
"webhook_url": "https://partner.example.com/webhooks/peppol",
},
)
result = response.json()
if result["verified"]:
print("Verified until", result["verified_until"])
print("Badge URL:", result["badge_url"])
else:
print("Still failing:", result["failed"])
print("Not yet attempted:", result["remaining"])
Verification responseโ
{
"verified": true,
"status": "verified",
"passed": [
"test-success",
"test-recipient-not-found",
"test-timeout",
"test-duplicate-event",
"webhook-signature"
],
"failed": [],
"remaining": [],
"results": [
{
"scenario": "test-success",
"passed": true,
"transaction_id": "550e8400-e29b-41d4-a716-446655440000",
"error": null,
"verified_at": "2026-08-05T10:30:00Z"
}
],
"badge_url": "https://goroute.ai/assets/goroute-verified.svg?token=...",
"verified_until": "2027-08-05T10:30:00Z",
"next_attempt_allowed_at": null,
"attempts_remaining_today": 9
}
| Field | Type | Meaning |
|---|---|---|
verified | boolean | True only when all five scenarios passed |
status | string | pending, verified, expired or revoked |
passed | array | Scenario names that passed |
failed | array | Scenario names that were attempted and failed |
remaining | array | Scenario names not yet attempted |
results | array | Per-scenario detail: scenario, passed, transaction_id, error, verified_at |
badge_url | string or null | Badge URL carrying your badge token. Present only while verified โ see Verification Badge |
verified_until | string or null | Expiry, 365 days after the badge was granted |
next_attempt_allowed_at | string or null | Set when you are rate limited |
attempts_remaining_today | integer | Attempts left in the current 24-hour window |
Rate limitโ
10 verification attempts per organisation per 24 hours. Exceeding it returns HTTP 429:
{
"detail": {
"error": "rate_limit_exceeded",
"error_code": "VERIFICATION_RATE_LIMIT",
"message": "Maximum 10 verification attempts per day",
"next_attempt_allowed_at": "2026-08-06T00:00:00Z",
"attempts_today": 10
}
}
Check your current statusโ
GET /api/v1/partners/verification โ requires the org:manage permission. It does not
consume a verification attempt.
status = requests.get(
"https://app.goroute.ai/peppol-api/api/v1/partners/verification",
headers={"X-API-Key": "your_api_key"},
).json()
print(status["status"], status["passed_scenarios"], status["remaining_scenarios"])
| Field | Type | Meaning |
|---|---|---|
status | string | pending, verified, expired or revoked |
verified | boolean | Whether currently verified |
passed_scenarios | array | Scenarios that have passed |
remaining_scenarios | array | Scenarios not yet passed |
verified_at | string or null | When verification was granted |
verified_until | string or null | When it expires |
badge_url | string or null | Badge URL if verified |
attempts_today | integer | Attempts used today |
max_attempts_per_day | integer | 10 |
Public lookup of a verified organisationโ
GET /api/v1/partners/verified/{org_id} โ no authentication required. {org_id} is an
organisation UUID or slug.
curl https://app.goroute.ai/peppol-api/api/v1/partners/verified/acme-corp
{
"verified": true,
"status": "verified",
"organization_id": "550e8400-e29b-41d4-a716-446655440000",
"organization_name": "Acme Corp",
"verified_since": "2026-08-05T10:30:00Z",
"verified_until": "2027-08-05T10:30:00Z",
"status_checked_at": "2026-08-05T12:00:00Z",
"revocation_reason": null
}
- Rate limit: 60 requests per minute per IP. Responses are cached for 5 minutes โ use
status_checked_atto know how fresh the answer is. - Anti-enumeration: an unknown organisation, and an organisation that has never been verified, both return 404. A 404 does not distinguish "no such organisation" from "not verified".
Checking a trading partner on the Peppol networkโ
This is the other question, and it is a different endpoint. To find out whether a trading partner is registered and what they can receive, use participant lookup:
GET /api/v1/participants/lookup โ pass peppol_id (preferred) or identifier, in
scheme:value form.
import requests
def lookup_partner(peppol_id: str) -> dict:
"""Look a trading partner up on the Peppol network."""
response = requests.get(
"https://app.goroute.ai/peppol-api/api/v1/participants/lookup",
params={"peppol_id": peppol_id},
headers={"X-API-Key": "your_api_key"},
)
return response.json()
result = lookup_partner("0106:12345678")
if result["found"]:
print("Found:", result["name"], result["country"])
print("Can receive:", ", ".join(result["capabilities"]))
else:
print("Not found:", result["message"])
| Field | Type | Meaning |
|---|---|---|
found | boolean | Whether the participant resolved on the network |
participant_id | string | The scheme:value you asked about |
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 |
A request that is not in scheme:value form returns 400 with
Provide peppol_id as scheme:value, e.g. 0248:OM1100099003. Passing the scheme and the
identifier as two separate query parameters does not work.
Verifying multiple partnersโ
There is no batch verification endpoint. Look partners up one at a time and keep your request rate within your plan's rate limit.
def lookup_many(peppol_ids: list[str]) -> list[dict]:
"""Look several partners up by looking each one up in turn."""
return [
{"peppol_id": pid, "lookup": lookup_partner(pid)}
for pid in peppol_ids
]
for entry in lookup_many([
"0106:12345678",
"0106:87654321",
"0208:0123456789",
]):
status = "โ
" if entry["lookup"]["found"] else "โ"
print(f"{status} {entry['peppol_id']}")
Integration patternsโ
Pre-send checkโ
Confirm the receiver is reachable before you send:
class SecureSender:
def send_invoice(self, receiver_peppol_id: str, ubl_xml: str) -> dict:
lookup = lookup_partner(receiver_peppol_id)
if not lookup["found"]:
raise ValueError(f"Receiver not on the network: {lookup['message']}")
if not lookup["capabilities"]:
raise ValueError("Receiver advertises no document types")
return self.do_send(receiver_peppol_id, ubl_xml)
An SMP lookup costs a network round trip. Cache the result for the lifetime of a batch rather than looking the same receiver up once per invoice.
Onboardingโ
async def onboard_partner(partner_data: dict) -> dict:
"""Onboard a new trading partner after a network lookup."""
lookup = lookup_partner(partner_data["peppol_id"])
if not lookup["found"]:
return {
"status": "manual_review",
"reason": lookup["message"],
"message": "Partner is not resolvable on the Peppol network",
}
partner = await create_partner(**partner_data, capabilities=lookup["capabilities"])
return {"status": "active", "partner_id": partner.id}
Periodic re-checkโ
Participants leave the network and change capabilities. Re-run the lookup on a schedule โ GoRoute does not push you a notification when a trading partner's registration changes.
from datetime import datetime, timedelta, timezone
async def recheck_partners():
"""Re-look-up partners that have not been checked recently."""
stale = datetime.now(timezone.utc) - timedelta(days=30)
partners = await db.query(
"SELECT * FROM partners WHERE last_checked < $1", stale
)
for partner in partners:
lookup = lookup_partner(partner.peppol_id)
await db.execute(
"UPDATE partners SET found = $1, last_checked = $2 WHERE id = $3",
lookup["found"], datetime.now(timezone.utc), partner.id,
)
if not lookup["found"]:
await notify_admin(f"Partner {partner.name} no longer resolves on Peppol")
GoRoute publishes no partner events of any kind โ its event catalogue contains none. An earlier revision of this page showed subscribing to a partner status-change event and gave a full payload for it; no such event was ever emitted. If you need to know that a trading partner has left the network, poll the lookup endpoint as shown above.
Tiered due diligenceโ
Everything in this section describes work GoRoute intends to build. None of it exists
today. There is no level parameter on POST /api/v1/partners/verify, there is no credit
check, no sanctions screening, and no trust score anywhere in the product. Do not write
code against this section, and do not tell a compliance team that GoRoute performs these
checks. It is kept on this page so the intended direction stays visible, not because it can
be called.
The planned model tiers the depth of the check:
| Planned tier | Intended to cover |
|---|---|
| Basic | Existence on the Peppol network โ which is what participant lookup already does today |
| Standard | Business registry and trade register validation |
| Extended | Credit check and sanctions screening for high-value or sensitive counterparties |
No request shape, parameter name or response field is documented for these tiers, because none has been built and any example would be a guess.
Next stepsโ
- Verification Badge โ what the badge URL is and how it is issued
- Participant Lookup โ the full lookup reference