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โ€‹