Skip to content
Devix Open Source

Guide

Reading and checking

import { decodeInvoice, check, describe, isZatcaPayload } from '@devix-labs/e-invoice-qr';

decodeInvoice(payload)

decodeInvoice('AQVTYWxsYQIKMTIzNDU2Nzg5MQMU…');
// {
//   seller: 'Salla', vatNumber: '1234567891',
//   timestamp: '2021-07-12T14:25:09Z', total: '100.00', vatTotal: '15.00',
//   phase: 1,
//   unknown: [],
// }

Values come back exactly as they were encoded — strings, not numbers — because that is what was stamped. unknown holds any tag outside 1–9 rather than dropping it, so nothing is lost on the way through.

A payload that is not base64, or whose lengths do not add up, throws a TlvError that says where:

TlvError: Tag 1 says it is 20 bytes but only 12 remain

isZatcaPayload(text) is the quiet version: true or false, no throwing, for deciding whether a scanned string is an invoice at all before you try to read it.

check(payload, options)

const result = check(payload, { rate: 0.15 });

result.ok;        // boolean
result.phase;     // 1 | 2 | null
result.invoice;   // the decoded fields, so you need not decode twice
result.problems;  // [{ code, field, actual }]
Option Default
rate — The VAT rate to sanity-check against, as a fraction. Saudi Arabia is 0.15. Omit it and the rate is not checked at all.
tolerance 0.02 How far the VAT may sit from that rate.
now the clock What counts as "the future".

The reason codes

Code Means
missing, empty A required tag is absent or blank.
too-long A value is over the 255 bytes a length field holds.
vat-number-length Not 15 digits.
vat-number-digits Something in it is not a digit.
vat-number-prefix Does not begin and end with 3.
timestamp-format Not ISO 8601.
timestamp-future Dated later than now, with an hour of slack for clock drift.
amount-format Not a plain decimal number.
amount-negative Below zero.
vat-exceeds-total More VAT than invoice.
vat-implausible Does not match the rate you gave.
phase-2-incomplete Some of the stamp tags but not all of them.

describe(problem) turns one into a sentence — "vatNumber does not begin and end with 3 (1234567891)" — for a log or a form. MESSAGES is the wording, so you can translate it.

What a VAT number check can honestly tell you

Fifteen digits, beginning and ending with 3. That is all.

Saudi Arabia publishes no check digit for the VAT registration number, so there is no arithmetic that can tell a real one from a well-formed invention. Any library implying otherwise is overselling. The only way to confirm a number is registered is ZATCA's own lookup.

This is the same position the rest of our regional validators take: say what was checked, and never let a passing result mean more than it does.

Checking as you issue, not after

The useful place for this is the moment before an invoice is saved:

const payload = encodeInvoice(invoice);
const result = check(payload, { rate: 0.15 });

if (!result.ok) {
  throw new Error(`This invoice would not pass: ${result.problems.map(describe).join('; ')}`);
}

Every problem it reports is one a scanner would have found later, in front of a customer or an inspector.

Updated 15 Sep 2026