BR-KSA-CL-04VAT exemption reason code is not one ZATCA accepts
Mutabiq checks Saudi ZATCA Phase 2 e-invoice XML before you submit it: pre-validation against published ZATCA requirements, not an approval by the authority. This page explains one rule, BR-KSA-CL-04.
Five checks a month on the Free plan. The file is checked in memory and is never stored.
The message ZATCA prints
[BR-KSA-CL-04]-If VAT category Code (BT-118) is 'Z', or 'E', VAT exemption (or exception) reason code (BT-121) must exist with one of the values from paragraph 11.2.4 of XML implementation standards on Tax exemption (or exception) reason code - specific to Saudi Arabia.
What Mutabiq reports
One entry for each way this rule can be reported.
Reported as
What kind of value was found — empty, not_15_digits and the like — never the value itself.
VAT exemption reason code is not one ZATCA accepts
Reported asreason_code_not_in_codelista reason code ZATCA does not accept
What is wrong
The cbc:TaxExemptionReasonCode of a zero-rated, exempt or out-of-scope breakdown holds a non-empty value that is not on ZATCA's list for its category. SDK 3.4.8 flags the document under BR-KSA-CL-04 as a warning, not a rejection: the finding is reported without failing the document by itself. Earlier SDK releases (3.0.8) checked one list of fourteen values shared by the Z and E categories; SDK 3.4.8 splits the list by category and adds a fourth category: Z (zero-rated) accepts sixteen values - VATEX-SA-32, -33, -34-1 through -34-5, -35, -36, -EDU, -HEA, -MLTRY, -DIPLOMAT, -DUTYFREE, -ROYALDECREE and -32(bis); E (exempt) accepts three - VATEX-SA-29, -29-7 and -30; O (out-of-scope) accepts one - VATEX-SA-OOS. The value is compared after leading and trailing whitespace is normalised away and is otherwise exact and case-sensitive, so a lower-case vatex-sa-29 is not the same value as VATEX-SA-29. A blank or missing code does not land here: BR-KSA-69, BR-KSA-23 and BR-KSA-24 report that case instead, one per category. The node set this rule reads is root-anchored - a breakdown nested inside an invoice line is outside it - and it carries no test that the tax scheme is VAT, so a category lettered Z, E or O under any other scheme is inside it.
What to check in your ERP
Look at the mapping between your VAT code table and the exemption reason code. The frequent causes are an internal reason code from your own system exported as it stands, the reason text written into the code element, and a code correct for another country's e-invoicing profile - EN 16931 has reason codes of its own and none of them is a VATEX-SA value. An export built to the European rules will often carry no such field at all, because those rules accept the reason text on its own. Which of the sixteen (zero-rated), three (exempt) or one (out-of-scope) values applies depends on the breakdown's category, and is a tax question either way, so settle it once per tax code and then map it.
VAT exemption reason code contains a space
Reported asreason_code_contains_spacethe reason code contains a space
What is wrong
The cbc:TaxExemptionReasonCode of a zero-rated, exempt or out-of-scope breakdown has a space inside its value. ZATCA writes BR-KSA-CL-04 as two separate conditions and this is the second one failing: the value must be found on the list for its category, and it must contain no space. SDK 3.4.8 flags this as a warning, not a rejection: the finding is reported without failing the document by itself. The two are not the same test, which is why this is reported apart from a code that is simply not on the list. Membership is tested by padding the value with a space at each end and looking for the result inside that category's list written out as one string with a single space between its members - so a value made of two published codes with a space between them, VATEX-SA-29 followed by VATEX-SA-29-7 on an exempt breakdown, really is found inside that string and passes the membership test. The no-space condition is the only thing that flags it. An empty value is not reported here at all: SDK 3.4.8 exempts a blank code from BR-KSA-CL-04 (KSA:2561), and BR-KSA-69, BR-KSA-23 and BR-KSA-24 report it instead.
What to check in your ERP
The value is a concatenation of something. The usual causes are two codes joined because the tax code carries more than one exemption reason, a code followed by its description from a lookup table that returns the label along with the key, and a fixed-width field padded with spaces that were not trimmed on export. Trim the value and map the tax code to a single VATEX-SA value. Leading and trailing spaces on their own are not what this note is about - they are normalised away before the check - so the space that failed is inside the value. If the supply really falls under two exemption articles the breakdown still carries one code, and which one it carries is a decision for whoever owns your VAT code table.
Zero-rated or exempt breakdown gives no VAT exemption reason code
Reported asreason_code_missingthe reason code is missing
What is wrong
A cac:TaxCategory coded Z - zero-rated - or E - exempt - inside a cac:TaxTotal/cac:TaxSubtotal carries no cbc:TaxExemptionReasonCode. Only rule packs before 2.0.0 report this under BR-KSA-CL-04: SDK 3.0.8's BR-KSA-CL-04 required the code, checked it against one list of fourteen VATEX-SA values shared by the Z and E categories, and refused the document without it. In SDK 3.4.8, which rule pack 2.0.0 reads, BR-KSA-CL-04 stays silent on an absent or blank code (KSA:2561) and checks a code that is present against the list for its category. The missing code is reported instead by BR-KSA-69 on a zero-rated breakdown, BR-KSA-23 on an exempt one and BR-KSA-24 on an out-of-scope one, each at warning, so a report produced under 2.0.0 never carries this note.
What to check in your ERP
The code belongs in cac:TaxSubtotal/cac:TaxCategory/cbc:TaxExemptionReasonCode and is a property of the breakdown rather than of a line. Your VAT code table is where this is settled: every zero-rated, exempt and out-of-scope tax code carries one code from the list for its category - sixteen VATEX-SA values for zero-rated supplies, three for exempt ones, and VATEX-SA-OOS for out-of-scope ones. The commonest cause by far is an export built to the EN 16931 profile, which has no field for this at all because the European rules accept the reason text on its own. Which code applies is a tax question rather than a mapping one, so settle it once per tax code with whoever owns the VAT code table and then map it.
The clause that runs
Quoted from the validation-rule stylesheet ZATCA publishes, at the file and line named below. The executed guard is what the check runs; the reported test is what it prints when it fails, and the two are not always the same.
Executed guard
not(exists(cbc:TaxExemptionReasonCode) and ((not(contains(normalize-space(cbc:TaxExemptionReasonCode), ' ')) and normalize-space(cbc:ID) ='Z' and contains(' VATEX-SA-32 VATEX-SA-33 VATEX-SA-34-1 VATEX-SA-34-2 VATEX-SA-34-3 VATEX-SA-34-4 VATEX-SA-34-5 VATEX-SA-35 VATEX-SA-36 VATEX-SA-EDU VATEX-SA-HEA VATEX-SA-MLTRY VATEX-SA-DIPLOMAT VATEX-SA-DUTYFREE VATEX-SA-ROYALDECREE VATEX-SA-32(bis) ', concat(' ', normalize-space(cbc:TaxExemptionReasonCode), ' ')))) or ((not(contains(normalize-space(cbc:TaxExemptionReasonCode), ' ')) and normalize-space(cbc:ID) ='E' and contains(' VATEX-SA-29 VATEX-SA-29-7 VATEX-SA-30 ', concat(' ', normalize-space(cbc:TaxExemptionReasonCode), ' ')))) or ((not(contains(normalize-space(cbc:TaxExemptionReasonCode), ' ')) and normalize-space(cbc:ID) ='O' and contains(' VATEX-SA-OOS ', concat(' ', normalize-space(cbc:TaxExemptionReasonCode), ' ')))) or normalize-space(cbc:TaxExemptionReasonCode)='' )
As shipped in ZATCA SDK 3.4.8: the copy quoted here is, byte for byte, the file that release ships. The date in the file name is the name ZATCA gives the file; the rules in it are those of SDK 3.4.8.
A worked example
A synthetic document from the measured test corpus that trips this rule, shown where it differs from one that passed. Mutabiq's report names the class of the value, never the value itself.
Document
bad-71-zero-rated-unknown-reason-code.xml
Mutabiq's verdict
Passed
Reported as
reason_code_not_in_codelista reason code ZATCA does not accept