Worked example: key to cleared invoice¶
Every call below was made on the sandbox on 2026-09-27 with a throwaway key, in this order, and the bodies are what came back with the ids shortened. The Quickstart explains each step; this page shows the wire.
The interactive reference for every endpoint here is Swagger UI on your gateway host, with an Authorize button that takes a clix_ key:
https://<your-gateway-host>/v1/api/integrations/docs Swagger UI
https://<your-gateway-host>/v1/api/integrations/openapi.json OpenAPI 3 document
Both are public. Point a client generator at the JSON; use the UI to try a call with your own key.
sequenceDiagram
participant Admin as Admin, in the app
participant App as Your system
participant Clix
participant ZATCA
Admin->>Clix: API Keys, Create key
Clix-->>Admin: key shown once
App->>Clix: 1 GET /api/usage/current/
App->>Clix: 2 GET reference data
App->>Clix: 3 GET /api/devices/, /api/buyers/, /api/items/
App->>Clix: 4 POST /api/invoices/preview/
Clix-->>App: 201 computed invoice
App->>Clix: 5 POST /api/invoices/
Clix-->>App: 202 location
Clix->>ZATCA: clearance
ZATCA-->>Clix: verdict
App->>Clix: 6 GET location (poll)
Clix-->>App: zatca_response_status
App->>Clix: 7 GET pdf-a3
Admin->>Clix: 8 Revoke
0. Create the key¶
In the app, an Admin opens API Keys, clicks Create key, enters a Key name and an Access level (Create and view invoices is the default), and clicks Create key. The Copy your key now dialog shows the key once; I have stored it closes it. Steps with screenshots: API keys.
Every call below sends it as:
The 12-character prefix clix_CdHmGYM is the key's public name; it appears in listings and in the revoke dialog and is safe to log.
1. Check the key and the plan¶
200, with these headers and body:
{
"month": "2026-09-01",
"subscribed_plan": "business",
"plan_name": "Business",
"invoices_used": 0,
"invoices_limit": -1,
"api_calls_used": 0,
"api_calls_limit": 0,
"public_api_validations_used": 0,
"public_api_validations_limit": 0,
"grace_used": false,
"grace_exceeded": false,
"invoices_allowed": true
}
| Field | Type | Meaning |
|---|---|---|
month |
date | first day of the current Riyadh calendar month |
subscribed_plan, plan_name |
string | plan slug and display name |
invoices_used, invoices_limit |
int | invoices submitted this month, and the allowance; -1 is unlimited |
api_calls_used, api_calls_limit |
int | always 0; API calls are not metered |
grace_used, grace_exceeded |
bool | whether the 10 percent grace above the limit is in use or spent |
invoices_allowed |
bool | the one field to branch on before a create |
With a wrong or revoked key, the same call returns 401:
2. Read the codes¶
curl -s "$CLIX_API/api/invoices/vat-categories/" -H "Authorization: Bearer $CLIX_KEY"
curl -s "$CLIX_API/api/invoices/payment-means-types/" -H "Authorization: Bearer $CLIX_KEY"
curl -s "$CLIX_API/api/invoices/invoice-type-codes/" -H "Authorization: Bearer $CLIX_KEY"
Each is a map keyed by the enum name, never a list:
{"STANDARD_RATED": {"code": "S", "description": "Standard Rated", "description_ar": "…"}, "ZERO_RATED": {…}}
{"IN_CASH": {"code": 10, "description": "In cash", "description_ar": "نقداً"}, "CHEQUE": {…}}
{"TAX_INVOICE": {"code": 388, "description": "Tax Invoice", "description_ar": "…"}, "CREDIT_NOTE": {…}}
code is a string for VAT categories and an integer for payment means and invoice types. Full lists: Reference data.
3. Find the device, the buyer and an item¶
{
"devices": [
{
"device_id": "53bf5f28-…",
"seller_id": "79ec7b95-…",
"common_name": "Docs verification",
"serial_number": "1-AZMX|2-V1|3-53bf5f28-…",
"location": "Riyadh",
"unit_name": "https://clix.azmx.sa",
"industry": "Design",
"invoice_type": "1100",
"business_type": "BOTH",
"device_status": "pcsid_generated",
"ccsid_status": "ISSUED",
"pcsid_status": "ISSUED",
"CSIDCertificateData": {"not_valid_before": "2026-09-27T13:25:24Z", "not_valid_after": "2031-09-26T21:00:00Z", "days_until_expiry": 1825, "expiry_status": "valid"},
"PSIDCertificateData": {"not_valid_before": "2024-01-11T09:19:30Z", "not_valid_after": "2029-01-09T09:19:30Z", "days_until_expiry": 834, "expiry_status": "valid"},
"branch_id": null,
"is_branch_device": false,
"counter": "No invoices generated yet.",
"created_at": "2026-09-27"
}
],
"total": 1, "num_pages": 1, "current_page": 1,
"total_active_devices": 0, "total_ccsid_expired": 0, "total_pcsid_expired": 0
}
| Field | Use it for |
|---|---|
device_id |
the device of the invoice; the field is not called uuid |
seller_id |
the seller of the invoice; same row, so one read gives both |
device_status |
pcsid_generated is the Active connection in the app; only such a device can issue |
invoice_type |
1000 standard only, 0100 simplified only, 1100 both |
PSIDCertificateData.not_valid_after |
when the production certificate expires |
curl -s "$CLIX_API/api/buyers/" -H "Authorization: Bearer $CLIX_KEY"
curl -s "$CLIX_API/api/items/" -H "Authorization: Bearer $CLIX_KEY"
Buyers come back as {"buyers": [...]}, each with uuid, buyer_name, buyer_name_arabic, vat_registration_number, business_type, building_number, city, contact_email and the rest of the address; items as {"items": [...]} with uuid and item_name. A standard invoice needs a buyer with a vat_registration_number.
4. Preview¶
The body, invoice.json, exactly as sent:
{
"device": "53bf5f28-…",
"seller": "79ec7b95-…",
"buyer": "efe0d610-…",
"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",
"is_sample_invoice": false,
"add_prepaid_amount": false,
"reason_id": null,
"notes": [{"language_id": "en", "note": "Docs verification"}],
"form_lines": [
{
"item_name": "QA Walk item 2026-09-21",
"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": [],
"document_level_charges": []
}
| Field | Type | Rule |
|---|---|---|
device, seller, buyer |
uuid strings | from step 3 |
type_code |
int | 388 here; notes use the notes endpoint |
transaction_code |
string | 0100000 standard, 0200000 simplified; must suit the device's invoice_type |
issue_date, issue_time |
YYYY-MM-DD, HH:MM:SS |
set by the server, which discards what you send: this request carried 10:00:00 and the invoice came back 13:27:38. Required keys on release 0.108.7, optional after it |
supply_date, supply_end_date |
YYYY-MM-DD |
yours: when the goods or service were delivered |
currency, exchange_rate |
string, number | SAR and 1 for a domestic sale |
payment_means_type_code |
string | "10" cash; "30" credit transfer additionally requires payment_terms |
form_lines[] |
objects | the lines you write; lines stays [] |
form_lines[].item_net_price |
string decimal | |
form_lines[].vat_category_code, vat_rate |
S/Z/E/O, number |
zero-rated and exempt lines need vat_exemption_reason_code |
reason_id, original_invoice_reference, prepaid_invoices |
for notes and advance payments; empty here |
What each of these constants means and what else it may be: Codes and constants.
curl -s -X POST "$CLIX_API/api/invoices/preview/" -H "Authorization: Bearer $CLIX_KEY" \
-H "Content-Type: application/json" -d @invoice.json
201:
{
"message": "Invoice preview created successfully",
"invoice": {
"uuid": "0f1ad997-…",
"ref_num": "158-1",
"device": "53bf5f28-…",
"issue_date": "2026-09-27",
"issue_time": "13:27:38",
"is_sample_invoice": false,
"add_prepaid_amount": false,
"…": "totals, vat_breakdown and every other field of a full invoice"
}
}
A preview runs the same validation as a create. The two refusals we hit while building this body, both 400:
{"message": ["invoice_schema_data: Value error, Missing required fields: device. Optional fields not provided: reason_id"]}
The first came from sending device: null; the second from payment_means_type_code: "30" without payment_terms.
5. Create¶
curl -s -X POST "$CLIX_API/api/invoices/" -H "Authorization: Bearer $CLIX_KEY" \
-H "Content-Type: application/json" -d @invoice.json
202, verbatim:
{
"message": "Tax Invoice creation request completed successfully, it will be cleared or reported to ZATCA soon",
"task_id": "e0f94fed-…",
"location": "/api/invoices/9f12304f-…/"
}
Keep location. task_id has no endpoint.
Sending the identical body again inside five minutes, 400:
{"message": "You just submitted an invoice with the exact same data. Please wait 295 seconds before retrying."}
6. Poll¶
200. The response carries every field of the invoice; these are the ones a poller reads:
{
"uuid": "9f12304f-…",
"ref_num": "158-1",
"zatca_submitted_at": "2026-09-27T13:27:39.026Z",
"zatca_response_received_at": "2026-09-27T13:27:39.699Z",
"zatca_response_status": "NOT_CLEARED",
"zatca_response_code": 400,
"zatca_response_data": {
"clearedInvoice": null,
"clearanceStatus": "NOT_CLEARED",
"validationResults": {
"status": "ERROR",
"infoMessages": [
{"code": "XSD_ZATCA_VALID", "type": "INFO", "status": "PASS", "category": "XSD validation",
"message": "Complied with UBL 2.1 standards in line with ZATCA specifications"}
],
"errorMessages": [
{"code": "certificate-permissions",
"message": "User only allowed to use the vat number that exists in the authentication certificate"}
]
}
},
"payment_status": "Pending",
"invoice_pdf_url": null,
"qr_code": "…",
"share_link": "…",
"invoice_total_amount_without_vat": "100.00",
"invoice_total_vat_amount": "15.00",
"invoice_payable_amount": "115.00",
"vat_breakdown": [ { "vat_category_code": "S", "vat_rate": "15", "vat_category_taxable_amount": "100.00", "vat_category_tax_amount": "15.00" } ],
"lines": [ … ], "buyer": { … }, "seller": { … }, "device": { … }
}
| Field | Meaning |
|---|---|
zatca_response_status |
CLEARED, REPORTED, NOT_CLEARED, NOT_REPORTED; stop polling on any of them |
zatca_response_code |
ZATCA's HTTP status; 200 accepted, 400 rejected |
zatca_response_data.validationResults.errorMessages[] |
why, each with code, category, message; infoMessages[] are the checks that passed |
zatca_response_data.clearedInvoice |
the signed XML on success, null on rejection |
invoice_pdf_url |
set once the invoice is final |
payment_status |
Pending, Partially paid, Paid, Overdue; yours to move through payments, not ZATCA's |
The answer came within a second. This run was rejected on purpose: the sandbox signs with one shared certificate, and certificate-permissions is what any other VAT number gets there. On app.goclix.ai the same body from an active device clears. A rejected invoice is fixed and resubmitted with PATCH /api/invoices/{invoice_id}/clearance-reporting/.
7. PDF¶
Before the invoice is final, 404:
Once invoice_pdf_url is set the same call returns the PDF/A-3 bytes with the ZATCA XML embedded.
8. Revoke¶
In the app, API Keys, Revoke on the row, Revoke key in the Revoke this key? dialog. The very next call answered:
with status 401, within a second of the click.
What was not run¶
Notes (POST /api/invoices/{invoice_id}/notes/), client creation (POST /api/onboarding/organizations/clients/), item creation (POST /api/items/) and resubmission (PATCH …/clearance-reporting/) are on the surface and in Swagger but were not exercised in this run. Their schemas are in the OpenAPI document above.
Related¶
- Quickstart, the same steps with the reasoning
- API reference, the full surface and the Swagger links
- Errors and rate limits
- Public tools, the VAT calculator example, also run live