Developers

Integrate Clix with your systems

Create and read ZATCA-compliant invoices from your ERP, POS or accounting software with an API key. You add clients, items and your ZATCA device once in the Clix app; the API reads them, it does not create them.

Open the API reference

Get access

Get a key in four steps

01

A plan with API access

API access is included in the Business plan. Free and Pro organisations see an upgrade prompt instead of the key form.

02

An Admin of the organisation

Only Admins can issue, list and revoke keys. Ask your organisation Admin, or have them issue the key for you.

03

Create the key

In the Clix app open Settings → API Keys → Create key. Name it after the system that will use it and pick an access level.

04

Copy it once

The full key is shown a single time. We store only a hash and cannot show it again — keep it in your secrets manager, never in code.

Clix is listed in ZATCA’s Solution Providers Directory as a Phase 2 (Integration) qualified provider — the same clearance and reporting flow your integration uses.

Authenticate

Send the key as a bearer token on every request: Authorization: Bearer clix_…. No OAuth flow and no organisation header — a key is bound to the organisation that issued it.

Your first call

List one invoice to confirm the key works:

curl -H 'Authorization: Bearer clix_…' https://<server>/api/invoices/?page_size=1

Replace <server> with the base URL under Servers at the top of the API reference. It already includes the version, e.g. …/v1.

Errors

Every error is JSON with a message; validation errors add details naming the field.

  • 400 — the invoice failed a business rule (a date, a code, a missing item), or an invoice with the same data was created in the last five minutes.
  • 401 — the key is missing, wrong or revoked.
  • 403 — the key’s access level does not allow this endpoint, or the plan has no API access.
  • 404 — an unknown invoice, client or item uuid, or a path outside the Integration API.
  • 422 — the body does not match the schema: a wrong type or a missing required field, listed per field.
  • 429 — too many requests; wait the number of seconds in the Retry-After header, then retry.

Access levels

  • ViewInvoice — read invoices, clients and items. Cannot create anything.
  • CreateInvoice — create and read invoices; the usual choice for ERP and POS integrations.
  • Accountant — invoices plus credit and debit notes, payments and VAT reports.

Limits

API keys come with Business. Requests are limited per minute; over the limit the API answers 429 with Retry-After.

Revoking a key

Settings → API Keys → Revoke. Calls with that key stop within a minute. Revoke immediately if a key ever lands in a log, a ticket or a repository, and issue a new one.

Walkthrough

Create your first invoice

  1. 01

    Prepare once, in Clix

    Add your clients and items and onboard at least one ZATCA device in the Clix app. The API reads them; it does not create them.

  2. 02

    Read your device and seller ids

    GET /api/devices/ and take device_id and seller_id from the device you sign with. Both go on every invoice.

  3. 03

    Find the client and the item

    GET /api/buyers/ and GET /api/items/ return your catalogue with a uuid per row. Match by name, VAT number or SKU and keep the uuids.

  4. 04

    Load the reference codes

    GET /api/invoices/vat-categories/ and /payment-means-types/ list the codes ZATCA accepts. Cache them; they rarely change.

  5. 05

    Preview

    POST /api/invoices/preview/ with the payload below. 201 returns the computed totals; 400 returns the validation errors. Nothing is stored.

  6. 06

    Create

    POST /api/invoices/ with the same payload. 202 Accepted returns a location; the invoice is signed and sent to ZATCA in the background. An invoice with the same data within five minutes is refused with 400. If your request times out before the 202, do not resend blindly: list recent invoices with GET /api/invoices/ and look for your note first.

  7. 07

    Poll the location

    GET the location every couple of seconds until zatca_response_status is CLEARED, REPORTED or REJECTED — usually within seconds. The PDF is stored a moment after clearance: wait until invoice_pdf_url is filled too. ref_num and qr_code arrive with the status.

  8. 08

    Fetch the PDF

    GET /api/invoices/{uuid}/pdf-a3/ returns the PDF/A-3 with the embedded XML, ready to send to your customer. It answers 404 until invoice_pdf_url is filled, and a REJECTED invoice has no PDF.

The invoice payload

One body serves both preview and create. type_code 388 is a standard tax invoice, transaction_code 0100000 a standard B2B sale, and payment_means_type_code 10 is cash; the other codes come from step 4. Lines reference your catalogue items by uuid and Clix prices them from the catalogue.

The empty arrays are part of the contract — send them as shown. They carry allowances, prepayments and references to earlier invoices when you need them.

JSON
{
  "device": "<device_id from GET /api/devices/>",
  "seller": "<seller_id from the same device>",
  "buyer": "<buyer uuid from GET /api/buyers/>",
  "issue_date": "2026-09-10",
  "issue_time": "12:00:00",
  "type_code": 388,
  "transaction_code": "0100000",
  "currency": "SAR",
  "supply_date": "2026-09-10",
  "supply_end_date": "2026-09-10",
  "payment_means_type_code": "10",
  "notes": [{ "language_id": "en", "note": "Order 1042" }],
  "lines": [{ "item_id": "<item uuid from GET /api/items/>", "quantity": 2 }],
  "form_lines": [],
  "prepaid_invoices": [],
  "document_level_allowances": [],
  "original_invoice_reference": [],
  "exchange_rate": 1,
  "add_prepaid_amount": false
}

Placeholders in angle brackets come from steps 2 and 3; the dates are examples.

Code samples

The whole sequence in code

Steps 2 to 8, end to end: look up the ids, preview, create, wait for ZATCA, download the PDF. Python needs the requests package; the Node.js sample runs on Node 18 or newer with no dependencies. Both read the base URL and the key from environment variables.

# pip install requests
import os
import time

import requests

BASE = os.environ["CLIX_BASE_URL"]  # the "Servers" URL at the top of the API reference, ends in /v1
api = requests.Session()
api.headers["Authorization"] = f"Bearer {os.environ['CLIX_API_KEY']}"

# 1. The device you sign with, and its seller id
device = api.get(f"{BASE}/api/devices/", params={"page_size": 1}).json()["devices"][0]

# 2. The client and the item, prepared once in the Clix app
buyers = api.get(f"{BASE}/api/buyers/", params={"page_size": 100}).json()["buyers"]
items = api.get(f"{BASE}/api/items/", params={"page_size": 100}).json()["items"]
buyer = next(b for b in buyers if b["buyer_name"] == "Acme Trading")
item = next(i for i in items if i["item_name"] == "Consulting hour")

today = time.strftime("%Y-%m-%d")
invoice = {
    "device": device["device_id"],
    "seller": device["seller_id"],
    "buyer": buyer["uuid"],
    "issue_date": today,
    "issue_time": time.strftime("%H:%M:%S"),
    "type_code": 388,  # standard tax invoice
    "transaction_code": "0100000",  # standard B2B
    "currency": "SAR",
    "supply_date": today,
    "supply_end_date": today,
    "notes": [{"language_id": "en", "note": "Order 1042"}],
    "lines": [{"item_id": item["uuid"], "quantity": 2}],
    "form_lines": [],
    "prepaid_invoices": [],
    "document_level_allowances": [],
    "exchange_rate": 1,
    "original_invoice_reference": [],
    "payment_means_type_code": "10",  # cash
    "add_prepaid_amount": False,
}

# 3. Preview: totals and validation errors, nothing stored
preview = api.post(f"{BASE}/api/invoices/preview/", json=invoice)
preview.raise_for_status()
print("payable:", preview.json()["invoice"]["invoice_payable_amount"])

# 4. Create: 202 Accepted - signed and sent to ZATCA in the background.
#    A 400 here means an invoice with the same data was created in the last
#    five minutes; do not retry blindly.
created = api.post(f"{BASE}/api/invoices/", json=invoice)
created.raise_for_status()
location = created.json()["location"]  # "/api/invoices/<uuid>/"

# 5. Poll until ZATCA has answered AND the PDF is stored (usually a few
#    seconds). The status turns CLEARED first; invoice_pdf_url follows shortly
#    after, and /pdf-a3/ answers 404 until it does.
FINAL = ("CLEARED", "REPORTED", "REJECTED")
for _ in range(30):
    result = api.get(f"{BASE}{location}").json()
    status = result.get("zatca_response_status")
    if status == "REJECTED" or (status in FINAL and result.get("invoice_pdf_url")):
        break
    time.sleep(2)
else:
    # Still pending after a minute: the invoice exists, keep polling the location later.
    raise SystemExit(f"ZATCA has not answered yet for {location}")
print(status, result["ref_num"])
if status == "REJECTED":
    # Review why in the Clix app (Invoices -> Rejected), fix the data, and create a new invoice.
    raise SystemExit(f"rejected: {result.get('zatca_response_code')}")

# 6. The PDF/A-3 with the embedded XML, for your customer
pdf = api.get(f"{BASE}{location}pdf-a3/")
pdf.raise_for_status()
with open(f"{result['ref_num']}.pdf", "wb") as f:
    f.write(pdf.content)

Replace the client and item names with your own. The samples are not translated: the API, its field names and the comments are in English.

Engineering

How we build Clix

What stands behind the Clix API and invoicing engine.

Layered design

Requests flow one way: API, services, domain, storage. ZATCA rules live in the domain layer.

Predictable API

Resources with standard HTTP methods and status codes, an OpenAPI reference, 202 Accepted with polling for ZATCA submission, and documented errors including 429 with Retry-After.

Two-person review

Every change needs a second engineer's approval and gets an automated code review.

Tests on every change

Automated tests run on every pull request.

Reference and help

API reference

The Integration API: invoices, ZATCA reference codes, clients, items and your remaining quota — with request and response schemas. Paste your key into Authorize to try calls live.

Open the reference

Talk to an engineer

Planning a larger integration or need an endpoint that is not listed? We will walk through it with you.

Contact us

Ready to build?

Get API access on the Business plan

Unlimited invoices, unlimited API calls, and engineers who help you connect your ERP.

Developers