This hands-on guide is aimed at developers building a direct integration with the Fatoora platform in Node.js, within the requirements of e-invoicing from Qoyod. The goal is simple and clear: take the code from here, paste it into your project, adjust the sensitive values, and get a complete journey from building the authentication header to receiving the platform’s response for a real invoice.
The examples here use standard libraries in the Node.js environment: the built-in module crypto to compute the hash value, the module Buffer for Base64 encoding, the axios library or the built-in function fetch to send requests. Every code snippet is written in a style async/await with real error handling and a retry mechanism, because integrating with any government API requires robustness, not just a request that succeeds under ideal conditions.
Before you begin, make sure to read three essential articles in this series: authentication in the invoicing API to understand how credentials are built, andthe API endpoints to know the correct paths, andsending the invoice via API to understand the full sequence of the process. This article is the practical application of those concepts in Node.js specifically.
Why Node.js is well suited for e-invoicing integration
The Node.js environment is asynchronous by nature, which makes it a logical choice for any integration that relies on sequential network requests. When you send an invoice to the Fatoora platform, you wait for a response from a remote server, and during that wait you do not want to freeze the entire application. The single Event Loop model in Node.js lets you fire dozens of requests in parallel without consuming extra processing threads.
The npm package system also provides mature libraries for everything you need: encryption, data encoding, HTTP request management, and XML handling. In most cases, however, you will not need many external libraries, because Node.js’s built-in modules cover the bulk of the work. In the examples below we will show you how to accomplish each step with the least possible reliance on external packages.
The most important point: correct integration is not about “sending one successful request.” Correct integration means handling network outages, error responses from the platform, timeouts, and cases that require a retry. Code that works in the development environment and then crashes in production is code that ignored these cases. That is why you will find clear error handling in every example.
Before we dive into the code, let’s set the big picture. Integrating with the Fatoora platform goes through five interconnected stages. It begins with building the credentials that prove your identity to the platform. Then you compute a digital fingerprint for each invoice to guarantee its integrity. After that you convert the invoice content into a format transferable over the network. Then you actually send the request to the correct path depending on the invoice type. Finally, you read the platform’s response and act on it. Each stage depends on the one before it, and any flaw in an early stage shows up as an obscure rejection at a later stage.
This order is not arbitrary; it reflects the requirements of the Zakat, Tax and Customs Authority for the second phase of e-invoicing. Every invoice must be digitally signed, carry a unique identifier, be linked to a hash chain, and carry a QR code. The code we will build handles the technical part of these requirements, but it remains up to you to understand the logic behind it so that you can troubleshoot problems when they appear.
Another advantage of Node.js worth mentioning: its large community and rich documentation. Any problem you face in encryption or HTTP requests has most likely been faced by someone else before, and its solution is available. This reduces time wasted solving low-level problems and lets you focus on the business logic of your invoices. The debugging tools in the ecosystem are also mature and help you trace requests and responses precisely.
Build the UBL file and sign it
Compute the SHA-256 hash
Base64 encoding and building the Basic header
POST to clearance or reporting
Read the response and save the result
Step one: building the authentication header (Basic Auth)
Every request sent to the Fatoora platform must carry an Authorization Header. The platform uses Basic Authentication, which is a Base64 encoding of the Compliance Cryptographic Stamp Identifier (CSID) together with its associated password, separated by a colon.
Pay close attention to the term: the correct abbreviation is CSID, i.e. Compliance Cryptographic Stamp Identifier. You obtain this identifier from the Zakat, Tax and Customs Authority when you register your issuing unit. Do not confuse it with any other abbreviation.
In Node.js, we build this header using the built-in module Buffer. We create a string that combines the identifier and the password, then encode it into Base64. The following code demonstrates this:
Notice that we pass 'utf8' explicitly as the encoding for the input, and this matters because the identifier may contain special characters. Passing the encoding explicitly prevents unexpected behavior across different Node.js versions. We also verify that both values exist before building, because an incomplete authentication header will be rejected by the platform with an unclear message, and early validation saves you from tracing the problem later.
The golden rule here: never store the CSID or the password directly in the code. Read them from environment variables or from a secrets management service. Any credentials written in the source code are a security vulnerability, especially if the project is pushed to a shared repository.
Step two: computing the invoice hash value (SHA-256 invoiceHash)
E-invoicing in the second phase requires computing a hash value for each invoice using the SHA-256 algorithm. This value guarantees content integrity and is used in the chain that links consecutive invoices, where each invoice carries the hash of the invoice before it to prevent tampering.
The built-in module crypto in Node.js handles this computation in effectively a single line. We pass the invoice content in UBL XML format to the hash function and extract the result in Base64 encoding as the platform’s specification requires:
The function createHash('sha256') creates a hash object, then update() feeds it the content, and finally digest('base64') finalizes the computation and returns the result ready. Some integrations need the hash in hexadecimal (Hex) format instead of Base64, and in that case you replace 'base64' with 'hex'. Always check the required format in the documentation of the endpoint you are dealing with.
A subtle point often overlooked: the order of spaces and lines inside the UBL XML content affects the hash value. Any minor change in formatting, even an extra space, produces a completely different hash. Therefore you must compute the hash on the final content exactly as it will be sent, not on a formatted copy of it. This is one of the most common points of failure in the integrations of beginner developers.
Invoice 1 + its hash
Invoice 2 carries hash 1
Invoice 3 carries hash 2
Step three: encoding the UBL content to Base64
Before sending the invoice, the entire UBL XML content must be encoded to Base64. The Fatoora platform receives the invoice as an encoded string within the request body in JSON format, not as a raw XML file. This encoding ensures the content is transferred over HTTP without problems with special characters.
As with computing the hash, we use the Buffermodule. The encoding here is a single direct operation:
We added the decoding function decodeInvoice because it is very useful during development. When the server rejects your invoice, the first thing you do is decode what you sent and make sure the actual content matches what you expected. Many errors are caused by the fact that the content that was encoded is not the content you thought you were sending.
Remember that the hash from step two is computed on the original XML content before encoding, not on the Base64-encoded copy. This is a fundamental difference: the hash is on the raw content, and what is sent is the encoded content. Confusing the two produces a rejection from the platform.
Step four: sending the request to the clearance path or the reporting path
Now we reach the heart of the integration: actually sending the invoice. Here you must understand a fundamental distinction imposed by the Zakat, Tax and Customs Authority between two types of invoices:
- The tax invoice (B2B): is sent to the clearance path and must be approved by the platform immediately before it is delivered to the buyer.
- The simplified tax invoice (B2C): is sent to the reporting path within 24 hours of its issuance, and is delivered to the buyer directly.
The programming logic is similar between the two paths; the difference is in the endpoint address and in some fields. We will build a unified function that handles both cases. First, the version based on the axios library because it is the most commonly used:
Focus on several details in this code. First, we set a timeout timeout of 30 seconds. Government APIs may be slow to respond under load, and without a defined timeout your request could wait indefinitely and hang the process. Second, we build the request body with the three required fields: the hash value, the unique identifier, and the encoded invoice. Third, the endpoint address changes depending on the type, and this is the only practical difference between clearance and reporting in the sending layer.
If you prefer not to add an external library, the built-in function fetch available in Node.js since version 18 serves the same purpose. Here is the equivalent version:
The fundamental difference is that fetch does not support the timeout directly, so we used AbortController with a timer to abort the request after 30 seconds. Also, unlike axios, fetch does not throw an error on failure responses such as 400 or 500, so we manually check res.ok and throw the error ourselves. This difference confuses many developers coming from other libraries.
Step five: parsing the platform’s response and handling errors
A successful request is not the end of the story. The platform returns a response in JSON format that contains the invoice’s status, which may be “accepted,” “accepted with notes,” or “rejected.” Your code must handle all three cases, not assume success every time.
The idea here is to convert the platform’s response into a clear data structure that the rest of your system understands. The boolean ok field simplifies branching in the subsequent code, and the warnings field is important because an invoice may be accepted but with notes that you must record and review, otherwise silent problems accumulate in your system.
Error handling at the network level is another story. When a request fails due to a temporary outage, a 502 server error, or a timeout, the correct solution is to retry using exponential backoff, not to retry immediately. The following code wraps the send function with a smart retry mechanism:
This function distinguishes between retriable errors and final errors. An error such as 400 (bad request) or 401 (failed authentication) is not worth retrying, because the problem is in your request, not in the server. But 429 (rate limit exceeded), 502, 503, 504, or a timeout are temporary and deserve a retry. The delay doubles with each attempt: one second, then two, then four seconds, giving the server a chance to recover without flooding it with successive requests.
Why exponential backoff specifically and not a fixed delay? Imagine the platform is under temporary load, and that hundreds of applications retry at the same moment. If everyone retried after a fixed one second, requests would pile up in synchronized waves that increase the load. Exponential backoff spreads the attempts over spaced intervals and gives the remote system room to recover. Some advanced applications add a small random element to the delay to avoid synchronization entirely, a technique known as jitter.
Watch out for an important limit: do not retry an invoice that may have actually succeeded. If the timeout elapses after the platform received and processed your invoice but before the response reached you, resending may create a duplicate invoice. The solution is to use the invoice’s unique identifier so the platform recognizes the duplicate request and rejects it or returns the previous response. Design your system to be idempotent from the start.
Assembling the complete journey in a single function
Now we combine all the previous pieces into one integrated flow that takes an invoice from its start to receiving the final result. This is the code you will actually call from your system:
This function represents good practice in production integration: it reads credentials from the environment, selects the path according to the invoice type, retries on temporary failure, classifies the result, and logs every case clearly. Logs are not a luxury; when a problem occurs in production, these logs will be your only way to understand what happened.
A ready e-invoicing integration without writing a single line
Qoyod handles signing, stamping, and linking invoices to the Fatoora platform automatically, with immediate clearance for tax invoices and reporting within 24 hours for simplified ones. Try the platform and focus on your business instead of building the integration from scratch.
Project organization and configuration management
Before moving on to the common errors, it is worth pausing at the project structure itself. You noticed in the previous examples that we separated each responsibility into an independent module: authentication in one file, hashing in another, encoding in a third, and sending in a fourth. This separation is not organizational luxury; it is a practice that makes testing each part in isolation possible and makes error tracing faster. When an invoice fails, you immediately know which layer to look in.
Configuration management deserves special care in e-invoicing integration. Values such as the platform base address, the request timeout, the number of attempts, and the batch size must be adjustable without modifying the code. Gather them in a single configuration module that reads from environment variables with reasonable default values. This way you move from the development environment to testing to production without touching a single line of code.
Call validateConfig once when the application starts, not on every request. The philosophy here is to fail fast and clearly: if the credentials are missing, it is better for the application to stop immediately at startup with a clear message, rather than run and then fail on the first invoice with an obscure message from the platform. This pattern is called startup validation, and it saves hours of debugging in production.
Another point relates to handling dates and time zones. E-invoicing is very time-sensitive, especially for simplified invoices that must be reported within 24 hours. Make sure your server runs on a unified, known time, and preferably handle dates in a standard format everywhere. Relying on the server’s local time without specifying the time zone is a common source of errors when the application is deployed on servers in different regions.
Testing the integration before production
Do not test your integration for the first time on real invoices. The Fatoora platform provides a separate testing environment (Sandbox) that lets you send trial invoices and get realistic responses without any actual tax impact. Point the platform base address in your configuration to the testing environment during development, then switch it to production only after your code passes all scenarios.
The scenarios you must test are not limited to the happy path. Test a fully valid invoice, then an invoice with a missing field to see the rejection message, then an invoice with a wrong hash value, then a simulated network outage to make sure the retry works, then exceeding the rate limit to see how your code behaves with a 429 error. Each of these scenarios reveals a potential gap in your error handling.
Writing automated tests for these scenarios is an investment that pays off quickly. Using any testing framework in Node.js, you can simulate the platform’s various responses and make sure the parsing and retry functions behave as expected. Automated testing protects you from a later change breaking logic that was working, something that happens often in long-lived systems.
| The criterion | Clearance (B2B) | Reporting (B2C) |
|---|---|---|
| The endpoint | clearance/single | reporting/single |
| Clearance-Status | 1 | 0 |
| The timing | Before delivery | Within 24 hours |
Common errors in Node.js integration and how to avoid them
After reviewing dozens of integrations, a set of errors recurs that deserve warning. The first is computing the hash on formatted content instead of the final content. As we mentioned, any extra space or line changes the hash value. Compute the hash on the actual bytes you will send.
The second error is ignoring the encoding when building the authentication string. If the identifier or the password contains non-Latin characters, incorrect encoding produces a corrupt authentication header. Always pass 'utf8' as we did in the first step.
The third error is relying on the request’s success without inspecting the response content. The request may return a 200 status while the invoice is rejected inside the response body. Always inspect the status field in the JSON, not the HTTP status alone.
The fourth error is the absence of timeout and retry management. In the development environment everything is fast, but in production under load you will face delays and outages. Code that does not retry intelligently will drop invoices without you knowing.
The fifth error, and the most dangerous from a security standpoint, is writing credentials in the code or in the application logs. Do not print the CSID or the password in any console.log, and do not store them in the source code. Use environment variables or a dedicated secrets service.
Handling invoices in bulk (Batch)
When you need to send a large number of invoices, the asynchronous nature of Node.js shines. But do not send them all at once, because that may exceed the platform’s rate limits and cause 429 errors. The solution is to process them in limited-size batches:
We used Promise.allSettled instead of Promise.all for an important reason: allSettled waits for all requests whether they succeeded or failed, whereas all stops at the first failure. In invoice processing you do not want an error in a single invoice to fail the processing of the rest of the batch. Each invoice is processed independently and its result is logged individually.
The default batch size of five invoices is a conservative, safe value. Adjust it according to the platform’s actual rate limits, and watch the 429 responses to know when to reduce the size or increase the delay between batches.
When dealing with thousands of invoices daily, consider adding a processing queue between your system and the platform. The queue separates the moment the invoice is created from the moment it is sent, lets you control the flow rate precisely, and lets you reprocess failed invoices later without losing them. This pattern turns your integration from a simple script into a production system capable of scaling as your business grows. The queue also protects you from losing invoices when the platform goes down for a period, since the invoices remain saved until the service returns.
A practical summary for the developer
In this guide we built a complete integration in Node.js that covers every stage of sending the electronic invoice: the authentication header via Buffer and Base64, computing the hash via crypto, encoding the UBL content, sending via axios or fetch, and parsing the response with smart retries. The code is written to be pasted into your project with only the sensitive values adjusted.
The principle that ties everything above together: robust integration assumes failure and prepares for it. Timeouts, retries, inspecting the response content, logging notes, and protecting credentials are not minor details; they are the difference between an integration that works in a demo and an integration that holds up in production for years.
Remember too that e-invoicing requirements evolve, and that the Zakat, Tax and Customs Authority issues updates to the specifications from time to time. Design your code so it is easy to update: isolate the specification-specific logic in defined modules, watch the Authority’s announcements, and test any change in the testing environment before moving it to production. Flexibility in design today saves you a large rebuild tomorrow when the rules change.
Finally, if building and maintaining a complete integration consumes time you would prefer to dedicate to your core product, that is a business decision before it is a technical one. Many organizations find that using a ready accounting platform that handles the integration on their behalf is cheaper and faster than building everything in-house. Whether you build your integration yourself or rely on a ready solution, understanding the principles in this guide remains a foundation for making an informed decision.
The same logic is available in other languages within this series (PHP, Python, Java, and .NET) for those building their integration in a different language. And to return to the fundamental concepts, start from authentication then the API endpoints then sending the invoice.