When the e-invoicing system rejects your invoice, the rejection doesn’t always come from a broken XML file. Many rejections happen while the XML is perfectly well-formed but carries a logical or arithmetic error. These are business-rules validation errors, and they are a completely different layer from structural errors. This guide is aimed at developers building an integration with the Fatoora platform in Phase 2, and it explains the most common business-rule codes that reject your invoice, the message behind each code, and how to fix it before the request reaches ZATCA.
This guide is part of the Qoyod E-Invoicing Guide, and it complements the Validation Rules for the Invoice guide, which explains the rules as a complete specification. Here we focus on the practical side: reading the error message, identifying its cause, and fixing it.
Last updated: 24 June 2026.
Validation Errors Are Not XML Errors
Before we dive into the codes, the separation between the two layers must be clear in your mind. Confusing the two is the number-one reason for wasting time tracking down errors.
The first layer is structural validation via the XSD schema. Its job is to answer a single question: “Is the document structure correct?” Are the elements present? Is their order right? Does the data type in each field match? These errors appear as parser messages and carry no business-rule code. We covered them in detail in the separate XML errors guide, and that is the level you must handle first.
The second layer is logical validation via business rules. It runs after the schema passes, takes the values that made it through XSD, and checks their relationships: is the total correct? Does the tax amount equal the base multiplied by the rate? Is the field that becomes mandatory for a certain tax category present? XSD cannot answer these questions because they require calculation and comparison across several fields.
The summary in one sentence: the schema guarantees that the invoice is written correctly, and business rules guarantee that it is actually correct. A perfectly well-formed invoice can be rejected because it carries a single arithmetic error. This guide is exclusively about the second type.
Where Do Business-Rule Codes Come From?
Every code ZATCA uses to reject your invoice comes from one of two sources. The first source is the European standard EN 16931, which defines the core semantic model of the invoice. Its rules start with the prefix BR for the core rules, BR-CO for calculation coherence, and BR-S, BR-Z, and BR-E for the standard, zero-rated, and exempt tax categories. The Saudi invoice is built on top of this foundation via the UBL 2.1 standard.
The second source is the Kingdom-specific rules that ZATCA adds on top of the European standard, carrying the prefix BR-KSA. They handle local requirements such as the format of the Saudi VAT number, the signature and stamp fields, the QR code, and the invoice type. Your system’s job is to apply both sources together internally before sending.
| Criterion | Schema Validation (XSD) | Business Rules (BR) |
|---|---|---|
| Scope | Document structure | Data logic and correctness |
| Reference | XSD schema | BR-KSA codes |
| Error example | Missing element / order | Mismatched total |
The Five Families of Business Rules
Business rules are many, but they logically fall into five families. Understanding the family before the individual rule makes reading a rejection faster, and helps you build an organized validation engine instead of a scattered list of conditions.
First family: Conditional Mandatory. Makes a field mandatory only when a certain condition is met. The most common example is the exemption-reason field: it isn’t required on a normal invoice, but it becomes obligatory the moment the tax category of one of the lines becomes exempt or out of scope.
Second family: Calculation. The most important family and the biggest cause of rejection. It checks that the totals are consistent across the invoice. It carries the prefix BR-CO. We’ll dedicate a full section to it because it deserves one.
Third family: Format. Checks that a field’s value follows a specific pattern, and may include logic that XSD cannot express, such as a number-verification algorithm. The classic Saudi example is the VAT number.
Fourth family: Date and Sequence. Checks the logic of dates and the sequencing of invoices. In Phase 2 an extra dimension comes in: linking the invoice to its predecessor via the unique identifier (UUID) and the hash of the previous invoice.
Fifth family: Code List. Checks that a field’s value belongs to an approved list. The currency code from the ISO 4217 list, the invoice-type code from the list ZATCA defines, and the tax-category code from the approved set.
Calculation-Coherence Errors (BR-CO): The Most Common in Production
If you’re going to focus on one family, make it this one. BR-CO rules reject the most invoices in the Saudi production environment, often because of a small rounding difference the developer doesn’t notice.
The logic is strict but simple. The sum of line net amounts must equal the invoice total before tax. The sum of tax per category must equal the total tax. The amount due must equal the total before tax plus tax minus any prepayments. Any deviation beyond the allowed rounding margin means an immediate rejection.
Here are the most common codes in this family and their practical meaning:
- BR-CO-10: The sum of line net amounts must equal the total value of the lines in the invoice.
- BR-CO-13: The taxable total must equal the invoice total without tax.
- BR-CO-15: The amount due must equal the total without tax plus the total tax.
- BR-CO-17: The tax amount per category must equal the base subject to that category multiplied by the rate divided by one hundred.
An arithmetic error message as it arrives from the platform
When ZATCA’s system rejects an invoice for an arithmetic error, the response comes as an array of errors, each carrying the code of the broken rule and its message. This is a real-world example of a BR-CO-15 rejection:
Read it top to bottom. The status field tells you the invoice was rejected. The code field is the code of the broken rule, and it is your key for searching ZATCA’s rules document. The category field classifies the rule. The message field explains the rule as an equation. The fix here is clear: recalculate the amount due so that it equals the base plus tax exactly.
The Hidden Cause Behind Most BR-CO Errors: Rounding
The most common error we see in this family is the rounding difference. If you round each line individually and then sum, you may get a total that differs by one halala from the result ZATCA expects. The rule usually allows a tiny rounding margin (often SAR 0.01), but exceeding it means a rejection.
Let’s illustrate with a numeric example. Suppose an invoice has two lines: the first SAR 33.33 and the second SAR 33.34, both subject to the standard 15% rate. The tax on the first line is SAR 5.00 after rounding, and the tax on the second line is SAR 5.00, so the total tax at line level is SAR 10.00. But if you calculate the tax on the aggregate base of SAR 66.67 directly, you get SAR 10.0005, which rounds to SAR 10.00. Here they matched. Change the values slightly and you’ll find a deviation that exceeds the margin.
First lesson: calculate the tax at the aggregated-category level, not at the individual-line level. This is what rule BR-CO-17 expects.
The second lesson concerns the data type in the code. Using a floating-point type (float) in amount calculations introduces subtle precision errors that accumulate across dozens of lines. Use a precise decimal type (decimal or its equivalent in your language), and set the number of decimal places explicitly at final rounding. This alone removes a large proportion of BR-CO rejection cases.
The third lesson concerns invoice-level discounts. Many BR-CO-13 rejection cases come from a discount applied to the invoice total without being correctly distributed across the tax bases of each category. The rule expects the taxable total for each category to be consistent with the lines after the discount. If you subtract the discount from the final total only, without reflecting it in each category’s base, the coherence breaks and the rejection arrives. Distribute the discount at the category level, then calculate the tax on the base after the discount, not before it.
A second coherence example: an invoice with two categories, the first standard at 15% with a base of SAR 1,000 and the second exempt with a base of SAR 200. The total before tax is SAR 1,200, the tax is SAR 150 on the standard category only, and the amount due is SAR 1,350. If you mistakenly calculate the tax on the whole total (1,200 × 15%), you get SAR 180 and rule BR-CO-17 rejects you because the exempt category is not subject to the rate. Tax is calculated per category separately, not on the total.
How Qoyod Helps You
Qoyod applies the business-rule and calculation-coherence checks locally before sending the invoice, so your invoice reaches the Fatoora platform only when it is free of BR errors. Calculating tax at the category level, rounding with the correct margin, and verifying the three total equations are all applied internally. You issue the invoice, and we take care of the rules. And because Qoyod is officially approved by the Zakat, Tax and Customs Authority, the local validation matches what ZATCA’s system actually applies.
VAT-Number Format Errors (BR-KSA-39 and BR-KSA-40)
The Saudi VAT number is the field that fails most often in the format family. The rule isn’t satisfied with it being a 15-digit string; it requires that it start with the digit 3 and end with the digit 3, and that the middle digits be valid. Many developers pass a 15-digit number that clears XSD, then it is rejected at the business-rule level because it doesn’t match the pattern.
This is an example of a seller VAT number rejected for not matching the format:
The fix is to verify the number locally before building the document, using the following pattern:
Rule BR-KSA-40 relates to the buyer’s VAT number in certain cases. The principle is the same: verify the pattern locally before sending, and don’t rely on XSD to catch it because it sees the number as structurally valid text.
Note also that the requirement for the buyer’s VAT number changes depending on the invoice type. In a tax invoice addressed to a business (B2B), the buyer’s number becomes mandatory above a certain threshold, whereas in a simplified invoice addressed to a consumer (B2C) it isn’t required. This is an intersection between the format family and the conditional-mandatory family: the field is checked not only for its pattern, but for its very presence depending on the invoice context. Wire these conditions into your local layer according to the document type, not as a single fixed rule for all invoices.
Conditional-Mandatory and Tax-Category Errors (BR-S, BR-Z, and BR-E)
These rules link the tax-category code to the fields that must accompany it. The standard category S requires a positive base and a valid rate. The zero-rated category Z has its own rules. The exempt category E requires an exemption-reason text.
The most common codes in this family:
- BR-S-08: The base subject to the standard category must be positive. Category S without a positive base is rejected.
- BR-Z-05: A rule specific to the zero-rate category linking the rate to zero.
- BR-E-05 and BR-E-10: Rules for the exempt category, chief among them the requirement of an exemption-reason text.
The clearest example is the exemption-reason field. The rule says: if the tax-category code equals E (exempt), then an exemption-reason text must be present. If you issue an exempt line without this text, it passes XSD but the business rule rejects you.
The fix is simple in logic: link the field’s appearance to the category value. As soon as the category becomes E, enable the exemption-reason field as mandatory in your local validation layer before building the document.
Category S (taxable 15%): no exemption reason needed
Category Z (zero-rated): a reason may be required
Category E (exempt): exemption reason mandatory (BR-E-10)
A missing reason in E rejects the invoice
Invoice-Type and Code-List Errors (BR-KSA-15)
The invoice-type code must be from the list ZATCA defines: 388 for a tax invoice, 383 for a debit note, 381 for a credit note. Rule BR-KSA-15 checks that the invoice-type code is consistent with the attached fields. If you send an invoice-type code that doesn’t match the actual content, it is rejected here.
Example: a credit note (381) must carry a reference to the original invoice it adjusts. If you send it without this reference, it breaks the coherence rule between the document type and its fields.
There’s also a code worth noting: BR-KSA-EN16931. It appears when a Saudi value breaks a rule from the original European standard. That is, the core rule and the local rule conflict in your case, which usually means one of the Saudi fields was entered with a value the core semantic standard does not accept.
Date and Sequence Errors in Phase 2
Date rules check the logic of dates: the issue date may not be in the future, and the due date must not precede the issue date. In Phase 2, the integration system adds a critical dimension: the sequencing and linking of invoices.
Every invoice carries a counter (ICV) that must increment without gaps, a unique identifier (UUID), and a hash (PIH) of the previous invoice that links it into a chain. Any break in this chain, such as a repeated counter or a broken hash, is caught in this family. The simplified rule:
The practical lesson here is that the source of these errors is usually a race condition in generating the counter, or the loss of the previous invoice’s state after a service restart. Treat the counter and the hash as persistent state, not as an in-memory value. Any solution that keeps the counter in memory only will fail on the first redeploy or when more than one instance of the service runs in parallel.
A second point often overlooked: concurrency in issuing invoices. If two users issue two invoices at the same moment, and the service reads the previous counter before one of them writes its new value, both users get the same counter and ZATCA rejects the second one. The solution is a database-level lock when generating the counter, or an atomic sequence whose uniqueness the database guarantees.
How to Build a Local Validation Layer That Prevents Rejection Before It Happens
The best strategy isn’t fixing the rejection after it occurs, but preventing it. Build into your system a local validation layer that mimics the business rules before building and sending the final document. This layer saves entire network round trips and catches the error in your environment where it’s easy to trace.
Arrange this layer by the five families, in the same order ZATCA’s system executes. Start with format and code lists because they are the cheapest computationally: verify the VAT number, the invoice-type code, the currency code, and the tax-category codes. Then move to conditional mandatory: for each tax category, verify the presence of the fields it requires. After that, run the calculation coherence because it is the most expensive and the most sensitive to rounding. Finally, verify date and sequence.
This order isn’t arbitrary. Running the cheap checks first means you catch most errors before reaching the expensive calculation. And matching your order to ZATCA’s order makes the rejection messages you receive from the platform, if any, consistent with what your system catches locally, so surprises are fewer.
Log every error you catch locally with the corresponding rule code in ZATCA’s system. This creates a shared language between your team and ZATCA’s documentation, and makes any later rejection directly mappable to the check’s location in your code. A team that speaks the same code language fixes errors far faster than a team that describes them in its own words.
Testing in the Developer Environment Before Production
Before you issue a real invoice, test your integration in the test environment the platform provides. The goal is to see the actual rejection messages on deliberately designed edge cases, rather than discovering them with your first customer.
Design a set of test cases that deliberately break each family. An invoice with a mismatched total to prove you catch BR-CO-15. An invoice with a wrong VAT number for BR-KSA-39. An exempt line without an exemption reason for BR-E-10. An invoice with an inconsistent type code for BR-KSA-15. Each case must produce the expected code, otherwise your local validation layer doesn’t match ZATCA’s behavior.
Keep this set as permanent regression tests. Whenever ZATCA updates its rules, rerun them to catch any deviation immediately. Integrating with e-invoicing isn’t a one-off project but a system that evolves with ZATCA’s updates, and the test layer is what protects you from silent breakage.
A Unified Method for Reading Any Rejection From the Platform
However varied the codes, the method is one. When a rejection response reaches you, follow these steps in order instead of guessing:
- Read the
codefield first. It is the identity of the broken rule and the key for searching ZATCA’s document. - Read the
categoryto know the family: BR is core arithmetic, or BR-KSA is local. This points you to the right place in your code. - Read the
messageas an equation or condition, not as a passing sentence. Each message describes the broken relationship precisely. - Reproduce the case locally with the same values. An arithmetic error is always reproducible.
- Fix the logic in the local validation layer, not in the XML building only, so the error doesn’t recur in other invoices.
The difference between a team that fixes errors quickly and one that goes in circles is this method. The code isn’t an obstacle, but a precise address for the location of the error in your logic.
And remember that a rejection message may carry more than one error in a single array. Handle them all before resending, not one per cycle. Fixing one error then resending to discover the next doubles the number of cycles needlessly, and slows your integration in the production environment. Read the whole array, sort the errors by their families, and fix them all at once. This turns integration from a series of stumbling attempts into a disciplined process where the invoice arrives on the first try.
| Code | Fix |
|---|---|
| BR-CO-15/17 | Match the totals to the lines |
| BR-S-08 | Verify the 15% tax calculation |
| BR-E-10 | Add the exemption reason |
| BR-KSA-* | Review ZATCA’s specific requirements |
Leave business-rule validation to us
Qoyod applies the business rules and calculation coherence locally before sending, so your invoice reaches ZATCA only when it is free of BR errors. You issue, and we take care of the rules.
Where to Go After This Guide?
This guide covered the logical layer. To complete the picture, head to the following resources according to your need:
- For the structural layer: see the XML errors guide, which handles the document structure and the XSD schema, and is the level preceding validation errors.
- For the validation rules as a complete specification and how they are written: Validation Rules for the Invoice.
- For handling API errors and response codes during integration: Error Handling in the API.
- For the list of endpoints and request structures: API Endpoints for the Fatoora Platform.
Manually Validating the XML File via the ZATCA Platform
If you want to validate the e-invoice XML file directly — without needing technical expertise or downloading any software — the Zakat, Tax and Customs Authority provides a free tool called the Compliance Validation Portal for Non-Technical Users, available on the test environment (Sandbox).
Steps to use it
- Export the XML file from Qoyod: From the invoice page, choose the Export XML option.
- Open the tool’s link: https://sandbox.zatca.gov.sa/compliance
- Click the tab “Compliance Validation Portal”.
- Read the terms and check “I agree to the terms and conditions”.
- Upload the XML file (maximum 2 MB, and up to 5 files at a time).
- Click the “Validate” button — the check result will appear immediately with details of any errors.
Frequently Asked Questions
What is the difference between XML errors and validation errors?
XML errors are structural: a missing element or a wrong data type, caught by the XSD schema layer. Validation errors are logical and arithmetic: a mismatched total, an incorrectly calculated tax, or a missing conditional field, caught by the business-rules layer after the schema passes. A structurally sound invoice may be rejected with a validation error.
Why is my invoice rejected with a BR-CO code even though the numbers look correct?
The most common cause is the rounding difference. Calculating the tax at the individual-line level and then summing it may produce a total that differs by a halala from the expected one. Calculate the tax at the aggregated-category level, and use a precise decimal type (decimal) rather than a float.
What does the BR-KSA prefix mean?
These are the Kingdom-specific business rules that ZATCA adds on top of the European standard EN 16931. They handle local requirements such as the VAT-number format, the QR code, invoice types, and Phase-2 fields. Rules with the BR and BR-CO prefixes come from the core European standard.
How do I validate the Saudi VAT number before sending?
Verify the pattern locally: fixed length of 15 digits, starts with the digit 3 and ends with the digit 3, per the regular expression ^3[0-9]{13}3$. Don’t rely on XSD to catch the error because it sees the number as structurally valid text, while rule BR-KSA-39 rejects it.
Can I detect validation errors locally before sending them to ZATCA?
Yes, and this is the correct approach. Apply the calculation-coherence, conditional-mandatory, and field-format rules inside your system before building the final document. Qoyod does this automatically, so the invoice reaches the Fatoora platform only after passing local validation. And for manual validation of the XML file after exporting it, you can use the Compliance Validation Tool available on the ZATCA platform — see the previous section for details.
What is the correct invoice-type code for each document type?
388 for the tax invoice, 383 for a debit note, 381 for a credit note. The code must match the document’s content and fields, otherwise the invoice is rejected with code BR-KSA-15. A credit note, for example, must carry a reference to the original invoice.