<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Mousa Hammad</title>
    <description>The latest articles on DEV Community by Mousa Hammad (@mousah20).</description>
    <link>https://dev.to/mousah20</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4067512%2F885404bc-4c1f-4e31-be28-f4af10df811f.png</url>
      <title>DEV Community: Mousa Hammad</title>
      <link>https://dev.to/mousah20</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mousah20"/>
    <language>en</language>
    <item>
      <title>ZATCA Wave 25: What Developers Integrating Before 1 February 2027 Need to Know</title>
      <dc:creator>Mousa Hammad</dc:creator>
      <pubDate>Fri, 07 Aug 2026 13:23:27 +0000</pubDate>
      <link>https://dev.to/mousah20/zatca-wave-25-what-developers-integrating-before-1-february-2027-need-to-know-4lkn</link>
      <guid>https://dev.to/mousah20/zatca-wave-25-what-developers-integrating-before-1-february-2027-need-to-know-4lkn</guid>
      <description>&lt;p&gt;On 24 July 2026, Saudi Arabia's ZATCA announced the selection criteria for &lt;strong&gt;Wave 25&lt;/strong&gt; of Phase 2 e-invoicing integration: taxpayers whose taxable turnover exceeded &lt;strong&gt;SAR 187,500&lt;/strong&gt; in 2022, 2023 or 2024 must integrate their systems with the Fatoora platform no later than &lt;strong&gt;1 February 2027&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That is the lowest threshold in the entire rollout. Waves 1 through 24 walked the number down from SAR 3 billion to SAR 375,000; Wave 25 halves it again. In practice it means Phase 2 now reaches almost every VAT-registered business in the Kingdom - and the developers building for them are meeting the integration for the first time.&lt;/p&gt;

&lt;p&gt;If that is you, here is what actually matters, from having shipped one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Phase 2 is not "add a QR code"
&lt;/h2&gt;

&lt;p&gt;Phase 1 was a printed TLV QR with five fields. Phase 2 is a different animal:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Onboarding&lt;/strong&gt; - generate a keypair, build a CSR with ZATCA-specific extensions, exchange it for a compliance certificate (CCSID), pass compliance checks, then exchange those for a production certificate (PCSID).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Signing&lt;/strong&gt; - canonical UBL 2.1 XML, an invoice hash over a precisely-scoped subset of the document, an ECDSA signature on the secp256k1 curve, and a XAdES signature block.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;QR&lt;/strong&gt; - now nine TLV tags, including the invoice hash, your signature, your public key, and the CA's signature over your certificate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Submission&lt;/strong&gt; - simplified (B2C) invoices are &lt;em&gt;reported&lt;/em&gt; after issue; standard (B2B) invoices are &lt;em&gt;cleared&lt;/em&gt; before the buyer gets them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chain&lt;/strong&gt; - every invoice carries a strictly-increasing counter (ICV) and the previous invoice's hash (PIH), making deletions provable.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Budget accordingly. The XML is the easy part.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part nobody warns you about
&lt;/h2&gt;

&lt;p&gt;ZATCA's real contract lives in its gateway's behavior, not in its documentation. A few examples that each cost me a live rejection to find:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Each environment demands a different CSR certificate template.&lt;/strong&gt; &lt;code&gt;TSTZATCA-Code-Signing&lt;/code&gt; for sandbox, &lt;code&gt;PREZATCA-Code-Signing&lt;/code&gt; for simulation, &lt;code&gt;ZATCA-Code-Signing&lt;/code&gt; for production. Send the wrong one and the gateway replies with a bare &lt;code&gt;Invalid Request&lt;/code&gt; - before it reads anything else in your CSR.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The device serial belongs in the surname OID (2.5.4.4)&lt;/strong&gt;, not serialNumber (2.5.4.5), because ZATCA's own OpenSSL config wrote &lt;code&gt;SN=&lt;/code&gt; and OpenSSL maps that to surname. The semantically correct field is rejected.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;secp256k1 works in Node and dies in Electron.&lt;/strong&gt; Electron links BoringSSL, which refuses the curve (&lt;code&gt;UNKNOWN_GROUP&lt;/code&gt;). Every test passes on your machine; the packaged desktop app throws at the first signature.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The XAdES SignedProperties block must exist in two different byte-shapes&lt;/strong&gt; - one you hash, one you embed - and its digest is base64 of the &lt;em&gt;hex&lt;/em&gt; string, not of the raw bytes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compliance-green is not reporting-green.&lt;/strong&gt; &lt;code&gt;/compliance/invoices&lt;/code&gt; does not verify the SignedProperties digest at all. You can pass all four compliance documents, collect your production CSID, and fail on your very first real invoice with &lt;code&gt;signed-properties-hashing&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There are ten of these. I wrote them all up, free and in full: &lt;a href="https://github.com/mousah20/zatca-phase2-traps" rel="noopener noreferrer"&gt;github.com/mousah20/zatca-phase2-traps&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  A realistic plan for the six months you have
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Now to October:&lt;/strong&gt; get onboarding working against the sandbox. Any OTP is accepted there, so there is nothing to wait for. Getting a compliance CSID issued proves your CSR is correct, which is where most of the early time is lost.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;October to December:&lt;/strong&gt; sign and submit all four document types (simplified and standard invoices, credit and debit notes). Then move to the simulation environment with a real Fatoora-portal OTP and, crucially, get an accepted &lt;strong&gt;reporting&lt;/strong&gt; call - not just compliance checks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;December to February:&lt;/strong&gt; production onboarding with your taxpayer's certificate, then run both systems in parallel briefly before switching over. Leave room for the ICV/PIH chain work: the counter must be reserved inside the same database transaction that writes the invoice, or two concurrent sales take the same number and every invoice after the duplicate is rejected.&lt;/p&gt;

&lt;p&gt;Do not leave the integration to January. The gateway's error messages are terse, and each round trip through "reject, guess, retry" costs days.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shortcuts
&lt;/h2&gt;

&lt;p&gt;If you work in Node.js, the QR encoder alone (all nine Phase-2 tags, byte-correct for Arabic seller names) is MIT-licensed on npm: &lt;a href="https://www.npmjs.com/package/zatca-qr-tlv" rel="noopener noreferrer"&gt;zatca-qr-tlv&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;And if you would rather not spend the six months at all, I packaged the entire integration - onboarding, signing, QR, reporting and clearance - as a &lt;a href="https://mousah20.github.io/zatca/" rel="noopener noreferrer"&gt;production-proven SDK&lt;/a&gt; with a live test that onboards a device against ZATCA's sandbox and gets an invoice REPORTED in about two minutes.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Not affiliated with ZATCA. Wave 25 criteria as announced on 24 July 2026; confirm your own wave and deadline through the Fatoora portal.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>api</category>
      <category>node</category>
    </item>
    <item>
      <title>Why Your ZATCA Phase 2 Invoice Passes Compliance and Fails Reporting</title>
      <dc:creator>Mousa Hammad</dc:creator>
      <pubDate>Fri, 07 Aug 2026 12:36:09 +0000</pubDate>
      <link>https://dev.to/mousah20/why-your-zatca-phase-2-invoice-passes-compliance-and-fails-reporting-16fc</link>
      <guid>https://dev.to/mousah20/why-your-zatca-phase-2-invoice-passes-compliance-and-fails-reporting-16fc</guid>
      <description>&lt;p&gt;If you are integrating ZATCA Phase 2 (Saudi Arabia's Fatoora e-invoicing) and you have seen this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ERROR"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"signed-properties-hashing"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"category"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CERTIFICATE_ERRORS"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Invalid signed properties hashing, SignedProperties with id='xadesSignedProperties'"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;...after your invoice sailed through &lt;code&gt;/compliance/invoices&lt;/code&gt;, this post is for you. It is the single most confusing failure mode in the whole integration, and the fix is not what the error suggests.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trap: SignedProperties exists in two byte-shapes
&lt;/h2&gt;

&lt;p&gt;The XAdES &lt;code&gt;SignedProperties&lt;/code&gt; block is referenced twice in your signed document:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;ds:Reference URI="#xadesSignedProperties"&lt;/code&gt; carries a &lt;strong&gt;digest&lt;/strong&gt; of the block.&lt;/li&gt;
&lt;li&gt;The block itself is &lt;strong&gt;embedded&lt;/strong&gt; inside &lt;code&gt;ds:Object &amp;gt; xades:QualifyingProperties&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The natural assumption is that both refer to the same bytes. They do not.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The hashed shape&lt;/strong&gt; carries namespace declarations and starts at column 0:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;xades:SignedProperties&lt;/span&gt; &lt;span class="na"&gt;xmlns:xades=&lt;/span&gt;&lt;span class="s"&gt;"http://uri.etsi.org/01903/v1.3.2#"&lt;/span&gt; &lt;span class="na"&gt;Id=&lt;/span&gt;&lt;span class="s"&gt;"xadesSignedProperties"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                                    &lt;span class="nt"&gt;&amp;lt;xades:SignedSignatureProperties&amp;gt;&lt;/span&gt;
                                        &lt;span class="nt"&gt;&amp;lt;xades:SigningTime&amp;gt;&lt;/span&gt;2026-08-07T02:14:33&lt;span class="nt"&gt;&amp;lt;/xades:SigningTime&amp;gt;&lt;/span&gt;
                                        &lt;span class="nt"&gt;&amp;lt;xades:SigningCertificate&amp;gt;&lt;/span&gt;
                                            &lt;span class="nt"&gt;&amp;lt;xades:Cert&amp;gt;&lt;/span&gt;
                                                &lt;span class="nt"&gt;&amp;lt;xades:CertDigest&amp;gt;&lt;/span&gt;
                                                    &lt;span class="nt"&gt;&amp;lt;ds:DigestMethod&lt;/span&gt; &lt;span class="na"&gt;xmlns:ds=&lt;/span&gt;&lt;span class="s"&gt;"http://www.w3.org/2000/09/xmldsig#"&lt;/span&gt; &lt;span class="na"&gt;Algorithm=&lt;/span&gt;&lt;span class="s"&gt;"http://www.w3.org/2001/04/xmlenc#sha256"&lt;/span&gt;&lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The embedded shape&lt;/strong&gt; carries no namespace declarations (they are inherited from ancestors) and its root element is indented to &lt;strong&gt;column 32&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;                                &lt;span class="nt"&gt;&amp;lt;xades:SignedProperties&lt;/span&gt; &lt;span class="na"&gt;Id=&lt;/span&gt;&lt;span class="s"&gt;"xadesSignedProperties"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
                                    &lt;span class="nt"&gt;&amp;lt;xades:SignedSignatureProperties&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Embed the hashed shape verbatim - the intuitive thing to do - and the gateway rejects with &lt;code&gt;signed-properties-hashing&lt;/code&gt;, even though your indentation "looks right".&lt;/p&gt;

&lt;h2&gt;
  
  
  The second half of the trap: the digest encoding
&lt;/h2&gt;

&lt;p&gt;The digest is &lt;strong&gt;not&lt;/strong&gt; the raw SHA-256 bytes in base64. It is base64 of the &lt;strong&gt;hex string&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;crypto&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// hashedShape = the namespaced, column-0 variant above&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;propsDigest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sha256&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hashedShape&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;base64&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you used &lt;code&gt;.digest('base64')&lt;/code&gt; you produced a valid-looking digest that ZATCA will never match. The same convention applies to the certificate digest.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why compliance checks did not catch it
&lt;/h2&gt;

&lt;p&gt;This is the part that costs people days: &lt;strong&gt;&lt;code&gt;/compliance/invoices&lt;/code&gt; does not verify this digest at all.&lt;/strong&gt; Only &lt;code&gt;/invoices/reporting/single&lt;/code&gt; (and clearance) does.&lt;/p&gt;

&lt;p&gt;So the workflow that feels safe - pass all four compliance sample documents, obtain your production CSID, celebrate - proves nothing about this block. Your first real invoice is the first time anything checks it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rule of thumb: compliance-green is not reporting-green.&lt;/strong&gt; The only proof your signing is correct is an accepted reporting call.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;Generate both shapes from one function so they can never drift:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;signedPropsBlock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;withNamespaces&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;xades&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;withNamespaces&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; xmlns:xades="http://uri.etsi.org/01903/v1.3.2#"&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ds&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;withNamespaces&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; xmlns:ds="http://www.w3.org/2000/09/xmldsig#"&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;   &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lead&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;withNamespaces&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;repeat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="se"&gt;\$&lt;/span&gt;&lt;span class="s2"&gt;{lead}&amp;lt;xades:SignedProperties&lt;/span&gt;&lt;span class="se"&gt;\$&lt;/span&gt;&lt;span class="s2"&gt;{xades} Id="xadesSignedProperties"&amp;gt;
                                    &amp;lt;xades:SignedSignatureProperties&amp;gt;
                                        &amp;lt;xades:SigningTime&amp;gt;&lt;/span&gt;&lt;span class="se"&gt;\$&lt;/span&gt;&lt;span class="s2"&gt;{signingTime}&amp;lt;/xades:SigningTime&amp;gt;
                                &amp;lt;/xades:SignedProperties&amp;gt;&lt;/span&gt;&lt;span class="se"&gt;\`&lt;/span&gt;&lt;span class="s2"&gt;;
};

const hashed   = signedPropsBlock(true);   // digest this one
const embedded = signedPropsBlock(false);  // put this one in the document
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two more details that bite here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;SigningTime&lt;/code&gt; must have &lt;strong&gt;no Z suffix and no milliseconds&lt;/strong&gt; - the validator reformats it as &lt;code&gt;%Y-%m-%dT%H:%M:%S&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The indentation inside the template is part of the contract. The validator rebuilds this block from a fixed template and compares digests, so "prettifying" your XML output breaks it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  And while you are here: the hash scope
&lt;/h2&gt;

&lt;p&gt;A related failure that produces a different opaque rejection - the invoice hash itself is SHA-256 over the canonical XML &lt;strong&gt;without&lt;/strong&gt; the XML declaration (C14N drops it) and &lt;strong&gt;without&lt;/strong&gt; three elements: &lt;code&gt;ext:UBLExtensions&lt;/code&gt;, &lt;code&gt;cac:Signature&lt;/code&gt;, and the &lt;code&gt;AdditionalDocumentReference&lt;/code&gt; whose ID is &lt;code&gt;QR&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;When you insert those blocks after hashing, they must add &lt;strong&gt;zero new text nodes&lt;/strong&gt;. The verifier strips element subtrees but keeps surrounding whitespace - so a single newline between the inserted QR reference and &lt;code&gt;cac:Signature&lt;/code&gt; changes the canonical form and breaks the hash. Butt them directly together, with no whitespace between the two elements.&lt;/p&gt;

&lt;h2&gt;
  
  
  Nine more of these
&lt;/h2&gt;

&lt;p&gt;ZATCA's real contract lives in its gateway's behavior, not its documentation. I hit ten undocumented rules like this while shipping a POS integration - each discovered through a live rejection, none caught by 256 green local tests. Things like: each environment demanding a different CSR certificate template (wrong one gives a bare &lt;code&gt;Invalid Request&lt;/code&gt; before your CSR is even read), the device serial belonging in the &lt;strong&gt;surname&lt;/strong&gt; OID because ZATCA's own OpenSSL config wrote &lt;code&gt;SN=&lt;/code&gt;, and secp256k1 working in Node but throwing &lt;code&gt;UNKNOWN_GROUP&lt;/code&gt; inside Electron.&lt;/p&gt;

&lt;p&gt;I documented all ten, free and in full: &lt;a href="https://github.com/mousah20/zatca-phase2-traps" rel="noopener noreferrer"&gt;github.com/mousah20/zatca-phase2-traps&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you just need the QR part, the TLV encoder (all 9 Phase-2 tags, byte-correct for Arabic seller names) is on npm as &lt;a href="https://www.npmjs.com/package/zatca-qr-tlv" rel="noopener noreferrer"&gt;zatca-qr-tlv&lt;/a&gt;, MIT licensed.&lt;/p&gt;

&lt;p&gt;And if you would rather not spend a month on this at all, I packaged the entire integration - onboarding, signing, QR, reporting and clearance - as a &lt;a href="https://mousah20.github.io/zatca/" rel="noopener noreferrer"&gt;production-proven Node.js SDK&lt;/a&gt; with a live test you can run against ZATCA's sandbox in two minutes.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Not affiliated with ZATCA. Behavior described as observed in August 2026.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>node</category>
      <category>api</category>
      <category>tutorial</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
