This guide is written for developers and technical integration teams connecting .NET (the C# language) to the Fatoora platform as part of e-invoicing in Saudi Arabia. Its single focus: how to build a complete end-to-end integration in C# that authenticates against the Fatoora platform APIs (Fatoora API), computes the invoice hash, encodes the XML in Base64, sends the invoice to both the Clearance and Reporting endpoints, and handles the response and errors safely.
We assume you have already read the Billing API authentication guide and know where the cryptographic stamp certificate (CSID) and its secret come from. We also assume you are familiar with the Fatoora platform API endpoints andsubmitting the invoice via the API. This guide starts where those guides end: from actual, copy-and-run code in a .NET project.
The examples here are written in modern C# using HttpClient from the library System.Net.Http, andSHA256 from System.Security.Cryptography, andSystem.Text.Json to parse the response. These are educational examples explaining the technical layer of a direct integration with the Fatoora platform, independent of the Qoyod product. If you are looking for a ready-made solution without writing code, Qoyod connects your establishment to the Fatoora platform automatically, with no need to build this integration yourself.
Why .NET is a practical choice for integrating with the Fatoora platform
Many ERP and point-of-sale systems in the Saudi market are built on the .NET platform. That makes connecting these systems to the Fatoora platform directly from within C# a natural choice for many teams. The library System.Security.Cryptography gives you everything you need to compute SHA-256 hashes and sign documents, while HttpClient handles the transport layer efficiently and securely.
A pivotal point we put before you from the start: the Fatoora platform billing APIs do not use OAuth. There is no endpoint to request an access token. Authentication relies on the CSID certificate directly in every request via a Basic header. Keep this fact in mind throughout this guide, as it determines how you build your HTTP client in C#.
We will build the integration in clear stages. First we prepare the Basic authentication header. Then we compute the invoice hash. After that we encode the XML in Base64. Then we send the request and read the response. Finally we add an error-handling and retry layer. Each stage comes with complete, copy-ready C# code.
Before we begin, it helps to picture the layers any request passes through. The first layer is the document-building layer, which produces an invoice in UBL XML compliant with the authority’s schema. The second layer is the cryptography layer, where you compute the hash and sign the document. The third layer is the transport layer, where you build an authenticated HTTP request and send it. The fourth layer is the response layer, where you read the result and handle errors. This separation of layers is not organizational luxury; it is what makes your integration testable and maintainable over the long term.
In serious .NET projects, this separation translates into independent classes. A class for authentication, a class for hashing, a class for encoding, a class for the HTTP client, and a class for parsing the response. The examples in this guide follow this division precisely, so you can move them as they are into your project without restructuring. Each class carries a single, clear responsibility, which makes it easier to write unit tests for each part on its own.
Stage one: building the Basic authentication header
Authentication on the Fatoora platform is Basic Authentication. You take the certificate identifier (CSID) and its accompanying secret, combine them in the form CSID:secret, then encode the result in Base64 and place it in the Authorization header. Note that the correct name is CSID and not CCSI, a common typo that confuses developers.
In C# we use Convert.ToBase64String on the bytes of the text with UTF-8 encoding. The following code builds the header:
Generating a Basic header in C#
Note that we read the CSID and the secret from environment variables, not from text written inside the code. This is sensitive data. Do not place it in the code, in a git repository, or in configuration files that get uploaded. Read it from environment variables or from a secrets vault such as Azure Key Vault. Securing this data is your responsibility, because anyone who obtains it can impersonate your device to the authority.
The encoding format deserves further clarification. Base64 encoding is not encryption; it is a reversible transformation. Anyone who obtains the header value can easily decode it and extract the CSID and the secret. Real protection therefore stays in the encrypted transport layer (TLS) and in keeping credentials away from prying eyes. Do not assume Base64 hides anything; it is merely a safe transport format within HTTP text.
In terms of the certificate lifecycle, your journey begins with a Compliance CSID that you use in the test environment to make sure your invoices pass the authority’s checks. After a successful compliance check, you obtain the Production CSID that you use in the live environment. These are two different certificates with two different secrets, and your application must separate them via environment settings. A common mistake is for the developer to use the test certificate in production or vice versa, so all requests are rejected with a 401 code for no clear reason. Make the certificate choice tied to the runtime environment, not written by hand.
In a practical .NET application, you store the CSID and secret values for each environment in the secrets vault, then inject them via the Configuration system into the class responsible for authentication. This way your code never touches the actual values, and you can perform Secret Rotation without redeploying the application. This is a fundamental security practice that separates code from sensitive data.
Combine csid:secret
Base64 encoding
Add the Basic prefix
Stage two: computing the invoice hash with the SHA-256 algorithm
Before sending any invoice, you need to compute its cryptographic hash. The Fatoora platform uses the SHA-256 algorithm to compute the hash of the invoice document in UBL XML. This hash is later used in the signature and in building the request body, and it proves the invoice was not modified after being signed.
In C# we use the SHA256 from System.Security.Cryptography class. The output is raw bytes, which we usually encode in Base64 to send in the JSON body. The following code computes the hash from the XML text:
Computing SHA-256 and encoding it in Base64 in C#
Pay attention to a subtle point that costs developers hours of debugging. The hash is computed on the document’s bytes exactly as they are. Any change, even in a single whitespace character or in the order of attributes inside the XML, produces a completely different hash, so the gateway rejects the invoice. Therefore compute the hash on the very same bytes you will send, and do not reformat the XML after computing the hash.
Here a common trap appears in .NET specifically. Many developers load the XML into an XmlDocument or XDocument object to process it, then rewrite it as text. This operation may change the encoding, add an XML declaration, or reorder namespaces, producing bytes different from the original. The safe rule: treat the invoice document after building it as a fixed byte sequence. Compute its hash, encode it, and send it from the same byte store, and do not pass it through any XML processor that reshapes it.
The root reason is that the SHA-256 algorithm is sensitive to every byte. Changing a single byte flips the entire hash due to the Avalanche Effect. This is exactly what makes the hash useful for proving document integrity, but it is also what makes any accidental modification catastrophic. Think of the hash as a digital fingerprint of the document: two identical fingerprints mean two documents identical byte for byte, and any difference, however slight, produces a different fingerprint.
For practical verification during development, save a copy of the bytes you computed the hash on and compare them with the bytes you actually sent. If they differ, a transformation crept in somewhere in the processing pipeline. This simple check saves you hours of tracing an obscure rejection from the gateway.
Stage three: encoding the UBL document in Base64
The request body sent to the Fatoora platform carries the invoice document encoded in Base64. The reason is that the XML may contain special characters that break the JSON structure if sent as they are. Base64 encoding converts the document into a string that is safe within JSON.
We use the same Convert.ToBase64String function we saw, but this time on the bytes of the full document:
Encoding the XML in Base64 in C#
Note that we computed the hash and encoded the document from the same ublXml variable and from the same bytes. This guarantees the hash matches the sent content. The uuid field is a globally unique identifier for the invoice, which we generate with Guid.NewGuid(), and it must match the identifier inside the XML itself.
invoiceHash: the invoice SHA-256 hash (Base64)
uuid: the invoice’s unique identifier
invoice: the invoice in Base64 encoding
Stage four: sending the invoice via HttpClient to Clearance and Reporting
The Fatoora platform handles two types of invoices, each with a different endpoint. Tax invoices addressed to businesses (B2B) go through the Clearance endpoint, where the authority verifies and stamps them before handing them to the buyer. Simplified tax invoices addressed to individuals (B2C) are sent via the Reporting endpoint within twenty-four hours of their issuance.
In C# we build a single HttpClient client that we reuse, add the required headers, then send the JSON body. The following code sends an invoice to the Clearance endpoint:
Sending the invoice to the Clearance endpoint in C#
The Accept-Version header specifies the API version, and the Clearance-Status header with a value of 1 requests actual clearance. To send a simplified invoice via the Reporting endpoint, you replace the path with /invoices/reporting/single and remove the clearance header. The structure is the same; only the endpoint changes.
The difference between Clearance and Reporting deserves a practical clarification that directly affects your application’s design. Clearance is a synchronous operation: you send the invoice and wait for the authority’s stamp before handing it to the buyer. This means the buyer does not receive their invoice until the gateway responds. The response time must therefore be accounted for in the user experience, and you must handle gateway slowness gracefully. Reporting is by nature more lenient in timing, as you have a window of up to twenty-four hours to send the simplified invoice after issuing it, which lets you batch invoices and send them in groups.
This difference suggests two different architectures. In the Clearance case, sending is part of the live request path, so you care about response time and timeout. In the Reporting case, you can place invoices in a Queue and process them with a background process, isolating gateway slowness from the point-of-sale experience. In .NET you build this background process using BackgroundService or a Hosted Service that pulls invoices from the queue and sends them at a steady pace.
Another practical point: do not send the same invoice twice on two different endpoints. The invoice type determines the path. The full tax invoice (B2B) goes to Clearance exclusively, and the simplified invoice (B2C) goes to Reporting exclusively. Determining the type comes from a field inside the UBL document itself, not from a decision in your code. Read the type from the document and route the request accordingly, as this ensures the path is consistent with the invoice content.
Stage five: parsing the response with System.Text.Json
The Fatoora platform returns a JSON response carrying the processing status and the validation result. In the B2B case, the reply returns the Cleared Invoice encoded in Base64, which is the approved version you hand to the buyer. We read these fields using System.Text.Json.
Reading the response fields in C#
We use TryGetProperty instead of accessing the field directly, because the response structure may differ between success and rejection. This approach protects you from an exception when an expected field is missing. After reading the stamped invoice, we decode the Base64 with Convert.FromBase64String and save the approved version.
The Fatoora platform response carries more than the clearance status. You usually find in it lists of Warnings and Errors, each with an identifier and a message. Warnings do not prevent the invoice from being accepted, but they point to issues you must address before they turn into errors in a later version of the API. Therefore do not ignore them. Read the warnings array and log it, as it is an early alarm that spares you future surprises.
In a well-organized .NET application, you convert these lists into clear Models instead of dealing with raw JsonElement everywhere. Define a class for the warning and a class for the error, and load the values into them once when parsing the response. This makes the rest of your code deal with Strongly Typed objects instead of digging through the JSON tree. The benefit is twofold: clearer code, and errors caught at compile time rather than at run time.
Logging and monitoring in a production integration
Integration with a government entity needs a log you can refer back to in any dispute or audit. For every invoice, log its unique identifier (UUID), its hash, the response code, the send time, and the processing result. This log is not a luxury but an operational necessity that proves you sent the invoice and that the authority accepted it.
In .NET you use the standard ILogger interface to record these events. Tie every log entry to the invoice identifier so you can trace its full journey across the logs. When a rejection occurs, this link is what lets you find the original request and the response together in moments, instead of searching blindly through log files.
Beware of logging sensitive data. Never write the CSID, the secret, or the Authorization header into the logs. Log identifiers, status codes, and results, not credentials. Leaking secrets through logs is a recurring security mistake, since logs are often collected into central systems viewed by many. Treat the log as readable from outside your team, and write into it only what everyone may see.
Add to logging operational metrics that monitor the integration’s health: the accepted-invoice ratio, the average response time, the number of retries, and the distribution of error codes. These metrics reveal service degradation before users notice it. In .NET you collect them via Counters and export them to a monitoring system, turning your integration from a black box into a visible system whose behavior you understand moment by moment.
Stage six: error handling and retry
Any serious production integration needs a clear error-handling layer. The Fatoora platform replies with different status codes, each with its own meaning and handling. Do not treat every error the same way. The following table summarizes the most important codes:
| Status code | Meaning | Correct handling |
|---|---|---|
| 200 | Full success | Save the stamped invoice |
| 202 | Success with notes | Save the invoice and log the notes |
| 400 | Error in the request or invoice structure | Fix the content; do not retry as is |
| 401 | Authentication failure | Check the CSID and the secret; do not repeat the request |
| 500 | Error on the authority’s server | Retry with exponential backoff |
| 503 | The service is temporarily unavailable | Retry with exponential backoff |
The governing rule: retry only on transient server errors (500, 503, and connection timeout). Never retry on 400 or 401, because repeating a bad request stays bad and wastes resources. The following code distinguishes between the two cases:
The exception class and the retry loop in C#
The loop applies Exponential Backoff: it waits one second after the first attempt, then two, then four. This gives the authority’s server time to recover and avoids flooding it with consecutive requests. The when (ex.IsRetryable) condition ensures we only retry on transient errors.
If you are building a production system, the Polly library is a mature choice for managing retry policies and the Circuit Breaker in .NET. It saves you from writing backoff logic by hand and provides ready-made, tested policies. But the principle stays the same: retry only on transient errors.
| Status code | The action |
|---|---|
| 2xx | Save the approved result |
| 400/401 | Permanent error: fix, do not retry |
| 5xx / timeout | Retry with exponential backoff |
Assembling the full end-to-end integration
Now we combine the six stages into a single flow. The complete scenario: we read the UBL document, compute its hash, encode it in Base64, build the request, send it via HttpClient with retry, then parse the response. The following code ties everything together:
The full flow in C#
Note an important point in resource management. We created a single HttpClient and reused it. Creating a new client for every request drains network ports (Socket Exhaustion) in high-traffic systems. In modern .NET applications, it is preferable to use IHttpClientFactory to manage the client’s lifecycle efficiently via Dependency Injection.
Common mistakes in .NET integration and how to avoid them
After building the integration, beware of three mistakes that recur often in C# projects. The first is reformatting the XML after computing the hash. Any formatting tool that changes whitespace or attribute order breaks the match between the hash and the content. Compute the hash on the final bytes you will send.
The second is mixing UTF-8 encoding with other encodings. Use Encoding.UTF8 explicitly when reading, when computing the hash, and when encoding. Relying on the system’s default encoding produces a different hash on different machines.
The third is treating error 401 as a transient error and retrying on it. Error 401 means the authentication data is wrong, and repeating the request will not fix it and may even lead to a temporary block. Handle 401 by reviewing the CSID and the secret, not by retrying.
How Qoyod helps you
Everything above explains how to build the integration yourself from scratch in C#. This is a suitable path for technical teams that want full control over the low-level layer. But building this integration and maintaining it as the authority’s requirements change consumes your team’s time.
Qoyod connects your establishment to the Fatoora platform automatically. It issues your e-invoices in compliance with the requirements of the Zakat, Tax and Customs Authority for the second phase of e-invoicing, and handles signing, clearance, and reporting without you writing a single line of code. You get the full result of this guide, ready and reliable, through a simple interface.
Compliant e-invoices without writing code
Let Qoyod handle the connection to the Fatoora platform, along with signing, clearance, and reporting automatically, and focus your technical team on your product instead of maintaining the billing integration yourself.
Related developer guides
This guide is part of a series of integration examples in the developer center. If you work in another language, each language has its own guide with the same structure. Start from the technical foundation in the Billing API authenticationguide, then get to know Fatoora platform API endpoints, and finally dive into the details of submitting the invoice via the API. These three guides together form the foundation on which you build any integration in .NET or another language.
For the full picture of e-invoicing and the authority’s requirements, refer to the e-invoicing guide from Qoyod.
Before you move this integration to production, review a short checklist. Do you read the CSID and the secret from a secure secrets vault for each environment? Do you compute the hash on the same bytes you send, without any reformatting? Do you retry only on transient errors and stop immediately at 400 and 401? Do you log every invoice’s identifier and its response code without leaking credentials? Do you reuse the HttpClient client instead of creating it for every request? Every one of these items marks a difference between an experimental integration and a resilient production one. Answer yes to all of them before you launch your system.
Finally, remember that the authority’s requirements evolve over time, and any integration you build yourself needs continuous maintenance to keep up with these changes. This operational burden is real, and it is the reason many establishments choose a solution that takes this responsibility off their shoulders. Whether you build the integration yourself or rely on Qoyod, the foundation we explained here remains your reference for understanding what happens behind the scenes in every e-invoice your establishment issues.