ZATCA API
ZATCA (Saudi e-invoicing) endpoints used by the POS app - Phase 1 print, Phase 2 submit-and-print and resync for invoices and vouchers, bulk send, the invoice ZATCA status fields and filters, and the dashboard ZATCA overview.
ZATCA is the Saudi tax authority's e-invoicing system (FATOORA). CloudPOS supports:
- Phase 1 - a printed invoice with a ZATCA QR code (seller name, VAT number, time stamp, total, VAT).
- Phase 2 - the backend signs the invoice, sends it to ZATCA and stores the result. B2C invoices are reported; B2B invoices are cleared and need the buyer's VAT and CR numbers.
Phases are switched on per company with the website settings ZATCA_PHASE_1 and ZATCA_PHASE_2 (Phase 2 can only be turned on after Phase 1). The POS shows the ZATCA actions only when the matching setting is on.
All endpoints below are POST, need X-Tenant and Authorization: Bearer <access-token>, and use the { "success": true/false, "message", "data" } envelope. Print and resync calls send the record id as a form field id (the server also accepts it as a query parameter).
Where ZATCA appears in the app
Screen | Action | Endpoint |
|---|---|---|
Transactions > Invoice row actions | ZATCA Phase 1 print |
|
Transactions > Invoice row actions | Send to ZATCA and print (Phase 2) |
|
Transactions > Invoice row actions | Resync |
|
Transactions > Invoice toolbar | Sync ALL, Sync Failed, Sync Not Send, Sync Selected |
|
Transactions > Customer Voucher row actions | Phase 2 print / resync (credit notes) |
|
Dashboard | ZATCA overview cards and the failed-ZATCA banner |
|
Every confirmed sale creates an invoice automatically. In the demo story, order ORD-004851 created invoice INV-OR1002060 with ZATCA status NOT SENT.
POST /api/v1/zatca/phase1/invoice/print
Purpose: generate the Phase 1 invoice PDF with QR code.
Request (form fields)
Name | Type | Required | Description |
|---|---|---|---|
| integer | yes | Invoice id (not the invoice number) |
Response (200)
{
"success": true,
"message": "Phase 1 invoice PDF generated successfully",
"data": {
"invoice_id": 4,
"invoice_number": "INV-OR1002060",
"filename": "<file>.pdf",
"download_url": "{{server}}/storage/invoices/<file>.pdf",
"phase": 1
}
}The app downloads download_url and opens the PDF. Missing id returns 400 {"success": false, "message": "Invoice ID is required"}.
POST /api/v1/zatca/phase2/invoice/print
Purpose: send the invoice to ZATCA if it has not been accepted yet, then return the Phase 2 PDF.
Request (form fields): id (invoice id, required).
Response (200)
{
"success": true,
"message": "Phase 2 invoice PDF generated successfully",
"data": {
"invoice_id": 4,
"invoice_number": "INV-OR1002060",
"zatca_invoice_id": 12,
"status": "success",
"filename": "<file>.pdf",
"download_url": "{{server}}/storage/invoices/<file>.pdf",
"phase": 2
}
}Errors
{"success": false, "message": "Failed to send invoice to ZATCA", ...}when ZATCA rejects the invoice. The app shows the message; the invoice's ZATCA status becomes failed.- 400 when
idis missing.
Ids in the examples are illustrative.
POST /api/v1/zatca/phase2/invoice/resync
Purpose: send the invoice to ZATCA again (after fixing data, or after a failure).
Request (form fields): id (invoice id, required).
Response (HTTP 200 on success, 500 on failure)
{
"success": true,
"message": "Invoice resync completed successfully",
"data": {
"invoice_id": 4,
"invoice_number": "INV-OR1002060",
"zatca_invoice_id": 13,
"resync_status": "success",
"zatca_response": { "...": "ZATCA reporting / clearance result" },
"error": null
}
}On failure success is false, message is "Invoice resync failed", resync_status is failed and error holds ZATCA's reason.
POST /api/v1/zatca/phase2/voucher/print
Purpose: send a customer voucher (credit note, for example from a sales return) to ZATCA and return its PDF.
Request (form fields): id (voucher id, required).
Response (200): {"success": true, "message": "Phase 2 voucher PDF generated successfully", "data": {..., "status": "success", "filename", "download_url"}}. Failure: "Failed to send voucher to ZATCA".
POST /api/v1/zatca/phase2/voucher/resync
Purpose: resend a voucher to ZATCA.
Request (form fields): id (voucher id, required).
Response: same shape as invoice resync, with resync_status success or failed.
POST /api/v1/zatca/bulk-send
Purpose: queue many invoices for ZATCA submission at once.
Used by: the bulk buttons on Transactions > Invoice:
Button | Flags sent |
|---|---|
Sync Selected | both flags |
Sync ALL |
|
Sync Not Send |
|
Sync Failed |
|
The app always includes the invoice ids it is working with in ids; the app asks you to confirm with the count before sending.
Request body (JSON)
{
"ids": [4, 5, 6],
"bulk_not_send": false,
"bulk_failed": false
}Server rules
bulk_not_send: truepicks every company invoice that has never been sent. This flag is checked first.- Otherwise
bulk_failed: truepicks every invoice that failed and has no successful submission. - Otherwise
idsmust be a non-empty list. With no ids and no flag the server returns 400 "Invoice IDs are required or bulk flags must be set". - Invoices already accepted by ZATCA are skipped.
- Invoices are queued as background jobs; B2B vs B2C is decided by whether the customer has a VAT number.
Response (200)
{
"status": "success",
"message": "Queued 3 invoices for ZATCA submission",
"data": {
"queued_count": 3,
"skipped_count": 0,
"total_found": 3,
"total_requested": 3
}
}Because sending is queued, refresh the list after a short while to see new statuses. The app waits up to 40 seconds for this call.
Automatic sending
The backend can send invoices to ZATCA by itself when an invoice is created. This happens only when all of these hold:
- the company has ZATCA settings and is onboarded (company prop
COMPANY_IDexists), and - the company prop
ZATCA_SYNC_B2C_QUEUE(for B2C customers) orZATCA_SYNC_B2B_QUEUE(for B2B customers) is set.
Otherwise invoices stay NOT SENT until someone uses print, resync or bulk send.
ZATCA fields on invoices
GET /api/v1/invoice/list-invoices (see Transactions, invoices and receipts API) returns ZATCA state per invoice:
Field | Values | Meaning |
|---|---|---|
|
| Result from ZATCA. The app shows pass/success/sent/reported/cleared as accepted |
|
| State of the request from CloudPOS |
| list | Each submission attempt |
ZATCA filter on the invoice list: the ZATCA Status filter sends zatca_status as:
Filter choice | Sent value |
|---|---|
Sent / Success |
|
Not Sent |
|
Failed |
|
Example: GET {{server}}/api/v1/invoice/list-invoices?zatca_status=not_sent&date_from=2026-10-27&date_to=2026-10-27&store_id=2.
Phase 1 QR on device receipts
Thermal and A4/A5 receipts printed by the app itself build the Phase 1 QR on the device (TLV format) from:
- seller name (
zatca_company_namefrom sign-in), - VAT number (
vat_numberfrom sign-in), - invoice time stamp,
- invoice total with VAT,
- VAT amount.
No API call is needed for that QR. Whether it prints is controlled by the document config flag showQRCode.
Dashboard ZATCA overview
GET /api/v1/dashboard/zatca-overview?period=today returns counts for invoices (success_zatca, not_sent, failed, warning) and vouchers (voucher_success, voucher_not_sent, voucher_failed, voucher_warning). Details: Dashboard API.
Related
- ZATCA in the POS app
- Transactions and accounts
- Customers - B2B customers need VAT and CR numbers
- Transactions, invoices and receipts API
- Dashboard API
- Settings and master data API