Skip to main content

Sending a Credit Note

Credit Notes are used to correct or cancel previously issued invoices. This guide covers when and how to send credit notes via Peppol.

You may not have to build the XML yourself

Most of this page shows you how to assemble a credit note by hand and post it to /api/v1/documents. There is a shorter route: GoRoute can work the credit note out from the original invoice, including the arithmetic for a partial credit. See Let GoRoute build the credit note. It does not suit every case โ€” three real limits are listed there โ€” so read that section before choosing between them.

When to Use a Credit Noteโ€‹

ScenarioUse Credit Note?Notes
Customer returned goodsโœ… YesFull or partial credit
Pricing error discoveredโœ… YesCorrect the amount
Quantity was wrongโœ… YesCredit for difference
Discount applied after invoiceโœ… YesCredit the discount amount
Cancel entire invoiceโœ… YesCredit full amount
Customer disputes chargeโš ๏ธ MaybeDepends on outcome
Payment already receivedโœ… YesCredit note for accounting

Let GoRoute build the credit noteโ€‹

Instead of assembling the XML yourself, you can hand GoRoute the original invoice and say what you want credited. GoRoute does the arithmetic, references the original invoice, picks the right country profile, and validates the result.

There are two calls. They take the same body; the difference is only whether the credit note is sent.

CallWhat it does
POST /api/v1/invoices/credit-noteBuilds the credit note and hands it back with a validation result and an XML preview. Sends nothing. Use it to show a draft, or to check the figures before committing.
POST /api/v1/invoices/credit-note/sendBuilds the same credit note and then issues it over Peppol.

The body takes the whole original invoiceโ€‹

This is the first thing to know, because it is not what most people expect: you pass the entire original invoice in the request body, as the original_invoice object, not an invoice ID or a transaction ID. GoRoute does not look the invoice up for you. Keep the invoice object you sent originally, or rebuild it, and pass it back.

FieldRequiredMeaning
original_invoiceYesThe complete invoice being credited, in the same shape you would send it.
credit_note_numberYesYour number for the credit note, 1 to 150 characters. GoRoute does not generate one.
credit_typeNofull (default), partial, or amount. See below.
reasonNoOne of exactly six values: return, correction, cancellation (the default), discount, damage, other. Anything else is refused with 400.
issue_dateNoYYYY-MM-DD. Defaults to today. A different format is refused with 400.
line_selectionsFor partialA list of {line_number, quantity?, amount?, reason?} saying which lines, and how much of each, to credit. Required when credit_type is partial. line_number is the line's number on the invoice as it was sent: the one you gave it, or its position (1 for the first line) if your invoice did not number its lines. quantity defaults to the whole line. amount is the net sum to credit for that line, whatever the quantity: the credit note line carries it as the price of the credited quantity, so it is credited exactly.
credit_amountFor amountThe amount to credit. Required, with credit_description, when credit_type is amount.
credit_descriptionFor amountWhat the credit is for. Required alongside credit_amount.
tax_categoryNoFor an amount credit. Defaults to standard.
tax_rateNoFor an amount credit, as a percentage. Defaults to 0.
notesNoExtra text for the credit note. A credit note carries one note, because Peppol BIS Billing 3.0 and PINT OM allow only one: GoRoute writes the reason, then the original invoice's number, then each of your notes, separated by ; followed by a space โ€” for example Reason: return; Credit note for invoice: INV-1; Goods returned damaged.
original_invoice_uuidFor OmanThe UUID the original invoice was sent with: the document_uuid that POST /api/v1/invoices returned for it. Required when the seller or the buyer is in Oman, because the credit note must cite it (IBR-032-OM); without it, or when it is not a UUID, the call is refused with 400. For an invoice issued before e-invoicing began, give 00000000-0000-0000-0000-000000000000. GoRoute does not work it out from original_invoice. Ignored for other countries.

The three credit types.

  • full credits the whole invoice โ€” a cancellation or a complete return.
  • partial credits particular lines, or part of a line's quantity. This is the one worth using the API for, because it is the arithmetic you would otherwise write yourself.
  • amount credits a flat sum against the invoice โ€” a discount or an adjustment agreed after the fact โ€” rather than anything line-by-line.

What comes backโ€‹

POST /api/v1/invoices/credit-note answers with success, the built credit_note object, a validation result and an xml_preview.

xml_preview is a preview, not the document

It is truncated at 2000 characters, with ... on the end when the document is longer. It is for showing a human what was built. Do not parse it, and do not send it โ€” use the send call, which renders the document in full.

POST /api/v1/invoices/credit-note/send answers with the transaction shape used elsewhere: transaction_id, status, message, validation and created_at. If validation fails, it answers 200 with transaction_id: null and status validation_failed, and nothing is sent โ€” so check status, not just the HTTP code.

The country profile is chosen for you from the invoice, so an Omani or Australasian seller gets the profile that market requires, and an Omani credit note is signed for Fawtara before it goes.

Three limits to check before you choose this routeโ€‹

These are the reasons you might still prefer to build the document yourself and post it to /api/v1/documents.

  1. The send call needs the buyer's Peppol identifier, and has no email fallback โ€” unless the seller is in Oman. It reads buyer.peppol_id off the credit note to address the delivery. For a seller outside Oman, a buyer who is not on the Peppol network means this call cannot deliver to them, and there is no "email it instead" path here of the kind the store apps use. Outside Oman, use it only for a buyer you know is on the network.

    For an Omani seller the case is handled, and it is the ordinary case. A credit note to a walk-in customer, or to a customer abroad who is not on Peppol, is validated, signed for Fawtara and stored. It is addressed to the ยง10.3.1 stand-in participant in place of the absent buyer, and the tax report owed to the Oman Tax Authority โ€” the Tax Data Document, or TDD โ€” is queued. The credit note itself is deliberately never put on the Peppol network: the Oman rules say a document addressed to a stand-in must not be exchanged with a receiving access point.

    Which stand-in it is addressed to depends on the buyer:

    • 0248:997770000097 โ€” the buyer is in a country other than Oman, or the credit note carries the export transaction kind.
    • 0248:997770000095 โ€” the buyer is in Oman and has a VAT number, but is not on Peppol.
    • 0248:997770000099 โ€” the buyer is in Oman with neither a participant nor a VAT number, so a consumer sale.

    The transaction then ends as held, not delivered, and nothing is retrying it. held means built, stored and auditable, and deliberately not transmitted; it is not a failure. See Tracking for that status, and the Oman country guide for the reporting side.

  2. It stores no copy of the original invoice on the transaction. Sending an invoice the ordinary way keeps the invoice object with the transaction, and the features that re-read that copy โ€” resending, and the invoice detail in history โ€” depend on it. A credit note sent this way has no such copy, so those features will not work for it.

  3. It accepts no idempotency key. Idempotency-Key is honoured on the ordinary invoice send path; these two calls do not read it. A retried call is a second credit note, not a repeat of the first. If your client retries on timeouts, make it safe yourself โ€” check whether the credit note number already exists before calling again.

The reply to the send says queued even for a credit note it has already held

POST /api/v1/invoices/credit-note/send answers status: "queued" with the message "Credit note created and queued for delivery" in every case, including the Omani stand-in case above, where the platform has already decided never to transmit the document. Do not treat that first reply as the outcome. Fetch GET /api/v1/transactions/{transaction_id} and read status there: for a stand-in-addressed credit note it reads held. A client that waits for the queued credit note to become delivered will wait for a delivery that is not coming.

Credit Note Structureโ€‹

A credit note must reference the original invoice:

<?xml version="1.0" encoding="UTF-8"?>
<CreditNote xmlns="urn:oasis:names:specification:ubl:schema:xsd:CreditNote-2"
xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"
xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">

<!-- 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>

<!-- Credit note identification -->
<cbc:ID>CN-2024-00045</cbc:ID>
<cbc:IssueDate>2024-01-20</cbc:IssueDate>
<cbc:CreditNoteTypeCode>381</cbc:CreditNoteTypeCode>
<cbc:DocumentCurrencyCode>EUR</cbc:DocumentCurrencyCode>

<!-- IMPORTANT: Reference to original invoice -->
<cac:BillingReference>
<cac:InvoiceDocumentReference>
<cbc:ID>INV-2024-00123</cbc:ID>
<cbc:IssueDate>2024-01-15</cbc:IssueDate>
</cac:InvoiceDocumentReference>
</cac:BillingReference>

<!-- Seller (same as original invoice) -->
<cac:AccountingSupplierParty>
<cac:Party>
<cbc:EndpointID schemeID="0106">12345678</cbc:EndpointID>
<cac:PartyLegalEntity>
<cbc:RegistrationName>Your Company BV</cbc:RegistrationName>
</cac:PartyLegalEntity>
</cac:Party>
</cac:AccountingSupplierParty>

<!-- Buyer (same as original invoice) -->
<cac:AccountingCustomerParty>
<cac:Party>
<cbc:EndpointID schemeID="0106">87654321</cbc:EndpointID>
<cac:PartyLegalEntity>
<cbc:RegistrationName>Customer Company BV</cbc:RegistrationName>
</cac:PartyLegalEntity>
</cac:Party>
</cac:AccountingCustomerParty>

<!-- Tax total (positive amounts) -->
<cac:TaxTotal>
<cbc:TaxAmount currencyID="EUR">10.50</cbc:TaxAmount>
<cac:TaxSubtotal>
<cbc:TaxableAmount currencyID="EUR">50.00</cbc:TaxableAmount>
<cbc:TaxAmount currencyID="EUR">10.50</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>

<!-- Credit totals (all positive values) -->
<cac:LegalMonetaryTotal>
<cbc:LineExtensionAmount currencyID="EUR">50.00</cbc:LineExtensionAmount>
<cbc:TaxExclusiveAmount currencyID="EUR">50.00</cbc:TaxExclusiveAmount>
<cbc:TaxInclusiveAmount currencyID="EUR">60.50</cbc:TaxInclusiveAmount>
<cbc:PayableAmount currencyID="EUR">60.50</cbc:PayableAmount>
</cac:LegalMonetaryTotal>

<!-- Credit line (describe what is being credited) -->
<cac:CreditNoteLine>
<cbc:ID>1</cbc:ID>
<cbc:CreditedQuantity unitCode="HUR">5</cbc:CreditedQuantity>
<cbc:LineExtensionAmount currencyID="EUR">50.00</cbc:LineExtensionAmount>
<cac:Item>
<cbc:Name>Consulting Hours - Return Credit</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:CreditNoteLine>
</CreditNote>

Key Differences from Invoiceโ€‹

ElementInvoiceCredit Note
Root element<Invoice><CreditNote>
Type code380381
Line element<InvoiceLine><CreditNoteLine>
Quantity element<InvoicedQuantity><CreditedQuantity>
ReferenceOptionalRequired
Amounts Are Positive

Credit note amounts are always positive. The document type (381) indicates it's a credit, so the receiver knows to subtract these amounts.

Sending a Credit Noteโ€‹

import requests

# Load credit note XML
with open("credit-note.xml", "r") as f:
credit_note_xml = f.read()

CREDIT_NOTE_TYPE = (
"urn:oasis:names:specification:ubl:schema:xsd:CreditNote-2::CreditNote##"
"urn:cen.eu:en16931:2017#compliant#"
"urn:fdc:peppol.eu:2017:poacc:billing:3.0::2.1"
)

# Send via the same endpoint as invoices, with the credit note document type
response = requests.post(
"https://app.goroute.ai/peppol-api/api/v1/documents",
headers={
"X-API-Key": "your_api_key",
"Content-Type": "application/json"
},
json={
"sender_scheme": "0106",
"sender_id": "12345678",
"receiver_scheme": "0106",
"receiver_id": "87654321",
"document_type": CREDIT_NOTE_TYPE,
"document": credit_note_xml,
}
)

result = response.json() # HTTP 202
print(f"Credit Note Transaction ID: {result['transaction_id']}")

The request body is flat โ€” sender_scheme, sender_id, receiver_scheme and receiver_id are top-level fields, and document carries the UBL XML as a string, not base64. sender_scheme and sender_id are optional; leave them out and your organisation's default participant is used. Only receiver_scheme, receiver_id and document are required.

Credit Note Typesโ€‹

Full Credit (Cancel Invoice)โ€‹

Credit the entire original invoice amount:

<!-- Reference the invoice being cancelled -->
<cac:BillingReference>
<cac:InvoiceDocumentReference>
<cbc:ID>INV-2024-00123</cbc:ID>
</cac:InvoiceDocumentReference>
</cac:BillingReference>

<!-- Note explaining the cancellation -->
<cbc:Note>Cancellation of invoice INV-2024-00123 due to order cancellation</cbc:Note>

<!-- Full amount credited -->
<cac:LegalMonetaryTotal>
<cbc:PayableAmount currencyID="EUR">121.00</cbc:PayableAmount>
</cac:LegalMonetaryTotal>

Partial Credit (Correction)โ€‹

Credit only part of the original invoice:

<cac:BillingReference>
<cac:InvoiceDocumentReference>
<cbc:ID>INV-2024-00123</cbc:ID>
</cac:InvoiceDocumentReference>
</cac:BillingReference>

<cbc:Note>Partial credit for returned items</cbc:Note>

<!-- Only the returned portion -->
<cac:LegalMonetaryTotal>
<cbc:PayableAmount currencyID="EUR">30.25</cbc:PayableAmount>
</cac:LegalMonetaryTotal>

Price Correctionโ€‹

Correct a pricing error:

<cac:BillingReference>
<cac:InvoiceDocumentReference>
<cbc:ID>INV-2024-00123</cbc:ID>
</cac:InvoiceDocumentReference>
</cac:BillingReference>

<cbc:Note>Price correction: Original price โ‚ฌ100, Correct price โ‚ฌ90</cbc:Note>

<!-- Credit the difference -->
<cac:CreditNoteLine>
<cbc:ID>1</cbc:ID>
<cbc:CreditedQuantity unitCode="EA">1</cbc:CreditedQuantity>
<cbc:LineExtensionAmount currencyID="EUR">10.00</cbc:LineExtensionAmount>
<cac:Item>
<cbc:Name>Price adjustment - Consulting Services</cbc:Name>
</cac:Item>
<cac:Price>
<cbc:PriceAmount currencyID="EUR">10.00</cbc:PriceAmount>
</cac:Price>
</cac:CreditNoteLine>

Generating Credit Notes Programmaticallyโ€‹

from lxml import etree

def generate_credit_note(
credit_note_id: str,
original_invoice_id: str,
original_invoice_date: str,
seller_id: str,
buyer_id: str,
lines: list,
reason: str
) -> str:
"""Generate a Peppol BIS 3.0 credit note."""

ns = {
None: "urn:oasis:names:specification:ubl:schema:xsd:CreditNote-2",
"cac": "urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2",
"cbc": "urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2"
}

root = etree.Element("CreditNote", nsmap=ns)

# Peppol identifiers
etree.SubElement(root, "{%s}CustomizationID" % ns["cbc"]).text = \
"urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0"
etree.SubElement(root, "{%s}ProfileID" % ns["cbc"]).text = \
"urn:fdc:peppol.eu:2017:poacc:billing:01:1.0"

# Credit note details
etree.SubElement(root, "{%s}ID" % ns["cbc"]).text = credit_note_id
etree.SubElement(root, "{%s}IssueDate" % ns["cbc"]).text = date.today().isoformat()
etree.SubElement(root, "{%s}CreditNoteTypeCode" % ns["cbc"]).text = "381"
etree.SubElement(root, "{%s}DocumentCurrencyCode" % ns["cbc"]).text = "EUR"
etree.SubElement(root, "{%s}Note" % ns["cbc"]).text = reason

# Billing reference
billing_ref = etree.SubElement(root, "{%s}BillingReference" % ns["cac"])
invoice_ref = etree.SubElement(billing_ref, "{%s}InvoiceDocumentReference" % ns["cac"])
etree.SubElement(invoice_ref, "{%s}ID" % ns["cbc"]).text = original_invoice_id
etree.SubElement(invoice_ref, "{%s}IssueDate" % ns["cbc"]).text = original_invoice_date

# ... add seller, buyer, tax totals, lines ...

return etree.tostring(root, pretty_print=True, xml_declaration=True, encoding="UTF-8")

Validationโ€‹

Credit notes go through the same validation as invoices:

# Validate before sending
response = requests.post(
"https://app.goroute.ai/peppol-api/api/v1/documents/validate",
headers={
"X-API-Key": "your_api_key",
"Content-Type": "application/json"
},
json={
"document": credit_note_xml,
"document_type": CREDIT_NOTE_TYPE,
}
)

result = response.json()
if not result["valid"]:
print(f"Credit note validation failed: {result['error_count']} error(s)")
for issue in result["issues"]:
print(f" [{issue['severity']}] {issue['code']}: {issue['message']}")

Errors and warnings come back in a single flat issues array, distinguished by severity โ€” see Validation for the full response shape.

Common Credit Note Errorsโ€‹

ErrorCauseFix
Missing BillingReferenceNo invoice referenceAdd <cac:BillingReference>
Invalid CreditNoteTypeCodeWrong type codeUse 381 for credit notes
Negative amountsAmounts should be positiveUse positive values
Missing seller endpointSeller ID missingAdd <cbc:EndpointID>

Best Practicesโ€‹

  1. Always Reference Original โ€” Include the original invoice ID and date
  2. Clear Reason โ€” Add a note explaining why the credit is issued
  3. Same Parties โ€” Seller and buyer must match the original invoice
  4. Match Tax Rates โ€” Use the same VAT rates as the original invoice
  5. Timely Issuance โ€” Issue credit notes promptly after the event

Tracking Credit Note Statusโ€‹

# Get credit note transaction status
response = requests.get(
f"https://app.goroute.ai/peppol-api/api/v1/transactions/{transaction_id}",
headers={"X-API-Key": "your_api_key"}
)

status = response.json()
print(f"Credit Note Status: {status['status']}")
print(f"Delivered At: {status.get('delivered_at', 'Pending')}")

Next Stepsโ€‹