Developers

API reference: Invoices

Generated from the published OpenAPI document (version 1.4.0). https://fatoora.tw.sa/api/v1

GET /invoices

List invoices

Requires scope invoices:read

Tax invoices only, newest first.

Parameters

Field Type Description
limit integer · query Page size.
cursor string · query meta.pagination.next_cursor of the previous page.
status array<string> · query One or more statuses (status[]=paid&status[]=partial, or comma-separated). overdue is worked out from the due date.
zatca_status array<string: pending, not_submitted, cleared, reported, rejected, retrying, unknown> · query One or more ZATCA statuses.
customer_id integer · query
issue_date_from string (date) · query
issue_date_to string (date) · query
updated_since string (date-time) · query
external_reference string · query
number string · query

Responses

  • 200 A page of invoices.
  • default An error. error.code says which.
POST /invoices

Create an invoice, as a draft or issued at once

Requires scope invoices:draft

Issues the invoice unless issue is false; issuing also needs the invoices:issue scope. Counts against the plan's monthly invoice allowance. Sending your own external_reference (an order number) makes the call safe to retry: a repeat answers 200 with replayed: true and the invoice already made. A key without invoices:read is told only the invoice's id, number and status. Paid already, through your own payment gateway or till? Send payment and the payment is recorded in the same call, so the invoice arrives paid (or partial). It needs the payments:write scope and an issued invoice; amount defaults to the whole total. If the payment is refused, nothing is created.

Parameters

Field Type Description
Idempotency-Key string · header 8 to 255 visible ASCII characters, unique per operation you mean to perform.

Request body application/json

Field Type Description
issue_date required string (date)
due_date string | null (date)
issue boolean | null false creates a draft.
currency string | null Defaults to SAR.
exchange_rate number | null SAR per unit of currency; required for a foreign currency.
locale string | null: ar, en, null
notes string | null
terms string | null
external_reference string | null Your own id for the sale.
source_system string | null
document_discount_type string | null: amount, percent, null
document_discount_value number | null
payment object | null Paid already: recorded with the invoice, which then arrives paid (or partial). Needs payments:write; not with issue: false.
payment.amount number | null Defaults to the invoice total.
payment.method string | null cash (default), bank_transfer, mada, visa_mastercard, apple_pay, stc_pay, cheque, other, or a code the company added.
payment.reference string | null Your gateway transaction id, for example.
payment.paid_on string | null (date) Defaults to today; never later than today.
payment.notes string | null
payment.account_id integer | null The company cash or bank account the money went into.
customer object | null An existing customer by id, or a new one by its details.
customer.id integer | null
customer.name string | null
customer.vat_number string | null
customer.type string | null: individual, business, null Defaults to business with a VAT number, individual without. A Saudi business gets a standard invoice even without a VAT number.
customer.cr_number string | null Commercial registration; 10 digits for a Saudi buyer.
customer.email string | null
customer.phone string | null
customer.address_line1 string | null
customer.address_line2 string | null
customer.building_number string | null National address building number; 4 digits for a Saudi buyer.
customer.additional_number string | null National address additional number; 4 digits for a Saudi buyer.
customer.district string | null
customer.city string | null
customer.country string | null
customer.postal_code string | null
items required array<object>
items[].description required string
items[].quantity required number
items[].unit_price required number
items[].unit string | null
items[].discount number | null
items[].vat_rate number | null
items[].tax_rate_id integer | null
items[].product_id integer | null
items[].vat_exemption_code string | null
items[].vat_exemption_reason string | null

Responses

  • 201 The invoice was created.
  • 200 A repeat of an external_reference already invoiced.
  • default An error. error.code says which.
curl -X POST https://fatoora.tw.sa/api/v1/invoices \
  -H "Authorization: Bearer wbk_test_xxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{
  "issue": false,
  "issue_date": "2026-11-03",
  "customer": {
    "name": "Example Trading Co.",
    "vat_number": "300000000000003",
    "address_line1": "King Fahd Rd",
    "city": "Riyadh",
    "postal_code": "12345",
    "country": "SA"
  },
  "items": [
    { "description": "Consulting", "quantity": 2, "unit_price": 500, "vat_rate": 15 }
  ]
}'
<?php
$response = $client->post('invoices', [
    'json' => [
        'issue'      => false,          // a draft: no legal number, nothing sent to ZATCA
        'issue_date' => '2026-11-03',
        'customer'   => [
            'name'          => 'Example Trading Co.',
            'vat_number'    => '300000000000003',
            'address_line1' => 'King Fahd Rd',
            'city'          => 'Riyadh',
            'postal_code'   => '12345',
            'country'       => 'SA',
        ],
        'items' => [
            ['description' => 'Consulting', 'quantity' => 2, 'unit_price' => 500, 'vat_rate' => 15],
        ],
    ],
    'http_errors' => false,
]);

$body = json_decode((string) $response->getBody(), true);
if ($body['ok']) {
    echo $body['data']['invoice']['number']; // DRAFT-… until it is issued
} else {
    echo $body['error']['code'];
}
const res = await fetch("https://fatoora.tw.sa/api/v1/invoices", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.WONDERFATOORA_API_KEY}`,
    "Content-Type": "application/json",
    Accept: "application/json",
  },
  body: JSON.stringify({
    issue: false, // a draft: no legal number, nothing sent to ZATCA
    issue_date: "2026-11-03",
    customer: {
      name: "Example Trading Co.",
      vat_number: "300000000000003",
      address_line1: "King Fahd Rd",
      city: "Riyadh",
      postal_code: "12345",
      country: "SA",
    },
    items: [{ description: "Consulting", quantity: 2, unit_price: 500, vat_rate: 15 }],
  }),
});

const body = await res.json();
if (!body.ok) throw new Error(`${body.error.code}: ${body.error.message}`);
console.log(body.data.invoice.id, body.data.invoice.number);
res = requests.post(
    "https://fatoora.tw.sa/api/v1/invoices",
    headers={
        "Authorization": f"Bearer {os.environ['WONDERFATOORA_API_KEY']}",
        "Accept": "application/json",
    },
    json={
        "issue": False,  # a draft: no legal number, nothing sent to ZATCA
        "issue_date": "2026-11-03",
        "customer": {
            "name": "Example Trading Co.",
            "vat_number": "300000000000003",
            "address_line1": "King Fahd Rd",
            "city": "Riyadh",
            "postal_code": "12345",
            "country": "SA",
        },
        "items": [
            {"description": "Consulting", "quantity": 2, "unit_price": 500, "vat_rate": 15}
        ],
    },
    timeout=30,
)
body = res.json()
print(res.status_code, body["data"]["invoice"]["number"] if body["ok"] else body["error"])
GET /invoices/{invoice}

Get an invoice

Requires scope invoices:read

Parameters

Field Type Description
invoice required integer · path

Responses

  • 200 The invoice.
  • default An error. error.code says which.
curl https://fatoora.tw.sa/api/v1/invoices/1024 \
  -H "Authorization: Bearer wbk_test_xxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "Accept: application/json"
<?php
$response = $client->get('invoices/1024', ['http_errors' => false]);
$invoice = json_decode((string) $response->getBody(), true)['data']['invoice'] ?? null;

if ($invoice && $invoice['zatca']['deliverable']) {
    // Safe to hand to the buyer: send $invoice['public_url'].
}
const res = await fetch("https://fatoora.tw.sa/api/v1/invoices/1024", {
  headers: {
    Authorization: `Bearer ${process.env.WONDERFATOORA_API_KEY}`,
    Accept: "application/json",
  },
});
const { ok, data, error } = await res.json();
if (ok && data.invoice.zatca.deliverable) {
  // Safe to hand to the buyer: data.invoice.public_url
}
res = requests.get(
    "https://fatoora.tw.sa/api/v1/invoices/1024",
    headers={"Authorization": f"Bearer {os.environ['WONDERFATOORA_API_KEY']}",
             "Accept": "application/json"},
    timeout=30,
)
invoice = res.json().get("data", {}).get("invoice")
if invoice and invoice["zatca"]["deliverable"]:
    print(invoice["public_url"])
GET /invoices/{invoice}/status

Where an invoice stands

Requires scope invoices:read

Its status, payments and ZATCA status, without its lines. Made for polling.

Parameters

Field Type Description
invoice required integer · path

Responses

  • 200 The status.
  • default An error. error.code says which.
POST /invoices/{invoice}/validate

Check whether a draft can be issued

Requires scope invoices:draft

Runs every check issuing runs, and changes nothing.

Parameters

Field Type Description
invoice required integer · path

Responses

  • 200 Whether it can be issued, and what is missing.
  • default An error. error.code says which.
POST /invoices/{invoice}/issue

Issue a draft

Requires scope invoices:issue

Gives the draft its tax-invoice number and QR and, for a company linked to ZATCA, submits it. Irreversible: an issued invoice is corrected with a credit or debit note. An invoice already issued answers 200 with already_issued: true.

Parameters

Field Type Description
invoice required integer · path
Idempotency-Key string · header 8 to 255 visible ASCII characters, unique per operation you mean to perform.

Responses

  • 200 The issued invoice.
  • default An error. error.code says which.
GET /invoices/{invoice}/pdf

Download the invoice PDF

Requires scope invoices:download

A standard invoice ZATCA has not cleared yet prints without a QR and is marked as not yet valid.

Parameters

Field Type Description
invoice required integer · path

Responses

  • 200 The PDF.
  • default An error. error.code says which.
GET /invoices/{invoice}/xml

Download the legal XML

Requires scope invoices:download

The document ZATCA holds: the cleared XML of a standard invoice, the signed XML of a simplified one. document_not_available (409) until there is one; xml_not_applicable (404) for a company not linked to ZATCA.

Parameters

Field Type Description
invoice required integer · path

Responses

  • 200 The XML.
  • default An error. error.code says which.
GET /invoices/{invoice}/payments

Customer payments of an invoice

Requires scope invoices:read

Parameters

Field Type Description
invoice required integer · path

Responses

  • 200 The payments, oldest first, and the invoice balance.
  • default An error. error.code says which.
POST /invoices/{invoice}/payments

Record a customer payment on an issued invoice

Requires scope payments:write

For money received after the invoice was made (a bank transfer, a late confirmation from your gateway). Never more than the balance still owed (payment_exceeds_balance), never on a draft, cancelled or refunded invoice (invoice_not_payable). Paid before the invoice was made? Send payment with POST /invoices instead.

Parameters

Field Type Description
invoice required integer · path
Idempotency-Key string · header 8 to 255 visible ASCII characters, unique per operation you mean to perform.

Request body application/json

Field Type Description
amount required number In the invoice currency; at most the balance.
method string | null cash (default), bank_transfer, mada, visa_mastercard, apple_pay, stc_pay, cheque, other, or a code the company added.
reference string | null
paid_on string | null (date) Defaults to today; never later than today.
notes string | null
account_id integer | null The company cash or bank account the money went into.

Responses

  • 201 The payment and the invoice new balance.
  • default An error. error.code says which.

Back to the API reference

Ready to build?

API access is part of the Business and Enterprise plans. Create a key in Settings → API keys and make your first call in minutes.