Wise Hustlers — Digital Product & App Development Studio Logo
Get Consultation
By Wise Hustler Admin9/22/20268 min read

The ZATCA invoice counter and hash chain: the failure you cannot fix afterwards

The ZATCA invoice counter and hash chain: the failure you cannot fix afterwards

ZATCA requires every compliant invoicing solution to maintain a tamper-resistant counter that cannot be reset or reformatted, incrementing for every invoice and every credit or debit note, with its value recorded in each document. Each document also carries the hash of the document before it. Together these make your invoice history an append-only chain: you cannot renumber, backfill, or quietly drop a document without it being visible.

This is the part of Phase 2 that punishes ordinary engineering habits. Retries, rollbacks, database restores, blue-green deploys and "just re-run the failed batch" are all normal, and all of them can corrupt a chain that cannot be repaired afterwards.

Engineering context, not tax advice.

What the rules actually say

Four requirements, stated plainly in ZATCA's guidelines:

  • The solution must have a tamper-resistant electronic invoice counter that cannot be reset or reformatted. It increments for each generated invoice or note, and the solution records the counter value in each document.
  • Resetting the counter must not be a feature of the solution, and access to the counter value must be protected from system users.
  • Documents are linked by placing the hash of the previous invoice in the next one — the field ZATCA's validation messages call `KSA-13`.
  • A rejected document keeps its own hash and counter value. ZATCA's wording: "every document shall have its own hash and counter value. Rejected document's hash and counter value should not be changed or updated."

And one that ties them together: there must be no gaps in the counter value or in the previous-invoice-hash sequence — including where the same billing system also issues invoices for supplies not subject to VAT, which do not themselves need to be submitted.

The Invoice Reference Number (IRN) is, in ZATCA's words, just another name for the invoice sequence number.

The rejection trap

This is the single most expensive misunderstanding, so it is worth being unambiguous.

When a clearance call returns 400, the natural engineering instinct is to treat it as "the invoice was not created". Fix the data, retry, same number. That instinct is wrong here. The rejected document has already consumed its counter value and its position in the chain. You correct the data and submit a new document with the next counter value.

Two broken patterns follow from getting this wrong:

Reuse. The system retries with the same counter value and previous hash. Now two documents claim the same position. The chain has a fork, and the duplicate is permanent — ZATCA does not allow the same document to be submitted twice, though notably a duplicate is not rejected at submission time, so your logs may show success.

Skipping. The system decides the failed number is "burned" and jumps ahead. Now there is a gap, and a gap is exactly what the no-gaps rule prohibits.

The correct model is boring: allocate the counter at document generation, never at submission, and never release it. A document that was generated exists, whatever ZATCA later says about it. Submission outcome is a separate attribute.

Where chains actually break

In production, corruption almost never comes from the invoice code. It comes from operations.

Database restores. Restore last night's backup after an incident and the counter goes backwards while the invoices issued this morning still exist, stamped, in customers' hands. The chain now has a duplicated range. If the counter lives in the same database as everything else, any restore is a chain event.

Horizontal scaling. Two application instances both reading "last counter = 4,812" and both allocating 4,813 is the classic distributed-systems bug, and here it produces a permanent compliance defect rather than a recoverable one. Counter allocation needs a single serialisation point — a database sequence with proper isolation, an atomic increment, or a lock. Not an application-level max() + 1.

New devices and redeployments. Each onboarded solution unit has its own identity and its own chain. Spin up a replacement till and it starts a new chain — which is fine, as long as you intended it and the old chain is closed cleanly rather than abandoned mid-sequence. Container deployments make it easy to create a "new device" by accident.

Environment bleed. A sandbox pointed at production data, or a staging system sharing a counter, will interleave documents into a chain that then cannot be explained.

Timestamps. ZATCA's guidance explicitly prohibits changing the time or date on the solution in a way that would produce inaccurate timestamps, or modifying the timestamp value during generation. A system with drifting clocks or a "backdate this invoice" feature is a problem waiting to be found.

Designing so this cannot happen

Five positions that hold up:

Make counter allocation a single, atomic, audited operation. One place in the code. Use a database sequence or an atomic increment, not read-then-write. Log every allocation with the document it went to. If you can allocate a counter from two code paths, you eventually will.

Never let application code set the counter. No parameter, no admin override, no import tool that accepts a number. ZATCA requires that resetting is not a feature; the way to guarantee that is for the value to be unsettable by design.

Store the chain as its own append-only record, separate from the mutable invoice row. Counter value, document UUID, hash, previous hash, timestamp, submission outcome. Invoices get edited, voided, superseded; the chain entry never changes. This also gives you a cheap continuity check: walk the sequence and assert that each row's previous-hash equals the prior row's hash, and that counter values are contiguous.

Run that continuity check as a scheduled job, and alert on it. A break detected the same day is a conversation with your tax adviser. A break detected at audit, eighteen months of invoices later, is something else. This is the single highest-value piece of monitoring in a ZATCA integration and it is perhaps thirty lines of code.

Treat the counter as production data with the same care as money. Backups, restore procedures and failover all need to account for it. If your disaster-recovery plan restores the invoice database, write down what happens to the chain — before you need the plan.

Hashing the right bytes

One technical detail worth repeating from the security standard: the previous-invoice hash is generated by applying the same transform used for the cryptographic stamp, then SHA-256. That means the hash is over the canonicalised form produced by the signature machinery, not over whatever bytes you happen to have stored.

Hash your stored file and you will get a value that is stable, plausible, and wrong — and the failure will surface as a validation message about KSA-13 that looks like a missing field rather than a wrong one. The mechanics are covered in cryptographic stamping, CSID and the QR code.

For the response codes and the flow around rejections, see clearance versus reporting. For the obligation itself, our ZATCA Phase 2 integration guide and what Wave 25 means.

Frequently asked questions

If an invoice is rejected, can I reuse its number?

No. The rejected document keeps its own hash and counter value. Submit a corrected document as a new one with the next counter value.

Does that leave a gap in my invoice numbering?

No — the rejected document still occupies its position in the sequence. The gap would be created by skipping it, which is what you must not do.

What about invoices that do not need to be submitted?

Where a transaction covers solely supplies not subject to VAT, no tax invoice is required. But if the same solution also issues standard-rated invoices, ZATCA's guidance is that there must be no gaps in counter value or the previous-invoice-hash sequence.

Can I reset the counter at the start of a financial year?

No. Resetting or reformatting the counter must not be a feature of the solution.

We restored a database backup and lost some counter values. What now?

Stop issuing, establish exactly which documents were issued and stamped from the lost range, and take it to your tax adviser. Do not attempt to re-issue into the gap or reuse values — that compounds it. Then fix the restore procedure.

How do I run multiple application instances safely?

Allocate counters from a single serialisation point — a database sequence or atomic increment under proper isolation. Never SELECT max(counter) + 1.

Is the Invoice Reference Number the same as the counter?

ZATCA states that IRN is another name for the invoice sequence number. Keep your terminology consistent internally so nobody implements them as two different things.

How would I know if my chain is already broken?

Walk it: assert counter values are contiguous and each document's previous-hash matches the prior document's hash. Do it now, and then nightly.

Sources

Related articles