The ticket reads "every merchant on our platform needs ZATCA Phase 2". It lands at POS vendors, ERP and accounting products, booking and clinic systems, and any SaaS whose customers sell in Saudi Arabia. The first design decision is seeing what it really asks for: not one integration, but one ZATCA identity per merchant, and one signing unit per till or branch inside it. Get that model right and the rest is engineering. Get it wrong — one certificate for everyone — and every invoice you sign is in the wrong name.
What Phase 2 means for each merchant
Each merchant is the issuer of its own invoices, under its own VAT number, whatever your platform does for it. Per merchant, that means:
- Its own onboarding OTP. ZATCA issues the one-time password to the taxpayer inside the Fatoora portal. It never reaches you or any vendor: the merchant generates it and hands it over. Codes are single-use, valid for an hour, one per device.
- Its own certificates. The compliance CSID and then the production CSID are issued for that merchant's identity. Name, VAT number and registration number are bound into the certificate, which is why they cannot change after connecting.
- Its own chain per device. Every signing unit, ZATCA's EGS, keeps an invoice counter (
ICV) and the previous invoice hash (PIH). A merchant with four tills has four chains, each onboarded with its own OTP. - Two transmission paths. A standard (B2B) invoice is cleared by ZATCA before the buyer receives it; a simplified (B2C) one is handed over at once and reported within 24 hours. The buyer decides which.
Your own subscription invoice to each merchant is a separate matter: your supply, under your VAT number, never mixed into theirs.
The build path
Building it means, per merchant and per device: a key pair and CSR, the compliance CSID, the compliance documents ZATCA validates before it releases the production CSID, UBL 2.1 generation, XAdES signing, ICV and PIH state under a lock, clearance and reporting with their different failure semantics, validation codes turned into messages a cashier can act on, and renewal of every certificate before it expires. What a direct integration really involves goes through each step. For a platform the multiplier is what hurts: onboarding, chain state and renewals are per device, across every merchant, for as long as they stay with you. The OTP and CSID lifecycle is where most support tickets start.
Building is the right call in three cases: your tills must sign on the device with no connection, a customer's policy forbids invoice data leaving your infrastructure, or compliance is the product you sell.
The partner path
ZATCA Tools, an independent e-invoicing platform, offers the other route as a Partner API. Everything below is from that reference and the partner guide.
- One partner key. A single
ztkp_live_key, kept server-side. There are no per-merchant keys. - A merchant per request.
POST /api/partner/merchantswith the merchant's name, owner email and VAT number, and optionally its national short address, from which the full address is resolved. The registration number is required before connecting. The response carries the merchant's id and a ready onboarding URL. - The merchant connects to Fatoora. Send them the signed onboarding link, valid 72 hours, or collect the OTP in your own screen, pass it to
POST /api/partner/merchants/{id}/connectand poll/status; certificate and compliance checks typically settle in under two minutes. Missing details come back asmerchant_incompletebefore the single-use code is spent. - Branches and devices.
POST /api/v1/branches, then one device per till, each with its own OTP, certificate and chain. Every merchant and device reports itscertificate_expires_at, and re-onboarding a device takes a fresh OTP from the merchant. - Issue as the merchant. The same
/api/v1endpoints as the API reference, with your partner key and anX-Merchant-Idheader.
POST /api/v1/invoices
Authorization: Bearer ztkp_live_...
X-Merchant-Id: 34
Idempotency-Key: order-18821
{ "type": "simplified", "branch_id": 3, "device_id": 7,
"prices_include_vat": true,
"lines": [ { "name": "Printer paper A4, box", "quantity": 2, "unit_price": 57.50 } ] }
The response carries status (accepted, warnings or rejected), the totals, the channel and counter, and links to the signed XML, the PDF and the QR code. A partner portal shows your operations team which merchants are connected, which are stuck at the OTP and which have rejections.
What stays your platform's job
A service can sign, chain and transmit. It cannot know what was sold. Four things stay with you:
- The sale data. Lines, quantities and prices as your system holds them. If your prices include VAT, send them unchanged with
prices_include_vat: dividing by 1.15 and rounding each line loses halalas the invoice cannot recover. - The tax category per line.
Sat 15%, orZ,EorOwith atax_reason_codefrom ZATCA's list, such asVATEX-SA-35for medicines. One invoice may mix categories. A reason is never guessed for you: left out, the line takes the reason that merchant last used for that category, or goes without one and ZATCA accepts it with a warning. - The buyer's identity. A standard invoice needs the buyer's name and VAT number or, for a buyer without VAT registration, another identifier with its scheme, such as
700for a government body orNATfor a Saudi national ID (BR-KSA-14). A line exempt as private education or healthcare needs the buyer's national ID even on a simplified invoice (BR-KSA-49). - The type. Standard or simplified is decided per sale, by the buyer, in your checkout.
Offline tills and the 24-hour window
A simplified invoice is handed to the customer at the sale and reported within 24 hours of issue. The window runs from issue, not from the moment a rejection arrives, so a rejected report is fixed the same day (reading the code).
Be clear about the architecture. Our API signs in the cloud and reports in the same call: the document, its QR code and its place in the device's chain exist only when the call returns, so a till must reach the API at the moment it issues. If your tills must print Phase 2 receipts through an outage, that takes signing on the device itself, which is the build path. What the API gives an intermittently connected till is safe retries. What a till prints while no invoice has been issued is a policy to agree with your merchants, not a receipt dressed up as one.
B2B clearance latency
A standard invoice is cleared inside the same call, so the response already holds ZATCA's verdict and, for a cleared document, the XML ZATCA stamped. The reference puts signing and clearance at one to three seconds and recommends a 30-second client timeout. Design the B2B checkout to wait for that answer: nothing goes to the buyer before clearance returns.
Idempotency and failure handling
Send your order id as external_id or an Idempotency-Key header. A repeat with the same key returns the document already issued — 200 instead of 201, same body — never a duplicate, even when repeats arrive together. Keys are unique within one merchant and namespaced per document kind, so a refund's credit note can reuse its order's id. Two rules follow: never reuse a key for a different sale, because the earlier document comes back; and if one merchant sells through two sources whose order numbers can repeat, prefix the key with the source.
| Response | Meaning | What to do |
|---|---|---|
| 422 and other 4xx | Refused before signing; nothing numbered | Fix what the message names, resend |
| 429 | Rate limit reached | Wait for Retry-After, retry |
| 5xx or timeout | Outcome unknown to you | Retry with the same external_id |
201, warnings | Accepted, with notes | Issued; fix the data upstream |
201, rejected | Refused by ZATCA; keeps its number and chain position | Correct the data, issue a new document |
403 seat_released | The merchant's seat was released | Reinstate it; reads still work |
Refunds and corrections are never edits: POST /api/v1/notes against the original, with the reason ZATCA requires (BR-KSA-17).
How partner seats work
Partner merchants issue without an invoice ceiling. Each merchant occupies one seat, reserved when you create it, with its paid term starting on the day the merchant connects to ZATCA and running monthly or yearly under your agreement. A merchant that never connects costs nothing, an expired term never stops a merchant issuing, and a merchant who leaves is released so the seat can serve someone else, with nothing deleted, since ZATCA requires a six-year archive. Your merchants never see an invoice from us; what you charge them is yours to decide. Seat numbers and terms are agreed per partner, so there is no price list.
If your platform serves Saudi merchants, tell us how many and on which system, and read the Partner API reference; the guide for software houses covers your own invoicing. If you are one business connecting its own system, the API is the route: one week free, no payment, from the day you connect, then very competitive plans: 49 SAR a month for Growth and 149 for Business, on a smooth, fast system that signs each invoice and sends it to ZATCA in seconds — start here.