Published · 31 August 2026 · 12 min read
ZATCA ECDSA Cryptographic Stamping Explained (in Plain English)
How the ECDSA signature, the previous-invoice hash, the UUID counter, and C14N canonicalization actually fit together on a ZATCA Phase 2 invoice — and what to audit in any vendor solution.
Every ZATCA Phase 2 invoice carries a cryptographic stamp that ZATCA can verify — even years after issuance — without needing to phone home. The stamp is an ECDSA signature over the canonicalized invoice XML, produced with a private key bound to your CSID certificate. This page is the plain-English walkthrough of how the pieces fit together, why each one exists, and what to check in any solution you evaluate. For the high-level picture, see our complete Phase 2 guide; for the byte-level XML layout, see the UBL 2.1 schema walkthrough; for the onboarding flow that produces the key, see the onboarding step-by-step.
What ECDSA actually is
ECDSA stands for Elliptic Curve Digital Signature Algorithm. It is a public-key signature scheme, in the same family as RSA, but based on a different hard math problem. Where RSA relies on the difficulty of factoring a very large integer, ECDSA relies on the difficulty of the elliptic curve discrete logarithm problem. The practical consequence is that ECDSA gives you the same security as RSA-2048 with a much shorter key — typically 256 bits — and produces much shorter signatures. That matters for QR codes and PDF embed sizes, both of which ZATCA constrains.
ZATCA specifically requires ECDSA over the NIST P-256 curve (also called secp256r1 or prime256v1). The signature is two integers, r and s, each 256 bits, encoded in base64. The total signed payload is roughly 88 bytes. For comparison, an equivalent RSA-2048 signature is 256 bytes. ECDSA also signs faster and verifies faster — important when you are processing thousands of invoices per day.
The four moving parts inside a ZATCA Phase 2 stamp
There is a tendency to talk about "the signature" as if it were a single thing. In fact, the cryptographic stamp on a ZATCA Phase 2 invoice is a coordinated interaction between four distinct fields:
- The CSID (Certificate Solution Identifier) — a JWT issued by ZATCA that contains your public key, your VAT number, and the device identifier. Generated as part of the onboarding flow. Validity: 1 year for production, 90 days for compliance.
- The UUID / counter (PIH — Previous Invoice Hash, or PIH used as a counter) — a monotonically increasing per-device counter. ZATCA accepts either a sequential integer or a UUID v4. The UUID is the safer default: an integer counter that gets a voided invoice skipped will fail validation on the next call.
- The PIH hash (the previous invoice hash) — a SHA-256 hash of the previous invoice XML, base64-encoded, included in this invoice. It chains your invoices together so any tampering with an earlier invoice is detectable from any later one.
- The ECDSA signature — the actual signature, computed over the canonicalized invoice XML, base64-encoded, embedded in the
<ds:SignatureValue>element. The W3C XML-Signature standard governs the embedding format.
If any of these four is missing, wrong, or stale, Fatoorah rejects the invoice. Below is the data flow that produces all four.
Step 1: Build the UBL XML invoice
This is the UBL 2.1 XML document you already built in cluster 2. The only thing to know for cryptography is that the final XML must be canonicalized before signing — that is, the bytes you hash and sign must be the canonicalized form, not the pretty-printed form. We will come back to canonicalization in step 4.
Step 2: Compute the previous invoice hash (PIH)
Read the last successful invoice your system issued, take the SHA-256 hash of its canonicalized XML bytes, base64-encode it, and put it in the <cbc:PrecedingInvoiceHash> field. For the very first invoice in a device series, leave the field empty (or set it to base64 of 32 zero bytes, depending on the ZATCA spec version you are targeting).
Pseudocode:
prev_hash_b64 = ""\nif last_successful_invoice is not None:\n canonical_bytes = c14n(last_successful_invoice.xml)\n prev_hash_b64 = base64(sha256(canonical_bytes))
The chain only works if every invoice signs the canonicalized bytes of its predecessor, and every invoice includes the resulting hash. Tamper with invoice #5 and the hash stored in #6 will not match — and ZATCA detects the break on the very next call.
Step 3: Generate the counter / UUID
ZATCA requires the counter field to be a UUID v4 (recommended) or a sequential integer. The field is <cbc:UUID> in the UBL document, and it must be unique per device across the entire device lifetime. A UUID v4 gives you 122 bits of randomness — collision risk is effectively zero. An integer counter gives you simplicity, but breaks if you ever void a number.
Pseudocode:
new_uuid = uuid4() # e.g. "9d7e8f1a-3b2c-4e5d-8a9b-1c2d3e4f5a6b"\ninvoice.uuid = new_uuid\nledger.append(invoice.uuid)
Store the counter in a database that supports ACID transactions. If your counter store loses a write, your next call fails. Most teams use a separate zatca_counters table with a unique constraint on the device ID + UUID, so duplicate UUIDs are caught at insert time.
Step 4: Canonicalize, hash, and sign
This is the part most developers get wrong on the first try. You must:
- Take the invoice XML, including the placeholder
<ds:Signature>block with an empty<ds:SignatureValue>. - Apply Exclusive XML Canonicalization (C14N 1.0) per the W3C spec. The default canonicalization ZATCA accepts is C14N with comments stripped. Some implementations also require C14N 1.1 ("exclusive") to avoid namespace prefix issues.
- Compute the SHA-256 hash of the canonicalized bytes.
- Sign the hash with your private key using ECDSA P-256 + SHA-256. The output is two integers
rands, each 32 bytes, concatenated and base64-encoded. - Insert the base64-encoded signature into the
<ds:SignatureValue>element.
The signing flow in pseudocode:
canonical_xml = c14n(invoice_xml) # C14N 1.0, no comments\ndigest = sha256(canonical_xml)\nraw_sig = ecdsa_sign(private_key, digest) # 64 bytes: r || s\nsig_b64 = base64(raw_sig)\ninvoice.signature_value = sig_b64
Two common pitfalls. First, do not sign the pretty-printed XML; sign the canonicalized bytes. Different libraries produce different whitespace, line endings, and namespace ordering, and any of them will produce a different signature. Second, do not sign the hash of the signature placeholder; sign the canonicalized bytes of the entire invoice, signature placeholder included.
Step 5: Submit to Fatoorah
POST the signed XML to /e-invoicing/clearance/single (B2B) or /e-invoicing/reporting/single (B2C). Include the CSID in the Authentication header as a Bearer token. Fatoorah verifies the signature, the chain, the counter, and the schema. If everything matches, it returns a clearance hash (B2B) or a reporting receipt (B2C). You stamp that hash on the printed PDF.
Pseudocode:
response = http.post(\n "https://fatoorah.zatca.gov.sa/e-invoicing/clearance/single",\n headers={\n "Authorization": f"Bearer {csid_jwt}",\n "Content-Type": "application/xml",\n },\n body=signed_xml,\n)\nclearance_hash = response.json()["clearanceHash"]\ncounter = response.json()["counter"]\npdf.stamp_with(clearance_hash, counter)
The clearance hash is itself a SHA-256 hash that ZATCA computes over your signed XML. It is what the next invoice in your series will reference via PrecedingInvoiceHash. Treat the ledger of (clearance_hash, counter, signed_xml) as your source of truth — if ZATCA ever audits you, this is what they ask for.
What to audit in a vendor solution
If you are evaluating a ZATCA Phase 2 vendor rather than building the integration yourself, the cryptographic correctness is the single most important thing to check. The vendor is the custodian of your private key and the producer of every signature. If their crypto is sloppy, your invoices are non-compliant even if everything else looks fine.
- Where is the private key stored? On disk, encrypted at rest? In a hardware security module (HSM)? In a cloud KMS like AWS KMS, Azure Key Vault, or Google Cloud KMS? The on-disk option is the cheapest but the riskiest. The HSM / cloud KMS option is the safest.
- What canonicalization does the vendor use? Ask for the exact library and version. C14N 1.0 with comments stripped is the minimum. C14N 1.1 (exclusive) is safer. If the vendor cannot name their library, walk away.
- How is the counter generated? UUID v4 from a CSPRNG, or a database sequence? UUIDs are safer. Sequences break if the database loses a write.
- Does the vendor sign the canonicalized bytes or the pretty-printed bytes? If the answer is "we do not know", that is the answer. Find a vendor who can answer the question.
- Does the vendor retain the clearance hash and counter? You will need them for the next invoice in the series. If the vendor does not persist them, the chain breaks.
- What happens on a failed call? Does the vendor retry, and does it retry with the same UUID? Retries with a new UUID break the chain.
Common cryptographic errors in production
After 4 years of Phase 2 deployments in Saudi Arabia, a small set of crypto errors accounts for the majority of integration failures:
1. Signing the pretty-printed XML. The most common error. The fix is to always canonicalize before signing, and to sign the canonical bytes only. Most languages have a canonicalization library: lxml in Python, Apache Santuario in Java, xml-c14n in Node.
2. Wrong hash of the previous invoice. The chain hash must be the SHA-256 of the canonicalized previous invoice, not the pretty-printed bytes, not the clearance hash that ZATCA returned, not the invoice XML stored in your ERP. The chain hash you store on your side is what you sign on the next call. Get it from the same code path that signs.
3. CSID expiry in production. CSIDs expire after 1 year (production) or 90 days (compliance). If you forget to rotate, every signature you produce is invalid. Set a calendar reminder 60 days before expiry.
4. Reused UUIDs across devices. A CSID is bound to a device. If you share a private key across two devices and issue invoices with overlapping UUID ranges, the chain breaks on the device boundary. One CSID per device. Always.
5. Skipping the chain after a failed POST. If your POST to Fatoorah fails after you have already signed, do not retry with a new UUID. Retry the same signed invoice with the same UUID. If you have to generate a new UUID because the previous one is permanently stuck, you must explicitly mark the gap in your ledger so the auditor can see the gap.
What ZATCA actually checks when verifying
When Fatoorah receives your invoice, it runs five checks in this order:
- Schema validation. Is the XML UBL 2.1 + ZATCA extension valid? Schema errors are rejected before any crypto runs.
- Counter uniqueness. Has this UUID been seen before? If yes, reject.
- Chain hash check. Does the
PrecedingInvoiceHashfield match the SHA-256 of the previous invoice in your series? If no, reject. - ECDSA signature verification. Does the
SignatureValueverify against theSignedInfoblock, using the public key in the CSID? If no, reject. - Canonicalization equivalence. Does the canonicalized form of the invoice match the canonicalized form the signature was computed over? If no, reject.
Steps 2 through 5 are what makes the cryptographic stamp useful. A malicious actor who tampers with an invoice XML cannot produce a valid signature because they do not have the private key. A malicious actor who tampers with the previous invoice XML cannot keep the chain valid because the SHA-256 hash will not match. The combination is what gives the system its fraud-detection power — and is why a stolen private key is a "shame and blame" event worth treating with the seriousness of a production database breach.
Frequently asked questions
Can I use RSA-2048 instead of ECDSA?
ZATCA accepts both, but ECDSA P-256 is the recommended default in the 2024+ spec versions. RSA-2048 produces larger signatures and slower verifications, and the Fatoorah sandbox is more strict about RSA padding modes than ECDSA. If you are starting fresh, use ECDSA. If you have an existing RSA-2048 integration that works, leave it; ZATCA will not break it.
What happens if the Fatoorah platform goes down for maintenance?
You cannot issue B2B invoices during a Fatoorah outage. B2C invoices (reporting) you can still issue locally, but you have to retroactively report them when Fatoorah returns — and you must still include a valid signature, even if the reporting POST is delayed. The signature does not depend on Fatoorah being online.
Can a buyer verify the signature on an invoice they received?
Yes, if the buyer has a copy of the seller's CSID. The buyer decrypts the public key, computes SHA-256 over the canonicalized XML, and verifies the ECDSA signature. ZATCA publishes a verification tool for this, and the Fatoorah portal includes a "verify invoice" page that anyone can use.
Do I need a different key for production and sandbox?
Yes. The Compliance CSID and the Production CSID are issued from different certificate hierarchies. A private key that signs in sandbox will be rejected in production, and vice versa. Keep them strictly separate, or you risk the production Fatoorah endpoint rejecting invoices because the certificate chain does not match.
How long does a signature remain valid?
The signature itself does not expire, but the CSID does. Once your CSID expires, old signatures still verify, but you cannot issue new invoices until you rotate. The audit trail is intact; only future issuance is blocked. That is the right design — it means a signature is forever verifiable, but the active signing key is rotated on a calendar.
Is there a way to make the integration simpler?
Yes — a ZATCA-accredited SaaS solution handles all of this for you. The trade-off is cost and a dependency on the vendor. If you are processing fewer than ~5,000 invoices per month, the SaaS route is the right one. For higher volume, the cost of direct integration amortizes within 6-12 months. Our waves guide covers the cost analysis in more detail.
Tags
Related tools
See all →Related guides
See all guides →-
31 Aug 2026 · 7 min read
ZATCA Phase 2 (Fatoorah): The Complete 2026 Guide for Saudi Businesses
-
31 Aug 2026 · 8 min read
ZATCA Fatoorah Onboarding: Step-by-Step for SMEs (2026)
-
31 Aug 2026 · 10 min read
ZATCA XML Invoice Structure: UBL 2.1 Fields Explained
-
31 Aug 2026 · 12 min read
ZATCA Phase 2 Waves & Deadlines: 2026 Compliance Calendar
-
25 Jul 2026 · 1 min read
ZATCA Phase 1 vs Phase 2: What Changes for Your Business
Related articles
References
Primary sources used in this guide:
Get the next Saudi finance guide
One short email per month with a new Saudi finance guide, calculator update, or ZATCA tip. No spam, unsubscribe anytime.
Create your ZATCA tax invoice with QR
Add your 15-digit VAT number, totals, and download a Phase 1 compliant PDF — free, online, no signup.