Qoyod
Pricing

Knowledge Base

Sample API Request (Clearance & Reporting) | Qoyod

This guide gives you copy-and-paste ready request samples for integrating e-invoicing with ZATCA’s Fatoora platform. The idea is simple: instead of reading the specification field by field, here you take a complete request with all its headers and body, replace the sample values with your own, and send it. You’ll find a complete Clearance request sample for a B2B invoice, a complete Reporting request sample for a B2C invoice, and a cURL version of each that you can copy straight into the terminal.

The samples here complement the specification, they don’t replace it. If you want an explanation of each endpoint and its limits, go back to Fatoora platform API endpoints (Endpoints). And if you want the full detail of each interface, go back to Clearance API interface andReporting API interface. The role of this guide is to put the request in your hands ready to use.

Before you start, remember three facts that govern all the following samples. The correct abbreviation for the cryptographic stamp identifier is CSID, and any other ordering of the letters is a common mistake that confuses integration teams. The value in the Clearance-Status header is what distinguishes a clearance request from a reporting request. And the request body is always JSON, even if it carries a Base64-encoded XML document inside it.

This guide is aimed at developers who connect their systems directly to the Fatoora platform. If you’re a business owner looking for compliance without writing code, the last section is for you and explains how Qoyod handles everything that follows on your behalf. But if you’re building the integration yourself, read the samples in order, because each section builds on the one before it: we start with the fixed fields, then the full clearance sample, then the reporting sample, then the details of building each part separately.

The fixed fields in every request

Every sample in this guide consists of three parts: the request line (method and path), the headers, then the JSON body. The three fields inside the body are identical across both interfaces, and they are the backbone of any invoice request:

  • invoiceHash: The invoice hash, a SHA-256 value computed over the XML version after canonicalization, then Base64-encoded. Any minor change in the content changes the hash, so compute it on the exact final signed version.
  • uuid: The invoice’s unique identifier in UUID version 4 format. It must match the identifier inside the invoice itself literally. Repeating it across different invoices causes rejection.
  • invoice: The complete invoice document in UBL 2.1 format after Base64-encoding it. This is the actual content the Authority inspects and adds its stamp to in the clearance case.

The values in the following samples are abbreviated with an ellipsis (…) because real Base64 values are very long. Replace them with your full actual values without any truncation, because any missing character breaks the hash and rejects the request.

Anatomy of a single API request
The three layers in a clearance or reporting request.
Request layers

Request line: POST + endpoint path

Headers: Authorization, Accept-Version, Content-Type and Clearance-Status

JSON body: invoiceHash, uuid and invoice (Base64)

These layers repeat in every request, with the endpoint and status changing.

The full clearance request sample (Clearance B2B)

This request is used for the standard tax invoice (B2B). The invoice is sent to the Authority before being delivered to the buyer, and waits for its stamp. Note the Clearance-Status value here equals 1, and this is what makes it a clearance request, not a reporting one. The relative path is POST /invoices/clearance/single, appended after the base URL of the environment you’re working on.

Copy the following request in full, then replace the Authorization header value and the three values in the body with your own:

POST /invoices/clearance/single HTTP/1.1
Host: gw-fatoora.zatca.gov.sa
Authorization: Basic VFNUMDExL...<Base64(CSID:secret)>
Clearance-Status: 1
Accept-Version: V2
Content-Type: application/json
Accept-Language: ar
Accept: application/json

{
  "invoiceHash": "NWZlY2ViNjZmZmM4NmYzOGQ5NTI3ODZjNmQ2OTZjNzk=",
  "uuid": "3cf5ee18-ee25-44ea-a444-2c37ba7f28be",
  "invoice": "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4..."
}

Here is a line-by-line reading of what the request carries:

  • Request line: A POST method over HTTPS to the relative path of single clearance. Don’t embed the base URL inside the code; make it a variable in the configuration that switches between the test and production environments.
  • Authorization: Basic-type authentication. The value is not a human username and password, but the cryptographic stamp identifier (CSID) as the username and its associated secret as the password, joined by a colon, Base64-encoded, and prefixed with the word Basic.
  • Clearance-Status: Its value is 1 in clearance. This value asks the Authority for full authentication of the invoice, not just inspection.
  • Content-Type: Its value is application/json. The body is always JSON even if it carries an encoded XML document inside it.
  • Accept-Language: The language you want the response messages in, such as ar or en.

The value in the invoice field above is the complete invoice document after Base64-encoding it. To build it in the correct order: build the UBL 2.1 document, add the invoice counter and the previous invoice hash, sign the document with the cryptographic stamp, compute the SHA-256 hash over the canonicalized version, then Base64-encode the final document. The result is the invoice value, and the hash is the invoiceHash value.

cURL version of the clearance request

If you want to try the request from the terminal directly before writing it in code, copy the following cURL command. Replace the Authorization header value and the three values in the body with your own, and set the base URL to your environment:

curl --location --request POST \
  'https://gw-fatoora.zatca.gov.sa/e-invoicing/core/invoices/clearance/single' \
  --header 'Authorization: Basic VFNUMDExL...<Base64(CSID:secret)>' \
  --header 'Clearance-Status: 1' \
  --header 'Accept-Version: V2' \
  --header 'Content-Type: application/json' \
  --header 'Accept-Language: ar' \
  --header 'Accept: application/json' \
  --data-raw '{
    "invoiceHash": "NWZlY2ViNjZmZmM4NmYzOGQ5NTI3ODZjNmQ2OTZjNzk=",
    "uuid": "3cf5ee18-ee25-44ea-a444-2c37ba7f28be",
    "invoice": "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4..."
  }'

Two practical notes on the cURL command: the –data-raw flag prevents cURL from interpreting any special character inside the JSON, which matters because Base64 values may contain equals signs. And using single quotes around the body preserves its structure as is, without the terminal interfering.

Start today

Don’t build the integration from scratch

manages clearance, reporting, and the connection with the Fatoora platform on your behalf. Your Authority-compliant e-invoices are issued without you writing a single API request yourself.

Start your free trial and issue your compliant invoices

Clearance request vs. reporting request
The difference between the two requests in the sample.
Criterion Clearance (B2B) Reporting (B2C)
Endpoint clearance/single reporting/single
Clearance-Status 1 0
Timing Before delivery Within 24 hours
Change the endpoint and the Clearance-Status value depending on the invoice type.

The full reporting request sample (Reporting B2C)

This request is used for the simplified tax invoice (B2C). The simplified invoice is signed locally with the cryptographic stamp identifier associated with the device and delivered to the customer immediately, then the Authority is notified of it within a twenty-four-hour window. The fundamental difference from clearance is that the Clearance-Status value here equals 0, and the relative path is POST /invoices/reporting/single.

Copy the following request in full, then replace the values with your own:

POST /invoices/reporting/single HTTP/1.1
Host: gw-fatoora.zatca.gov.sa
Authorization: Basic VFNUMDExL...<Base64(ProductionCSID:secret)>
Clearance-Status: 0
Accept-Version: V2
Content-Type: application/json
Accept-Language: ar
Accept: application/json

{
  "invoiceHash": "NmY0OWNhZGE5ZmZjODZmMzhkOTUyNzg2YzZkNjk2Yzc5",
  "uuid": "8d2e1f0a-7b4c-4f2e-9a1d-6c3b5e8f0a2d",
  "invoice": "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4..."
}

The three values in the body are identical in their structure to the clearance request. The whole difference is in the Clearance-Status header and in the path. Here is a reading of the headers:

  • Authorization: The value is the Production cryptographic stamp identifier (Production CSID) and its associated secret, joined and Base64-encoded. This is what proves to the Authority the identity of the device that is reporting.
  • Clearance-Status: Its value is 0 in the reporting path. This value is what distinguishes a reporting request from a clearance request, whose value is 1.
  • Content-Type: Its value is application/json as in clearance.

Remember that the three fields must be consistent with one another. The hash in invoiceHash must actually match the hash of the encoded document in invoice, and the identifier in uuid must match the identifier embedded inside the document itself. Any mismatch between the envelope and the document is rejected by the check.

cURL version of the reporting request

And this is the terminal version of the reporting request. Note that the only difference from the clearance command is the path and the Clearance-Status value:

curl --location --request POST \
  'https://gw-fatoora.zatca.gov.sa/e-invoicing/core/invoices/reporting/single' \
  --header 'Authorization: Basic VFNUMDExL...<Base64(ProductionCSID:secret)>' \
  --header 'Clearance-Status: 0' \
  --header 'Accept-Version: V2' \
  --header 'Content-Type: application/json' \
  --header 'Accept-Language: ar' \
  --header 'Accept: application/json' \
  --data-raw '{
    "invoiceHash": "NmY0OWNhZGE5ZmZjODZmMzhkOTUyNzg2YzZkNjk2Yzc5",
    "uuid": "8d2e1f0a-7b4c-4f2e-9a1d-6c3b5e8f0a2d",
    "invoice": "PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz4..."
  }'

Where the identifier values come from before the first request

The samples above assume you already own a valid cryptographic stamp identifier and its secret. But these values are not generated out of nothing; they come after a registration path in the Fatoora platform that passes through two consecutive stages. Understanding this path spares you the confusion of “where do I get the Authorization value from” on the first attempt.

You start by requesting an activation code (OTP) from the Authority’s portal, then generate a certificate signing request (CSR) containing your establishment’s data and its tax number. You send the signing request along with the activation code to the compliance interface, and in return you receive the Compliance cryptographic stamp identifier (Compliance CSID). This identifier opens only the test environment interfaces for you, and you use it to send sample invoices covering the required cases: a standard invoice, a simplified invoice, a credit note, and a debit note.

Upon passing these checks you request a certificate upgrade, and you receive the Production identifier (Production CSID). This identifier is the one that signs every live invoice afterward, and it is the value you place in the Authorization header in production requests. The rule here is strict: the compliance identifier is for testing only, and the production identifier is for real invoices, and it is not valid to use either one in the other’s environment.

In practice, you won’t manually deal with certificate signing requests for every branch or device. Certified systems manage this path on your behalf for each point of sale or branch, so you don’t store the secret manually nor rebuild the certificates every time. Know the path to understand where the values come from, but leave its management to the system so that secrets don’t leak or certificates expire unnoticed.

Anatomy of the encoded invoice field before you send it

The most confusing field in the samples above is the invoice field, because its visible value is just a long Base64 string that reveals nothing of its structure. But behind this string is an XML document built according to the UBL 2.1 standard, and any flaw in its construction is immediately reflected in the hash and the request is rejected. So this field deserves a pause before sending.

The document you encode must contain, in addition to the usual invoice data, four mandatory elements tied to e-invoicing: the sequential invoice counter, the previous invoice hash that links invoices in a single chain, the digital signature with the cryptographic stamp, and the QR code. The absence of any of these elements or their wrong order makes the check reject the invoice even if the commercial data looks sound.

The order in which you build these elements matters and does not accept swapping. You build the UBL 2.1 document first, including the previous invoice hash and the invoice counter, then add the digital signature and the QR code, then compute the document hash with the SHA-256 algorithm over its canonicalized version, and finally Base64-encode the complete document and place it in the invoice field. Any advancing or delaying in these steps changes the final result.

The word canonicalization here is not a minor detail. A single invoice may be written in more than one formally valid XML form that differs in whitespace or attribute order, and each form produces a different SHA-256 hash. So the invoice is canonicalized according to approved rules that give it a single standard form before computing the hash. Always compute the hash on the canonicalized signed version, not on any intermediate draft.

Consistency of the three fields inside the body

The three fields in the body are not independent, but strictly interlinked, and the check verifies this linkage before anything else. Understanding the relationship between them explains most rejection messages at the start of integration.

First, the invoiceHash value must actually be the SHA-256 hash of the same document encoded in the invoice field. If you compute the hash on one version then encode another version in the invoice field, the check detects the mismatch and rejects the request with a message that the invoice hash does not match the sent invoice. This is the most famous error of all.

Second, the uuid value in the body must match the unique identifier embedded inside the XML document itself literally. The identifier appears twice: once in the JSON request envelope, and once inside the invoice. Any difference between the two places, even in a single character, is rejected by the check. Generate the identifier once and write it in both places from the same source.

Third, the unique identifier is a value that never repeats across invoices. Repeating it across two different invoices makes the system consider the second an attempt to resend the first, so it rejects it. Generate a new UUID version 4 for each invoice, and keep it associated with the invoice in your database so you can resend the same one when needed.

How to build the base URL for each environment

The relative path is fixed and does not change between the two environments. What changes is the base URL and the cryptographic stamp identifier. Each environment has a different URL and a different identifier, so make the URL a variable in your settings and don’t hard-code it.

For example, the clearance and reporting URLs are built by combining the base URL with the relative path in this way:

# Test environment (Sandbox / Developer Portal)
https://gw-fatoora.zatca.gov.sa/e-invoicing/developer-portal/invoices/clearance/single
https://gw-fatoora.zatca.gov.sa/e-invoicing/developer-portal/invoices/reporting/single

# Production environment (Production / Core)
https://gw-fatoora.zatca.gov.sa/e-invoicing/core/invoices/clearance/single
https://gw-fatoora.zatca.gov.sa/e-invoicing/core/invoices/reporting/single

The decisive rule: real reporting and real clearance run on the production identifier at the production URL, while your system’s compliance check runs on the compliance identifier in the test environment. Don’t mix the Compliance CSID and the Production CSID, as each one works only in its own environment.

Building the encoded invoice field
Five steps to prepare the invoice before sending.
1

UBL 2.1 file

2

Counter and previous hash

3

Signature

4

SHA-256 over the canonicalized version

5

Base64 encoding

The sequence produces the invoice value ready to send.

How to build the Authorization header step by step

The most common error in the first request is building the Authorization header incorrectly. The header follows the standard Basic scheme, but the inputs differ from the familiar. You take the cryptographic stamp identifier and its secret, join them with a colon, Base64-encode the result, then prefix it with the word Basic.

In order: you have two values, the identifier and the secret. You write them in this form before encoding:

# Before encoding
TST0110...:abc123secret...

# After Base64 encoding and placing the word Basic in front of it
Authorization: Basic VFNUMDExMC4uLjphYmMxMjNzZWNyZXQ...

The colon between the identifier and the secret is part of the value before encoding; don’t forget it. And don’t use the compliance identifier in the production environment, nor the production identifier in the test environment, because each identifier works only in its own environment.

The subtle differences between the two requests in one line

If you look at the two samples side by side, you notice they are almost identical in everything, and differ in only two places. This match is intentional in the interface design, and it means that building one sound request gives you ninety percent of the other request for free.

The first difference is in the path: clearance calls /invoices/clearance/single, and reporting calls /invoices/reporting/single. The second difference is in the header: clearance sends Clearance-Status with a value of 1, and reporting sends it with a value of 0. Other than that, the headers are the same, the three body fields are the same, and the way the authentication is built is the same.

There is a third difference not visible in the request but it governs when you send it. Clearance runs before delivering the invoice to the buyer and in a synchronous real-time manner, so the invoice is not delivered until after the Authority’s stamp. Reporting, on the other hand, runs after delivering the invoice to the customer, within a twenty-four-hour window, because the simplified invoice is valid as soon as it is signed locally. This difference in timing determines your system’s architecture: do you wait for the response before printing, or print immediately and report later.

In practice, make a single function in the code that builds the request, and pass it only two variables: the path and the Clearance-Status value. This reduces duplication and prevents the error that arises when you copy the clearance function to build reporting and forget to change the value. The standard tax invoice goes to clearance, the simplified invoice goes to reporting, and the single function serves both paths.

Common errors in the first request and how to avoid them

Before you send your first real request, review this list. Most rejection cases at the start of any integration trace back to one of these causes:

  • Hash mismatch: If you compute invoiceHash on a version other than the one encoded in the invoice field, the check rejects the request with a message that the hash does not match the invoice. Compute the hash on the exact final signed version.
  • Duplicate or non-matching unique identifier: If uuid repeats across different invoices, or differs from the identifier inside the XML, the request is rejected. Generate a unique identifier for each invoice and make it match the identifier inside the document literally.
  • Wrong Clearance-Status value: Sending 0 for a B2B invoice or 1 for a B2C invoice routes the request to the wrong interface. The value is 1 for clearance and 0 for reporting; don’t mix them up.
  • Mixing environments: Using the test identifier at the production URL or vice versa fails authentication. Set the base URL and the identifier together according to the environment.
  • Misspelling CSID: The correct abbreviation is CSID (cryptographic stamp identifier). Any other ordering of the letters in the code or documentation confuses integration teams, so get it right from the start.

When resending the same invoice after a network error, resend the same request body literally. Don’t generate a new uuid nor compute a new hash, because that makes the invoice look different and breaks the counter and chain sequence.

What to expect in the response after sending

After you send one of the samples above, the response comes back in JSON format too. The response differs between clearance and reporting, but its general logic is one: an overall status for the request, then details of warnings or errors if any exist. Understanding the expected shape of the response helps you build sound handling from the first request.

In clearance, the response on success carries a field named clearedInvoice, which is the cleared invoice in Base64 format. You decode it to get the final XML signed by the Authority. This is the only official version that is delivered to the buyer and kept, and any local version before clearance is not considered an approved invoice.

In reporting, the response carries a status field named reportingStatus, whose value is REPORTED when the report is accepted. Reporting does not return an invoice signed by the Authority, because the simplified invoice was signed locally and delivered to the customer before sending in the first place. Here it is enough to update the invoice status on your side to “reported” and store its hash to be an input to the next invoice in the chain.

Responses may also come with a “cleared with warnings” status in clearance, or with explicit errors in both cases. A warning does not prevent approving the invoice but deserves review, whereas an error requires addressing the cause and resending. We will dedicate a separate guide to reading the response and interpreting its status codes, with ready-made samples.

Resend discipline and record-keeping

Synchronous integration with the Authority needs discipline in the order of operations and in keeping copies, not just in building the request. On any network error or interruption before the response arrives, the rule is to resend the same request body literally. Don’t generate a new uuid nor compute a new hash, because that makes the invoice look like a different invoice, breaking the counter sequence and the chain linked to the previous hash.

This means in practice that you store the built request body, with its identifier and hash, before sending, not after. So if the connection fails, you resend from the stored version, not from a new version. This behavior, known as the idempotency principle on retry, protects your invoice sequence from a break that is hard to fix later.

Alongside that, log for each call the invoice identifier, the response code, and the clearance or reporting status, with the codes of any warnings or errors. This log is essential for auditing and for tracing any rejection, and it shortens the problem-resolution time when reviewing an old invoice. Also keep the cleared version (clearedInvoice) in clearance for the statutory period of keeping tax records.

How Qoyod helps you

All of the above assumes you will build this integration yourself: generating the UUID, computing the hash, building the authentication header, managing two separate identifiers for two environments, and routing each invoice to its correct path. This is a large technical burden on the development team, and every step in it is prone to error.

Qoyod handles all of this on your behalf. The system is officially certified by the Zakat, Tax and Customs Authority and integrated with the Fatoora platform in its second phase. Qoyod manages the cryptographic stamp identifier for each branch or device, builds the authentication header automatically, computes the hash and generates the unique identifier for each invoice, and routes the standard tax invoice to the clearance path and the simplified invoice to the reporting path without any intervention from you.

In practice, this means you issue your invoice from Qoyod’s simple interface, and the system takes care of everything in this guide behind the scenes: building the UBL 2.1 document, signing, encoding, calling, and reading the response. You get an Authority-compliant invoice without writing a single API request.

Qoyod also keeps a complete log for each invoice: its identifier, its hash, its status, and the cleared version returned from the Authority. This spares you from building the logging and auditing layer we explained above, and guarantees the integrity of the invoice sequence and its chained hashes without any intervention from you. The system also manages the cryptographic stamp identifier for each branch or device, so you don’t store secrets manually nor worry about certificate expiry. The result is that your team turns to building its product instead of chasing the details of integrating with the Fatoora platform.

Related links in the Developer Center

This guide is part of the e-invoicing Developer Center. To complete the picture, go back to the following guides:

After you have sent the request and understood its structure, the logical next step is to read the response returned from the Authority and interpret its fields and status codes. We will dedicate a separate guide to that, with ready-to-copy response samples.

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.