← Blog Guides 6 min read · 3 October 2026

ZATCA Python SDK: open-source Phase 2 e-invoicing in three calls

A dark cover card with a terminal icon and the words: ZATCA e-invoicing in Python, open source, three calls
onboard() once, create_invoice() signs on your machine, submit() sends it to ZATCA and returns the answer.

A developer at a Saudi software house, or a freelancer whose client asked to "add ZATCA Phase 2", usually has two options. Build the integration from scratch: the certificate, XML signing, the hash chain, the QR code and the conversation with the ZATCA API. Or send invoices to an intermediary service that does it for them. ZATCA Tools SDK is a third option: an open-source Python library under the MIT licence that runs inside your application and talks to ZATCA directly, with no account with us and no server of ours in the middle. Your data goes from your server to ZATCA and never passes through us.

Install, and a first invoice in a minute

Python 3.10 or later, and one package for invoices, ZATCA and printed PDFs:

pip install zatca-tools-sdk

Then this example as it stands, against the public ZATCA sandbox, with nothing to set up:

from zatca_tools import Zatca

zatca = Zatca("sandbox")
zatca.onboard()  # the sandbox's test credentials, straight from ZATCA

invoice = zatca.create_invoice({
    "number": "INV-1001",
    "type": "simplified",
    "items": [
        {"name": "Product", "quantity": 2, "unit_price": 100},
    ],
})

result = zatca.submit(invoice)
if result.success:
    print("ZATCA says:", result.status)  # REPORTED
    invoice.save_xml("INV-1001.xml")
    invoice.save_pdf("INV-1001.pdf")
else:
    print(result.error.message)
    print(result.error.help_url)

The output is one line: ZATCA says: REPORTED. Three calls:

  • onboard() connects your system to ZATCA once. In the sandbox it fetches the shared test credentials in a few seconds and asks you for nothing.
  • create_invoice() checks the invoice, computes VAT and totals and signs it on your machine. Nothing is sent yet.
  • submit() sends it to ZATCA and returns the answer.

ZATCA's answer, with a link that explains the error

The hard part of an integration is not sending, it is understanding a rejection. The result answers three questions with three fields. Did it work? result.success. If not, why? result.error.message. How do I fix it? result.error.help_url, a link to the explanation: the code's guide in the ZATCA error code reference when it has one (the Authority's official message, the causes and the fix), its entry in the reference otherwise. Warnings returned alongside acceptance are in result.warnings, each with its own link.

success is true only when ZATCA actually reported or cleared the invoice. Everything else is named by status:

  • REPORTED or CLEARED: ZATCA accepted it. Store it and read the warnings.
  • NOT_REPORTED or NOT_CLEARED: ZATCA rejected it. Fix what the errors say and issue a corrected invoice: a new one, since the rejected one already took its place in the chain.
  • NOT_SENT: no connection, and ZATCA never saw it. Send the same invoice again.
  • UNKNOWN: sent, but no answer came back. Send the same invoice again, never a new one for the same sale.
  • FAILED: ZATCA refused the request, not the invoice. Fix the cause in error and send it again.

From the sandbox to real invoices

The sandbox certificate is shared by everyone, and nothing issued with it is a tax invoice. To send your own invoices, your system (ZATCA calls it an EGS unit: a server, a till, an app) needs its own certificate, in one call. You need three things:

  1. Your company details as registered with ZATCA: VAT number, legal name, CR number and address.
  2. An OTP from the Fatoora portal for each system you connect. It works once and expires after sixty minutes, and no library or service can generate it for you: only the taxpayer can.
  3. A safe place for a secret (a secret manager or encrypted storage), and two values in your database.

Then Zatca("production", seller=...) and onboard(otp="..."). The SDK creates the private key on your machine, sends ZATCA the certificate request, signs and sends the sample invoices ZATCA requires, and obtains your production certificate. The private key stays in the credentials and is never sent anywhere. Between the sandbox and production there is the simulation environment, with your own identity and no tax invoices. The code is the same in all three; only the first argument changes.

What stays your responsibility

The SDK runs inside your application, so part of the job is yours, and we say so plainly:

  • The credentials: keep them as a secret and pass them back on every start. Lose the private key and the system has to be onboarded again.
  • The invoice chain: ZATCA links each invoice to the one before it with a counter and a hash. The SDK moves the chain forward itself; you save it in your database after each invoice and pass it back when your application restarts.
  • One client per system: two processes sharing one system's chain would break it.
  • The invoice type: a simplified invoice (B2C) is reported, handed to the customer at once and reported within 24 hours. A standard invoice (B2B) is cleared before it reaches the buyer, and the cleared copy is the one the buyer gets. The difference is explained here.

The printed invoice

Always keep the XML: it is the e-invoice itself. Alongside it, the SDK prints a PDF/A-3 file with the XML embedded inside, so the copy a person reads and the copy a system reads travel as one file, in Arabic and English, with your logo and colour. The printed copy of a standard invoice carries ZATCA's cleared version.

How we know it works

Measured, not promised:

  • It runs end to end against the ZATCA sandbox: a simplified invoice reported, a standard invoice cleared, credit and debit notes, certificate renewal.
  • The arithmetic and the XML are the same as the ZATCA Tools platform that signs real invoices in production.
  • Every PDF in our test runs passed veraPDF as PDF/A-3b, and its printed QR code scans back to the signed one.
  • More than 600 automated tests pass on Linux and Windows, for Python 3.10 to 3.13.

The SDK or the platform?

The SDK is for teams that want the integration inside their own system and are ready to run it: secrets, the chain, retries. Teams that would rather run none of that can use the hosted ZATCA Tools API instead: one JSON request, and we keep the certificate and the chain and talk to ZATCA. The full SDK documentation is at SDK docs: connecting to ZATCA, then every field, notes and VAT treatments. The source is on GitHub and the package on PyPI.

The SDK is free. If you choose the hosted platform instead: one week free, no payment, from the day you connect, then very competitive plans: 49 SAR a month for Growth and 149 for Business, on a smooth, fast system that signs each invoice and sends it to ZATCA in seconds. Start here.

Frequently asked questions

Is there a Python library for ZATCA e-invoicing? +
Yes. ZATCA Tools SDK is an open-source Python library under the MIT licence. Install it with pip install zatca-tools-sdk on Python 3.10 or later. It creates and signs Phase 2 invoices on your machine and sends them to the Fatoora platform directly, with no account with us and no server of ours in between.
Is the SDK free? +
Yes. It is free and open source under MIT, and its source is public on GitHub. What we charge for is something else: the hosted platform, for teams that would rather not run their own integration.
Can I try it without an OTP? +
Yes, against the public ZATCA sandbox. Zatca("sandbox") followed by onboard() with no arguments fetches the shared test credentials from ZATCA in a few seconds, and invoices go out under the sandbox test VAT number. None of them is a real tax invoice.
What do I need to send real invoices? +
Your VAT number, registered name, CR number and address, and one OTP from the Fatoora portal per system you connect, single use and valid for sixty minutes. Then onboard(otp=...) once in production, keep the credentials as a secret, and keep the invoice chain in your database.
Ready to connect your business?

Connecting is free and takes under five minutes.

Start for free