Skip to main content

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

CodeStatusDescription
APApprovedInvoice approved for payment
RERejectedInvoice rejected
ABAcknowledgedInvoice received, under review
IPIn ProcessInvoice being processed
UQUnder QueryInvoice has issues, querying sender
CAConditionally AcceptedAccepted with conditions
PDPaidInvoice 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:

CountryRequirement
🇮🇹 ItalyInvoice Response mandatory within 15 days
🇳🇴 NorwayResponse recommended for B2G
🇳🇱 NetherlandsOptional but encouraged

Next Steps