لم تُترجم هذه الصفحة بعد
تعرض هذه الصفحة النص الإنجليزي حتى تصدر ترجمتها.
Quickstart¶
From an API key to a ZATCA-cleared invoice with a PDF.
sequenceDiagram
participant App as Your system
participant Clix
participant ZATCA
App->>Clix: GET /api/usage/current/
Clix-->>App: 200, invoices_allowed
App->>Clix: GET /api/devices/, /api/buyers/
Clix-->>App: device_id, seller_id, buyer uuid
App->>Clix: POST /api/invoices/preview/
Clix-->>App: 201, computed totals
App->>Clix: POST /api/invoices/
Clix-->>App: 202, location
Clix->>ZATCA: clearance or reporting
ZATCA-->>Clix: CLEARED / REPORTED / NOT_CLEARED
loop until final
App->>Clix: GET location
Clix-->>App: zatca_response_status
end
App->>Clix: GET …/pdf-a3/
Clix-->>App: PDF/A-3
Before you start¶
- An API key with role
CreateInvoiceorAccountant— see Authentication. - A device registered with ZATCA in the organisation. Invoices are signed by a device; without one, creation fails. See Devices and certificates.
- For a standard invoice, a client with a VAT number. Create one in the app or with
POST /api/onboarding/organizations/clients/. A simplified invoice may leavebuyerout. - Items only if you bill from the catalogue with
lines. This page writes its line out in full withform_linesand needs none.
Your base URL is the API gateway for your environment and already includes the version segment. It is given to you with your key; this page writes it as $CLIX_API.
1. Check the key works¶
A 200 with your plan and invoice counters means the key, the plan and the organisation are all good.
2. Look up the codes you will need¶
The code lists are static and safe to cache:
{
"STANDARD_RATED": { "code": "S", "description": "Standard Rated", "description_ar": "..." },
"ZERO_RATED": { "code": "Z", "description": "Zero Rated", "description_ar": "..." }
}
These are objects, not arrays
Every reference endpoint returns a map keyed by the enum member name, not a list. Index into it by name, or take Object.values(). See Reference data.
3. Build the payload¶
Three ids come from your own organisation, and one read gives you two of them:
Each row in devices carries device_id and seller_id. Use a device whose device_status is pcsid_generated, the Active connection in the app. Then take a buyer uuid from GET /api/buyers/ and, if you use the catalogue, an item uuid from GET /api/items/.
invoice.json, a standard invoice for one line:
{
"device": "<device_id from /api/devices/>",
"seller": "<seller_id from the same row>",
"buyer": "<uuid from /api/buyers/>",
"type_code": 388,
"transaction_code": "0100000",
"issue_date": "2026-09-27",
"issue_time": "10:00:00",
"supply_date": "2026-09-27",
"supply_end_date": "2026-09-27",
"currency": "SAR",
"exchange_rate": 1,
"payment_means_type_code": "10",
"add_prepaid_amount": false,
"reason_id": null,
"notes": [{ "language_id": "en", "note": "Order 1042" }],
"form_lines": [
{ "item_name": "Consulting", "invoiced_quantity": 1,
"invoiced_quantity_unit_of_measure": "PCE", "item_net_price": "100",
"vat_category_code": "S", "vat_rate": 15.0,
"vat_exemption_reason_code": "", "vat_exemption_reason_text": "" }
],
"lines": [], "original_invoice_reference": [], "prepaid_invoices": [],
"document_level_allowances": []
}
issue_date and issue_time are stamped by the server from its own clock, in Riyadh time, and whatever you put there is discarded. Send the two keys anyway on backend release 0.108.7, which demands them; later releases make them optional. Every other value is yours; Codes and constants lists what each one may be.
The server demands these keys, none of them null or an empty string: device, seller, type_code, transaction_code, currency, exchange_rate, supply_date, supply_end_date, payment_means_type_code, notes, lines, form_lines and document_level_allowances. An empty list counts as present, so [] satisfies lines, notes and document_level_allowances. Exactly one of lines and form_lines must have entries.
is_sample_invoice, document_level_charges and payment_terms are not input fields. Clix sets them itself and ignores a value you send.
What every fixed value in that body means, and what else it may be. The complete lists, read from the backend's own enumerations, are on Codes and constants.
| Field | In the example | Meaning | Other accepted values |
|---|---|---|---|
type_code |
388 |
a tax invoice, a sale | 381 credit note and 383 debit note, on this same endpoint with reason_id and original_invoice_reference; 386 advance payment |
transaction_code |
"0100000" |
standard invoice to a business, cleared by ZATCA | "0200000" simplified to a consumer, reported; "0100100" standard export. Nothing else is accepted. The device must allow the type. |
payment_means_type_code |
"10" |
paid in cash | "20" cheque, "30" credit transfer, "48" bank card, "54" credit card, "55" debit card and six more. "30" also needs payment terms, which Clix takes from the description of the reason named in reason_id. Without one the server answers 400 {"message": "Payment terms are required for credit payment."} |
currency, exchange_rate |
"SAR", 1 |
domestic sale | SAR takes exactly 1. Another ISO 4217 code, for an export invoice, takes its rate to SAR, greater than zero and not 1 |
vat_category_code, vat_rate |
"S", 15.0 |
standard rated at 15 percent | S also 5.0; Z zero rated, E exempt and O not subject take 0.0, and each needs both vat_exemption_reason_code (a VATEX-SA-… code of its category) and vat_exemption_reason_text |
invoiced_quantity_unit_of_measure |
"PCE" |
piece | PCS, PKT, CTN, BOX, KGM, LTR, MTR, MTK and thirty more UN/ECE codes |
item_name |
"Consulting" |
the line's description | 4 to 50 characters |
item_net_price |
"100" |
unit price before VAT | greater than zero, at most two decimals, below 1,000,000,000 |
notes[].language_id |
"en" |
note in English | "ar". Each note must be non-empty |
add_prepaid_amount, prepaid_invoices |
false, [] |
no advance payment applied | true with the uuids of the 386 invoices to deduct |
reason_id, original_invoice_reference |
null, [] |
not a correction | a credit or debit note needs both: the uuid of an active reason set up in the app, and the uuids of the invoices it corrects |
device, seller |
ids | from one GET /api/devices/ row: device_id and seller_id |
the device's device_status must be pcsid_generated, the app's Active, and the seller must be your own organisation |
buyer |
id | a business buyer with a VAT number on record | required on a standard invoice, which the server refuses without one; optional on a simplified invoice |
form_lines, lines |
one line, [] |
form_lines writes each line out in full |
lines instead bills catalogue items: [{"item_id": "<uuid>", "quantity": 1}], and the item's price, unit and VAT apply |
4. Preview before you commit¶
POST /api/invoices/preview/ runs the same validation and calculation as a create and returns the computed invoice with its totals and a ref_num, but submits nothing to ZATCA and uses no invoice number. Use it to check your payload.
curl -s -X POST "$CLIX_API/api/invoices/preview/" \
-H "Authorization: Bearer $CLIX_KEY" \
-H "Content-Type: application/json" \
-d @invoice.json
Expect 201 with {"message": "Invoice preview created successfully", "invoice": {…}}. A 400 here is the same 400 a create would give, so fix it now. A preview does not check your monthly allowance or the five-minute duplicate guard; the create does.
5. Create the invoice¶
curl -s -X POST "$CLIX_API/api/invoices/" \
-H "Authorization: Bearer $CLIX_KEY" \
-H "Content-Type: application/json" \
-d @invoice.json
Expect 202 Accepted — submission to ZATCA is asynchronous:
{
"message": "Tax Invoice creation request completed successfully, it will be cleared or reported to ZATCA soon",
"task_id": "…",
"location": "/api/invoices/8f3c…/"
}
Keep location. Ignore task_id — it is informational, and there is no task-status endpoint.
If Clix saved the invoice but could not queue it for ZATCA, the 202 reads "… saved, but the submission to ZATCA failed. Please retry submission." and carries invoice_uuid and location. Retry that one with PATCH /api/invoices/{invoice_id}/clearance-reporting/; do not create it again.
6. Poll until it is final¶
Watch zatca_response_status. It reaches CLEARED for standard invoices or REPORTED for simplified ones, usually within a few seconds. Poll with a sensible backoff — a second or two between attempts, not a tight loop.
If it reads NOT_CLEARED or NOT_REPORTED, ZATCA rejected the invoice. The reason is in zatca_response_data.validationResults.errorMessages[], each with a code, category and message; zatca_response_code carries ZATCA's HTTP status. A rejection is final for that invoice: its XML is signed and cannot be edited, and PATCH …/clearance-reporting/ answers 409 for it. Fix the cause and create a new invoice. The new one counts toward your monthly allowance. A rejected invoice has no PDF.
If it reads No response, ZATCA gave no answer Clix recognises. That invoice has no decision yet; PATCH /api/invoices/{invoice_id}/clearance-reporting/ submits it again and answers 202.
There are no outbound webhooks. Polling is the supported pattern.
7. Fetch the PDF¶
Once the invoice is final and invoice_pdf_url is set:
PDF/A-3 with the ZATCA XML embedded — the archival form. Before the invoice is final the endpoint answers 404 {"message": "Invoice PDF not found"}.
Do not retry a create blindly¶
Clix fingerprints invoice payloads for five minutes. Resending an identical body returns 400:
You just submitted an invoice with the exact same data. Please wait {n} seconds before retrying.
{n} is in seconds, up to 300. If a create times out, poll before retrying — the invoice may already exist. Retrying on a network error is how integrations produce duplicate invoices, and a cleared invoice cannot be deleted.
What you can call¶
21 paths in total — invoices, invoice comments, clients, items, devices, usage and the code lists. The authoritative list is the machine-readable spec:
GET /api/integrations/openapi.json- Browsable at
/api/integrations/docs
Both are public; no key needed to read them.