Qoyod
Pricing

Knowledge Base

Rejected Invoices and Reasons for Rejection

In e-invoicing integration with the Fatoora platform, rejection is not a rare exception but a possible outcome of every clearance or reporting request. An invoice that «looks» correct in your environment may be rejected on submission because a single validation rule was not met. This guide is aimed at the developer building the submission layer: it explains what it means for an invoice to be rejected, how to tell rejection apart from acceptance with a warning, the most common reasons for rejection, how to act after a rejection, and how to prevent it in the first place through pre-submission validation.

This article is part of the Developer Hub within Qoyod E-Invoicing, specifically within the Error Handling Hub. It assumes you have read Error Handling in the API and understand the general error-response structure, in addition to Invoice Validation Rules which define when an invoice is rejected, andClearance in E-Invoicing which governs the full path of a tax invoice between the first party (B2B).

What does it mean for an invoice to be rejected?

When you send an invoice to the Fatoora platform, it passes through a validation-rule check before it is stamped. The result takes one of three states: a clean acceptance, an acceptance with a warning, or a rejection. Rejection means the invoice failed a validation rule of type error (ERROR), so it was not stamped and did not become a valid tax document.

The clearest signal of rejection in the clearance interface is the value of the field clearanceStatus which equals NOT_CLEARED. This value literally means: clearance was not performed, and the invoice is not recognized by the Zakat, Tax and Customs Authority (ZATCA). In the reporting interface it is matched by the field reportingStatus with the value NOT_REPORTED, and in both cases rejection is usually accompanied by an HTTP status code in the 422 class.

The core rule every developer must build into their mindset: a rejected invoice does not exist legally. You may not hand it to the customer, nor count it in the VAT return, nor consider the transaction complete. It is a failed draft until it is corrected and accepted again.

The most dangerous mistake here is not the rejection itself, but a rejection that passes without your system catching it. If the code handles only the successful response and ignores the value of clearanceStatus, you will think the invoice was issued while it is actually rejected, and the customer discovers the gap weeks later when reconciling their records with the Authority.

The difference between «accepted with a warning» and «rejected»

The most common misunderstanding developers fall into is confusing an invoice accepted with a warning with a rejected invoice. Both carry notes, but one is valid and the other is not. Distinguishing between them determines your system’s entire behavior.

Accepted vs. warning vs. rejected
Three invoice outcomes and what each means.
Outcome Meaning and action
CLEARED Valid and delivered, no action
CLEARED with a warning Valid with notes to be fixed later
NOT_CLEARED Rejected: correct and resubmit with a new identifier
A warning does not prevent acceptance, whereas a rejection halts delivery.

An invoice accepted with a warning has passed every error-type validation rule but triggered one or more rules of type warning (WARNING). Its clearanceStatus value is CLEARED, and it is stamped and becomes a valid document that can be delivered to the customer. A warning is a note for later review; it does not prevent issuance. It is usually accompanied by an HTTP code in the 2xx class, showing a populated warningMessages array while errorMessages stays empty.

A rejected invoice, by contrast, failed a validation rule of type error. Its clearanceStatus value is NOT_CLEAREDvalue is errorMessages , it was not stamped, and it may not be delivered. The

array carries at least the reason for rejection, and may also carry additional warnings that must not distract you from the original error. clearanceStatus The practical difference is simple in code: do not read the HTTP code alone; always read the value of

. Warnings are logged and reviewed periodically, whereas errors halt issuance and force correction and resubmission. Treating a warning as a rejection needlessly disables valid invoices, and treating a rejection as a warning leaks failed invoices to the customer. NOT_CLEARED Here is an example of a rejected-invoice response (accompanied by an HTTP code in the 422 class), where you see the errorMessages:

Copy

{
“validationResults”: {
“status”: “ERROR”,
“errorMessages”: [
{
“type”: “ERROR”,
“code”: “BR-KSA-EN16931-08”,
“category”: “BR-KSA”,
“message”: “Total tax amount does not equal the sum of line item taxes”
}
],
“warningMessages”: []
},
“clearanceStatus”: “NOT_CLEARED”
} status Notice three matching signals in this response, all saying «rejected»: the value of validationResults inside ERRORequals errorMessages , the clearanceStatus inside NOT_CLEAREDarray is not empty, and the value of

. Your logic must rely on these signals together, not on just one of them. clearanceStatus To reinforce the point, here is an accepted-with-a-warning response so you can compare it directly with the rejection response above. Notice that CLEARED here is

despite the presence of a message, because the message is of type warning, not error:

{
“validationResults”: {
“status”: “WARNING”,
“errorMessages”: [],
“warningMessages”: [
{
“type”: “WARNING”,
“code”: “BR-KSA-F-06”,
“category”: “BR-KSA-F”,
“message”: “A recommended detail field is not filled in”
}
]
},
“clearanceStatus”: “CLEARED”
} status To reinforce the point, here is an accepted-with-a-warning response so you can compare it directly with the rejection response above. Notice that WARNING The difference between this response and the rejection response is a single character in logic: the value is errorMessages and the status array is empty, so the invoice passes. There it was ERROR and the error array was populated, so it is rejected. Make your acceptance condition in code explicit: an invoice is accepted only when errorMessages is empty andclearanceStatus inside CLEARED, and anything else is treated as a rejection.

HTTP status codes accompanying rejection

Before you reach the response body, the HTTP status code gives you the general class of the result. In the rejection context specifically, distinguishing between codes determines whether or not you retry. Confusion here is costly: retrying on a data rejection repeats the failure and consumes the rate limit, while not retrying on a transient server error loses valid invoices.

Code Meaning in the rejection context Do you retry?
400 Bad Request The request envelope itself is malformed: invalid JSON or a mandatory field missing in the request, not in the invoice. No. Fix the request structure first.
401 Unauthorized Authentication failure: an expired CSID certificate or an invalid token. No, not before renewal. Renew your credentials, then send.
422 Unprocessable Entity The invoice failed a validation rule. This is the most frequent class for an actual rejection. No, not before correction. Read errorMessages and fix the data.
429 Too Many Requests You exceeded the rate limit. This is not a rejection of the invoice but a throttling of sending. Yes, later. Respect the retry header.
500 / 503 A transient server error at the platform. The invoice may be correct. Yes. Retry with exponential backoff.

The most important code on the subject of rejection is 422. It means the request arrived structurally sound but the invoice did not pass the validation rules. This is exactly where you find the value of NOT_CLEARED and the error details in errorMessages. Never retry on 422 with the same data, because the result will be an identical rejection.

Beware a common trap: do not infer acceptance from a 2xx code alone. On some paths a success code may arrive at the transport level while the invoice carries NOT_CLEARED in the response body. The final arbiter is always the response-body fields, not the HTTP code. Read them together, and weight the response body in any conflict.

The most common reasons for invoice rejection

Invoices are not rejected at random. The overwhelming majority of rejection cases trace back to a limited number of recurring causes. Knowing them in advance lets you validate for them before submission and reduce rejection to a minimum. The following table summarizes the most frequent categories along with a typical rule code and the required fix.

Reason for rejection Typical code Required fix
The total does not match the sum of the line items BR-CO-15 Recompute the invoice total inclusive of tax so it equals the sum of the net line items plus tax.
The total tax does not equal the sum of the line item taxes BR-KSA-EN16931-08 Verify the 15% calculation on each line item, then sum them with the same numeric precision.
The VAT registration number is invalid BR-KSA-39 Make sure the number is 15 digits, starts and ends with the digit 3, and follows the Authority’s format.
A break in the hash chain of the previous invoice BR-KSA-26 Send invoices in order and do not skip an invoice. Fix the PIH value of the previous invoice.
The cryptographic stamp is invalid or the CSID certificate is expired BR-KSA-60 Renew the CSID certificate and make sure the key is valid before signing.
The date format or time zone does not match BR-KSA-70 Use the approved time format in Kingdom time as specified by the Authority.
The invoice type does not match its content BR-KSA-08 Match the invoice type code (simplified B2C or tax B2B) with the correct path.

The most common of these reasons by far is a mismatch of totals. It often happens because of a difference in decimal rounding between your system and the platform’s calculation, or because of applying a discount at the invoice level without adjusting the line item totals. The safe rule is to compute tax at the level of each line item with a unified precision, then sum the same numbers you sent, not re-rounded numbers.

The second most annoying reason is a break in the hash chain. Every invoice carries the hash of the previous invoice (Previous Invoice Hash) to form a connected chain. If you send invoices out of order, resend an old invoice, or lose the hash value, the chain breaks and the invoice is rejected. The solution is organizational before it is programmatic: make sending sequential and guaranteed, and store the last hash safely.

As for authentication and CSID certificate errors, they usually appear en masse: invoices succeed and then all begin to fail at once. This is an indicator of an expired certificate, not an error in the data of a specific invoice. Monitor the certificate’s expiry date and renew it well before the deadline.

There is also a rejection category related to the cryptographic stamp and the QR code. The simplified tax invoice (B2C) carries a QR code that includes the invoice data and its signature, and any flaw in generating this code or in the cryptographic stamp results in rejection. The cause is often an outdated signing library, or a field order different from the specification, or wrong data encoding. These are structural errors caught early in the test environment before production, so do not rush the move to production before diverse test invoices pass.

Also pay attention to character-encoding errors in Arabic text fields. A seller name or line item description in an incorrect (non-UTF-8) encoding may break the signature or fail validation without a clear message. Unify the encoding across all layers of your system from the database to the request body, and test the Arabic fields specifically because they are more prone to this kind of silent failure.

The unifying rule for all these causes: a rejection is always tied to a specific rule that has a code. Do not treat rejection as a vague blob; read the code and the message, and build an internal map linking each code to a known corrective action. Over time this map turns into a valuable asset that shrinks diagnosis time from hours to minutes.

What do you do after a rejection?

Rejection is not the end of the path but its beginning. After you capture the rejection response, your system is responsible for a clear sequence of steps that ends with an accepted invoice. Order matters, and skipping any step returns you to a new rejection.

What to do after a rejection
Five steps for handling a rejected invoice.
1

Detect NOT_CLEARED

2

Read errorMessages

3

Correct the data

4

Generate new ICV and UUID

5

Resubmit and confirm CLEARED

A rejected invoice is not resubmitted as is; it is corrected with a new identifier.

Step one: capture the rejection and log it. Store the full response including the error codes, their messages, and the invoice identifier. This log is essential for diagnosis and for proving the attempt when needed. Do not settle for logging «failure»; save the actionable details.

Step two: read errorMessages and present the reason in understandable language. The accountant user does not understand the code BR-KSA-EN16931-08, but they understand «the total tax does not match the sum of the line item taxes». Translate the code into a clear message that guides them to the fix, and do not show the raw code alone.

Step three: actually correct the invoice data. Do not settle for resubmitting in the hope it will succeed, because the invoice with the same data will be rejected again. Treat the root cause: recompute the totals, or correct the tax registration number, or fix the hash sequence according to what the error tells you.

Step four, the most important and most neglected: generate a new identifier for the invoice when resubmitting. The corrected invoice is a new document from the platform’s perspective, not an edit to the old one. It must carry a new Invoice Counter Value and a new UUID, and refer to the correct hash of the previous invoice. Resubmitting the invoice with its old identifier produces a new rejection or a duplicate that breaks the sequence.

Step five: resubmit and verify the result. Do not assume success; read the value of clearanceStatus in the new response. If it becomes CLEARED the operation is complete and you store the cleared invoice and the QR code. If it remains NOT_CLEARED the error was not fully corrected; go back to reading the messages again.

The following example illustrates the central idea: the same invoice after correction is sent with new identifiers, so the platform returns the value CLEARED:

{
  "validationResults": {
    "status": "PASS",
    "errorMessages": [],
    "warningMessages": []
  },
  "clearanceStatus": "CLEARED",
  "clearedInvoice": "PHA...base64...",
  "qrCode": "AQ...base64..."
}

Note that the original rejected invoice is not deleted from your records but remains documented as a failed attempt. This preserves the audit trail and prevents you from reusing its identifier by mistake. The new accepted invoice is the only document valid for delivery and the return.

Rejection due to duplication and the importance of unique identifiers

A category of rejection that developers often overlook is duplication rejection. The Fatoora platform rejects an invoice if it carries an identifier that was previously used, because every tax document must be unique. This often happens as a result of naive retry logic: the code sends the invoice, the network drops before the reply arrives, so it resends with the same identifier, and the second is rejected as a duplicate or breaks the sequence.

The three identifiers that ensure uniqueness are the counter value (ICV) that increments with every invoice, the globally unique UUID, and the previous invoice hash (PIH) that links the invoice to its predecessor. Any duplication in the first two or flaw in the third produces a rejection. The rule: do not reuse an identifier after sending it even if the request failed, unless you are certain the platform did not receive it.

Handle network interruption carefully. When no reply reaches you, the invoice is in an undecided state, not a failed one. The correct solution is to query the invoice status by its identifier before any resubmission; if it turns out it arrived and was signed, do not resend it, and if it did not arrive, send it with the same identifier without generating a new one. Blindly generating a new identifier on every interruption breaks the sequence and produces cascading rejections that are hard to trace.

Store the state of each invoice locally with at least three values: «sending», «accepted», and «rejected». A «sending» invoice that has received no reply is handled by querying, not by resubmitting. This distinction protects you from invoice duplication and the rejection that results from it.

How do you prevent rejection before it happens?

The best way to handle rejection is to prevent it before the invoice reaches the platform. The Fatoora platform applies the same logic you can apply locally, so every validation rule it fails on you could have caught before submission. Pre-validation saves network requests, speeds up the experience, and reduces rejection to the truly rare cases.

Prevent rejection before it happens
Local checks before submission reduce rejection.
Prevention

Totals match the line items

Validity of the tax numbers

Completeness of mandatory fields

Integrity of the hash chain (ICV/PIH)

Local validation before submission prevents most rejection cases.

Start with arithmetic validation locally. Before you send, make sure the sum of the net line items plus tax equals the invoice total to the single halala. This check alone prevents the most common reasons for rejection. Unify the rounding policy across all layers of your system so the numbers do not differ between calculation and submission.

Then verify the completeness of the mandatory fields and the correctness of their formats. The 15-digit VAT registration number in the correct format, the date in the approved time zone, and the invoice type matching its content. These are simple checks but they catch a large share of structural rejection cases.

Monitor the integrity of the hash chain continuously. Keep the hash value of the last accepted invoice, link it to every new invoice before submission, and prevent any path that sends an invoice out of order. A coherent hash chain is a fundamental condition for the acceptance of every subsequent invoice.

Finally, monitor the validity of the CSID certificate well before it expires. Make the system alert when expiry approaches rather than discovering it through a wave of mass rejection. Proactive renewal turns a potential problem into a routine maintenance task.

Handling rate limits and retries

Not every unsuccessful response is a rejection of the invoice. The code 429 means you exceeded the allowed request rate limit within a time window, and the invoice itself is sound and was not rejected. The difference is fundamental: if you treated 429 as a rejection and corrected the invoice data, you would have treated the wrong symptom. The right approach is to pause sending temporarily, then retry with the same data.

Apply exponential-backoff logic with 429 and 5xx server errors. Start with a short interval, then double it with each failed attempt up to a reasonable upper bound, and add slight randomness (jitter) that prevents attempts from synchronizing. Respect the Retry-After header if present, as it is an explicit instruction from the platform about when it is allowed. Do not ignore it and retry immediately, because that prolongs the throttling period.

Explicitly separate two categories in your retry logic. The first category is retryable with the same data: 429, 5xx, and network interruption, because the invoice is correct and the problem is transient. The second is not: 400, 401, and 422, because resubmitting without change repeats the failure inevitably. Mixing the two categories is the most common mistake that turns the submission layer into a failing loop that consumes resources without result.

Set a maximum number of attempts. An invoice that fails after an agreed number of attempts on a server error should move to a manual-review queue, not stay in an endless loop. This protects your system from resource drain and ensures no invoice is silently lost.

Logging and monitoring rejection in production

The rejection-handling layer is not complete without monitoring. For every rejected invoice, log: its identifier, the error code, its message, the HTTP status code, the time of the attempt, and the number of attempts. This log turns rejection from a vague event into analyzable data that reveals recurring patterns.

Monitor the rejection rate as a health indicator. A sudden rise is often not a coincidence but a symptom of a single flaw: a CSID certificate near expiry, a change in totals calculation after an update, or a break in the hash sequence. A high rate calls for immediate diagnosis before stuck invoices accumulate.

Aggregate the reasons for rejection by rule code. If a single code tops the list, it points you to a single fix that resolves a large share of the cases at once. Aggregated analysis is far more effective than handling each invoice individually, because it attacks the root cause, not the symptoms.

Finally, make stuck invoices visible. An invoice that was rejected and not yet corrected must appear in a clear queue with its reason, not be buried in a log that is never read. The goal is to measure the time between rejection and correction, and to leave no rejected invoice without follow-up, because every stuck invoice is a potential gap in the customer’s return with the Authority.

Start today

Let Qoyod handle validation, clearance, and rejection

Qoyod validates the invoice before sending it to the Fatoora platform, manages the CSID certificate, signing, and instant clearance automatically, and displays the reasons for rejection in clear language without you building the validation layer yourself.

Start your free trial and issue your invoices without rejection

A practical summary for the developer

Rejection is a natural part of e-invoicing integration, not an emergency. A mature system assumes the possibility of rejection in every request, always reads the value of clearanceStatus and distinguishes acceptance with a warning from actual rejection. This way you avoid the worst scenario: a rejected invoice that passes as if it had succeeded.

The difference between a fragile integration and a resilient one does not show in the ideal path, but in how the system behaves at the first invoice rejected in production. A fragile system stops or leaks a failed invoice, while a resilient system captures the rejection, translates it for the user, corrects it, resubmits with new identifiers, and confirms acceptance. This full cycle is what separates a customer’s sense of confidence from a sense that the system fails them under pressure.

Remember the four rules: do not trust the HTTP code alone, read errorMessages and present it in the user’s language, generate new identifiers when resubmitting after correction, and validate locally before submission to prevent rejection at its root. Applying these rules turns the submission layer from a point of fragility into a component that holds up in production. For more details on the general response structure, see Error Handling in the API, and to understand the rules that decide acceptance from rejection see Invoice Validation Rules.

Guides

Continue your learning journey

Explore the rest of Qoyod’s guides, or start applying what you’ve learned.

Live webinars hosted by the Qoyod team to help you use the software easily and answer your questions.

Discover Qoyod’s latest updates, ongoing improvements, and new features in one place.

Our team is ready to help you and provide instant support for any issue you face, around the clock.