Write one, then read it back
Fill in an invoice, watch the payload build, and paste any ZATCA QR string in to see what a tax authority's scanner would say about it.
<div class="demo demo--wide">
<div class="demo-field">
<label class="demo-label" for="seller">Seller — try Arabic</label>
<input class="demo-input" id="seller" value="متجر ديفكس">
</div>
<div class="demo-grid-2">
<div class="demo-field">
<label class="demo-label" for="vat">VAT number</label>
<input class="demo-input" id="vat" value="300000000000003">
</div>
<div class="demo-field">
<label class="demo-label" for="when">Issued</label>
<input class="demo-input" id="when" value="2025-03-01T10:00:00Z">
</div>
<div class="demo-field">
<label class="demo-label" for="total">Total, with VAT</label>
<input class="demo-input" id="total" value="115.00">
</div>
<div class="demo-field">
<label class="demo-label" for="vatTotal">VAT</label>
<input class="demo-input" id="vatTotal" value="15.00">
</div>
</div>
<div class="demo-field">
<label class="demo-label" for="payload">The QR payload — editable, so paste one in</label>
<textarea class="demo-input" id="payload" rows="3" spellcheck="false"></textarea>
</div>
<p class="demo-out" id="verdict"></p>
<ul class="problems" id="problems"></ul>
<p class="demo-hint" id="bytes"></p>
</div>
import { encodeInvoice, check, describe, byteLength } from 'https://www.devix.pk/cdn/oss/e-invoice-qr@1.0.0/index.js';
const fields = ['seller', 'vat', 'when', 'total', 'vatTotal'].map((id) => document.getElementById(id));
const payload = document.getElementById('payload');
const verdict = document.getElementById('verdict');
const problems = document.getElementById('problems');
const bytes = document.getElementById('bytes');
const report = (text) => {
const result = check(text, { rate: 0.15 });
verdict.textContent = result.ok
? `✓ Valid, phase ${result.phase}`
: `✗ ${result.problems.length} problem${result.problems.length === 1 ? '' : 's'}`;
verdict.dataset.state = result.ok ? 'ok' : 'bad';
problems.innerHTML = result.problems.map((p) => `<li><code>${p.code}</code> ${describe(p)}</li>`).join('');
const seller = fields[0].value;
bytes.textContent = seller
? `The seller name is ${[...seller].length} characters and ${byteLength(seller)} bytes — the length byte carries the second number, which is what trips most implementations up.`
: '';
};
const build = () => {
try {
payload.value = encodeInvoice({
seller: fields[0].value,
vatNumber: fields[1].value,
timestamp: fields[2].value,
total: fields[3].value,
vatTotal: fields[4].value,
});
report(payload.value);
} catch (error) {
payload.value = '';
verdict.textContent = `✗ ${error.message}`;
verdict.dataset.state = 'bad';
problems.innerHTML = '';
}
};
for (const field of fields) field.addEventListener('input', build);
// Editing the payload reads it back instead, which is the other half of the package.
payload.addEventListener('input', () => report(payload.value));
build();
#payload { font: 12px var(--demo-mono); resize: vertical; word-break: break-all; }
#verdict[data-state='ok'] { color: var(--demo-ok); font-weight: 600; }
#verdict[data-state='bad'] { color: var(--demo-danger); font-weight: 600; }
.problems { display: grid; gap: 4px; margin: 0; padding: 0; list-style: none; font-size: 12.5px; }
.problems li { color: var(--demo-muted); }
.problems code {
padding: 1px 5px;
border: 1px solid var(--demo-line);
border-radius: 5px;
background: var(--demo-code);
font: 11px var(--demo-mono);
color: var(--demo-fg);
}