For software companies and point-of-sale vendors

Partner API

Your system serves many merchants — one integration with us gives each of them a complete invoicing business with the Authority: its own certificate, its own chain, and unlimited invoices. You create the accounts programmatically, and we take on the ZATCA complexity.

BASE URL https://zatcatools.com/api/partner
Building the integration? The full English API Reference — every endpoint, parameter and error code — lives at /docs/partner-api. API Reference →

How it works

The Authority issues certificates to each taxpayer separately — there is no shared certificate that signs on everyone's behalf. So every merchant you create through this API is a complete, independent business: its own VAT number, its own certificate, its own invoice chain (ICV/PIH), and its own archive. What the API gives you is the ability to create and manage those businesses from your system, instead of every merchant registering on their own.

Step Who does it How
1 · Create the merchant's businessYour systemPOST /merchants
2 · Connect to the Fatoora portalYour system — and the merchant fetches the OTPPOST /merchants/{id}/connect
3 · Issue invoicesYour systemPOST /api/v1/invoices + X-Merchant-Id

Subscriptions: Your agreement with us sets how many paid merchant subscriptions you hold. Every active merchant uses one, and pausing the subscription of a merchant who has left returns it to your balance. When the balance runs out, the API returns 402 seat_limit_reached — contact us to raise it.

The truth about the Fatoora OTP — read this before you write a line of code

One step in the whole journey is the one no API can take over from the merchant: the OTP from the Fatoora portal. The Authority issues it to the taxpayer in person, inside its own portal at fatoora.zatca.gov.sa. It never reaches us and it never reaches you. This is a constraint the Authority places on the whole market, not one of ours.

The constraint is on where the OTP comes from only — entering it happens inside your own screen: show your merchant "Enter the OTP from the Fatoora portal", and pass what they type to POST /merchants/{id}/connect and follow the progress with GET /merchants/{id}/status — the certificate and the six compliance checks take about two minutes, and your merchant never left your system. The same rule applies to every branch device later on: a new OTP for every device (Onboard a new e-invoicing solution → number of devices) — the details are under Branches and devices.

Do not want to build the screen? onboarding_url stays a ready alternative: a signed link that opens our page at the same step.

Authentication

The partner key starts with ztkp_live_ and you receive it from us when the agreement is signed — it is shown once, so store it in an environment variable. And it is for systems only: the partner portal opens with your registered email and a one-time code, so your key never has to pass through anyone's clipboard. And it is your only key: it manages your account here, and it issues the invoices of every merchant you have on the invoicing API — you name the merchant on every call with the X-Merchant-Id header. One secret in your vault, one rotation to manage.

curl https://zatcatools.com/api/partner/account \
  -H "Authorization: Bearer ztkp_live_..."

Errors

HTTP code Meaning
401 unauthenticated The partner key is missing or wrong — or you sent a merchant key by mistake
402 seat_limit_reached All your paid subscriptions are in use — release one, or contact us to add more
403 partner_suspended The partnership account is suspended — contact us
404 not_found No merchant with that id belongs to your partnership
409 email_taken / vat_taken The email already has an account, or the VAT number is registered to another business
422 validation_failed Missing or invalid data — the message explains why
429 too_many_requests You went over the request limit (120/minute)

Your account and subscriptions

GET /api/partner/account

{
  "partner": {
    "name": "Smart POS Co.",
    "status": "active",
    "seats": { "limit": 50, "used": 34, "remaining": 16 }
  }
}

Create a merchant

POST /api/partner/merchants

Uses one subscription and creates a complete business whose invoicing is unlimited. name and email are required; vat_number is required too — it is the business's identity with the Authority, and your system always knows it for its own customer. The CR number is optional. For the address, send short_address — the national short address (4 letters and 4 digits, such as RRRD2929): we resolve the full detailed address from it automatically, exactly as in normal registration. If it cannot be resolved, the merchant completes the details in the Onboarding step — nothing breaks. The email must be new to us: an email that already has an account is rejected with 409 and is never taken over — knowing a merchant's email is not a door into an account they own.

curl -X POST https://zatcatools.com/api/partner/merchants \
  -H "Authorization: Bearer ztkp_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Gourmet Restaurant — Muruj branch",
    "email": "[email protected]",
    "vat_number": "310000000000003",
    "cr_number": "1010203040",
    "short_address": "RRRD2929"
  }'
HTTP/1.1 201 Created
{
  "merchant": { "id": 34, "name": "…", "status": "awaiting_onboarding", "seat": "active" },
  "onboarding_url": "https://…",     // signed, valid for 72 hours
  "seats": { "limit": 50, "used": 35, "remaining": 15 }
}

No key per merchant — store merchant.id against your own customer in your system: it is what goes in the X-Merchant-Id header, alongside your partner key, on every invoicing call.

Connecting a merchant — from inside your own screen

POST /api/partner/merchants/{id}/connect · GET /api/partner/merchants/{id}/status

Before the call, make sure the business details are complete — name and vat_number and cr_number and short_address — complete them with PATCH /merchants/{id} if any are missing. Missing data is rejected before the OTP is spent: a Fatoora OTP is single-use, and burning one on a missing field is the worst version of this mistake.

# The merchant typed the six digits into YOUR screen:
curl -X POST https://zatcatools.com/api/partner/merchants/34/connect \
  -H "Authorization: Bearer ztkp_live_…" \
  -H "Content-Type: application/json" \
  -d '{ "otp": "123456" }'

HTTP/1.1 202 Accepted
{ "status": "connecting", "poll": "…/merchants/34/status" }
# Poll every couple of seconds while your checklist spins:
GET /api/partner/merchants/34/status
{
  "connected": false, "connecting": true, "stalled": false,
  "compliance_passed": 4,        // of the 6 checks
  "error": null
}
# …until:
{ "connected": true, "compliance_passed": 6, "certificate_expires_at": "2029-08-31" }

error has two values that matter to you: otp_invalid — the OTP is wrong or expired; ask your merchant for a new one. And stalled: true — the count stopped; try again with a new OTP. Once connected, the identity fields (name, VAT number and CR number) are locked: the issued certificate carries them, and changing them would make the documents contradict their own certificate.

The merchant list and their details

GET /api/partner/merchants · GET /api/partner/merchants/{id}

The field your screen is built on is status:

awaiting_onboarding Created but never finished connecting — show them the link to complete it
connected Connected to the Authority — its invoices are issuing now
failed The last connection attempt failed — mint a new link and send them to it
trial Testing on the Authority's sandbox environment
released You released their subscription — the data is kept and no new invoicing happens

Issuing invoices

From here on you use the v1 invoicing API with the same partner key plus a header naming the merchant — issuing, credit and debit notes, XML, PDF and QR. Here is a sale from your point of sale:

curl -X POST https://zatcatools.com/api/v1/invoices \
  -H "Authorization: Bearer ztkp_live_…" \
  -H "X-Merchant-Id: 34"                  # the merchant this sale belongs to \
  -H "Content-Type: application/json" \
  -d '{
    "type": "simplified",
    "branch_id": 3,                  // selling from a branch? name the branch and its device together —
    "device_id": 7,                  // the same rule for tax and simplified invoices alike
    "lines": [
      { "name": "Lunch", "quantity": 2, "unit_price": 45.00 },
      { "name": "Medicine", "quantity": 1, "unit_price": 120.00,
        "tax_category": "Z", "tax_reason_code": "VATEX-SA-35" }
    ]
  }'

Lines accept tax_category (S by default · Z zero-rated · E exempt · O out of scope) — and the non-standard categories require a tax_reason_code from the Authority's list, and a single invoice can mix categories without any problem. A sale from a branch names branch_id and device_id together — for tax invoices and simplified tax invoices alike; the details are under Branches and devices below.

Branches and devices

Does your merchant have more than one point of sale? Every branch sits under the same VAT number, but each branch issues through its own device (EGS unit) — the Authority's own model: from the Fatoora portal the merchant generates an OTP for every device (Onboard a new e-invoicing solution → number of devices). You collect it in your screen and pass it to us, and each device comes out with its own certificate and its own independent invoice chain. The main business connection (above) is the head-office device; what follows is for the branches. Same key, plus the merchant header:

# 1. The branch
curl -X POST https://zatcatools.com/api/v1/branches \
  -H "Authorization: Bearer ztkp_live_…" -H "X-Merchant-Id: 34" \
  -d '{ "name": "Nakheel Mall branch", "cr_number": "1010999888", "short_address": "RESB3139" }'
# → 201 { "branch": { "id": 3, … } }

# 2. the branch device — an OTP collected from your merchant's screen
curl -X POST https://zatcatools.com/api/v1/branches/3/devices \
  -H "Authorization: Bearer ztkp_live_…" -H "X-Merchant-Id: 34" \
  -d '{ "otp": "482913", "name": "Till 1" }'
# → 202 { "device": { "id": 7, "status": "pending", … } }

# 3. poll until connected (usually under a minute)
curl https://zatcatools.com/api/v1/devices/7 \
  -H "Authorization: Bearer ztkp_live_…" -H "X-Merchant-Id: 34"
# → { "device": { "status": "connected", "usable": true, … } }

# 4. a sale from the branch — the invoice names its branch and device together
curl -X POST https://zatcatools.com/api/v1/invoices \
  -H "Authorization: Bearer ztkp_live_…" -H "X-Merchant-Id: 34" \
  -d '{ "type": "simplified", "branch_id": 3, "device_id": 7,
        "lines": [{ "name": "Meal", "quantity": 1, "unit_price": 45 }] }'

A single branch can carry several devices — repeat step 2 with a new OTP for each one, and each keeps its own chain, so your points of sale issue in parallel. A branch invoice names its device: store device_id at each point of sale and send it with branch_id on every invoice — one without the other is rejected (device_required / branch_required), and a device that is not connected is rejected with device_not_usable. Credit and debit notes follow the branch of their original invoice automatically, and naming a device on them is optional. Activation failed with otp_invalid? The OTP was wrong, its hour ran out, or it was already used — ask your merchant for a new one and activate the same device again: POST /v1/devices/{id}/connect. The full list of branches and their devices is always at GET /v1/account.

Pausing and restoring a subscription

POST /api/partner/merchants/{id}/release · POST /api/partner/merchants/{id}/reinstate

A merchant left your system? Pause their subscription so it can serve someone else. Pausing never deletes anything — the Authority requires a six-year archive, and the invoice chain must not break: the business, its documents and its owner's access all remain, and reading their archive over the API keeps working too (every GET call keeps working for a paused merchant — exporting and auditing never stop); what stops is new invoicing and every action that creates something. Merchant came back? reinstate reactivates them on a free subscription and resumes their chain from where it stopped.

Usage limits

The partner API: 120 requests per minute. The invoicing API: 60 requests per minute on your key — enough for hundreds of points of sale in real use; and if you reach a volume beyond it, write to us and we will raise it for you.

Ready to start? Write to us at [email protected] — we agree the subscriptions, you receive your key, and your first merchant is issuing within a day.