This guide is for developers and technical integration teams who connect their systems to the e-invoicing API from Qoyod and the Fatoora platform using Python. Its focus is singular: how to build a complete invoice-submission flow in Python, from constructing the authentication header to parsing the authority’s response and handling errors. Everything below is copy-and-run ready once you set the sensitive values specific to your establishment.
We assume you have already read the core concepts. This guide does not explain what a Cryptographic Stamp Identifier (CSID) is, nor the difference between authentication and reporting; it translates those concepts into actual Python code. For the theoretical foundation, see the guide on Authentication in the invoicing API, for the complete submission recipe see the guide on Invoice Submission via API, and for the list of endpoints see the guide on API Endpoints for the Fatoora platform.
This guide is one of a series of practical examples covering the languages most used by integration teams in Saudi Arabia. We chose Python here because it is fast for prototyping and rich in cryptography and networking libraries. The same logic applies to any language; only the syntax differs. If you work in a different language, the six steps we walk through here stay constant, and what changes is the library name and how you call the function.
Before we dive into the code, we want to draw clear boundaries around what this guide covers. We assume you already have a signed UBL document ready. Building the document itself and signing it digitally is a broad topic with its own tools and libraries, and we covered it in other guides in the series. What we focus on here is what comes after signing: how you turn a signed document into a correct HTTP request, how you send it, and how you read what comes back. This is the part where most developers stumble, because a small error in encoding or a header translates into a cryptic rejection from the gateway.
Why is Python a practical choice for integrating with the Fatoora platform?
Python combines readable simplicity with the power of ready-made libraries. The requests library reduces HTTP requests to a few lines, and the hashlib andbase64 modules are part of the standard library, so you do not need to install anything to compute a hash or encode. This makes Python well suited to building an integration prototype within hours, then evolving it into a production service later.
Remember that the Fatoora platform does not know what language your system is written in. What reaches the gateway is an HTTP request carrying headers and a JSON body. Your job is to build this request correctly. Everything below focuses on this point: turning invoice data into a request the authority accepts, and reading what it returns.
Before you start, install the only external library we need. The rest of the tools are in the Python standard library.
We recommend using an isolated virtual Python environment for each integration project, so library versions do not clash between your projects. This protects you from surprises that are hard to diagnose later in the production environment. Also pin a specific version of the requests library in your dependencies file, so your service’s behavior does not suddenly change on an automatic library update.
We also advise separating your integration code from your business logic from the start. Put the functions that build the header, compute the hash, and send the request in a standalone module. This separation makes it easy to test each function in isolation, and lets you swap the test environment for production by changing only the settings, without touching the submission logic. Integration written with this discipline is easier to maintain when the authority’s requirements evolve later.
The big picture: six steps between the signed document and the result
Before we write any function, let’s picture the full path in our minds. Sending a single invoice is not one request but a chain of linked transformations. It starts with a signed UBL document, passes through computing the hash, then encoding, then building the header, then sending, and ends with parsing the response and saving the result. Each step depends on the one before it, and any flaw in the order produces a rejection from the authority.
The order is not optional. The hash must be computed on the final signed content, because any later modification invalidates the value. And building the request body needs the document encoded in Base64. That is why we treat the steps as a strict production line: the output of each step is the input of the next. We will go through each one with a standalone Python function, then combine them at the end into a single function callable from your service.
Step one: build the basic authentication header
Every request you send to the Fatoora platform must carry an Authorization header of the Basic type. This header consists of the certificate identifier (CSID) and its accompanying secret, separated by a colon, then the result is Base64-encoded. Note that it is CSID and not any other abbreviation. A single-character mistake here means the request is rejected at the gateway with status code 401, before the system even looks at the invoice content.
The identifier you use in the test environment is the Compliance CSID. After finishing the issuance stage you move to the Production CSID. The code itself does not change; only the base URL and the identifier change. Do not write sensitive values directly inside the code; read them from environment variables as in the following example.
Combine csid:secret
Base64 encoding
Add the Basic prefix
The following code builds the authentication header from environment variables:
Notice that we read FATOORA_CSID andFATOORA_SECRET from environment variables. This separates the sensitive values from the source code, so your data does not leak into the code repository. Put these values in a secrets-management service or in the server’s environment variables, not in a text file inside the project.
The build_auth_header function combines the identifier and the secret with a colon between them, then converts them to bytes with UTF-8 encoding, then applies Base64. This is the rule that repeats in every language, with only the function names differing.
A subtle point often overlooked: the encoding here is UTF-8 and not any other encoding. If your identifier or secret contains special characters, a wrong encoding produces a header that does not match what the gateway expects. Always stick to UTF-8 when converting text to bytes before applying Base64. This is a good habit that spares you errors that are hard to diagnose later.
Remember too the fundamental difference between the role of the CSID in authentication and its role in signing. In authentication we send the certificate identifier and its secret in the header to prove that the sender is a device registered with the authority. In signing, however, we use the private key associated with the certificate to create the stamp inside the XML. The private key is never sent in any header; it stays local in your system. Confusing the two roles is a common source of security errors, so be careful to separate them in your design.
Step two: compute the invoice hash (invoiceHash)
After building and signing the UBL document, you compute the hash value (invoiceHash) using the SHA-256 algorithm. The hash is computed on the final content of the document, because any later modification to the XML invalidates the value. This hash is part of a linked chain: the previous invoice’s hash feeds into the computation of the next invoice. That is why each hash must be saved the moment it is issued.
The output you send to the authority is the Base64 representation of the binary hash value, not the textual hexadecimal (hex) representation. This is a subtle difference many developers get wrong, ending up with an inexplicable rejection. The following function computes the hash the correct way.
Note the call to .digest() not .hexdigest(). The first returns the binary bytes that we then encode with Base64, which is what the authority expects. The second returns a textual hex string that the gateway rejects. This is the most common error in computing the hash.
The reason the hash exists in the first place is to prove the invoice’s integrity. The hash is a unique fingerprint of the content. If a single byte changed in the document after it was signed, the fingerprint would change entirely, and the authority would detect the tampering immediately. That is why we compute the hash on the exact final content, after signing and not before. Any extra whitespace or change to the XML formatting after signing alters the hash, and the invoice is rejected.
There is another dimension to the hash that is no less important: the chaining of the sequence. Each invoice carries its predecessor’s hash within its data, forming a linked chain that prevents deleting an invoice from the middle or reordering it without the break showing. In practice this means you are obligated to save each invoice’s hash the moment you compute it, because it is a mandatory input for the next invoice. We will return to this point when we assemble the flow into a single function, because it is one of the errors that cost integration teams the most time in diagnosis.
Step three: encode the UBL document in Base64
The request body you send to the gateway carries the signed document encoded in Base64 inside a JSON field. Do not send the raw XML, but its encoded copy. The encoding here is not encryption, but converting the binary bytes into text safe for transport inside JSON.
Now you have three ready values: the authentication header, the invoice hash, and the encoded document. These are the inputs to the request you will send in the next step.
Some developers confuse encoding with encryption, thinking that Base64 protects the invoice data. This is not true. Base64 is a reversible text encoding that any party can read, and its purpose is to transport binary bytes safely inside JSON, not to hide them. Protecting invoice data in transit comes from the encrypted HTTPS channel, and its integrity comes from the digital signature and the hash. Do not rely on Base64 for any security purpose.
Step four: choose the path and send the request
The Fatoora platform distinguishes between two paths according to the invoice type. Full tax invoices between businesses (B2B) go through the Clearance path, where the authority clears the invoice before it is delivered to the buyer. Simplified tax invoices to the consumer (B2C) go through the Reporting path, where the authority is notified of them within a specified window after issuance. The same relative path is called under the base URL of the environment you work in; the difference between test and production is the base URL and the identifier, not the path.
| Criterion | Clearance (B2B) | Reporting (B2C) |
|---|---|---|
| Endpoint | clearance/single | reporting/single |
| Clearance-Status | 1 | 0 |
| Timing | Before delivery | Within 24 hours |
The following code assembles the headers and the request body and sends it with the requestslibrary. Change the base URL and the relative path according to your environment and invoice type:
Note the timeout=30 setting in the call. A request with no timeout may hang your service forever if the gateway is slow. Always specify a reasonable timeout, and handle exceeding it with a retry as in the following section. Note too the Accept-Version header that specifies the API version, a header the gateway requires on every call.
The difference between the two paths is deeper than the difference in endpoint. In the Clearance path you wait for the authority’s reply before delivering the invoice to the buyer, because the invoice is not considered valid until it is cleared. This forces your system to treat clearance as part of the invoice-issuance flow itself, not a later step. In the Reporting path, however, the invoice is issued to the consumer immediately, then the authority is notified of it within the prescribed window. This gives you flexibility to batch B2C invoices and send them later, but it puts on you the responsibility of not exceeding the window.
In practice, design your system to know the type of each invoice before sending, and route it to the correct path automatically. Do not leave the path choice a manual decision, because a single error here means attempting to clear a simplified invoice or report a full one, and both are rejected. Bind the invoice type to the path in one clear layer of your code.
Step five: parse the response and handle errors
The request returning does not necessarily mean the invoice succeeded. You must read the status code first, then parse the JSON body. The authority returns an object containing the clearance or reporting status, in addition to two lists of warnings and errors. An invoice may be accepted with warnings, and another may be rejected with errors explaining the reason. Handle both cases clearly.
The practical rule: a 2xx code with a CLEARED status or its reporting equivalent means success. A 401 code means an authentication error, so review the identifier and the secret. A 400 code means an error in the invoice structure; read errorMessages to learn the exact reason. The values in the following example are illustrative of the response shape.
The example reads validationResults and separates warnings from errors. A warning does not prevent the invoice from being accepted; an error does prevent it, and it carries a code and a message that explain the problem. Print or log these messages, because they are your key to diagnosing any rejection quickly instead of guessing.
Take warnings seriously even though they do not prevent acceptance. Today’s warning may become a blocking error in a later version of the authority’s rules. If you ignore warnings and let them accumulate, you may wake up one day to a mass rejection of your invoices after a gateway update. Make reviewing warnings part of your monitoring routine, and fix their causes early.
It is useful to build a map of the common error codes you encounter, linking each code to its cause and its fix. Rejection 401 leads you to authentication. Rejection 400 leads you to the document structure or the hash value. Over time you accumulate knowledge that turns every rejection from a puzzle consuming hours into a problem with a known solution. This map is one of the most valuable things mature integration teams build.
Common errors that cause rejection
Before we move on to retrying, it is worth gathering the most common causes of rejection in one place, because knowing them in advance saves you hours of diagnosis. The first is using hexdigest instead of digest when computing the hash, producing a textual representation the gateway rejects. The second is modifying the XML after signing, even by a whitespace, so the hash changes and the signature is invalidated.
The third is confusing the compliance identifier with the production identifier, so you send to the production environment with a test identifier or vice versa. The fourth is breaking the hash chain by neglecting to save the previous invoice’s hash. The fifth is routing the invoice to the wrong path, such as attempting to clear a simplified invoice. The sixth is forgetting the Accept-Version header or putting a wrong value in it. Review this list whenever you face a cryptic rejection, as the cause is usually one of them.
Step six: retry with exponential backoff
The network is not perfect. The gateway may return a 502 code or be slow so the timeout expires. In these cases retry instead of failing immediately, but do not retry immediately and endlessly, as that increases the load on the gateway. The correct approach is Exponential Backoff: you increase the interval between each attempt and the next.
Note an important point: retry only on temporary errors such as 502, 503, and timeout. Do not retry on 400 or 401, because these are errors in your request that repetition does not resolve. Repeating a wrong request stays wrong.
The interval starts at one second then doubles: 1 then 2 then 4 then 8 seconds. This gives the gateway time to recover and reduces the load on it. In production it is preferable to add slight randomness (Jitter) to the interval so that retries from several processes do not coincide at the same moment.
Note a subtle issue in retrying with invoices: the request may actually succeed at the gateway, but the response is cut off on the way back, so you think it failed and resend it. This creates the risk of sending the invoice twice. To avoid this, send a fixed unique identifier for each invoice, so the gateway recognizes the duplicate request and does not process it twice. This is what is called the Idempotency principle, and it is essential for any serious submission flow.
Do not make the number of attempts large without limit. Four attempts with exponential backoff are enough in most cases. If the request fails after them, the problem is most likely not transient, but a longer outage in the gateway or in your network. At this point, log the failure in a queue for manual or automatic handling later, instead of continuing to retry pointlessly and consuming your service's resources.
Assembling the flow into a single function
After you have understood each step separately, here is how they chain together into a single callable flow. The following function takes the signed document and its unique identifier, and passes through all the steps until it returns the authority's result. This is the structure on which you build your production service.
Notice the last line: saving the hash. This is not an optional step. Every new invoice depends on its predecessor's hash in a linked chain. Whoever neglects saving the hash breaks the chain, so the authority rejects the next invoice. Build a reliable storage layer before you send your first production invoice.
Build the UBL file and sign
Compute the SHA-256 hash
Base64 encoding and building the Basic header
POST to Clearance or Reporting
Read the response and save the result
Best practices before moving to production
The code above is a valid foundation, but a production service needs additional layers. Isolate sensitive values in a secrets-management service, not in exposed environment variables. Log every request and response in an organized logging system, so you can diagnose any rejection after it occurs. Test your full flow in the test environment with the compliance identifier until all samples succeed before you move to the production identifier.
Watch the rate limits too. If you send a large batch of invoices all at once you may hit the gateway's limit. Spread the sending over intervals, and make use of the retry logic to absorb any temporary slowdown. Successful integration is not the one that sends a single invoice successfully, but the one that withstands thousands of invoices and diverse failure cases.
Build your flow on the test environment first, and send trial invoices in it that cover every case: a full invoice, a simplified invoice, an invoice with multiple items, an invoice with a discount, and a credit invoice. Do not settle for a single successful case. Edge cases are what reveal the gaps in your flow before you discover them in production in front of your customers. Treat the test environment as a miniature production.
Finally, monitor your service after launching it. Collect simple metrics: the percentage of accepted invoices, the average response time, and the number of retry cases. These metrics alert you early to degradation before it turns into a crisis. Integration is not a project that ends at the first successful invoice, but a living service that needs continuous follow-up as the authority's requirements evolve.
Remember finally that the Qoyod platform handles this entire technical layer for you if you do not want to build it yourself. This guide is aimed at those building a custom integration. But if you are an establishment owner looking to issue invoices compliant with the Zakat, Tax and Customs Authority without writing a single line of code, the Qoyod system issues them and connects them to the authority directly.
Compliant e-invoicing without writing any code
Qoyod issues your e-invoices and connects them to the Fatoora platform directly, with no technical integration to build. Try the platform and issue your first compliant invoice in minutes.
The practical summary
In this guide we built a complete invoice-submission flow in Python: a Basic authentication header from the CSID and its secret, a SHA-256 hash Base64-encoded, an encoded UBL document, a POST request with the requests library, parsing of the response, and a retry with exponential backoff. Each function is copy-and-adapt ready for your system.
If you are building the integration in another language, the logic is constant and only the syntax differs. Review the equivalent examples in the rest of the series' languages, then return to the guides on Authentication andInvoice Submission andEndpoints whenever you need deeper detail on any step.