For developers

API Reference

Issue ZATCA Phase-2 compliant e-invoices from your own system. One request builds the document, signs it and submits it to the Authority — the same engine the platform uses.

Introduction

The API is JSON over HTTPS. Every call is stateless and scoped to the establishment that owns the key. Amounts are in Saudi Riyal (SAR) with two decimals, dates are YYYY-MM-DD, and timestamps are ISO 8601 in Riyadh time (UTC+3).

A standard invoice (B2B, tax invoice) is sent to the Authority for clearance and comes back stamped; a simplified invoice (B2C) is signed here and reported. Both happen inside the one POST /v1/invoices call, and the response already carries the outcome.

Serving several merchants from one integration? Use the Partner API: one ztkp_ key acts for every merchant you create, and the endpoints below accept it with an X-Merchant-Id header.

Authentication

Every request carries the establishment's API key as a bearer token. Generate it under Settings → API. The key is shown once, at generation, and it has full authority over the establishment's invoicing — keep it in an environment variable, never in source code or a repository. One key is active per establishment; generating a new one revokes the previous one immediately.

curl https://zatcatools.com/api/v1/account \
  -H "Authorization: Bearer ztk_live_..."

Errors

Every error uses the same envelope. The code is stable and meant for your program; the message is for the human reading the log.

{
  "error": {
    "code": "quota_exceeded",
    "message": "Free-plan invoice quota exhausted. Contact us to upgrade."
  }
}
HTTPcodeMeaning
401 unauthenticated The key is missing, malformed or revoked.
402 quota_exceeded The plan's invoice allowance is used up. Contact us to upgrade.
404 not_found No such resource under this establishment.
409 not_connected The establishment has not completed its ZATCA onboarding yet.
422 invalid_lines · customer_required · customer_incomplete · type_disabled · branch_required · device_required · device_not_usable … The request is incomplete or inconsistent. The code names what to fix; the message explains it.
422 submission_failed The document could not be signed or the Authority refused it. Nothing was recorded — fix and resend.
429 too_many_requests More than 60 requests in a minute on this key. Back off and retry.

A 2xx with "status": "rejected" is not an error of the API: the document reached the Authority and was refused. The reasons are in zatca.rejection, and each rule code is explained in the validation code guide.

Idempotency

Pass your own order identifier as external_id (or as an Idempotency-Key header). A repeated request with the same value returns the document that was already issued — 200 instead of 201, same body — and never a duplicate. Retries after a timeout are therefore always safe. Notes are namespaced by kind, so a credit note may reuse the id of the invoice it adjusts.

Rate limits

  • 60 requests per minute per key. Beyond that: 429.
  • Issuing a document consumes one unit of the establishment's plan. Watch what is left on GET /v1/account.
  • Signing and clearance take one to three seconds. Set client timeouts to 30 seconds and rely on idempotency for retries.
GET /v1/account

The establishment, its ZATCA connection, its branches with their devices, and what is left of the plan.

{
  "name": "مؤسسة النخبة للتجارة",
  "vat_number": "310000000000003",
  "cr_number": "1010101010",
  "address": {
    "street": "طريق الملك فهد", "building_number": "8228", "subdivision": "العليا",
    "city": "الرياض", "postal_zone": "12244", "short_address": "RRRD2929"
  },
  "zatca": { "connected": true, "environment": "production", "certificate_expires_at": "2027-07-01" },
  "branches": [
    { "id": 3, "name": "فرع الملز", "city": "الرياض", "connected": true,
      "devices": [ { "id": 7, "name": "كاشير 1", "status": "connected", "usable": true } ] }
  ],
  "quota": { "used": 12, "limit": 50, "remaining": 38, "unlimited": false }
}
POST /v1/invoices

Builds the invoice, signs it and submits it to ZATCA in one call — clearance for a standard invoice, reporting for a simplified one. A standard invoice needs a complete customer; a simplified one does not. Customers are matched by VAT number inside your establishment and created when new.

Request — standard (B2B) tax invoice
curl -X POST https://zatcatools.com/api/v1/invoices \
  -H "Authorization: Bearer ztk_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "type": "standard",
    "external_id": "SO-2026-1188",
    "customer": {
      "name": "شركة المثال للتجارة",
      "vat_number": "311111111101113",
      "cr_number": "1010101010",
      "short_address": "RRRD2929"
    },
    "lines": [
      { "name": "اشتراك سنوي", "quantity": 1, "unit_price": 1000 }
    ],
    "due_date": "2026-08-18",
    "notes": "السداد خلال 30 يومًا بالتحويل البنكي"
  }'
Request — simplified (B2C) invoice
{
  "type": "simplified",
  "external_id": "order-5501",
  "customer_email": "[email protected]",
  "prices_include_vat": true,
  "branch_id": 3,
  "device_id": 7,
  "lines": [
    { "name": "قهوة مختصة 250غ", "quantity": 2, "unit_price": 55,
      "description": "حبوب إثيوبية — تحميصة فاتحة" },
    { "name": "أدوات تحضير", "quantity": 1, "unit_price": 120,
      "discount": 10, "discount_type": "percent" },
    { "name": "دواء", "quantity": 1, "unit_price": 80,
      "tax_category": "Z", "tax_reason_code": "VATEX-SA-35" }
  ]
}

Parameters

FieldDescription
type REQUIREDstandard (B2B, cleared) or simplified (B2C, reported). Each must be enabled for the establishment in Settings, or the call answers type_disabled.
lines[] REQUIREDAt least one line with name, quantity, unit_price. Optional per line: description, discount + discount_type, tax_category + tax_reason_code — see Line items and Tax categories.
customer optionalRequired for standard. name and a valid 15-digit vat_number, plus the buyer's national short_address (RRRD2929) — street, building number, district, city and postal code are resolved from it. Or spell them out: street, building_number, subdivision, city, postal_zone. cr_number is optional. On a simplified invoice a customer with just a name is printed on the document.
discount optionalA document-level discount in SAR, allocated across the lines under ZATCA's rounding rules. For a discount on one item use the line's own discount instead.
prices_include_vat optionaltrue when every amount you send (lines and discount) is what the customer pays. Default false: amounts are net and 15% is added. See Prices including VAT.
issue_date optionalYYYY-MM-DD, defaults to today in Riyadh. supply_date (standard only) defaults to the issue date.
due_date optionalPayment due date of a standard invoice, YYYY-MM-DD. Printed on the document and used for receivables.
notes optionalFree text for the buyer, up to 2000 characters — payment terms, a delivery remark. Printed under the totals on the PDF; never part of the signed XML.
external_id optionalYour order id. Makes the call idempotent — see Idempotency.
customer_email optionalWhen present, the buyer receives the PDF (with its QR code) by email as soon as the document is accepted.
branch_id · device_id optionalA sale from a branch names the branch and the device that rang it — always both. Omit both for the head office. See Branches & devices.
Response 201 Created
{
  "uuid": "8d9f6e0a-4b7c-4f2e-9a1d-3c5b7e9f1a2b",
  "number": "INV-2026-00042",
  "kind": "invoice",
  "type": "standard",
  "status": "accepted",
  "issue_date": "2026-07-19",
  "created_at": "2026-07-19T09:30:00+03:00",
  "totals": {
    "lines": "1000.00", "discount": "0.00",
    "vat": "150.00", "grand": "1150.00", "currency": "SAR"
  },
  "customer": { "name": "شركة المثال للتجارة", "vat_number": "311111111101113" },
  "lines": [
    { "name": "اشتراك سنوي", "quantity": 1, "unit_price": 1000, "tax_category": "S", "total": 1000 }
  ],
  "due_date": "2026-08-18",
  "notes": "السداد خلال 30 يومًا بالتحويل البنكي",
  "zatca": {
    "channel": "clearance",
    "icv": 42,
    "submitted_at": "2026-07-19T09:30:02+03:00",
    "warnings": []
  },
  "links": {
    "view": "https://zatcatools.com/invoice/{uuid}/{token}",
    "xml": "https://zatcatools.com/api/v1/invoices/{uuid}/xml",
    "pdf": "https://zatcatools.com/api/v1/invoices/{uuid}/pdf",
    "qr":  "https://zatcatools.com/api/v1/invoices/{uuid}/qr.svg"
  }
}

Lines come back as they were sent: a line with a discount also carries discount, discount_type and the resulting net_unit_price; a line with a description carries it; and totals.line_discounts appears when any line was discounted. links.view is the one link to put in front of a person — an order screen, an email — it opens in any browser with no key. The xml, pdf and qr links need the bearer key and are for server-to-server use.

Always read status. 201 means the document was created, signed and submitted: accepted — accepted · warnings — accepted with warnings, listed in zatca.warnings · rejected — refused by the Authority, reasons in zatca.rejection. A rejected document keeps its number and its place in the chain, as the regulation requires; issue a corrected one.

Line items & discounts

FieldDescription
name REQUIREDWhat was sold. Printed as the item name.
quantity REQUIREDDecimal, greater than zero.
unit_price REQUIREDThe list price of one unit — net, or gross with prices_include_vat.
description optionalUp to 500 characters, printed under the item name.
discount optionalA discount on this line: an amount in SAR by default, or a percentage with "discount_type": "percent". It is taken off the unit price — the Authority requires quantity × unit price to equal the line's net (BR-KSA-EN16931-11) — so the document prints the agreed price with the discount beside it. A discount that reaches the whole line is refused (invalid_lines).
tax_category optionalS, Z, E or O, with a tax_reason_code for anything but S. Omit it and the line takes the establishment's own VAT treatment (Settings → Establishment; standard-rated unless changed), which is what most integrations want. See below.

Tax categories

Each line carries its own VAT treatment, so one basket can hold a standard-rated item beside zero-rated medicine; the document gets one subtotal per category, as the Authority requires. A zero-rated, exempt or out-of-scope line must say why — ZATCA rejects one without a reason code. Send the code; the official reason text is written into the XML for you.

tax_categoryMeaningtax_reason_code
S · 15% Standard rated — 15% VAT not needed
Z · 0% Zero rated — 0% VAT, reason required
VATEX-SA-32 — Export of goods VATEX-SA-33 — Export of services VATEX-SA-34-1 — International transport of Goods VATEX-SA-34-2 — International transport of passengers VATEX-SA-34-3 — Services directly connected and incidental to international passenger transport VATEX-SA-34-4 — Supply of a qualifying means of transport VATEX-SA-34-5 — Services relating to Goods or passenger transportation VATEX-SA-35 — Medicines and medical equipment VATEX-SA-36 — Qualifying metals VATEX-SA-EDU — Private education to citizen VATEX-SA-HEA — Private healthcare to citizen
E · 0% Exempt — reason required
VATEX-SA-29 — Financial services mentioned in Article 29 of the VAT Regulations VATEX-SA-29-7 — Life insurance services mentioned in Article 29 of the VAT Regulations VATEX-SA-30 — Real estate transactions mentioned in Article 30 of the VAT Regulations
O · 0% Out of scope of VAT — reason required
VATEX-SA-OOS — Outside scope of VAT

Prices including VAT

A shelf price in a Saudi shop already includes VAT; an agency quotes before it. With "prices_include_vat": true every amount in the request — line prices and the document discount — is what the customer actually pays, and the net is derived per line at that line's own rate, so a zero-rated line is never "un-taxed" by mistake. The stored, signed document is always net, as the regulation requires: the flag changes what a sent riyal means, never the shape of the invoice. Without it (the default) prices are net and 15% is added on top.

Branches & devices

One VAT number, many outlets. Each branch issues through its own device — ZATCA's own model, the EGS unit: every device is activated with its own OTP from the Fatoora portal (Onboard new solution unit/device; single-use, valid one hour), carries its own certificate and its own invoice chain, so branches issue in parallel. The head office's device is the establishment's main connection. A branch cannot issue until a device of its own is connected; a branch may have several.

POST /v1/branchesCreate a branch. name and short_address are required — street, district, city and postal code resolve from the code; cr_number optional. Explicit address fields are for one case only: the code could not be resolved (address_unresolved).
POST /v1/branches/{id}/devicesActivate a device for the branch with its Fatoora otp. Answers 202 at once; the CSR, certificate and six compliance checks complete in the background.
GET /v1/devices/{id}Poll until status is connected. failed with otp_invalid means: generate a fresh code and connect again.
POST /v1/devices/{id}/connectRe-activate a device that failed, or that was left behind by a trial graduation — a new otp, and its chain starts afresh.
DELETE /v1/devices/{id}Retire a device. The documents it issued stay as they are.

An invoice from a branch then names both branch_id and device_id — standard and simplified alike. The point of sale knows its till; the document carries the branch's address and CR and is signed by that device on its own chain. One without the other is refused (device_required / branch_required). The legal name and VAT number always stay the establishment's. Notes follow the branch of the invoice they adjust; device_id on a note is optional. The full list of branches and devices is on GET /v1/account, and they can also be managed under Settings → Establishment.

GET /v1/invoices

Newest first, paginated. This is also the polling trigger automation platforms build on: ask for what is newer than the last document you saw.

QueryDescription
kindinvoice (default) · credit · debit · all
statusaccepted · warnings · rejected · draft
typestandard · simplified
sinceAn ISO 8601 timestamp: only documents created after it. Feed back the newest created_at you have seen and a poll every minute costs one small query.
page · per_pagePage number, and page size up to 100 (default 25).
{
  "data": [ { "uuid": "…", "number": "INV-2026-00042", "kind": "invoice", "status": "accepted", "created_at": "…", … } ],
  "meta": { "page": 1, "per_page": 25, "total": 128, "last_page": 6 }
}
GET /v1/invoices/{uuid}

The full document — the same shape as the create response, including the lines, the note or due date if any, zatca.rejection when the Authority refused it, and origin_invoice (uuid, number) on a credit or debit note.

XML · PDF · QR

Every issued document has three downloads, under the same bearer key:

GET /v1/invoices/{uuid}/xml The signed XML, byte for byte as ZATCA received it
GET /v1/invoices/{uuid}/pdf A print-ready Arabic/English PDF with the QR code
GET /v1/invoices/{uuid}/qr.svg The QR code alone as SVG (the signed TLV payload)
GET /v1/invoices/{uuid}/html The same document as a self-contained HTML page

To show a document to a person, use links.view from the invoice object instead: it carries its own per-document token, opens in any browser, and never exposes your key.

POST /v1/invoices/{uuid}/email

Emails the PDF. to is optional and defaults to the account owner's address. Answers { "sent": true, "to": "bu…@example.com" }, or 422 invalid_recipient / 502 send_failed.

POST /v1/notes

A credit note (credit) or a debit note (debit) against an issued invoice named by invoice_uuid. The reason is mandatory (BR-KSA-17). Lines take the same shape as on an invoice — description and discount included — and are priced net: give the price the buyer actually paid. A returned quantity may not exceed the invoice's, and a credit note may not exceed the invoice's open balance after earlier notes (Article 40). The note is issued from the same branch as its invoice.

{
  "kind": "credit",
  "invoice_uuid": "8d9f6e0a-4b7c-4f2e-9a1d-3c5b7e9f1a2b",
  "reason": "إرجاع جزئي للبضاعة",
  "external_id": "return-5501-1",
  "lines": [
    { "name": "أدوات تحضير", "quantity": 1, "unit_price": 120, "discount": 10, "discount_type": "percent" }
  ]
}

The response is the note in the same shape as an invoice, with kind, a CRN- or DBN- number and origin_invoice. Errors: origin_not_found, reason_required, quantity_exceeds_origin, exceeds_open_balance.

Cancel & reinstate

An issued invoice is signed and reported: it is never deleted, it is reversed. Send a credit note with "full": true and no lines, and the open balance after any earlier notes is credited — computed here, so it can never exceed the legal ceiling. An invoice already fully covered answers 422 nothing_to_credit.

To reinstate, send the same request with "kind": "debit" and "full": true: a debit note for exactly what was written off, and the invoice is owed again (422 nothing_to_restore when nothing was). To undo one specific credit note rather than everything ever credited, pass its uuid as reverses instead. Use a different external_id for each cancel/reinstate cycle, or the earlier note is returned.

Questions about an integration? Contact us — we answer during business hours.