Generated from the published OpenAPI document (version 1.4.0). https://fatoora.tw.sa/api/v1
/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.codesays which.
/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_referencealready invoiced. -
default
An error.
error.codesays 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"])
/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.codesays 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"])
/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.codesays which.
/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.codesays which.
/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.codesays which.
/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.codesays which.
/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.codesays which.
/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.codesays which.
/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.codesays which.
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.