Skip to main content

Error Handling

This guide covers the errors you may encounter when sending documents via GoRoute and how to handle them effectively.

Error Categories​

GoRoute errors fall into these categories:

CategoryHTTP CodeRetriableDescription
Validation400❌ NoDocument failed validation
Authentication401❌ NoInvalid or missing API key
Authorization403❌ NoNot allowed to perform action
Not Found404❌ NoResource or receiver not found
Rate Limit429βœ… YesToo many requests
Server Error500+βœ… YesInternal error

Error Response Format​

All errors follow a consistent format:

{
"error": {
"code": "VALIDATION_ERROR",
"message": "Document validation failed",
"details": [
{
"rule": "BR-16",
"location": "/Invoice/cac:InvoiceLine[1]/cbc:LineExtensionAmount",
"message": "Amount MUST be rounded to maximum 2 decimals"
}
],
"request_id": "req_abc123xyz"
}
}

Common Errors​

Validation Errors (400)​

VALIDATION_ERROR​

The document failed one or more validation rules.

# Example: Handle validation errors
response = requests.post(url, headers=headers, data=invoice_xml)

if response.status_code == 400:
error = response.json()["error"]
if error["code"] == "VALIDATION_ERROR":
for detail in error["details"]:
print(f"Rule {detail['rule']}: {detail['message']}")
print(f" Location: {detail['location']}")

Fix: Review the validation details and correct your document.

INVALID_XML​

The XML is malformed or not well-formed.

{
"error": {
"code": "INVALID_XML",
"message": "XML parsing failed at line 45: unclosed tag 'cac:Party'"
}
}

Fix: Check XML syntaxβ€”ensure all tags are properly closed.

UNSUPPORTED_DOCUMENT_TYPE​

The document type is not supported.

{
"error": {
"code": "UNSUPPORTED_DOCUMENT_TYPE",
"message": "Document type 'Quote' is not supported"
}
}

Fix: Use a supported document type (Invoice, CreditNote, Order, etc.).

Authentication Errors (401)​

INVALID_API_KEY​

{
"error": {
"code": "INVALID_API_KEY",
"message": "The provided API key is invalid or expired"
}
}

Fix: Check your API key is correct and not expired.

MISSING_API_KEY​

{
"error": {
"code": "MISSING_API_KEY",
"message": "API key required in X-API-Key header"
}
}

Fix: Include the X-API-Key header in your request.

Authorization Errors (403)​

ORGANIZATION_MISMATCH​

{
"error": {
"code": "ORGANIZATION_MISMATCH",
"message": "Sender ID does not match your registered organization"
}
}

Fix: The sender's Peppol ID in the document must match your registered participant.

QUOTA_EXCEEDED​

{
"error": {
"code": "QUOTA_EXCEEDED",
"message": "Monthly document quota exceeded. Current: 1000/1000"
}
}

Fix: Upgrade your plan or wait until the next billing cycle.

Not Found Errors (404)​

RECEIVER_NOT_FOUND​

{
"error": {
"code": "RECEIVER_NOT_FOUND",
"message": "Participant 0106:99999999 not found on Peppol network"
}
}

Fix: Verify the receiver's Peppol ID is correct and they are registered.

# Check if receiver exists before sending
response = requests.get(
"https://app.goroute.ai/peppol-api/api/v1/participants/lookup",
params={"scheme": "0106", "identifier": "99999999"},
headers={"X-API-Key": api_key}
)

if response.status_code == 404:
print("Receiver is not registered on Peppol")

TRANSACTION_NOT_FOUND​

{
"error": {
"code": "TRANSACTION_NOT_FOUND",
"message": "Transaction txn_xyz not found"
}
}

Fix: Verify the transaction ID is correct.

Rate Limit Errors (429)​

RATE_LIMIT_EXCEEDED​

{
"error": {
"code": "RATE_LIMIT_EXCEEDED",
"message": "Rate limit exceeded. Retry after 60 seconds",
"retry_after": 60
}
}

Fix: Implement exponential backoff:

import time

def send_with_retry(url, headers, data, max_retries=5):
"""Send request with exponential backoff for rate limits."""
for attempt in range(max_retries):
response = requests.post(url, headers=headers, data=data)

if response.status_code == 429:
retry_after = int(response.headers.get("Retry-After", 60))
wait_time = min(retry_after, 2 ** attempt * 10)
print(f"Rate limited. Waiting {wait_time}s...")
time.sleep(wait_time)
continue

return response

raise Exception("Max retries exceeded")

Server Errors (500+)​

INTERNAL_ERROR​

{
"error": {
"code": "INTERNAL_ERROR",
"message": "An internal error occurred. Please try again.",
"request_id": "req_abc123"
}
}

Fix: Retry the request. If persistent, contact support with the request_id.

SERVICE_UNAVAILABLE​

{
"error": {
"code": "SERVICE_UNAVAILABLE",
"message": "Service temporarily unavailable"
}
}

Fix: Retry after a short delay. Check status page for outages.

Delivery Errors​

After a document is accepted, it may still fail during delivery:

DELIVERY_FAILED​

{
"transaction_id": "txn_abc123",
"status": "failed",
"error": {
"code": "DELIVERY_FAILED",
"message": "Receiver's Access Point rejected the message",
"details": {
"as4_error": "EBMS:0004",
"description": "Other error"
}
}
}

RECEIVER_UNAVAILABLE​

{
"transaction_id": "txn_abc123",
"status": "failed",
"error": {
"code": "RECEIVER_UNAVAILABLE",
"message": "Could not connect to receiver's Access Point after 3 attempts"
}
}

CERTIFICATE_ERROR​

{
"transaction_id": "txn_abc123",
"status": "failed",
"error": {
"code": "CERTIFICATE_ERROR",
"message": "Receiver's certificate has expired"
}
}

Robust Error Handling​

Implement comprehensive error handling:

import requests
from enum import Enum
import time
import logging

class ErrorCode(Enum):
VALIDATION_ERROR = "VALIDATION_ERROR"
INVALID_API_KEY = "INVALID_API_KEY"
RECEIVER_NOT_FOUND = "RECEIVER_NOT_FOUND"
RATE_LIMIT_EXCEEDED = "RATE_LIMIT_EXCEEDED"
INTERNAL_ERROR = "INTERNAL_ERROR"

class GoRouteError(Exception):
def __init__(self, code: str, message: str, details: list = None):
self.code = code
self.message = message
self.details = details or []
super().__init__(message)

class GoRouteClient:
def __init__(self, api_key: str):
self.api_key = api_key
self.base_url = "https://app.goroute.ai/peppol-api"
self.logger = logging.getLogger(__name__)

def send_invoice(self, xml: str) -> dict:
"""Send invoice with comprehensive error handling."""
try:
response = self._make_request("POST", "/api/v1/send", data=xml)
return response
except GoRouteError as e:
self._handle_error(e)
raise

def _make_request(
self,
method: str,
path: str,
data: str = None,
max_retries: int = 3
) -> dict:
"""Make HTTP request with retry logic."""
url = f"{self.base_url}{path}"
headers = {
"X-API-Key": self.api_key,
"Content-Type": "application/xml"
}

for attempt in range(max_retries):
try:
response = requests.request(
method, url, headers=headers, data=data, timeout=30
)

# Handle rate limiting
if response.status_code == 429:
retry_after = int(response.headers.get("Retry-After", 60))
self.logger.warning(f"Rate limited, waiting {retry_after}s")
time.sleep(retry_after)
continue

# Handle server errors with retry
if response.status_code >= 500:
if attempt < max_retries - 1:
wait = 2 ** attempt
self.logger.warning(f"Server error, retrying in {wait}s")
time.sleep(wait)
continue

# Parse response
if response.status_code >= 400:
error_data = response.json().get("error", {})
raise GoRouteError(
code=error_data.get("code", "UNKNOWN"),
message=error_data.get("message", "Unknown error"),
details=error_data.get("details", [])
)

return response.json()

except requests.exceptions.Timeout:
if attempt < max_retries - 1:
continue
raise GoRouteError("TIMEOUT", "Request timed out")

except requests.exceptions.ConnectionError:
raise GoRouteError("CONNECTION_ERROR", "Could not connect to server")

raise GoRouteError("MAX_RETRIES", "Maximum retries exceeded")

def _handle_error(self, error: GoRouteError):
"""Log and optionally notify on errors."""
self.logger.error(f"GoRoute Error [{error.code}]: {error.message}")

if error.code == "VALIDATION_ERROR":
for detail in error.details:
self.logger.error(f" - {detail['rule']}: {detail['message']}")

# Could add alerting here for critical errors
if error.code in ["INTERNAL_ERROR", "SERVICE_UNAVAILABLE"]:
self._send_alert(error)

def _send_alert(self, error: GoRouteError):
"""Send alert for critical errors."""
pass # Implement your alerting logic


# Usage
client = GoRouteClient("your_api_key")

try:
result = client.send_invoice(invoice_xml)
print(f"Success: {result['transaction_id']}")
except GoRouteError as e:
if e.code == "VALIDATION_ERROR":
print("Please fix validation errors:")
for detail in e.details:
print(f" {detail['message']}")
elif e.code == "RECEIVER_NOT_FOUND":
print("The receiver is not registered on Peppol")
else:
print(f"Error: {e.message}")

Logging Best Practices​

import logging

# Configure structured logging
logging.basicConfig(
format='%(asctime)s %(levelname)s [%(name)s] %(message)s',
level=logging.INFO
)

logger = logging.getLogger("goroute")

def send_invoice(xml: str):
"""Send invoice with proper logging."""
invoice_id = extract_invoice_id(xml) # Your function to extract ID

logger.info(f"Sending invoice {invoice_id}")

try:
result = client.send_invoice(xml)
logger.info(
f"Invoice {invoice_id} accepted",
extra={
"invoice_id": invoice_id,
"transaction_id": result["transaction_id"]
}
)
return result
except GoRouteError as e:
logger.error(
f"Failed to send invoice {invoice_id}: {e.message}",
extra={
"invoice_id": invoice_id,
"error_code": e.code,
"error_details": e.details
}
)
raise

Webhooks for Error Notification​

Set up webhooks to be notified of delivery failures:

# In your webhook handler
@app.post("/webhooks/goroute")
async def handle_webhook(request: Request):
event = await request.json()

if event["type"] == "transaction.failed":
transaction = event["data"]

# Alert on delivery failure
notify_team(
f"Peppol delivery failed for transaction {transaction['id']}",
error=transaction["error"]
)

# Maybe queue for retry
if is_retriable(transaction["error"]["code"]):
queue_for_retry(transaction["id"])

return {"status": "ok"}

Next Steps​