Sending an Invoice
This guide covers the complete process of sending an invoice through the Peppol network using GoRoute.
Quick Start
Send an invoice in three steps:
import requests
# 1. Prepare your invoice XML
with open("invoice.xml", "r") as f:
invoice_xml = f.read()
# 2. Send the invoice
response = requests.post(
"https://app.goroute.ai/peppol-api/api/v1/send",
headers={
"X-API-Key": "your_api_key",
"Content-Type": "application/xml"
},
data=invoice_xml
)
# 3. Check the result
result = response.json()
print(f"Transaction ID: {result['transaction_id']}")
print(f"Status: {result['status']}")
Complete Workflow
Step 1: Prepare the Invoice
Ensure your invoice meets Peppol requirements:
<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"
xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">
<!-- Required: Peppol identifiers -->
<cbc:CustomizationID>urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0</cbc:CustomizationID>
<cbc:ProfileID>urn:fdc:peppol.eu:2017:poacc:billing:01:1.0</cbc:ProfileID>
<!-- Invoice details -->
<cbc:ID>INV-2024-00123</cbc:ID>
<cbc:IssueDate>2024-01-15</cbc:IssueDate>
<cbc:DueDate>2024-02-15</cbc:DueDate>
<cbc:InvoiceTypeCode>380</cbc:InvoiceTypeCode>
<cbc:DocumentCurrencyCode>EUR</cbc:DocumentCurrencyCode>
<!-- Seller (your organization) -->
<cac:AccountingSupplierParty>
<cac:Party>
<cbc:EndpointID schemeID="0106">12345678</cbc:EndpointID>
<cac:PartyName>
<cbc:Name>Your Company BV</cbc:Name>
</cac:PartyName>
<cac:PostalAddress>
<cbc:StreetName>Main Street 1</cbc:StreetName>
<cbc:CityName>Amsterdam</cbc:CityName>
<cbc:PostalZone>1012AB</cbc:PostalZone>
<cac:Country>
<cbc:IdentificationCode>NL</cbc:IdentificationCode>
</cac:Country>
</cac:PostalAddress>
<cac:PartyLegalEntity>
<cbc:RegistrationName>Your Company BV</cbc:RegistrationName>
</cac:PartyLegalEntity>
</cac:Party>
</cac:AccountingSupplierParty>
<!-- Buyer (receiver) -->
<cac:AccountingCustomerParty>
<cac:Party>
<cbc:EndpointID schemeID="0106">87654321</cbc:EndpointID>
<cac:PartyName>
<cbc:Name>Customer Company BV</cbc:Name>
</cac:PartyName>
<cac:PartyLegalEntity>
<cbc:RegistrationName>Customer Company BV</cbc:RegistrationName>
</cac:PartyLegalEntity>
</cac:Party>
</cac:AccountingCustomerParty>
<!-- Payment terms -->
<cac:PaymentTerms>
<cbc:Note>Payment due within 30 days</cbc:Note>
</cac:PaymentTerms>
<!-- Tax totals -->
<cac:TaxTotal>
<cbc:TaxAmount currencyID="EUR">21.00</cbc:TaxAmount>
<cac:TaxSubtotal>
<cbc:TaxableAmount currencyID="EUR">100.00</cbc:TaxableAmount>
<cbc:TaxAmount currencyID="EUR">21.00</cbc:TaxAmount>
<cac:TaxCategory>
<cbc:ID>S</cbc:ID>
<cbc:Percent>21</cbc:Percent>
<cac:TaxScheme>
<cbc:ID>VAT</cbc:ID>
</cac:TaxScheme>
</cac:TaxCategory>
</cac:TaxSubtotal>
</cac:TaxTotal>
<!-- Invoice totals -->
<cac:LegalMonetaryTotal>
<cbc:LineExtensionAmount currencyID="EUR">100.00</cbc:LineExtensionAmount>
<cbc:TaxExclusiveAmount currencyID="EUR">100.00</cbc:TaxExclusiveAmount>
<cbc:TaxInclusiveAmount currencyID="EUR">121.00</cbc:TaxInclusiveAmount>
<cbc:PayableAmount currencyID="EUR">121.00</cbc:PayableAmount>
</cac:LegalMonetaryTotal>
<!-- Invoice lines -->
<cac:InvoiceLine>
<cbc:ID>1</cbc:ID>
<cbc:InvoicedQuantity unitCode="HUR">10</cbc:InvoicedQuantity>
<cbc:LineExtensionAmount currencyID="EUR">100.00</cbc:LineExtensionAmount>
<cac:Item>
<cbc:Name>Professional Consulting Services</cbc:Name>
<cac:ClassifiedTaxCategory>
<cbc:ID>S</cbc:ID>
<cbc:Percent>21</cbc:Percent>
<cac:TaxScheme>
<cbc:ID>VAT</cbc:ID>
</cac:TaxScheme>
</cac:ClassifiedTaxCategory>
</cac:Item>
<cac:Price>
<cbc:PriceAmount currencyID="EUR">10.00</cbc:PriceAmount>
</cac:Price>
</cac:InvoiceLine>
</Invoice>
Step 2: Validate (Optional but Recommended)
# Pre-validate to catch errors before sending
validation = requests.post(
"https://app.goroute.ai/peppol-api/api/v1/validate",
headers={
"X-API-Key": "your_api_key",
"Content-Type": "application/xml"
},
data=invoice_xml
)
if not validation.json()["valid"]:
print("Validation failed:")
for error in validation.json()["errors"]:
print(f" {error['rule']}: {error['message']}")
exit(1)
print("✅ Validation passed")
Step 3: Send the Invoice
response = requests.post(
"https://app.goroute.ai/peppol-api/api/v1/send",
headers={
"X-API-Key": "your_api_key",
"Content-Type": "application/xml"
},
data=invoice_xml
)
if response.status_code == 202:
result = response.json()
print(f"✅ Invoice accepted")
print(f" Transaction ID: {result['transaction_id']}")
print(f" Status: {result['status']}")
else:
print(f"❌ Sending failed: {response.json()}")
Step 4: Track Delivery
import time
transaction_id = result['transaction_id']
# Poll for delivery status
for attempt in range(10):
status = requests.get(
f"https://app.goroute.ai/peppol-api/api/v1/transactions/{transaction_id}",
headers={"X-API-Key": "your_api_key"}
)
transaction = status.json()
print(f"Status: {transaction['status']}")
if transaction['status'] == 'delivered':
print("✅ Invoice delivered successfully")
break
elif transaction['status'] == 'failed':
print(f"❌ Delivery failed: {transaction['error']}")
break
time.sleep(2) # Wait 2 seconds before checking again
Send Options
With Metadata
Add custom metadata to track invoices:
response = requests.post(
"https://app.goroute.ai/peppol-api/api/v1/send",
headers={
"X-API-Key": "your_api_key",
"Content-Type": "application/xml",
"X-Reference-ID": "your-internal-ref-123",
"X-Correlation-ID": "order-456"
},
data=invoice_xml
)
Async Mode (Default)
By default, invoices are sent asynchronously:
# Returns immediately with transaction ID
response = requests.post(
"https://app.goroute.ai/peppol-api/api/v1/send",
headers={"X-API-Key": "your_api_key", "Content-Type": "application/xml"},
data=invoice_xml
)
# Response: 202 Accepted
# {
# "transaction_id": "txn_abc123",
# "status": "pending",
# "message": "Invoice queued for delivery"
# }
Sync Mode
Wait for delivery confirmation:
response = requests.post(
"https://app.goroute.ai/peppol-api/api/v1/send?sync=true",
headers={"X-API-Key": "your_api_key", "Content-Type": "application/xml"},
data=invoice_xml,
timeout=30 # May take longer
)
# Response: 200 OK (if delivered)
# {
# "transaction_id": "txn_abc123",
# "status": "delivered",
# "delivered_at": "2024-01-15T10:30:45Z"
# }
Response Codes
| Status Code | Meaning |
|---|---|
202 Accepted | Invoice queued for delivery (async) |
200 OK | Invoice delivered (sync mode) |
400 Bad Request | Validation failed |
401 Unauthorized | Invalid API key |
404 Not Found | Receiver not found on Peppol |
429 Too Many Requests | Rate limit exceeded |
500 Server Error | Internal error (retry) |
Delivery Status
Track invoice status through its lifecycle:
pending → processing → sending → delivered
↘ failed
| Status | Description |
|---|---|
pending | Queued for processing |
processing | Validation in progress |
sending | Being transmitted to receiver's AP |
delivered | Successfully delivered |
failed | Delivery failed (see error) |
Complete Example
import requests
import time
class GoRouteClient:
def __init__(self, api_key: str, base_url: str = "https://app.goroute.ai/peppol-api"):
self.api_key = api_key
self.base_url = base_url
def send_invoice(self, xml: str, wait_for_delivery: bool = False) -> dict:
"""Send an invoice via Peppol."""
# Validate first
validation = self._validate(xml)
if not validation["valid"]:
raise ValueError(f"Validation failed: {validation['errors']}")
# Send the invoice
response = requests.post(
f"{self.base_url}/api/v1/send",
headers={
"X-API-Key": self.api_key,
"Content-Type": "application/xml"
},
data=xml
)
response.raise_for_status()
result = response.json()
if wait_for_delivery:
return self._wait_for_delivery(result["transaction_id"])
return result
def _validate(self, xml: str) -> dict:
response = requests.post(
f"{self.base_url}/api/v1/validate",
headers={
"X-API-Key": self.api_key,
"Content-Type": "application/xml"
},
data=xml
)
return response.json()
def _wait_for_delivery(self, transaction_id: str, max_attempts: int = 30) -> dict:
for _ in range(max_attempts):
status = self.get_transaction(transaction_id)
if status["status"] in ["delivered", "failed"]:
return status
time.sleep(2)
raise TimeoutError("Delivery timeout")
def get_transaction(self, transaction_id: str) -> dict:
response = requests.get(
f"{self.base_url}/api/v1/transactions/{transaction_id}",
headers={"X-API-Key": self.api_key}
)
response.raise_for_status()
return response.json()
# Usage
client = GoRouteClient("your_api_key")
with open("invoice.xml") as f:
invoice_xml = f.read()
result = client.send_invoice(invoice_xml, wait_for_delivery=True)
print(f"Invoice {result['status']}: {result['transaction_id']}")