When building an integration with the Fatoora platform to issue and clear electronic invoices, it is not enough for requests to merely “succeed” under ideal conditions. A real system deals with rejected invoices, warnings, dropped connections, and expired certificates. This guide explains how to handle API errors coming from the Fatoora platform methodically: from HTTP status codes, to the structure of error and warning responses, to distinguishing accepted-with-warning invoices from rejected ones, all the way to how you log an error, present it to the user, and fix it. The goal is for the developer to come away with a clear error-handling model that holds up in production.
This article is part of the Developer Hub within Qoyod Electronic Invoicing. It assumes you have read Fatoora platform API endpoints and know the difference between the Clearance API and the Reporting API, in addition to invoice validation rules which determine when an invoice is rejected in the first place.
Why you need a complete error-handling strategy
In an e-invoicing integration, an error is not a rare exception but an expected event you must plan for. An invoice may be rejected because the VAT number is invalid, or because the sum of line items does not equal the total, or because the chained hash sequence is broken. Each of these cases needs a different response.
A developer who handles only the successful response builds a fragile system. The first invoice rejected in production halts the entire issuance pipeline, or worse: the rejected invoice passes as if it had succeeded, and the customer discovers the problem days later when reviewing their records with the Zakat, Tax and Customs Authority (ZATCA). Good error handling prevents both of these scenarios.
A mature strategy begins with three questions for every response arriving from the Fatoora platform. First: what is the HTTP status code? This determines the general category of the outcome. Second: what is the invoice status inside the response body (the status field)? This distinguishes acceptance from acceptance-with-warning from rejection. Third: what are the details of the validation results (validationResults)? These tell you exactly what needs to be fixed.
HTTP status codes from the Fatoora platform
The first classification layer is the HTTP status code. Each code has a meaning that determines how your system behaves. Do not handle every code the same way: retrying on an invoice-data error wastes resources, while not retrying on a transient server error loses valid invoices.
The following table summarizes the codes you will actually see when integrating with the Clearance API and the Reporting API:
| Code | Meaning | Required action |
|---|---|---|
| 200 OK | The invoice was accepted and signed by the Authority. The Clearance API returns the cleared invoice. | Store the cleared invoice and the QR code. Complete the process. |
| 202 Accepted | The invoice was cleared with warnings. The invoice is valid but contains notes. | Store the invoice, log the warnings, and notify the responsible team to review them later. |
| 400 Bad Request | The request itself is malformed: invalid JSON, or a mandatory field missing from the request envelope. | Do not retry with the same data. Fix the request structure, then resend. |
| 401 Unauthorized | Authentication credentials are invalid or expired. The CSID certificate is invalid or the token has expired. | Renew the credentials. Verify the CSID certificate is valid before resending. |
| 422 Unprocessable Entity | The request is structurally correct but the invoice failed validation rules. This is the case you will see most. | Read validationResults, display the errors, fix the invoice data. Do not retry before fixing. |
| 429 Too Many Requests | You exceeded the allowed request rate within a time window. | Pause sending temporarily and respect the retry header. Review the rate-limits section later. |
| 500 / 502 / 503 | An error in the platform server or its temporary unavailability. Not a problem with your invoice. | Retry using exponential backoff logic. The invoice is valid; the error is transient. |
The core rule in classification: 4xx-class errors are your responsibility (data, authentication, or rate limit), and 5xx-class errors are the platform’s responsibility (transient server). The two classes require completely different logic. 4xx errors (except 429) are not resent with the same data because they will fail again, whereas 5xx and 429 errors are candidates for retry because the data is valid and the problem is temporary.
Note the practical difference between the 200 and 202 codes. Both mean the invoice is valid and cleared, but 202 carries an implicit signal that there is a note worth reviewing. Many developers treat every code in the 2xx range as a complete success and ignore warnings, so notes that no one reads pile up. It is better to distinguish the two codes in your logic: 200 passes silently, and 202 passes but its warning is logged and displayed in a periodic review dashboard.
Likewise, do not conflate 400 and 422 even though both belong to the four-hundred class. The 400 code means the request itself is structurally broken: invalid JSON text, a missing header, or an absent request-envelope field, meaning the platform could not even read the invoice. The 422 code means the platform read and understood the invoice but it failed a business rule. Distinguishing them guides the fix: 400 is fixed in the request-building layer, and 422 is fixed in the invoice data itself.
Illustration: HTTP status code decision tree
| Code | Action |
|---|---|
| 2xx | Success: continue (a warning may exist) |
| 4xx | Request error: correct or renew authentication |
| 5xx | Server error: retry later |
Structure of error and warning responses
The HTTP code tells you the general category, but the precise details come in the response body. The Fatoora platform follows a unified structure for validation results, and knowing this structure is the key to handling errors accurately. Every response carries a status field at the top level, and inside validationResults are two separate lists: errorMessages for errors that prevent acceptance, and warningMessages for warnings that do not.
Here is an example of a response for an invoice rejected due to validation failure (accompanied by HTTP code 422):
Note the essential elements. The status field at the validationResults level takes the value ERROR here because there is at least one error. The errorMessages list contains an object for each error, and each object carries four important fields: type (the message type), code (the code of the violated rule), category (the rule category), and message (the human-readable description). The clearanceStatus field confirms the invoice was not cleared.
The code field is the most important programmatically. Every validation rule has a unique, stable code that does not change, such as BR-KSA-EN16931-08 or BR-CO-15. Rely on this code in your logic, not on the message text, because the text may be reworded between platform versions while the code stays fixed. Build a map in your system that links each code to a specific fix action and a clear user message.
The essential fields in the error object
Each object in the errorMessages or warningMessages list has the following fields:
- type: The message type; takes the value ERROR or WARNING. It determines whether the message blocks acceptance or not.
- code: The unique code of the violated rule. Stable across versions, and you build your logic on it.
- category: The rule category, such as BR-KSA for Saudi-specific rules, or EN16931 for the European standard adopted by the Authority.
- message: A human-readable text description. Useful for displaying to the user but not a stable programmatic reference.
Illustration: anatomy of the error object in validationResults
type: message type (error/warning)
code: error code (the logic key)
category: error category
message: human-readable description
Distinguishing warnings from rejection
The point developers new to the Fatoora platform get wrong most is confusing “accepted with warning” with “rejected.” The difference is fundamental and has a direct impact on what you must do with the invoice.
An invoice accepted with a warning is legally valid and cleared by the Authority. It arrives with HTTP code 202, its status field is WARNING and not ERROR, and the errorMessages list is empty while warningMessages contains one note or more. This invoice is delivered to the buyer and stored as cleared. The warning is merely an alert for later review; it blocks nothing.
A rejected invoice, however, arrives with HTTP code 422, its status field is ERROR, and the errorMessages list contains at least one error. This invoice was not cleared, must not be delivered to the buyer, and its data must be fixed and resent.
Here is an example of an accepted-with-warning response:
Note the three differences from the previous example: status here is WARNING not ERROR, errorMessages is empty, and clearanceStatus equals CLEARED. This invoice succeeded despite the presence of a note. The practical rule: rely on the status field and the errorMessages list together to make the decision. If errorMessages is non-empty, the invoice is rejected regardless of anything else. If it is empty and warningMessages contains items, the invoice is accepted with a warning.
| Criterion | Accepted | Accepted with warning | Rejected |
|---|---|---|---|
| HTTP code | 200 | 202 | 422 |
| status | PASS | WARNING | ERROR |
| errorMessages | Empty | Empty | Contains items |
| warningMessages | Empty | Contains items | May contain |
| clearanceStatus | CLEARED | CLEARED | NOT_CLEARED |
| Delivered to the buyer? | Yes | Yes | No |
Illustration: visual comparison between acceptance, acceptance-with-warning, and rejection
| Criterion | Accepted (2xx) | Rejected (422) |
|---|---|---|
| State | Approved/reported | Rejected |
| The warning | May exist for later correction | Errors that halt acceptance |
| Delivery | Delivered/reported | Not delivered until corrected |
Common error categories
Most rejections recur within a limited number of categories. Knowing them in advance shortens diagnosis time and helps you build ready-made fix messages for each category. Below are the most common categories when integrating with the Fatoora platform.
Arithmetic errors in amounts. These are the most common category. They occur when the sum of line items does not match the total, or when the declared tax amount does not equal the sum of the actual line-item taxes. The cause is usually incorrect decimal rounding. Fix them by computing totals from the line items themselves with a unified decimal precision instead of entering them manually.
Tax registration number errors. These occur when the seller’s or buyer’s VAT number is invalid or does not match the required format (fifteen digits beginning and ending with three). Verify the number before sending with a local validation rule to save a full rejection cycle.
Hash chain errors. The Fatoora platform links each invoice to the hash of the previous invoice to ensure sequence integrity. If invoices are sent out of order, or a previous hash is lost, the sequence is rejected. Fix them by ensuring sequential sending and not skipping any invoice in the chain.
Certificate and signature errors. These occur when the CSID certificate is expired or invalid, or when the digital signature fails. This category is often accompanied by HTTP codes of type 401. The solution is to renew the CSID certificate and ensure the integrity of the signing key.
Missing mandatory field errors. These occur when a field required by the validation rules is absent, such as the issue date, the invoice type, or the unique invoice identifier (UUID). Review invoice validation rules for the complete list of mandatory fields and their conditions.
Date and time format errors. These occur when the invoice timestamp format does not match the required format, or when the issue date is in the future, or when the supply date precedes the issue date illogically. This category appears frequently when dealing with different time zones. Unify all timestamps to a single time standard before building.
Exemption classification and tax rate errors. These occur when a line item is declared exempt or zero-rated without stating the required exemption reason, or when the applied rate differs from the declared rate for the tax category. Fix them by linking each line item to its correct tax category and exemption reason when required.
The practical benefit of retaining these categories is that they let you build a ready-made fix map. Each rule code links to a category, and each category to a clear user message and a specific fix step. This turns handling a rejection from a manual diagnosis every time into a near-automatic response that shows the user exactly what to do. And as your logs accumulate, you discover which categories recur most for you, so you address their root cause in the build layer instead of repeating the manual fix.
Let Qoyod handle signing, clearance, and error handling
Qoyod manages the CSID certificate, signing, and instant clearance with the Fatoora platform automatically, and displays validation errors in clear language without you building the error-handling layer yourself.
Start your free trial and issue your compliant invoices with confidence
How to log an error and present it to the user
Detecting the error is half the job. The other half is logging it in a way that allows for later diagnosis, and presenting it to the user in language they understand. Good logging is the difference between a maintainable integration and one that turns into a closed black box.
In the logging layer, store for each rejected invoice the complete diagnostic bundle: the invoice identifier, the HTTP status code, the full list of error codes (code), a timestamp, and the request identifier if present. Do not log only the message text, because the code is what you search for when aggregating recurring errors and analyzing their patterns.
In the user-presentation layer, do not show the raw error code such as BR-KSA-EN16931-08 to the end user. Translate each code into a clear message that tells the user what happened and how to fix it. For example, instead of showing the code, show: “The total tax in the invoice does not match the sum of the line-item taxes. Review the line-item amounts.” This message is actionable, and the raw code is not.
Distinguishing the logging and presentation layers matters. The technical log for the developer contains the codes and full details for diagnosis, whereas the user interface displays a simplified, actionable message. Mixing the two layers confuses the user with technical details that do not concern them, or deprives the developer of the details they need.
Be careful not to log sensitive data in the logs. The invoice body may contain commercial information and tax registration numbers, so do not print the full payload in shared logs or external monitoring services. Suffice with the diagnostic bundle: the identifiers, codes, and timestamps. This balances diagnosability with protecting the customer’s data.
Add to that a severity level for each log entry. A rejection due to erroneous data is a warning level that needs intervention, whereas repeated five-hundred-class server errors across many invoices is an urgent alert level that calls for reviewing the platform’s status. Classifying logs by severity makes the monitoring dashboard readable instead of drowning in equally weighted messages.
An example of a useful diagnostic log structure:
This structure answers every diagnostic question: which invoice, which HTTP code, which rules failed, when, and what action was taken. When aggregating thousands of logs, you become able to compute the most frequently recurring error codes and address their root cause instead of treating the symptoms invoice by invoice.
How to fix the error and resend
After logging and presenting comes the fixing step. Here the difference between the HTTP classes appears clearly, because each class has a different fix path.
Invoice data errors (422) require correcting the data before any resend. There is no point in retrying with the same invoice because it will be rejected the same way. Correct the amounts, the tax registration number, or the missing field, then send the invoice as a new request. Remember that the invoice hash will change after correction, so treat it as a new invoice in the chain.
Authentication errors (401) require renewing the credentials. Verify the validity of the CSID certificate, renew it if expired, then resend. These errors have nothing to do with the invoice data; the invoice itself is valid.
Transient server errors (5xx) and rate-limit errors (429) require an organized retry with increasing intervals, because the invoice is valid and the problem is transient. But retrying has precise rules concerning the intervals, the number of attempts, and avoiding duplicate sending, and this is a topic in its own right.
To avoid rejection in the first place, apply a local validation layer before sending that checks the totals, the registration numbers, and the mandatory fields. This layer catches the bulk of 422 errors before they cost you a full network round trip. Consider it the first line of defense, and handling the platform’s response the second line of defense.
Finally, make the fix path traceable for the user themselves. When an invoice is rejected, do not leave it in an ambiguous state; instead, display its status clearly: rejected awaiting fix, being resent, or cleared after correction. A user who sees the full journey of their invoice trusts the system and acts quickly. An invoice that silently disappears after rejection turns into a problem that surfaces late during tax reconciliation, which is the hardest time to discover an error.
Be alert to a subtle point when resending: do not send the rejected invoice twice without change. Each send consumes from the rate limit, and may confuse your logs if you do not link the new attempt to the original. Give each invoice a stable internal identifier by which you track all its attempts, so that you retain a clear thread linking the original invoice to the successful fix attempt.
Also monitor the intermediate state in which you await a response that has not arrived. If the connection drops after sending the invoice and before the reply arrives, the invoice may have actually been processed by the platform without you knowing its outcome. In this case, do not blindly resend; instead, query the invoice status first by its unique identifier if the platform allows that, to avoid duplicate issuance. These edge cases are what separate a mature integration from one that works only in the lab.
What Qoyod handles on your behalf
Building a complete error-handling layer from scratch is a large undertaking: classifying HTTP codes, interpreting validationResults, managing the CSID certificate, translating codes into clear messages, and building a local validation layer. Qoyod handles this entire layer on the user’s behalf.
Qoyod manages the CSID certificate and the digital signature automatically, sends every B2B invoice for instant clearance with the Fatoora platform, and reports simplified B2C invoices within twenty-four hours. On validation failure, Qoyod displays the error in clear language that tells the user what to fix without having to understand the raw rule codes.
This does not mean the developer does not need to understand error handling. Whoever builds a direct integration with the platform, or connects an external system via Qoyod’s API, benefits from understanding this structure to interpret the responses they receive. But the end user who issues their invoices through Qoyod gets this layer ready-made without writing a single line of code.
It is worth recalling that Qoyod does not register the customer with the Authority on their behalf. Registering the CSID certificate with the Authority is a step the customer performs themselves, and Qoyod guides them through it. Likewise, Qoyod does not file the VAT return on the customer’s behalf; rather, it generates the return data for the customer to file through the Authority’s portal.
Next steps in the Developer Hub
After mastering error handling, the two directly complementary topics are retry logic and rate limits. Retry Logic explains how to resend valid invoices on transient server errors with increasing intervals without overwhelming the platform or duplicating the send. Rate Limits explains the number of requests allowed in the time window and how to respect the retry headers when reaching code 429. We will link these two guides here as soon as they are published.
For review, make sure you are familiar with the integration foundation via Fatoora platform API endpoints, and with the invoice acceptance conditions via invoice validation rules. These two guides together with this article form the solid foundation for any mature integration with the Fatoora platform.
Practical summary
Error handling in a Fatoora platform integration rests on three decision layers. The first layer is the HTTP status code, which determines the general category and its responsibility: 4xx is your responsibility, and 5xx is the platform’s. The second layer is the status field and the errorMessages and warningMessages lists, which distinguish acceptance from acceptance-with-warning from rejection. The third layer is the stable rule code (code), on which you build your fix logic and user messages.
A mature integration logs the codes, not the texts, translates them into actionable messages, distinguishes legitimate retry (5xx and 429) from mandatory fixing (422 and 401), and builds a local validation layer that catches errors before sending. Whoever prefers not to build this layer themselves gets it ready-made via Qoyod.