Troubleshooting
Each entry is a symptom, its cause and the fix. Messages are quoted as the package writes them, so you can search this page for the text you see.
Installation
Class "Darvis\UblPeppol\UblNlBis3Service" not found
Cause: Composer’s autoloader is not loaded, or the package is not installed. Fix: run composer require darvis/ubl-peppol. In a plain PHP script, add require __DIR__.'/vendor/autoload.php'; at the top.
Class "DOMDocument" not found
Cause: PHP’s dom extension is missing. Fix: install it (php-xml on Debian and Ubuntu) and restart PHP. php -m | grep dom must print dom.
Call to undefined function Darvis\UblPeppol\bcmod()
Cause: the bcmath extension is missing. The Dutch addPaymentMeans() and UblValidator::validateIban() use it to check an IBAN. From UblValidator the function in the message is Darvis\UblPeppol\Validation\bcmod(). Fix: install bcmath (php-bcmath on Debian and Ubuntu).
Class "SoapClient" not found
Cause: the soap extension is missing. Only ViesService needs it. Fix: install soap (php-soap on Debian and Ubuntu).
Command "peppol:cleanup" is not defined.
Cause: Laravel did not load the service provider. Usually package discovery is turned off for this package in composer.json (extra.laravel.dont-discover), or the cached package list is old. Fix: run php artisan package:discover. When discovery is off on purpose, add Darvis\UblPeppol\UblPeppolServiceProvider::class to bootstrap/providers.php.
Building a document
Document is already initialized. Avoid initializing the document multiple times.
Cause: createDocument() or createCreditNoteDocument() was called twice on one builder. In Laravel this happens on the second invoice when you take the Dutch builder from the container, because app(UblNlBis3Service::class) and app('ubl-peppol') always return the same instance. Fix: create a builder per document with new UblNlBis3Service(). See Laravel integration.
Root element is not initialized. Call createDocument() before adding elements.
Cause: an add...() method ran before createDocument(). Fix: call createDocument() first.
generateXml() throws Root element is not initialized.
Cause: generateXml() was called on a Dutch builder without createDocument(). (In 1.8 and 1.9 this was the PHP error Typed property Darvis\UblPeppol\UblNlBis3Service::$rootElement must not be accessed before initialization.) The Belgian builder returns an empty XML declaration in the same situation. Fix: call createDocument() and the add...() methods before generateXml().
Validation error(s) in invoice header:
The lines that follow name the problem:
| Line | Fix |
|---|---|
Invoice number is required and cannot be empty | Pass a number |
Invoice number cannot exceed 35 characters | Shorten it |
Invalid invoice date. Please use YYYY-MM-DD format, Invalid due date. Please use YYYY-MM-DD format | Pass 2026-01-15, not 15-01-2026 |
Invoice date cannot be in the future | The invoice date must be today or earlier. Check the timezone of your server when this happens around midnight |
Due date must be after the invoice date | A due date equal to the invoice date is refused as well |
Invoice date must be a string (YYYY-MM-DD) or DateTime object | You passed a DateTimeImmutable or CarbonImmutable. Pass $date->format('Y-m-d') |
Validation error(s) in addAccountingSupplierParty(): or Validation error(s) in customer information:
Cause: a required argument of the Dutch builder is empty, or the country code does not have two characters. The lines that follow name each field. Fix: fill the field. The arguments are positional; compare your call with the API reference.
VAT number must start with a 2-letter ISO 3166-1 alpha-2 country code (e.g., 'NL', 'BE'). Got: '...'
Cause: the customer’s vatNumber has no country prefix (BR-CO-09). Fix: pass NL123456789B01 or BE0999000228. Both builders write it in upper case.
Invalid IBAN format or Invalid BIC/SWIFT code format
Cause: the Dutch addPaymentMeans() verifies the IBAN checksum and the shape of the BIC. An IBAN with spaces fails, and so does a lower case BIC. Fix: str_replace(' ', '', $iban) and strtoupper($bic). To find out what is wrong with an IBAN, call UblValidator::validateIban($iban); it returns IBAN is too short, IBAN is too long, Invalid IBAN format or Invalid IBAN checksum.
Payment means code must be a numeric value
Fix: pass a code from UNCL 4461 as a string of digits, for example '30' for a bank transfer or '58' for a SEPA transfer.
Payment terms note is required and cannot be empty
Fix: pass a text to the Dutch addPaymentTerms(), or leave the call out.
PEPPOL BR-27 Validation Error: Item net price (BT-146) shall NOT be negative.
Also: PEPPOL BR-27 Validation Error: Line extension amount shall NOT be negative. The rest of the message is in Dutch.
Cause: an invoice line has a negative price or a negative line amount. Fix: a discount is not a negative line. Add it with addAllowanceCharge(false, $amount, $reason, ...). To credit an invoice, build a credit note.
Invoice line requires line_extension_amount or both price_amount and quantity to derive it.
Fix: pass price_amount and quantity in the line.
Warning: Undefined array key "currency" (or "name", "description", "unit_code")
Cause: a required key is missing from the array you passed to addInvoiceLine(). Fix: add the key. The lists are under Dutch invoices and Belgian invoices.
Tax entry #1: Tax scheme ID is required
Cause: an entry in the Dutch addTaxTotal() misses one of its six keys. The number is the position of the entry. Fix: pass taxable_amount, tax_amount, currency, tax_category_id, tax_percent and tax_scheme_id.
Line extension amount is required, Charge total amount is required, …
Cause: the Dutch addLegalMonetaryTotal() misses a key, or the value is not numeric. Fix: pass all five: line_extension_amount, tax_exclusive_amount, tax_inclusive_amount, charge_total_amount and payable_amount. Use 0.00 for no charges.
Amount cannot be negative, Reason for allowance/charge is required
Fix: the Dutch addAllowanceCharge() wants a positive amount and a reason. Whether it is a discount is decided by the first argument (false), not by the sign.
The invoice contains BUYER_REF or PO-001
Cause: they are the defaults of addBuyerReference() and addOrderReference(). Fix: always pass your own reference.
The invoice contains SupplierOfficialName Ltd, 4025:123:4343, or your VAT number with schemeID="0106"
Cause: versions before 1.10.0 wrote these fixed values. Fix: upgrade to 1.10.0 or newer. The supplier’s legal name is then the name you pass, the accounting cost is only written through addAccountingCost(), and the KvK number goes in with addSupplierLegalRegistration(); see Dutch invoices.
The receiver reports NL-R-003 or NL-R-005
Cause: the legal registration of a Dutch party (PartyLegalEntity/CompanyID) has a scheme other than 0106 or 0190. Fix: pass the KvK number with addSupplierLegalRegistration('12345678') or addCustomerLegalRegistration('87654321'), or an OIN with '0190' as the second argument.
Legal registration scheme must be a 4 digit ISO 6523 ICD code
Fix: pass '0106' for a KvK number, '0190' for an OIN, '0208' for a Belgian enterprise number.
Add the header before the accounting cost.
Cause: the Belgian addAccountingCost() places the element behind the currency, which the header writes. Fix: call addInvoiceHeader() or addCreditNoteHeader() first.
[BR-S-10] VAT category S (Standard rate) takes no exemption reason
Cause: an entry of addTaxTotal() has tax_exemption_reason_code or tax_exemption_reason on a category that allows none: S, Z, L or M (the rule in the message is BR-S-10, BR-Z-10, BR-AF-10 or BR-AG-10). Fix: leave both keys out for that category. Check first whether the category is right; see VAT categories.
[BR-CL-22] Exemption reason code '...' is not in the VATEX code list or may only be used with VAT category K, not with AE
Cause: the code is not in the VATEX list, or it belongs to another category (PEPPOL-EN16931-P0104 to P0111). Fix: use VatExemptionReason::codes() for the list and VatExemptionReason::categoryOf($code) for its category. For K, AE, G and O you can leave the code out: the builder writes the right one.
A & or < in a name
Nothing to fix. The builders escape text for you. Do not call htmlspecialchars() on your data first; that would put &amp; in the invoice.
Validation
UBL/Peppol validation failed:
Cause: generateXml(validateFirst: true) found errors. The lines that follow are the errors of validate(). Fix: read the rule code at the start of each line.
| Error starts with | Meaning |
|---|---|
No invoice lines found., No totals found., No VAT totals found. | Belgian builder: call addInvoiceLine(), addLegalMonetaryTotal() and addTaxTotal() before validate() |
Line 1: LineExtensionAmount (...) does not match PriceAmount (...) × Quantity (...) | The line total is not price times quantity |
BR-CO-10 | The lines do not add up to line_extension_amount |
BR-CO-13 | tax_exclusive_amount is not lines minus allowances plus charges |
BR-S-08 or TaxSubtotal 1: TaxAmount (...) does not match calculation | A tax entry does not match its lines or its rate |
BR-CO-15 | tax_inclusive_amount is not tax_exclusive_amount plus VAT |
BR-CO-16 | payable_amount is not tax_inclusive_amount minus prepaid_amount |
BR-CO-11, BR-CO-12 | The allowance or charge total is not the sum of your addAllowanceCharge() calls; see the next entry |
NL-R-003, NL-R-005 | The legal registration of a Dutch party is under a scheme other than 0106 or 0190. The endpoint scheme plays no part: 0088 or 9944 is fine |
NL-R-008 | Both parties are Dutch and the payment means code is not 30, 48, 49, 57, 58 or 59 |
NL-R-009 | A line has order_line_id and addOrderReference() was not called |
Invalid currency code format, Invalid schemeID format, Invalid payment means code format, Invalid tax category ID | A code has the wrong shape |
BR-CO-12: Sum of document charges (10.00) does not match ChargeTotalAmount (15.00)
Also BR-CO-11: Sum of document allowances (...) does not match AllowanceTotalAmount (...).
Cause: charge_total_amount (or allowance_total_amount) in addLegalMonetaryTotal() is not the sum of the amounts you passed to addAllowanceCharge(). Fix: make them equal. If the first number is 0.00 while you did add a charge, you are on a version before 1.10.0, where the Belgian validate() did not see addAllowanceCharge(); upgrade.
[BR-IC-11] or [BR-IC-12] from validate(), or from the receiver
Cause: an intra-community supply (category K) states when and where the goods went: the actual delivery date and the deliver to country. Fix: call addDelivery() with the date and the country code. The country alone is enough: addDelivery('2026-01-14', countryCode: 'BE') with the Dutch builder, addDelivery('2026-01-14', country: 'NL') with the Belgian one (since 1.11.1).
[BR-IC-05] A line in category K (...) has VAT rate 21.00; it must be 0., or [BR-IC-09] ... has VAT amount ...
Cause: the line or the breakdown says “no VAT because of an intra-community supply” and still charges VAT. The same message exists for AE, E, G and Z. Fix: pass tax_percent 0 on the lines and tax_amount 0 in the breakdown. If VAT is due after all, the category is S.
[BR-IC-01] A line uses category K, but the VAT breakdown has no entry for it
Cause: every category of a line, discount or charge needs its own entry in addTaxTotal(), and a category other than S exactly one. Fix: add an entry per category, with the sum of the amounts of that category.
[BR-IC-02] An intra-community supply (K) needs the buyer's VAT number (BT-48)
Cause: K needs the VAT numbers of both parties; AE the buyer’s VAT number or legal registration. Fix: pass $vatNumber to addAccountingCustomerParty().
[BR-E-10] The VAT breakdown of category E needs an exemption reason
Cause: an exempt breakdown has no reason. The code names the article of the VAT directive, so the builder cannot choose it for you. Fix: pass tax_exemption_reason_code, such as VATEX-EU-132-1C for medical care, or a tax_exemption_reason text. Ask the bookkeeper which article applies.
validate() passes and the receiver still rejects the invoice
Cause: validate() checks the rules this package implements. The Dutch validate() does not check amounts at all. Fix: upload the XML to an official validator and look up the rule code it reports in the PEPPOL BIS Billing 3.0 rules. If the package writes something the specification forbids, open an issue.
The receiver reports an element in the wrong place
Cause: with the Belgian builder, the calls were made in another order than the schema. Fix: follow the order of the calls. The Dutch builder sorts the elements itself.
Credit notes
Credit note validation failed: ...
The first line names the failing rules; getRuleIds() on the CreditNoteValidationException gives them as an array.
| Rule | Fix |
|---|---|
NL-R-001 (supplier in the Netherlands, either builder) | Call addBillingReference($invoiceNumber, $invoiceDate), with the number of the invoice you stored on the credit note |
UBL-PEPPOL-CN-03 | Pass positive totals to addLegalMonetaryTotal() |
UBL-PEPPOL-CN-04 | The same |
Before 1.13.0 this message started with Credit Note Validation Failed (PEPPOL BIS Billing 3.0 / EN 16931): and used the codes BR-CN-01 to BR-CN-04; the Belgian builder also refused a credit note without a billing reference under [BR-55]. See Credit notes.
[UBL-PEPPOL-CN-05] warning from validate()
Cause: a credit note from a supplier outside the Netherlands without a reference to the credited invoice. PEPPOL accepts it, but the receiver cannot match the credit to an invoice. Fix: call addBillingReference() with the number and issue date of the credited invoice. See The billing reference follows the supplier.
BuyerReference is not supported on credit notes by this package.
Cause: the Belgian builder cannot place a buyer reference on a credit note. The Dutch builder can. Fix: leave addBuyerReference() out on a Belgian credit note and call addOrderReference() before addBillingReference().
Call to undefined method Darvis\UblPeppol\UblNlBis3Service::createCreditNoteDocument()
Cause: the Dutch builder builds credit notes since 1.10.0; an older version is installed. Fix: composer update darvis/ubl-peppol.
Sending
Peppol URL is not configured (PEPPOL_URL)
Also Peppol username is not configured (PEPPOL_USERNAME) and Peppol password is not configured (PEPPOL_PASSWORD).
Cause: the value is missing from .env, or Laravel still uses a cached config from before you added it. Fix: add the value, then run php artisan config:clear. Check what the service sees with app(\Darvis\UblPeppol\PeppolService::class)->getConfig(). If you published config/ubl-peppol.php long ago, compare it with the current keys: the keys are url, username, password and log_retention_days.
Authentication failed - check credentials, Access denied - ..., Peppol URL not found - check PEPPOL_URL
These come from testConnection(). The table under Test the connection explains each.
'success' => false with 'status_code' => 0
Cause: the request did not get an answer, or something threw after the answer. error holds the reason.
cURL error 6: Could not resolve host: the host inPEPPOL_URLis wrong.- A database error that names
peppol_sent_at: the document was sent, andsendInvoice()then failed to update your model. Do not send it again. Add the column or switch tosendUblXml(); see the pitfall.
'success' => false with a 4xx status_code
Cause: your provider refused the document. error holds the provider’s answer. Fix: read that answer. A 400 or 422 is usually a rule the document breaks; a 401 is a wrong username or password.
log_id is null
Cause: there is no peppol_logs table. This is not an error. Fix: only if you want the log: php artisan vendor:publish --tag=ubl-peppol-migrations and php artisan migrate.
SQLSTATE[...]: no such table: peppol_logs (or Table '...peppol_logs' doesn't exist)
Cause: your own code queries PeppolLog and the migration was not run. PeppolService and peppol:cleanup check for the table; a direct PeppolLog::... call does not. Fix: run the migration, or ask PeppolLog::tableExists() first.
peppol:cleanup deletes nothing
Cause: it deletes rows whose created_at is older than log_retention_days, 60 by default. Fix: pass --days=30, or set PEPPOL_LOG_RETENTION_DAYS and run php artisan config:clear.
VAT numbers
valid is false for a number you know is right
Cause: look at error. When it is not null, VIES did not answer the question: Member state service unavailable, VIES service temporarily unavailable, Connection timeout. Fix: treat the number as unknown and try again later. See Tell “invalid” from “VIES is down”.
Still stuck
Open an issue with the code that builds the document and invented data. Never paste real customer data. Report a vulnerability through the security advisories instead.