Skip to content

Invoice Validation

Validation is explicit: each operation establishes only the guarantees below.

Operation Guarantee
Validated wrapper constructor or deserialization Existing validation and normalization for that wrapper
build() Selected field, range, and amount checks; computed totals
XML parsing Supported fields, wrapper and builder checks, and modeled supplied amounts
XSD validation Conformance to the bundled UBL schema
Explicit ZATCA validation Both pinned SDK rule profiles and applicable local integrity checks, with stage coverage
Signing Generates signature material
Parsing a signed invoice Extracts signature material without verifying authenticity

FinalizedInvoice does not imply full business-rule compliance. SignedInvoice may come from parsing and does not prove that its signature was verified. Use validate_zatca_invoice_from_str on original XML for the complete local profile. Inspect is_valid() in Rust or report["is_valid"] in Python/JSON. No local operation establishes remote acceptance or certificate issuer trust.

CountryCode, CurrencyCode, InvoiceTimestamp, InvoiceDate, and VatId deserialize through their constructors. Normalization is the same as construction; invalid values are rejected. VatId currently checks only that the trimmed value is nonempty.

Example

pub fn main() {
    use fatoora_core::config::{Config, EnvironmentType};
    use fatoora_core::invoice::validation::validate_xml_invoice_from_str;

    // Doc example: validate XML against the bundled schema.
    let invoice_xml_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
        .join("tests/fixtures/invoices/sample-simplified-invoice.xml");
    let config = Config::new(EnvironmentType::NonProduction);
    let xml = std::fs::read_to_string(&invoice_xml_path).expect("read invoice xml");
    validate_xml_invoice_from_str(&xml, &config).expect("validate invoice");
}
from fatoora.config import Config
from fatoora.validation import validate_xml_invoice_from_str

# invoice_xml_path = "path/to/invoice.xml"
config = Config()
xml = invoice_xml_path.read_text(encoding="utf-8")

result = validate_xml_invoice_from_str(config, xml)
assert result is True
import { Config, Xml } from 'fatoority';

export function validateInvoice(xml, previousHash, evaluatedAt) {
  return JSON.parse(Xml.validateZatca(Config.new_(0), xml, JSON.stringify({
    previous_invoice_hash: previousHash,
    evaluated_at: evaluatedAt, // Omit to use the JavaScript host clock.
  })));
}
// Inspect report.is_valid and report.stages before accepting an invoice.
    public static JsonDocument ValidateInvoice(
        string xml, string previousHash, string evaluatedAt)
    {
        using var config = Config.New(0);
        var options = JsonSerializer.Serialize(new
        {
            previous_invoice_hash = previousHash,
            evaluated_at = evaluatedAt
        });
        return JsonDocument.Parse(Xml.ValidateZatca(config, xml, options));
    }
    // The caller disposes the JsonDocument and inspects is_valid and stages.
#include "fatoora.h"
#include <stdio.h>
#include <stdlib.h>

static char *read_xml(const char *path) {
    FILE *file = fopen(path, "rb");
    if (!file) return NULL;
    char *xml = NULL;
    if (fseek(file, 0, SEEK_END)) goto done;
    long size = ftell(file);
    if (size < 0 || size > 1024 * 1024) goto done;
    rewind(file);
    xml = malloc((size_t)size + 1);
    if (!xml) goto done;
    if (fread(xml, 1, (size_t)size, file) != (size_t)size) {
        free(xml);
        xml = NULL;
    } else {
        xml[size] = '\0';
    }
done:
    fclose(file);
    return xml;
}

int main(int argc, char **argv) {
    if (argc != 2) {
        fprintf(stderr, "Usage: %s invoice.xml\n", argv[0]);
        return 1;
    }
    FatooraConfig *config = NULL;
    FatooraError *error = NULL;
    char *xml = read_xml(argv[1]);
    if (!xml) {
        fputs("Unable to read invoice XML.\n", stderr);
        return 1;
    }
    bool valid = false;
    FatooraStatus status = FATOORA_OK;
#define TRY(call) do { status = (call); if (status != FATOORA_OK) goto cleanup; } while (0)
    TRY(fatoora_config_new(FATOORA_NON_PRODUCTION, &config, &error));
    /* Validate original XML; parsing/reserializing would change the input. */
    TRY(fatoora_xml_validate(config, xml, &valid, &error));
    printf("XSD valid: %s\n", valid ? "true" : "false");
cleanup:
    if (status != FATOORA_OK)
        fprintf(stderr, "Error %d: %s\n", (int)status, fatoora_error_message(error));
    free(xml);
    fatoora_config_free(config);
    fatoora_error_free(error);
    return status == FATOORA_OK && valid ? 0 : 1;
}

Local ZATCA profile

The profile implements the SDK 238-R3.4.8 adaptations of CEN and Saudi rules: 105 CEN assertion sites and 152 Saudi sites. Validation uses original XML, with no Java or SDK runtime. Pass the predecessor hash from invoice history; missing context produces an incomplete report. The default clock is one UTC snapshot, or supply evaluated_at for repeatable results.

from fatoora import Config, validate_zatca_invoice_from_str

# For later invoices, use the actual predecessor digest from your invoice history.
initial_hash = "NWZlY2ViNjZmZmM4NmYzOGQ5NTI3ODZjNmQ2OTZjNzljMmRiYzIzOWRkNGU5MWI0NjcyOWQ3M2EyN2ZiNTdlOQ=="
with Config() as config:
    report = validate_zatca_invoice_from_str(
        config,
        invoice_xml_path.read_text(),
        previous_invoice_hash=initial_hash,
        evaluated_at="2026-09-23T12:00:00+03:00",
    )
assert report["is_valid"]
for stage in report["stages"]:
    for finding in stage["findings"]:
        print(stage["stage"], finding["code"], finding["message"])

Rust accepts ZatcaValidationOptions; C accepts the same fields as JSON and returns an owned report string. See the reference for signatures and error handling. All APIs retain warnings. A document rejection returns a report; an execution failure carries any partial report.

Standard invoices skip signature and QR checks under this SDK profile. Simplified invoices check the embedded certificate's key, reference digests, signature and QR fields. These checks establish local integrity only. The development guide records stricter local integrity policies where the SDK accepts inconsistent QR or signature metadata.

All 15 UBL schemas are embedded in the library and compiled in memory. Installed packages do not need the source checkout. Local validation uses the same profile in every configured API environment.

See also: Invoice Validation Reference

C examples include fatoora.h; compile the adapter once with your application. See the C build instructions.

See the JavaScript / TypeScript reference for installation and runtime contracts.

See the C# / .NET reference for installation, imports, and ownership rules.