DEV Community

eimza
eimza

Posted on

Implementing E-Invoice (e-Fatura) XML Validation in JavaScript: UBL-TR Deep Dive

Turkey's tax authority (GİB) requires all invoices above a certain threshold to be issued electronically in UBL-TR format — a localized version of Universal Business Language. Here's how to validate these XML documents client-side.

UBL-TR Structure

A Turkish e-Invoice XML follows this hierarchy:

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

    <cbc:UBLVersionID>2.1</cbc:UBLVersionID>
    <cbc:CustomizationID>TR1.2</cbc:CustomizationID>
    <cbc:ProfileID>TICARIFATURA</cbc:ProfileID>

    <cbc:ID>ABC2026000000001</cbc:ID>
    <cbc:IssueDate>2026-09-22</cbc:IssueDate>
    <cbc:InvoiceTypeCode>SATIS</cbc:InvoiceTypeCode>
    <cbc:DocumentCurrencyCode>TRY</cbc:DocumentCurrencyCode>

    <!-- Digital Signature (XAdES) -->
    <cac:Signature>
        <cbc:ID>...</cbc:ID>
        <cac:SignatoryParty>...</cac:SignatoryParty>
        <cac:DigitalSignatureAttachment>
            <cac:ExternalReference>
                <cbc:URI>#Signature_ABC2026000000001</cbc:URI>
            </cac:ExternalReference>
        </cac:DigitalSignatureAttachment>
    </cac:Signature>

    <!-- Supplier (Satıcı) -->
    <cac:AccountingSupplierParty>
        <cac:Party>
            <cac:PartyIdentification>
                <cbc:ID schemeID="VKN">1234567890</cbc:ID>
            </cac:PartyIdentification>
        </cac:Party>
    </cac:AccountingSupplierParty>

    <!-- Line Items -->
    <cac:InvoiceLine>
        <cbc:ID>1</cbc:ID>
        <cbc:InvoicedQuantity unitCode="C62">10</cbc:InvoicedQuantity>
        <cbc:LineExtensionAmount currencyID="TRY">1000.00</cbc:LineExtensionAmount>
        <cac:TaxTotal>
            <cbc:TaxAmount currencyID="TRY">200.00</cbc:TaxAmount>
            <cac:TaxSubtotal>
                <cbc:TaxableAmount currencyID="TRY">1000.00</cbc:TaxableAmount>
                <cbc:TaxAmount currencyID="TRY">200.00</cbc:TaxAmount>
                <cac:TaxCategory>
                    <cac:TaxScheme>
                        <cbc:TaxTypeCode>0015</cbc:TaxTypeCode>
                        <cbc:Name>KDV</cbc:Name>
                    </cac:TaxScheme>
                </cac:TaxCategory>
            </cac:TaxSubtotal>
        </cac:TaxTotal>
    </cac:InvoiceLine>
</Invoice>
Enter fullscreen mode Exit fullscreen mode

Client-Side Validation Engine

class EFaturaValidator {
    constructor(xmlString) {
        const parser = new DOMParser();
        this.doc = parser.parseFromString(xmlString, "text/xml");
        this.ns = {
            inv: "urn:oasis:names:specification:ubl:schema:xsd:Invoice-2",
            cbc: "urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2",
            cac: "urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"
        };
        this.errors = [];
    }

    xpath(expr, context = this.doc) {
        return this.doc.evaluate(expr, context, (prefix) => this.ns[prefix],
            XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
    }

    validate() {
        this.checkRequiredFields();
        this.checkVKN();
        this.checkTaxCalculations();
        this.checkInvoiceId();
        return { valid: this.errors.length === 0, errors: this.errors };
    }

    checkRequiredFields() {
        const required = [
            ["cbc:ID", "Fatura Numarası"],
            ["cbc:IssueDate", "Düzenleme Tarihi"],
            ["cbc:InvoiceTypeCode", "Fatura Tipi"],
            ["cbc:DocumentCurrencyCode", "Para Birimi"]
        ];
        for (const [path, label] of required) {
            if (!this.doc.querySelector(path)) {
                this.errors.push(`Zorunlu alan eksik: ${label} (${path})`);
            }
        }
    }

    checkVKN() {
        // Turkish tax number (VKN) must be exactly 10 digits
        const vknNode = this.doc.querySelector(
            'AccountingSupplierParty PartyIdentification ID[schemeID="VKN"]'
        );
        if (vknNode) {
            const vkn = vknNode.textContent.trim();
            if (!/^\d{10}$/.test(vkn)) {
                this.errors.push(`Geçersiz VKN: "${vkn}" (10 haneli olmalı)`);
            }
        }
    }

    checkTaxCalculations() {
        const lines = this.doc.querySelectorAll("InvoiceLine");
        for (const line of lines) {
            const lineId = line.querySelector("ID")?.textContent;
            const amount = parseFloat(
                line.querySelector("LineExtensionAmount")?.textContent || "0"
            );
            const taxAmount = parseFloat(
                line.querySelector("TaxTotal TaxAmount")?.textContent || "0"
            );
            const taxable = parseFloat(
                line.querySelector("TaxSubtotal TaxableAmount")?.textContent || "0"
            );

            if (Math.abs(amount - taxable) > 0.01) {
                this.errors.push(
                    `Satır ${lineId}: Matrah uyumsuzluğu (${amount}${taxable})`
                );
            }
        }
    }

    checkInvoiceId() {
        // GİB format: 3 letters + year + 9 digits
        const id = this.doc.querySelector("ID")?.textContent?.trim();
        if (id && !/^[A-Z]{3}\d{13}$/.test(id)) {
            this.errors.push(
                `Fatura numarası GİB formatına uymuyor: "${id}" (ABC2026000000001 gibi olmalı)`
            );
        }
    }
}

// Usage
const validator = new EFaturaValidator(xmlContent);
const result = validator.validate();
console.log(result);
// { valid: false, errors: ["Geçersiz VKN: ..."] }
Enter fullscreen mode Exit fullscreen mode

Tax Code Reference

Code Tax Type Rate
0015 KDV (VAT) 1%, 10%, 20%
0003 ÖTV (Special Consumption) Varies
0021 Banka Muamele Vergisi 5%
0071 Damga Vergisi (Stamp) 0.948%
9015 KDV Tevkifatı (Withholding) Varies

Try It Yourself

Our open-source e-Fatura XML Viewer parses and displays UBL-TR invoices entirely in the browser.

All tools: github.com/eimza-kep

Top comments (0)