Skip to main content

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>
# 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 CodeMeaning
202 AcceptedInvoice queued for delivery (async)
200 OKInvoice delivered (sync mode)
400 Bad RequestValidation failed
401 UnauthorizedInvalid API key
404 Not FoundReceiver not found on Peppol
429 Too Many RequestsRate limit exceeded
500 Server ErrorInternal error (retry)

Delivery Status

Track invoice status through its lifecycle:

pending → processing → sending → delivered
↘ failed
StatusDescription
pendingQueued for processing
processingValidation in progress
sendingBeing transmitted to receiver's AP
deliveredSuccessfully delivered
failedDelivery 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']}")

Next Steps