Qoyod
Pricing

Knowledge Base

Sample Invoice QR Code

This guide takes you from a single simplified invoice to a ready QR code, step by byte. We will not discuss the structure in the abstract; instead we take real values for five fields, convert each field into bytes in TLV form, concatenate them into a single string, then encode it to Base64 to get the text drawn inside the black square. After that we reverse the process: we take the Base64 text and decode it back to the original fields. The goal is to leave with a working example you can compare your result against, byte by byte.

This guide is purely practical and assumes you have read the fundamentals first. If you want the general idea of the code, why the Authority mandated it, and how to read it from your phone, the right place is our conceptual guide: The Quick Response (QR) code in the e-invoice. And if you want the full technical reference for the order of the nine tags and their construction rules, it is in the Technical structure of the invoice QR code guide. Here, however, we work on a single sample from start to finish, back and forth.

Qoyod’s e-invoicing software issues this code in conformance with the Authority’s specification on every invoice automatically, so do not build it by hand in production. But building a sample manually once gives you the ability to read any code, verify its validity during an audit, and diagnose any error that appears when integrating with the Fatoora platform.

The five values we will build the sample from

Phase One of e-invoicing requires five mandatory fields on the simplified tax invoice (B2C). We take the following values and fix them for the rest of the guide. Any number you see later is derived from exactly these values, so you can recompute and match:

  • Seller name (Tag 1): Qoyod Company
  • VAT number (Tag 2): 300000000000003
  • Timestamp (Tag 3): 2026-06-24T11:45:30Z
  • Total including VAT (Tag 4): 115.00
  • Total VAT (Tag 5): 15.00

These are the same values we used in the technical guide, but there we stopped at building the pieces. Here we continue all the way to the final text and reverse it. Note that the total 115.00 equals the net 100.00 plus tax 15.00 at 15%, so the numbers are accounting-consistent and not random.

From fields to QR code
How the example turns into a QR code and is then decoded.
1

The five fields

2

TLV pieces

3

73-byte string

4

Base64 → QR code

The code can be decoded in reverse to restore the original fields.

Building a TLV piece for each tag individually

Before anything, recall the rule: each tag is written as three consecutive fields. The first field is the tag number in one byte, the second field is the length of the value by its number of bytes not its number of characters, and the third field is the value’s bytes themselves in UTF-8 encoding. To go deeper into this rule, see the TLV encoding in the invoice QR code.

guide. We start with Tag 1 because it is the most important for exposing the confusion between characters and bytes. The seller name Qoyod Company consists of nine visible characters with a single space between them. But a single Arabic character occupies two bytes in UTF-8, and the space occupies one byte. So the total number of bytes is seventeen bytes, not nine:

// Tag 1: seller name (Arabic, 8 characters + space = 17 bytes UTF-8)
Tag    = 0x01
Length = 0x11           // 17 in decimal, not 9
Value  = "شركة قيود"
Value bytes (hex):
  D8 B4  D8 B1  D9 83  D8 A9  20  D9 82  D9 8A  D9 88  D8 AF
   ش      ر      ك      ة     (space) ق      ي      و      د

Here is the most important lesson in the entire guide: the length byte carries the number of bytes, not the number of characters. Whoever puts the number 9 in place of 17 breaks the whole string, because the reader will read only nine bytes for the name then count the tenth byte as the start of the next tag, so everything after it collapses.

The remaining four tags have Latin or numeric values, so each character in them is one ASCII byte, and the number of characters matches the number of bytes:

// Tag 2: VAT number (15 digits = 15 bytes)
Tag = 0x02   Length = 0x0F   Value = "300000000000003"
Value bytes: 33 30 30 30 30 30 30 30 30 30 30 30 30 30 33

// Tag 3: timestamp (20 characters = 20 bytes)
Tag = 0x03   Length = 0x14   Value = "2026-06-24T11:45:30Z"
Value bytes: 32 30 32 36 2D 30 36 2D 32 34 54 31 31 3A 34 35 3A 33 30 5A

// Tag 4: total including VAT (6 characters = 6 bytes)
Tag = 0x04   Length = 0x06   Value = "115.00"
Value bytes: 31 31 35 2E 30 30

// Tag 5: total VAT (5 characters = 5 bytes)
Tag = 0x05   Length = 0x05   Value = "15.00"
Value bytes: 31 35 2E 30 30

Note the length byte in each tag. Tag 2 has a length of 0x0F i.e. 15, because the Saudi VAT number is always fifteen digits. Tag 3 has a length of 0x14 i.e. 20, because the full ISO 8601 format ending in the letter Z occupies twenty characters. The values 115.00 and 15.00 keep two decimal places as the specification requires, so do not write 115 or 15 bare.

Concatenating the bytes into a single string

After building the five pieces, we concatenate them in order from Tag 1 to Tag 5 with no separator between them. No spaces, no separators, and no new lines. Each piece sticks directly to the one after it, so the decoder reads the tag number, then the length, then jumps the specified number of bytes, then automatically reaches the next tag.

The result is a string seventy-three bytes long. This is the full raw string in hexadecimal, each byte in two digits:

// The full string (73 bytes), separated by spaces for readability only
01 11 D8 B4 D8 B1 D9 83 D8 A9 20 D9 82 D9 8A D9 88
D8 AF 02 0F 33 30 30 30 30 30 30 30 30 30 30 30 30
30 33 03 14 32 30 32 36 2D 30 36 2D 32 34 54 31 31
3A 34 35 3A 33 30 5A 04 06 31 31 35 2E 30 30 05 05
31 35 2E 30 30

Trace the beginning to understand the structure. The first byte 01 is the tag number. The second byte 11 is the length, which is 17. It is followed by seventeen bytes that are the seller name, starting withD8 B4 (the letter sheen) and ending withD8 AF (the letter dal). Immediately after it comes 02 the start of the second tag. This is how you read the string visually without any tool.

Reading the byte string in the sample
How the 73-byte TLV string is read.
Reading the bytes

Each field: tag byte + length byte + value

Tag 1 has a length of 17 bytes (not 9 characters) after UTF-8

Five consecutive regions for the five fields

Reading ends at the last byte

The value length in bytes may exceed the number of characters in Arabic text.

Converting the string to Base64

The raw string is binary bytes that are not drawn directly as text. It is therefore converted to Base64, an encoding that represents every three bytes with four safe text characters. For details on how this encoding works, see the Base64 encoding in the e-invoice.

guide. We take the seventy-three bytes and pass them through the standard Base64 function. The final text result is what is drawn inside the QR code:

// Final Base64 text (the payload drawn in the QR code)
ARHYtNix2YPYqSDZgtmK2YjYrwIPMzAwMDAwMDAwMDAwMDAzAxQ
yMDI2LTA2LTI0VDExOjQ1OjMwWgQGMTE1LjAwBQUxNS4wMA==

This text is one hundred characters long, and it ends with two ==marks. This mark is padding that Base64 adds because the number of original bytes is not divisible by three without a remainder. Seventy-three bytes leave a remainder of one byte, so two padding marks are added to complete the last block. If you see text without this mark, it does not necessarily mean an error, since its presence depends on the length of the data.

This exact text is what the QR generation library turns into a square of dots. If you input the same text into any standard QR generator in text encoding, you will get a square matching what a specification-compliant system prints for an invoice with these values.

What happens in Phase Two: Tags 6 to 9

The previous example covers Phase One with its five fields. In Phase Two, four cryptographic tags are added that make the code verifiable, not just a visual summary. We explain how they join the string without dwelling on the cryptography details explained in their dedicated guides:

  • Tag 6: the invoice hash, which is a SHA-256 hash of the XML file after canonicalization, stored in Base64 with a length of forty-four characters.
  • Tag 7: the cryptographic stamp, a digital signature using the Elliptic Curve algorithm (ECDSA) computed over the hash.
  • Tag 8: the public key extracted from the seller’s certificate, allowing the reader to verify the signature.
  • Tag 9: the Authority stamp, a signature placed by the Zakat, Tax and Customs Authority on the public key to prove its accreditation.

These tags come after Tag 5 in the same order. Take Tag 6 as an example. Suppose the sample hash in Base64 is the following value, made up of forty-four characters:

// Tag 6: SHA-256 hash (illustrative sample value, 44 Base64 characters)
Tag    = 0x06
Length = 0x2C           // 44
Value  = "wucz9QQWPQCPT52xN2+3cx7Il/vuLJ0oFssvzhC0FLA="

Here the length is 0x2C i.e. 44, which is the number of characters in the Base64 text and not the number of bytes of the original hash (32 bytes). An important point: the Tag 6 value is stored as ready Base64 text, so its length is counted on characters not on the original binary bytes. After appending the four tags, the string grows a lot because the stamp and key are long bytes, so the payload easily exceeds three hundred bytes before Base64 encoding.

Remember a fundamental difference. The simplified invoice (B2C) carries all nine tags because it is shared with the buyer immediately and reported to the Authority within 24 hours. The standard tax invoice (B2B), however, goes through Clearance with the Authority, and its code content differs according to what the specification requires for each type.

Phase One vs. Phase Two code in the sample
How the code grows by adding the cryptographic fields.
Criterion Phase One Phase Two
Tags 5 tags 9 tags
Length ≈73 bytes Exceeds 300 bytes
Addition Hash, stamp, and public key
Phase Two adds the cryptographic fields, so the code size grows.

Decoding: from Base64 to the original fields

The real value in understanding the construction appears when reversing it. Verifying an invoice always starts with decoding its code and reading its fields. We take the Base64 text we built and reverse the process in three steps.

Step one: decode Base64 to restore the seventy-three raw bytes. Step two: we read the bytes in sequence. We read the tag byte, then the length byte, then cut out the specified number of bytes as the value, then jump to the next byte and repeat. Step three: we convert each value’s bytes from UTF-8 into readable text. The result matches exactly the five values we started with:

// Result of decoding the string byte by byte
Tag 1 (length 17): شركة قيود
Tag 2 (length 15): 300000000000003
Tag 3 (length 20): 2026-06-24T11:45:30Z
Tag 4 (length 6):  115.00
Tag 5 (length 5):  15.00

This closed loop is the essence of verification. If you decode the code of a real invoice and get a VAT number with a length different from 15, or a timestamp that does not match the ISO format, or a total tax that does not equal 15% of the net, then you are facing a suspicious invoice or a non-compliant generator. Decoding exposes the defect immediately.

For practical verification of the string, match three things after decoding. First, that the sum of the piece lengths plus the tag and length bytes for each tag equals the total string length. Second, that each length byte actually matches the number of bytes of its value. Third, that the values are accounting-logical, so the total including tax equals the net plus the tax.

Timestamp details and tag order

The timestamp in Tag 3 deserves special care, because it is the field most prone to silent error. The specification requires the full ISO 8601 format in Coordinated Universal Time, i.e. the date, then the letter T as a separator, then the time in hours, minutes, and seconds, then the letter Z indicating Greenwich Mean Time. In our sample the value 2026-06-24T11:45:30Z means the twenty-fourth of June 2026, at eleven forty-five and thirty seconds Coordinated Universal Time.

The common error is writing the time in local time without converting it, or deleting the letter Z, or using a space instead of the letter T. Each of these changes the value length or violates the format, so verification fails. Always compute the timestamp at the actual moment of issuing the invoice, and convert it to Coordinated Universal Time before writing it. And remember that its length is exactly twenty characters in this format, so if it deviates from twenty, the format is wrong.

As for tag order, it is a binding rule with no discretion. The string starts with Tag 1 and ends with Tag 5 in Phase One, or with Tag 9 in Phase Two, in this numeric sequence exclusively. It is not permitted to put the VAT number before the seller name, nor to delay the timestamp. The reader relies on the order to know the meaning of each piece, so if you swapped the order it would read the values in the wrong fields despite the byte-by-byte construction being sound.

A final note on numeric values. The amounts in Tags 4 and 5 are text, not numbers, so they are written exactly as displayed with the two decimal places. Do not apply programmatic number formatting to them that might drop trailing zeros or add a thousands separator. The value 115.00 is text of six characters, so if your program converted it to the number 115 it would shorten it and break its length. Treat all tag values as raw text from start to finish.

Why bytes differ from characters: the sample table

The confusion between the number of characters and the number of bytes is the source of most errors, so it deserves a longer pause. A character in the writer’s mind is a single unit, but the computer stores text as bytes according to its encoding. In UTF-8, a Latin letter, a digit, and punctuation each take one byte, while an Arabic letter takes two bytes, and some symbols three or four. This disparity is what makes the length byte in TLV count bytes, not characters.

Let us apply the rule to our five sample values and compare the two counts side by side. The last four values are Latin and numeric, so the two counts are equal, whereas the Arabic seller name differs:

  • Tag 1, Qoyod Company: nine visible characters, but seventeen bytes. Eight Arabic characters at two bytes each equal sixteen bytes, plus the one space byte.
  • Tag 2, the VAT number: fifteen numeric digits, and fifteen bytes. An exact match because the digits are Latin.
  • Tag 3, the timestamp: twenty characters among digits, dashes, and the letters T andZ and two colons, and twenty bytes. An exact match.
  • Tag 4, the total: six characters, and six bytes. An exact match.
  • Tag 5, the tax: five characters, and five bytes. An exact match.

The practical takeaway is simple. Whenever the value contains any Arabic letter, compute its length after UTF-8 encoding, not by counting its characters on screen. Every programming language provides a function that returns the number of bytes of the text after encoding, so use it instead of the ordinary string-length function. Relying on the visible string length is what breaks Tag 1 specifically, because it is the only tag with an Arabic value in the simplified invoice.

Where is the Base64 text injected inside the invoice?

The Base64 text we built does not live on its own; it is injected inside the e-invoice file in XML format built on the UBL 2.1 standard. The code is placed in a dedicated field within the document’s additional elements, so the file itself carries it alongside the rest of the invoice data. This is how the code reaches the Fatoora platform and the buyer in a single cohesive document.

When the invoice is printed or displayed, the system reads the Base64 text from its place in the XML and passes it through a QR generator to draw the black square on the receipt. The text is the source, and the square is a visual representation of it. Whoever reads the square with their phone camera restores the same text, then decodes it into fields as we did in the decoding section. This continuous chain from XML to the square to the camera is what makes verification possible anywhere without a database connection.

This interconnection explains the importance of byte-by-byte precision. Any error in building the string carries over from the XML to the printed square, so the auditor reads a code that does not match the invoice’s visible data, and the invoice is rejected. That is why Qoyod generates the code and injects it in its correct place automatically, guaranteeing the square matches the data every time.

Reading the code during audit and field verification

The most common application of understanding this structure is field verification. The Authority’s inspector or the accountant reviewing an invoice usually does not open the XML file; instead they scan the QR code with a reader app, which restores the Base64 text and decodes it into the five fields. Then they compare the seller name, VAT number, total, and tax with what is printed on the face of the invoice.

Here the value of the example we built appears. If the reader returns a VAT number with a length other than fifteen digits, the code is built wrong or the invoice is forged. And if the decoded total does not equal the net plus 15% tax, there is tampering or a calculation error. And if the total in the code does not match the printed total, the invoice is untrustworthy. Decoding turns the silent square into an instant detection tool.

In Phase Two, verification adds a deeper layer. After reading the five fields, the advanced reader verifies Tag 7 (the cryptographic stamp) using the public key in Tag 8, then confirms the key’s accreditation via the Authority stamp in Tag 9. This way verification does not merely match the visible values, but proves the invoice was signed with an accredited certificate and was not altered after signing. This entire chain starts from the same bytes we built in this example.

Common errors when building the sample manually

Most people who build the string by hand fall into recurring errors. Cataloguing them saves you hours of diagnosis:

  • Putting the number of characters in place of the number of bytes in the length byte, which is the most notorious error with Arabic values such as the seller name.
  • Writing the length in decimal instead of hexadecimal inside the byte, so 15 is written as 0x15 which is 21, not 15. The correct value is 0x0F.
  • Inserting a separator or space between the pieces, whereas the specification requires complete adhesion with no extra byte.
  • Dropping the two decimal places from the amounts, so 115 is written instead of 115.00, so the length changes and the comparison fails.
  • Encoding the Arabic name in an encoding other than UTF-8 such as windows-1256, so the bytes differ and the code becomes unreadable on standard devices.
  • Forgetting the padding = or adding it manually in the wrong place, whereas the standard Base64 function handles it automatically.

The golden rule: do not build the string manually in production. Use a standard library for the bytes and another for Base64, and keep this example only to test your output.

How does Qoyod help you with the compliant QR code?

Everything we explained here manually happens inside e-invoicing software automatically on every invoice. You do not build a TLV string, nor encode Base64, nor compute byte lengths yourself:

  • Qoyod generates a QR code compliant with the Phase Two specification on every simplified invoice, signed, stamped, and loaded with the nine tags.
  • It manages the cryptographic stamp certificate (CSID) automatically, and stores the invoice hash chain for verification and audit purposes.
  • It handles reporting within 24 hours for simplified invoices (B2C) and clearance in advance for standard invoices (B2B) with the Fatoora platform.
  • It issues an XML file compliant with the UBL 2.1 standard and injects the code into its correct place with no manual intervention.

Your role remains registering your certificate with the Authority, and Qoyod guides you through this step. And when you need to verify an invoice or diagnose an integration error, understanding this example gives you the ability to read any code and match it.

Start today

Authority-compliant QR codes on every invoice, automatically

Let Qoyod build the TLV string, encode it, and stamp it on every invoice, and you focus on your business instead of computing bytes by hand.

Start your free trial and issue invoices with compliant QR codes

Frequently asked questions

Why is the length of Tag 1 in our example 17 and not 9?
Because the length byte carries the number of bytes, not the number of characters. The seller name Qoyod Company is eight Arabic letters and a space, and each Arabic letter is two bytes in UTF-8 and the space is one byte, so the total is 17 bytes.

Why does the Base64 text in the example end with two == marks?
Because the raw data length of 73 bytes is not divisible by 3 without a remainder, so Base64 adds padding to complete the last block. Whether the mark is present depends on the data length alone, and it is not an error indicator.

Are the values in this example real and usable in an actual invoice?
No. The five values are for education only, and the VAT number and hash value are illustrative samples. Use the example to test your library’s output and match the lengths, not to issue an invoice.

How do I verify the validity of a string I built myself?
Decode Base64, then read the bytes in sequence (tag, then length, then value), and make sure each length byte matches the number of bytes of its value, that the sum of the lengths plus the tag bytes equals the total string length, and that the values are accounting-logical.

Does the sample differ between the simplified and standard invoice?
Yes. The simplified invoice (B2C) carries the nine tags and is reported to the Authority within 24 hours, and the standard one (B2B) goes through clearance in advance, and the code content differs according to what the specification requires for each type.

Do I need to build the code manually in production?
No. Qoyod’s e-invoicing software builds it, encodes it, and stamps it automatically on every invoice. Building the sample once is only for understanding and verification.

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.