Invoice Responses
Send Invoice Response messages (Message Level Response/MLR) to acknowledge or reject received invoices.
What is Invoice Response?
The Invoice Response (also known as MLR - Message Level Response) allows you to:
- Acknowledge receipt of an invoice
- Accept an invoice for processing
- Reject an invoice with a reason
- Provide status updates on invoice processing
Sender ──▶ Invoice ──▶ Receiver
│
▼
Invoice Response
│
▼
Sender receives
status update
Response Codes
| Code | Status | Description |
|---|---|---|
AP | Approved | Invoice approved for payment |
RE | Rejected | Invoice rejected |
AB | Acknowledged | Invoice received, under review |
IP | In Process | Invoice being processed |
UQ | Under Query | Invoice has issues, querying sender |
CA | Conditionally Accepted | Accepted with conditions |
PD | Paid | Invoice has been paid |
Send an Invoice Response
Acknowledge Receipt
import requests
def send_invoice_response(
invoice_document_id: str,
response_code: str,
reason: str = None
) -> dict:
"""Send an invoice response to the original sender."""
response_data = {
"invoice_document_id": invoice_document_id,
"response_code": response_code,
"response_reason": reason
}
response = requests.post(
"https://app.goroute.ai/peppol-api/api/v1/invoice-response",
headers={
"X-API-Key": "your_api_key",
"Content-Type": "application/json"
},
json=response_data
)
return response.json()
# Acknowledge receipt
result = send_invoice_response(
invoice_document_id="doc_xyz789",
response_code="AB",
reason="Invoice received and under review"
)
print(f"Response sent: {result['transaction_id']}")
Accept Invoice
# Accept for payment
result = send_invoice_response(
invoice_document_id="doc_xyz789",
response_code="AP",
reason="Invoice approved for payment"
)
Reject Invoice
# Reject with reason
result = send_invoice_response(
invoice_document_id="doc_xyz789",
response_code="RE",
reason="Invoice total does not match purchase order PO-2024-00123"
)
Response Structure
GoRoute generates the proper UBL ApplicationResponse:
<?xml version="1.0" encoding="UTF-8"?>
<ApplicationResponse xmlns="urn:oasis:names:specification:ubl:schema:xsd:ApplicationResponse-2"
xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"
xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">
<cbc:CustomizationID>urn:fdc:peppol.eu:poacc:trns:invoice_response:3</cbc:CustomizationID>
<cbc:ProfileID>urn:fdc:peppol.eu:poacc:bis:invoice_response:3</cbc:ProfileID>
<cbc:ID>IR-2024-00456</cbc:ID>
<cbc:IssueDate>2024-01-15</cbc:IssueDate>
<cbc:IssueTime>10:45:00</cbc:IssueTime>
<!-- Sender (you, responding to the invoice) -->
<cac:SenderParty>
<cbc:EndpointID schemeID="0106">87654321</cbc:EndpointID>
<cac:PartyLegalEntity>
<cbc:RegistrationName>Your Company BV</cbc:RegistrationName>
</cac:PartyLegalEntity>
</cac:SenderParty>
<!-- Receiver (original invoice sender) -->
<cac:ReceiverParty>
<cbc:EndpointID schemeID="0106">12345678</cbc:EndpointID>
<cac:PartyLegalEntity>
<cbc:RegistrationName>Supplier BV</cbc:RegistrationName>
</cac:PartyLegalEntity>
</cac:ReceiverParty>
<!-- Response details -->
<cac:DocumentResponse>
<cac:Response>
<cbc:ResponseCode>AP</cbc:ResponseCode>
<cbc:Description>Invoice approved for payment</cbc:Description>
</cac:Response>
<cac:DocumentReference>
<cbc:ID>INV-2024-00123</cbc:ID>
</cac:DocumentReference>
</cac:DocumentResponse>
</ApplicationResponse>
Response Workflow
Implement a typical invoice processing workflow:
from enum import Enum
import asyncio
class InvoiceStatus(Enum):
RECEIVED = "AB" # Acknowledged
UNDER_REVIEW = "IP" # In Process
QUERIED = "UQ" # Under Query
APPROVED = "AP" # Approved
REJECTED = "RE" # Rejected
PAID = "PD" # Paid
class InvoiceWorkflow:
def __init__(self, api_key: str):
self.api_key = api_key
async def process_invoice(self, document_id: str):
"""Process invoice through workflow stages."""
# Step 1: Acknowledge receipt
await self.send_response(document_id, InvoiceStatus.RECEIVED)
# Step 2: Validate and review
invoice = await self.fetch_invoice(document_id)
validation_result = await self.validate_invoice(invoice)
if not validation_result["valid"]:
# Query the sender about issues
await self.send_response(
document_id,
InvoiceStatus.QUERIED,
f"Issues found: {validation_result['issues']}"
)
return
# Step 3: Match to PO
po_match = await self.match_to_purchase_order(invoice)
if not po_match["matched"]:
await self.send_response(
document_id,
InvoiceStatus.REJECTED,
f"No matching purchase order found for reference {invoice['po_reference']}"
)
return
# Step 4: Approve for payment
await self.send_response(document_id, InvoiceStatus.APPROVED)
# Step 5: Schedule payment
await self.schedule_payment(invoice)
async def send_response(
self,
document_id: str,
status: InvoiceStatus,
reason: str = None
):
"""Send invoice response."""
response = requests.post(
"https://app.goroute.ai/peppol-api/api/v1/invoice-response",
headers={
"X-API-Key": self.api_key,
"Content-Type": "application/json"
},
json={
"invoice_document_id": document_id,
"response_code": status.value,
"response_reason": reason
}
)
response.raise_for_status()
return response.json()
# Usage
workflow = InvoiceWorkflow("your_api_key")
await workflow.process_invoice("doc_xyz789")
Detailed Rejection Reasons
When rejecting, provide specific reasons:
REJECTION_REASONS = {
"PO_MISMATCH": "Invoice does not match any purchase order",
"AMOUNT_MISMATCH": "Invoice amount does not match agreed price",
"DUPLICATE": "This invoice has already been received",
"WRONG_RECIPIENT": "Invoice sent to wrong company",
"INVALID_VAT": "VAT registration number is invalid",
"MISSING_INFO": "Required information is missing",
"GOODS_NOT_RECEIVED": "Goods/services have not been received"
}
def reject_invoice(document_id: str, reason_code: str, details: str = ""):
"""Reject an invoice with a specific reason."""
reason = REJECTION_REASONS.get(reason_code, reason_code)
full_reason = f"{reason}. {details}".strip()
return send_invoice_response(
invoice_document_id=document_id,
response_code="RE",
reason=full_reason
)
# Example
reject_invoice(
"doc_xyz789",
"AMOUNT_MISMATCH",
"PO-2024-00123 specifies €1000, invoice shows €1200"
)
Query the Sender
When there are questions about an invoice:
def query_invoice(document_id: str, question: str):
"""Send a query to the invoice sender."""
return send_invoice_response(
invoice_document_id=document_id,
response_code="UQ",
reason=question
)
# Examples
query_invoice(
"doc_xyz789",
"Please provide purchase order reference for this invoice"
)
query_invoice(
"doc_xyz789",
"Line 3 quantity (100 units) differs from delivery note (80 units). Please clarify."
)
Payment Notification
Notify the sender when invoice is paid:
def notify_payment(document_id: str, payment_date: str, payment_reference: str):
"""Notify sender that invoice has been paid."""
return send_invoice_response(
invoice_document_id=document_id,
response_code="PD",
reason=f"Payment made on {payment_date}. Reference: {payment_reference}"
)
# Example
notify_payment(
"doc_xyz789",
payment_date="2024-02-15",
payment_reference="PAY-2024-00789"
)
Best Practices
1. Acknowledge Promptly
async def handle_incoming_invoice(document_id: str):
# Immediately acknowledge receipt
await send_invoice_response(document_id, "AB", "Invoice received")
# Then process asynchronously
await queue.add("process_invoice", document_id)
2. Provide Helpful Rejection Reasons
# ❌ Unhelpful
reject_invoice(document_id, "RE", "Rejected")
# ✅ Helpful
reject_invoice(
document_id,
"RE",
"Invoice amount €1,200 exceeds PO-2024-00123 limit of €1,000. "
"Please issue a corrected invoice or contact purchasing@company.com"
)
3. Track Response Status
# Get status of sent responses
response = requests.get(
f"https://app.goroute.ai/peppol-api/api/v1/documents/{document_id}/responses",
headers={"X-API-Key": "your_api_key"}
)
responses = response.json()
for resp in responses["items"]:
print(f"{resp['sent_at']}: {resp['response_code']} - {resp['reason']}")
Response Requirements by Country
Some countries have specific requirements:
| Country | Requirement |
|---|---|
| 🇮🇹 Italy | Invoice Response mandatory within 15 days |
| 🇳🇴 Norway | Response recommended for B2G |
| 🇳🇱 Netherlands | Optional but encouraged |