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:
| Category | HTTP Code | Retriable | Description |
|---|---|---|---|
| Validation | 400 | β No | Document failed validation |
| Authentication | 401 | β No | Invalid or missing API key |
| Authorization | 403 | β No | Not allowed to perform action |
| Not Found | 404 | β No | Resource or receiver not found |
| Rate Limit | 429 | β Yes | Too many requests |
| Server Error | 500+ | β Yes | Internal 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"}