Qoyod
Pricing

Knowledge Base

QR Code Errors in E-Invoicing: Causes and Fixes

The QR code on an e-invoice is not a decorative image; it is an encoded data string that carries the invoice’s identity and its digital signature. When this code is generated with faulty formatting, it fails to read in the Fatoora Portal app, the auditor rejects the invoice, and reporting can break with Qoyod’s e-invoicing. What makes it harder is that the eye cannot detect these faults: the code may print and look perfectly fine while its internal data is corrupt.

This guide isolates the technical errors specific to the QR code alone, and avoids the other integration errors covered in separate guides. We will cover corrupt TLV encoding, wrong tag ordering, missing mandatory Phase Two tags (hash value, signature, public key, and the Authority stamp), Base64 encoding errors, scan and verification failures in the Fatoora Portal app, and the common confusion between byte count and character count when calculating length. For each error we show the visible symptom, then the root cause, then the practical step-by-step fix.

If you are looking for an explanation of the code’s correct structure before diagnosing its faults, start with the guide The technical structure of the QR code on the invoice. And if you want a full panel of every ZATCA integration fault, review the guide ZATCA e-invoicing errors. As for the steps to confirm that an issued invoice is valid, you will find them in the guide Verifying the e-invoice.

Why does the QR code fail even though it looks fine?

The core problem is that a QR code may print and appear as a normal black image, yet the data inside it is corrupt. The human eye cannot tell a correct code from a corrupt one, because the corruption does not lie in the image but in the encoding layer that precedes it. This layer is assembled from three sequential stages: building the TLV fields, then serializing them into a single string, then converting the string into Base64 text. Any flaw at any stage passes silently into the final code.

The code’s string is made of consecutive fields, each in TLV format. The first letter of the abbreviation means the Tag, a number that identifies the field type; the second is the Length, the number of content bytes; and the third is the Value, the actual content in bytes. Any flaw in one of these three elements breaks the entire string, because the reader parses it sequentially and has no way to skip over a corrupt field. An error in the length at the first tag makes the reader read the rest of the bytes in the wrong positions, so decoding collapses entirely even though the following fields are sound in origin.

This cascading chain is what makes diagnosing QR errors confusing for a beginner: one small fault in an early field looks like total corruption of the invoice. That is why the first rule in diagnosis is to start from the first tag and move forward in order, not to judge the code as a single block.

Qoyod’s e-invoicing software generates this code automatically and in conformity with the Authority’s specification, so you do not build it by hand. But if you integrate through a custom API, or import invoices from a legacy system, or write an in-house invoicing engine, the following errors may appear, and knowing them cuts diagnosis time from hours to minutes.

Illustration: the data path from the fields to the QR code and the points of failure

Where the error occurs in building the QR code
Five steps to build the code, and which step may fail.
1

The fields

2

TLV encoding

3

Byte serialization

4

Base64 -> the code

Most QR errors come from incorrect TLV or Base64 encoding.

1. Corrupt TLV encoding or wrong tag ordering

The most common error is writing the tags in an order other than the mandatory one, or calculating the length incorrectly. The Phase Two specification imposes a fixed order for the tags from number 1 to number 9, and leaves no room for improvisation. Swapping any tag, omitting the length element, or inserting an extra byte between two fields breaks decoding immediately.

The reason is that the reader does not look for tags by name; it reads them by position: it takes the first byte as a tag, then the next byte as a length, then jumps a number of bytes equal to that length to read the value, then returns to read the next tag. This mechanism makes every byte dependent on the accuracy of the one before it. So if a field declares its length as ten bytes when it is actually eight, the read pointer shifts by two bytes, and the reader reads a phantom tag out of position.

Symptom: The Fatoora Portal app reads the code and shows empty fields or distorted values, or rejects the code entirely with an “invalid data” message. Sometimes the seller name appears in the tax number field, or part of the timestamp appears inside the seller name, all symptoms of the read pointer shifting away from the correct byte positions.

Cause: The written length does not equal the actual byte count of the value, or the length element is missing entirely so two tags merge, or the tags are ordered non-ascending, or a single byte is used for the length while the value exceeds 255 bytes.

Fix: Rebuild each field with the three-part sequence tag then length then value, and keep to the ascending order from tag 1 through tag 9. Verify that the length equals the value’s length in bytes, not in characters. Test decoding locally before printing: parse the string byte by byte and make sure the tags appear in the order 1, 2, 3 and so on with no gaps. The following snippet shows the difference between a correct build and a corrupt one for the first two tags:

// correct: Tag, Length (bytes), Value in order
Tag=01  Len=14  Value="Al-Waha Store"    // 20 bytes UTF-8
Tag=02  Len=0F  Value="300000000000003"  // 15 bytes

// corrupt: length does not match the bytes and the order is reversed
Tag=02  Len=0F  Value="300000000000003"
Tag=01  Len=08  Value="Al-Waha Store"    // length 8 while the content is longer => shift

Note that “Al-Waha Store” in Arabic occupies more bytes than its character count, because each Arabic character in UTF-8 encoding consumes two bytes. Ignoring this rule is the source of most length errors, and we will devote a full section to it later given its importance. The takeaway in this section is that TLV integrity starts from correct ordering and a length measured in bytes.

An extra point many overlook: the length value itself has limits. When the value exceeds 127 bytes, some formats require an extended length byte, and neglecting that truncates the field at the limit. The most common long values are the signature and public-key tags, which are by nature longer than a seller name or tax number. Make sure your length-writing logic handles long values the way it handles short ones, and test it with a real signature value rather than a short dummy value, because testing with short values only hides this fault until the first real signed invoice is issued.

2. Missing Phase Two tags (6, 7, 8, and 9)

In Phase One the code carried only five tags: the seller name, the tax number, the timestamp, the invoice total, and the tax amount. These tags were sufficient because Phase One did not require a digital signature or real-time clearance. Phase Two added four mandatory technical tags for the simplified tax invoice (B2C), and their absence makes the code unacceptable and classifies the invoice as non-compliant.

  • Tag 6: The invoice hash value (Hash) in Base64 format, which links the invoice to the chain of previous invoices to guarantee against tampering.
  • Tag 7: The invoice’s digital signature (Signature) generated with a private key.
  • Tag 8: The corresponding public key (Public Key), used by the reader to verify the signature.
  • Tag 9: The Authority’s encrypted stamp, which is the Authority’s signature on the public key to prove it was issued by an accredited certificate.

These four tags are not data you write; they are values the signing engine generates after stamping the invoice. Their absence therefore does not mean an error in writing the fields; it means the signing layer itself did not run. This is an important distinction in diagnosis: a problem with tags 1 to 5 lies in building the TLV, whereas a problem with tags 6 to 9 lies in the link with the certificate.

Symptom: The app reads only the first five tags and shows a message that the invoice carries no digital signature, or classifies it as an incomplete Phase One invoice. In a field audit the invoice is considered non-compliant with Phase Two, and observations from the Authority may follow.

Cause: The system generates the code with Phase One logic, or has not been linked to a signing certificate, so it does not have the values of tags 7, 8, and 9 in the first place. Sometimes the cause is that the certificate has expired, or that the signing engine failed silently so the system completed generating the code with the available tags only.

Fix: Make sure your system is configured for Phase Two and linked to the Compliance Stamp Identifier certificate (CSID) registered with the Authority. Tags 6 to 9 are not written by hand; the signing engine generates them after stamping the invoice, so first verify the certificate’s status and validity. If you are on Qoyod, the code is issued with its full nine tags automatically whenever the invoice is simplified, and after linking the certificate just once. And if the tags are still missing despite that, the fault is in the signing layer, not in building the code, which points diagnosis straight to the certificate.

Illustration: the nine tags and which of them belong to Phase Two

The nine tags and the sources of error
The fields the code carries and the ones most prone to errors.
QR tags

1-5: core data (Phase One)

6: invoice hash

7: signature

8: public key

9: Authority stamp – missing 6-9 is a common Phase Two error

A missing tag among 6-9 is a recurring cause of verification failure.

3. Wrong or corrupt Base64 encoding

After serializing the TLV fields, the resulting binary string is converted into Base64 text, and this text is what gets injected into the code. The error here has three recurring forms: applying Base64 to the individual values instead of the combined string, using URL-safe Base64 instead of the standard mode, or truncating the string so it loses its equals padding at the end.

The difference between the two modes is subtle but fatal. The standard mode uses the characters “+” and “/” with “=” padding at the end, while the URL-safe mode replaces them with “-” and “_” and usually drops the padding. A reader that expects the standard mode fails to decode the other one, producing bytes that do not form a sound TLV structure.

Symptom: The reader decodes Base64 and gets bytes that do not form a valid TLV structure, so it rejects the code or shows unreadable characters. In a confusing case, the code scans visually with success because the image is sound, but programmatic verification fails at the decoding step, so the user thinks the problem is in printing when it is in the encoding.

Cause: Using a non-standard Base64 alphabet, or removing the padding marks, or encoding each tag separately instead of encoding the whole string once, or inserting line breaks inside the encoded text.

Fix: Apply standard Base64 once to the entire combined binary string, keep the equals padding, and do not insert any line breaks. Test a full round trip locally: encode the string, then decode it immediately, and make sure the output matches the input byte for byte. The following snippet shows the expected output of a sound Base64 decode versus a corrupt one:

// sahih: fak Base64 yueed silsilat TLV qabila lil-tafkeek
{
  "decoded": "0114...020F300000000000003...",
  "tlv_valid": true,
  "tags_found": [1,2,3,4,5,6,7,8,9]
}

// talif: hashwa naqisa + tarmeez URL-safe
{
  "error": "INVALID_BASE64_PADDING",
  "tlv_valid": false,
  "tags_found": []
}

The practical rule is to treat Base64 as a single final step over the entire string, not as an operation you apply to parts. This alone prevents most cases of “the code scans but does not decode.”

It is worth noting that tags 6 and 9 carry values that are Base64-encoded in origin (the hash value and the Authority stamp), so these values pass through Base64 encoding twice logically: once as a value inside the field, and once when encoding the whole string. Confusing the two layers is a common source of corruption, since some developers decode the outer layer and then forget that the tag’s value itself is still encoded. When verifying, decode the outer layer first to reach the TLV structure, then handle each tag’s value according to its encoding type as documented in the Authority’s specification.

4. The code does not scan or verification fails in the Fatoora Portal app

The data may be perfectly sound, yet the code itself fails to scan. Here you must distinguish between two different failures: visual scan failure, and verification failure after the scan. The first is a problem in the image, and the second is a problem in the data or the signature. Confusing them wastes diagnosis time.

A visual scan failure is usually caused by too small a size, image compression at print time, weak color contrast, or an insufficient Quiet Zone around the code. The camera needs a white area around the code to define its edges, so if the code is stuck to text or a border, capturing it becomes impossible.

Symptom: The camera does not capture the code at all, or the Fatoora Portal app opens but does not complete the read, or it reads the code and then returns a signature verification failure message despite a successful scan.

Cause: In a scan failure: low print resolution, or the code is smaller than the recommended minimum, or it was placed on a colored background that weakens contrast, or the image was compressed in a lossy format. And in a post-scan verification failure: a problem in the signature tags 7, 8, and 9, or an invalid certificate, or a hash value that does not match the invoice content.

Fix: Print the code at a sufficient size with a white margin around it, at high contrast between the black modules and the white background, and avoid placing it on colored or gradient backgrounds. If the scan succeeds and only verification fails, the problem is in the data layer, not in the image, so review the signature tags 7, 8, and 9 and the certificate’s status. The complete steps to confirm an invoice is valid are in the e-invoice verification guide referenced at the top of the page.

Start today

A correct QR code on every invoice with no manual intervention

Qoyod issues the QR code with its nine tags and TLV and Base64 encoding in conformity with the Authority’s specification on every simplified invoice automatically, so you need not worry about encoding, signing, or length-calculation errors.

Start your free trial and issue invoices with a compliant QR code

5. Confusing byte count with character count in length calculation

This error deserves its own section because it is the most hidden and hardest to detect source of TLV corruption. The length element in each field must equal the number of bytes the value occupies, not the number of its characters. In English text and numbers the two counts usually match, because each Latin character in UTF-8 encoding occupies one byte. But in Arabic they differ fundamentally, because each Arabic character occupies two bytes.

This difference creates a deceptive pattern in the faults: invoices with English names and numbers pass successfully, because the character length equals the byte length by coincidence, while invoices with Arabic seller names fail, because the length written in characters is less than the true length in bytes. This selective pattern is by itself a strong diagnostic indicator.

Symptom: Invoices with English names pass successfully, while invoices with Arabic seller names fail to read or appear distorted. The higher the proportion of Arabic characters in the value, the worse the shift. A name of seven Arabic characters declares its length as seven while it is actually fourteen bytes, so the reader shifts by seven bytes.

Cause: The code calculates the text length with a character-count function instead of measuring its length after encoding it to UTF-8 bytes. These functions differ by programming language, and many of them return the character count by default, so the developer falls into the error without noticing as long as they test with English names only.

Fix: Encode the value to UTF-8 bytes first, then take the length of the resulting byte array, and use this number in the length element. Never measure the length from the text directly. Always test with real Arabic names that contain varied characters, not with English names only. The following snippet shows the practical difference:

// error: length = character count
value = "Al-Waha Store"    // 11 char (with the space)
length = value.length      // = 11  X

// correct: length = UTF-8 byte count
bytes  = utf8_encode(value)   // 20 bytes
length = bytes.length         // = 20  OK

The governing rule: do not measure the length from the text, but from its bytes after encoding. This step alone solves the majority of “the Arabic invoice does not read while the English one works” cases, and it is among the most frequent in a Saudi environment where most invoices carry Arabic seller names.

A structured diagnosis methodology instead of guessing

The previous errors resemble each other in their visible symptoms: the code is either rejected, read distorted, or fails verification. So the developer needs a methodology that separates the layers instead of random guessing. The optimal order is to move from the outer layer to the inner: first verify the visual scan, then Base64 decoding, then the TLV structure, then the signature values.

Start by asking: does the code scan visually at all? If it does not scan, the problem is in the image alone, so stop at the size, contrast, and margin without opening the encoding. And if it scans, move to decoding Base64 locally: if decoding fails, the fault is in the encoding mode or the padding. And if decoding succeeds, parse the TLV structure and make sure the tags appear in the order 1 to 9 with no gaps. And if the tags appear sound but verification fails, the problem is in the signature values 6 to 9 or in the certificate.

This sequence saves hours of searching, because it isolates the fault in a single layer instead of examining the code as an obscure block. At each step, record the actual output against the expected one; the gap between them points directly to the broken layer. It also helps to build a small test set containing a long Arabic seller name, an English name, a zero tax value, and a high one, to expose length and encoding errors before the invoice reaches the customer.

Finally, keep a copy of the encoded text for every failed invoice, not just the image. The text is what can be analyzed and decoded, while the image does not reveal the location of the fault. This habit alone turns diagnosis from visual guessing into precise data-based examination.

How Qoyod helps you avoid QR code errors

All the previous errors arise when the code is built by hand or through an incomplete integration. Qoyod’s e-invoicing software takes on building the code on your behalf for every simplified tax invoice: it orders the nine tags in the mandatory order, calculates the length in bytes not characters, generates the hash, signature, public-key, and Authority-stamp values after linking the Compliance Stamp Identifier certificate (CSID), then applies standard Base64 to the combined string once.

This means that Arabic and English invoices are issued with the same precision, and that the code scans and verifies in the Fatoora Portal app without any intervention from you. Your role is limited to registering your certificate with the Authority once, and Qoyod guides you through this step. This makes an entire class of encoding errors that drain development teams’ time in hand-built systems disappear.

Illustration: a quick diagnosis checklist for QR code faults

Symptom -> Cause -> Fix
A quick diagnosis of the most common QR code errors.
Symptom Fix
The code does not read Check the Base64 encoding and the padding
Missing fields Add tags 6-9
Mismatched data Regenerate the code from the signed invoice
Match the code’s content with the actually signed invoice.

Frequently asked questions

Why does the QR code look fine but the Fatoora Portal app rejects it?

Because the soundness of the image does not mean the soundness of the data inside it. The encoding may be corrupt in the TLV or Base64 layer even though the code scans visually. Check the tag ordering and length calculation first, then verify the Base64 encoding.

Which tags distinguish a Phase Two code?

Tags 6, 7, 8, and 9: the hash value, the digital signature, the public key, and the Authority stamp. Their absence makes the code an incomplete Phase One code that is non-compliant with Phase Two.

Why do my Arabic invoices fail while the English ones work?

Usually because the length in the TLV field is calculated by character count rather than byte count. Each Arabic character in UTF-8 encoding occupies two bytes, so the length calculation breaks with Arabic text only. Calculate the length from UTF-8 bytes after encoding.

Do I need to build the QR code by hand in Qoyod?

No. Qoyod issues the code automatically with its nine tags and correct encoding on every simplified tax invoice, in conformity with the Authority’s specification, after linking your certificate once.

The code scans but signature verification fails, where is the fault?

The problem is in the data and signature layer, not in the image. Review tags 7, 8, and 9 and make sure the system is linked to the Compliance Stamp Identifier certificate (CSID) registered with the Authority and that it is valid.

Is enlarging the code enough to solve the scan problem?

Size is an important factor, but you also need high contrast between the black modules and the white background, a sufficient white margin around the code, and good print resolution without excessive image compression.

Guides

Continue your learning journey

Explore the rest of Qoyod’s guides, or start applying what you’ve learned.

Live webinars hosted by the Qoyod team to help you use the software easily and answer your questions.

Discover Qoyod’s latest updates, ongoing improvements, and new features in one place.

Our team is ready to help you and provide instant support for any issue you face, around the clock.