Invoices
Create simplified (B2C), standard (B2B), credit, and debit documents. Every create requires egs_unit_id and supports Idempotency-Key.
Before integrating endpoints, read Responses (200 / 202 / warnings) and Errors. Request shapes: API objects. Branch setup: Quickstart.
Hybrid submit
{
"status": "reported",
"zatca_compliant": false,
"zatca_warning_count": 1,
"zatca_warnings": [
{
"code": "BR-KSA-37",
"message": "The seller address building number must contain 4 digits."
}
],
"resubmit": false
}Endpoints
Jump to an operation — or use the nested links under Invoices in the left nav.
Submit simplified (B2C) invoice
Creates a simplified tax invoice (B2C). Signs and persists first, then reports to ZATCA. Returns HTTP 200 when reported inline, or HTTP 202 when queued. Always includes QR code, pdf_url, and report_deadline when applicable. See /docs/responses for compliance fields.
Request body
| Attribute | Type | Presence | Description |
|---|---|---|---|
| egs_unit_id | string | Required | Required. Active EGS unit ID for the issuing branch or POS device. |
| invoice_number | string | Required | — |
| invoice_date | string | Required | YYYY-MM-DD |
| invoice_time | string | Required | HH:mm:ss |
| seller | SellerDto | Required | — |
| line_items | LineItemDto[] | Required | — |
| currency | string | Optional | Default: SAR |
| Attribute | Type | Presence | Description |
|---|---|---|---|
| name | string | Required | Seller legal name |
| name_ar | string | Optional | Arabic name |
| vat_number | string | Required | 15-digit VAT registration number (PartyTaxScheme CompanyID) |
| cr_number | string | Optional | Commercial Registration Number for PartyIdentification schemeID=CRN. Do not send the VAT number here. If omitted, company-profile cr_number is used; one of the two is required. |
| address | AddressDto | Optional | Optional when a Saudi address is on the company profile (invoice overrides profile per field). Otherwise MISSING_SELLER_ADDRESS. |
| Attribute | Type | Presence | Description |
|---|---|---|---|
| street | string | Required | Street name |
| building_number | string | Optional | ZATCA expects 4 digits for SA (BR-KSA-37). Omitted or invalid values pass through to Fatoora. |
| district | string | Optional | District / CitySubdivisionName. Recommended for SA. |
| city | string | Required | — |
| postal_code | string | Optional | ZATCA expects 5 digits for SA (BR-KSA-66). Invalid formats pass through to Fatoora. |
| country | string | Required | ISO code, e.g. "SA" |
| Attribute | Type | Presence | Description |
|---|---|---|---|
| description | string | Required | Item description |
| description_ar | string | Optional | — |
| quantity | number | Required | Min 0.0001 |
| unit_price | number | Required | In SAR |
| vat_category | "S" | "Z" | "E" | "O" | Optional | S = 15% standard; Z/E/O = 0%. Default S. For Z/E/O, send vat_exemption_code (ZATCA VATEX-SA-*); missing/wrong codes are validated by Fatoora (warnings/errors), not API validation errors. |
| vat_exemption_code | string | Optional | Recommended when vat_category is Z, E, or O; code should match category (e.g. Z→VATEX-SA-32, E→VATEX-SA-29). Missing/wrong codes pass through to Fatoora. |
Code examples
const res = await fetch('https://api.esnadapi.com/v1/invoices/simplified', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${apiKey}`,
'Idempotency-Key': 'unique-key-123',
},
body: JSON.stringify({
egs_unit_id: egsUnitId,
invoice_number: 'INV-001',
invoice_date: '2025-01-15',
invoice_time: '14:30:00',
seller: {
name: 'Acme Ltd',
vat_number: '300012345600003',
cr_number: '1010010000',
address: { street: 'King Fahd Rd', building_number: '1234', district: 'Al Olaya', city: 'Riyadh', postal_code: '12271', country: 'SA' },
},
line_items: [
{ description: 'Product A', quantity: 2, unit_price: 50.0 },
],
}),
});
const invoice = await res.json();Success response (200)
{
"invoice_id": "uuid",
"uuid": "invoice-uuid",
"status": "reported",
"subtotal": 100,
"vat": 15,
"total": 115,
"invoice_date_hijri": "1446-07-15",
"qr_code": "base64-encoded-qr",
"reported_at": "2025-01-15T10:05:00.000Z",
"report_deadline": "2025-01-16T10:00:00.000Z",
"pdf_url": "/v1/invoices/uuid/pdf",
"zatca_warning_count": 0,
"zatca_warnings": [],
"zatca_compliant": true,
"resubmit": true
}Error responses
- 400
{ "error": { "code": "EGS_UNIT_REQUIRED", "message": "egs_unit_id is required in the request body so the invoice is attributed to the correct EGS unit (branch/POS).", "retryable": false } } - 400
{ "error": { "code": "EGS_NOT_READY", "message": "EGS unit onboarding not complete", "retryable": false } } - 400
{ "error": { "code": "SELLER_VAT_MISMATCH", "message": "seller.vat_number must match the company profile VAT.", "retryable": false } } - 400
{ "error": { "code": "SELLER_CRN_REQUIRED", "message": "seller.cr_number (Commercial Registration Number) is required for ZATCA. Set it on the invoice seller object, or save cr_number on the company profile.", "retryable": false } }
Submit standard (B2B) invoice
Creates a standard tax invoice (B2B). Signs and persists first, then requests ZATCA clearance. Returns HTTP 200 when cleared inline, or HTTP 202 when queued. Do not deliver to the buyer until status is cleared. Includes signed_xml_url and pdf_url.
Request body
| Attribute | Type | Presence | Description |
|---|---|---|---|
| egs_unit_id | string | Required | Required. Active EGS unit ID for the issuing branch or POS device. |
| invoice_number | string | Required | — |
| invoice_date | string | Required | — |
| invoice_time | string | Required | — |
| seller | SellerDto | Required | — |
| buyer | BuyerDto | Required | Required for B2B |
| line_items | LineItemDto[] | Required | — |
| currency | string | Optional | — |
| Attribute | Type | Presence | Description |
|---|---|---|---|
| name | string | Required | Seller legal name |
| name_ar | string | Optional | Arabic name |
| vat_number | string | Required | 15-digit VAT registration number (PartyTaxScheme CompanyID) |
| cr_number | string | Optional | Commercial Registration Number for PartyIdentification schemeID=CRN. Do not send the VAT number here. If omitted, company-profile cr_number is used; one of the two is required. |
| address | AddressDto | Optional | Optional when a Saudi address is on the company profile (invoice overrides profile per field). Otherwise MISSING_SELLER_ADDRESS. |
| Attribute | Type | Presence | Description |
|---|---|---|---|
| street | string | Required | Street name |
| building_number | string | Optional | ZATCA expects 4 digits for SA (BR-KSA-37). Omitted or invalid values pass through to Fatoora. |
| district | string | Optional | District / CitySubdivisionName. Recommended for SA. |
| city | string | Required | — |
| postal_code | string | Optional | ZATCA expects 5 digits for SA (BR-KSA-66). Invalid formats pass through to Fatoora. |
| country | string | Required | ISO code, e.g. "SA" |
| Attribute | Type | Presence | Description |
|---|---|---|---|
| name | string | Required | — |
| name_ar | string | Optional | — |
| vat_number | string | Required | — |
| cr_number | string | Optional | Buyer Commercial Registration Number (PartyIdentification schemeID=CRN). VAT goes in PartyTaxScheme. |
| address | BuyerAddressDto | Required | — |
| Attribute | Type | Presence | Description |
|---|---|---|---|
| street | string | Required | — |
| building_number | string | Optional | ZATCA expects 4 digits for SA (BR-KSA-37). Omitted or invalid values pass through to Fatoora. |
| district | string | Optional | District / CitySubdivisionName. Recommended for SA. |
| city | string | Required | — |
| postal_code | string | Optional | ZATCA expects 5 digits for SA (BR-KSA-66). Invalid formats pass through to Fatoora. |
| country | string | Required | — |
| Attribute | Type | Presence | Description |
|---|---|---|---|
| description | string | Required | Item description |
| description_ar | string | Optional | — |
| quantity | number | Required | Min 0.0001 |
| unit_price | number | Required | In SAR |
| vat_category | "S" | "Z" | "E" | "O" | Optional | S = 15% standard; Z/E/O = 0%. Default S. For Z/E/O, send vat_exemption_code (ZATCA VATEX-SA-*); missing/wrong codes are validated by Fatoora (warnings/errors), not API validation errors. |
| vat_exemption_code | string | Optional | Recommended when vat_category is Z, E, or O; code should match category (e.g. Z→VATEX-SA-32, E→VATEX-SA-29). Missing/wrong codes pass through to Fatoora. |
Code examples
const res = await fetch('https://api.esnadapi.com/v1/invoices/standard', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${apiKey}`,
'Idempotency-Key': 'unique-key-456',
},
body: JSON.stringify({
egs_unit_id: egsUnitId,
invoice_number: 'INV-002',
invoice_date: '2025-01-15',
invoice_time: '14:30:00',
seller: {
name: 'Acme Ltd',
vat_number: '300012345600003',
cr_number: '1010010000',
address: { street: 'King Fahd Rd', building_number: '1234', district: 'Al Olaya', city: 'Riyadh', postal_code: '12271', country: 'SA' },
},
buyer: {
name: 'Client Co',
vat_number: '300098765400003',
address: { street: 'Olaya St', building_number: '2322', district: 'Al Murabba', city: 'Riyadh', postal_code: '12271', country: 'SA' },
},
line_items: [
{ description: 'Service B', quantity: 1, unit_price: 115.0 },
],
}),
});
const invoice = await res.json();Success response (200)
{
"invoice_id": "uuid",
"uuid": "invoice-uuid",
"status": "cleared",
"subtotal": 100,
"vat": 15,
"total": 115,
"qr_code": null,
"signed_xml_url": "/v1/invoices/uuid/xml",
"pdf_url": "/v1/invoices/uuid/pdf",
"cleared_at": "2025-01-15T10:05:00.000Z",
"zatca_compliant": true,
"zatca_warning_count": 0,
"zatca_warnings": [],
"resubmit": true
}Error responses
- 400
{ "error": { "code": "EGS_UNIT_NOT_FOUND", "message": "EGS unit not found", "retryable": false } } - 400
{ "error": { "code": "EGS_NOT_READY", "message": "EGS unit onboarding not complete", "retryable": false } }
Submit credit note
Creates a credit note referencing an original invoice. Required: original_invoice_uuid, reason, seller, line_items. If the original was B2B, buyer is required. Credit note total must not exceed original invoice total. Same hybrid submit as other creates: HTTP 200 when reported/cleared inline, HTTP 202 when queued.
Request body
| Attribute | Type | Presence | Description |
|---|---|---|---|
| egs_unit_id | string | Required | Required. Active EGS unit ID for the issuing branch or POS device. |
| invoice_number | string | Required | — |
| invoice_date | string | Required | — |
| invoice_time | string | Required | — |
| original_invoice_uuid | string | Required | UUID of the original invoice |
| reason | string | Required | — |
| reason_ar | string | Optional | — |
| seller | SellerDto | Required | — |
| buyer | BuyerDto | Optional | Required if original was B2B |
| line_items | LineItemDto[] | Required | — |
| currency | string | Optional | — |
| Attribute | Type | Presence | Description |
|---|---|---|---|
| name | string | Required | Seller legal name |
| name_ar | string | Optional | Arabic name |
| vat_number | string | Required | 15-digit VAT registration number (PartyTaxScheme CompanyID) |
| cr_number | string | Optional | Commercial Registration Number for PartyIdentification schemeID=CRN. Do not send the VAT number here. If omitted, company-profile cr_number is used; one of the two is required. |
| address | AddressDto | Optional | Optional when a Saudi address is on the company profile (invoice overrides profile per field). Otherwise MISSING_SELLER_ADDRESS. |
| Attribute | Type | Presence | Description |
|---|---|---|---|
| street | string | Required | Street name |
| building_number | string | Optional | ZATCA expects 4 digits for SA (BR-KSA-37). Omitted or invalid values pass through to Fatoora. |
| district | string | Optional | District / CitySubdivisionName. Recommended for SA. |
| city | string | Required | — |
| postal_code | string | Optional | ZATCA expects 5 digits for SA (BR-KSA-66). Invalid formats pass through to Fatoora. |
| country | string | Required | ISO code, e.g. "SA" |
| Attribute | Type | Presence | Description |
|---|---|---|---|
| name | string | Required | — |
| name_ar | string | Optional | — |
| vat_number | string | Required | — |
| cr_number | string | Optional | Buyer Commercial Registration Number (PartyIdentification schemeID=CRN). VAT goes in PartyTaxScheme. |
| address | BuyerAddressDto | Required | — |
| Attribute | Type | Presence | Description |
|---|---|---|---|
| street | string | Required | — |
| building_number | string | Optional | ZATCA expects 4 digits for SA (BR-KSA-37). Omitted or invalid values pass through to Fatoora. |
| district | string | Optional | District / CitySubdivisionName. Recommended for SA. |
| city | string | Required | — |
| postal_code | string | Optional | ZATCA expects 5 digits for SA (BR-KSA-66). Invalid formats pass through to Fatoora. |
| country | string | Required | — |
| Attribute | Type | Presence | Description |
|---|---|---|---|
| description | string | Required | Item description |
| description_ar | string | Optional | — |
| quantity | number | Required | Min 0.0001 |
| unit_price | number | Required | In SAR |
| vat_category | "S" | "Z" | "E" | "O" | Optional | S = 15% standard; Z/E/O = 0%. Default S. For Z/E/O, send vat_exemption_code (ZATCA VATEX-SA-*); missing/wrong codes are validated by Fatoora (warnings/errors), not API validation errors. |
| vat_exemption_code | string | Optional | Recommended when vat_category is Z, E, or O; code should match category (e.g. Z→VATEX-SA-32, E→VATEX-SA-29). Missing/wrong codes pass through to Fatoora. |
Code examples
await fetch('https://api.esnadapi.com/v1/invoices/credit-note', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${apiKey}`,
'Idempotency-Key': 'credit-001',
},
body: JSON.stringify({
egs_unit_id: egsUnitId,
invoice_number: 'CN-001',
invoice_date: '2025-01-20',
invoice_time: '11:00:00',
original_invoice_uuid: 'original-invoice-uuid',
reason: 'Goods returned',
seller: { name: 'Acme Ltd', vat_number: '...', cr_number: '1010010000', address: { street: '...', building_number: '1234', district: '...', city: '...', postal_code: '...', country: 'SA' } },
line_items: [{ description: 'Refund Item A', quantity: 1, unit_price: 57.5 }],
}),
});Success response (200)
{
"invoice_id": "uuid",
"uuid": "credit-note-uuid",
"status": "reported",
"document_type": "credit_note",
"subtotal": 50,
"vat": 7.5,
"total": 57.5,
"reported_at": "2025-01-20T11:00:00.000Z",
"cleared_at": null,
"zatca_compliant": true,
"zatca_warning_count": 0,
"zatca_warnings": [],
"resubmit": true
}Error responses
- 404
{ "error": { "code": "ORIGINAL_INVOICE_NOT_FOUND", "message": "Original invoice not found", "retryable": false } } - 400
{ "error": { "code": "ORIGINAL_INVOICE_NOT_READY", "message": "Original invoice must be reported or cleared", "retryable": false } } - 400
{ "error": { "code": "BUYER_REQUIRED_FOR_B2B_NOTE", "message": "Buyer required when original invoice was B2B", "retryable": false } } - 400
{ "error": { "code": "CREDIT_NOTE_TOTAL_EXCEEDS_ORIGINAL", "message": "Credit note total must not exceed original invoice total", "retryable": false } }
Submit debit note
Same request body as credit note. Creates a debit note referencing the original invoice. Same validation rules apply (original must be reported/cleared; buyer required for B2B; total must not exceed original). Hybrid submit: HTTP 200 when reported/cleared inline, HTTP 202 when queued.
Request body
| Attribute | Type | Presence | Description |
|---|---|---|---|
| egs_unit_id | string | Required | Required. Active EGS unit ID for the issuing branch or POS device. |
| invoice_number | string | Required | — |
| invoice_date | string | Required | — |
| invoice_time | string | Required | — |
| original_invoice_uuid | string | Required | — |
| reason | string | Required | — |
| reason_ar | string | Optional | — |
| seller | SellerDto | Required | — |
| buyer | BuyerDto | Optional | — |
| line_items | LineItemDto[] | Required | — |
| currency | string | Optional | — |
Code examples
await fetch('https://api.esnadapi.com/v1/invoices/debit-note', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${apiKey}`,
'Idempotency-Key': 'debit-001',
},
body: JSON.stringify({
egs_unit_id: egsUnitId,
invoice_number: 'DN-001',
invoice_date: '2025-01-20',
invoice_time: '12:00:00',
original_invoice_uuid: 'original-invoice-uuid',
reason: 'Additional charges',
seller: { name: 'Acme Ltd', vat_number: '...', cr_number: '1010010000', address: { street: '...', building_number: '1234', district: '...', city: '...', postal_code: '...', country: 'SA' } },
line_items: [{ description: 'Extra fee', quantity: 1, unit_price: 23.0 }],
}),
});Success response (200)
{
"invoice_id": "uuid",
"uuid": "debit-note-uuid",
"status": "reported",
"document_type": "debit_note",
"subtotal": 20,
"vat": 3,
"total": 23,
"reported_at": "2025-01-20T12:00:00.000Z",
"cleared_at": null,
"zatca_compliant": true,
"zatca_warning_count": 0,
"zatca_warnings": [],
"resubmit": true
}Error responses
- 404
{ "error": { "code": "ORIGINAL_INVOICE_NOT_FOUND", "message": "Original invoice not found", "retryable": false } } - 400
{ "error": { "code": "ORIGINAL_INVOICE_NOT_READY", "message": "Original invoice must be reported or cleared", "retryable": false } } - 400
{ "error": { "code": "BUYER_REQUIRED_FOR_B2B_NOTE", "message": "Buyer required when original invoice was B2B", "retryable": false } }
List invoices (cursor pagination)
Returns a paginated list of invoices. Filter by status, invoice_type, document_type, date range, and egs_unit_id.
Query parameters
| Name | Type | Required | Description |
|---|---|---|---|
| cursor | string | No | Pagination cursor from previous response |
| limit | number | No | Page size |
| status | string | No | Filter by status |
| invoice_type | string | No | simplified | standard |
| document_type | string | No | invoice | credit_note | debit_note |
| date_from | string | No | YYYY-MM-DD |
| date_to | string | No | YYYY-MM-DD |
| egs_unit_id | string | No | Filter by EGS unit |
Code examples
const res = await fetch(
`${BASE}/v1/invoices?limit=20&status=reported&egs_unit_id=${egsUnitId}`,
{ headers: { Authorization: `Bearer ${apiKey}` } }
);
const { data, pagination } = await res.json();Success response (200)
{
"data": [
{
"invoice_id": "uuid",
"uuid": "invoice-uuid",
"invoice_number": "INV-001",
"invoice_type": "simplified",
"document_type": "invoice",
"status": "reported",
"total": 115,
"invoice_date": "2025-01-15",
"created_at": "2025-01-15T10:00:00.000Z",
"environment": "sandbox",
"zatca_compliant": true,
"zatca_warning_count": 0,
"resubmit": true
}
],
"pagination": {
"limit": 20,
"next_cursor": "cursor-token",
"has_more": true,
"total_count": 150
}
}Invoice detail with timeline
Returns full invoice details including line items and status timeline. Always includes zatca_compliant, zatca_warning_count, zatca_warnings, and resubmit — treat reported/cleared with zatca_compliant: false as accepted-but-not-fully-compliant (do not resubmit). When submission is still in progress or failed, also returns retry_count, next_retry_at, and last_error — use these (or webhooks) after a create returned HTTP 202.
Code examples
const res = await fetch(`${BASE}/v1/invoices/${invoiceId}`, {
headers: { Authorization: `Bearer ${apiKey}` },
});
const invoice = await res.json();Success response (200)
{
"invoice_id": "uuid",
"uuid": "invoice-uuid",
"invoice_number": "INV-001",
"egs_unit_id": "egs-uuid",
"invoice_type": "simplified",
"document_type": "invoice",
"status": "reported",
"environment": "sandbox",
"subtotal": 100,
"vat": 15,
"total": 115,
"currency": "SAR",
"invoice_date": "2025-01-15",
"invoice_date_hijri": "1446-07-15",
"reported_at": "2025-01-15T10:05:00.000Z",
"cleared_at": null,
"report_deadline": "2025-01-16T10:00:00.000Z",
"retry_count": 0,
"next_retry_at": null,
"last_error": null,
"zatca_status": "REPORTED",
"zatca_compliant": true,
"zatca_warning_count": 0,
"zatca_warnings": [],
"resubmit": true,
"signed_xml_url": "/v1/invoices/uuid/xml",
"pdf_url": "/v1/invoices/uuid/pdf",
"qr_code": null,
"line_items": [
{
"line_number": 1,
"description": "Product A",
"quantity": 2,
"unit_price": 50,
"vat_rate": 15,
"line_total": 115
}
],
"timeline": [
{
"status": "pending",
"at": "2025-01-15T10:00:00.000Z"
},
{
"status": "submitted",
"at": "2025-01-15T10:00:05.000Z"
},
{
"status": "reported",
"at": "2025-01-15T10:05:00.000Z"
}
]
}Example when status is queued (after HTTP 202)
{
"invoice_id": "uuid",
"uuid": "invoice-uuid",
"status": "queued",
"retry_count": 2,
"next_retry_at": "2025-01-15T10:07:00.000Z",
"last_error": "Fatoora gateway timeout",
"reported_at": null,
"cleared_at": null,
"signed_xml_url": "/v1/invoices/uuid/xml",
"pdf_url": "/v1/invoices/uuid/pdf",
"qr_code": "base64-tlv...",
"timeline": [
{ "status": "pending", "at": "2025-01-15T10:00:00.000Z" },
{ "status": "queued", "at": "2025-01-15T10:00:01.000Z" }
]
}Download signed XML
Returns the signed UBL XML for the invoice (from S3 archive or DB when S3 is disabled). Response is application/xml with Content-Disposition attachment. Use for ZATCA compliance and archiving. signed_xml_url is null when XML is not stored (e.g. older invoices).
Code examples
const res = await fetch(`${BASE}/v1/invoices/${invoiceId}/xml`, {
headers: { Authorization: `Bearer ${apiKey}` },
});
const blob = await res.blob();
// Save or process blob (XML)Success response (200)
(Binary XML stream with Content-Type: application/xml)Error responses
- 404
{ "message": "Signed XML not available for this invoice." }
Download PDF
Returns a human-readable PDF of the invoice (seller, buyer if B2B, line items, totals). Use for display, printing, or sending to customers. For ZATCA compliance use the signed XML endpoint instead. Requires same Bearer token as other invoice endpoints.
Code examples
const res = await fetch(`${BASE}/v1/invoices/${invoiceId}/pdf`, {
headers: { Authorization: `Bearer ${apiKey}` },
});
const blob = await res.blob();
// e.g. save or open in new tab
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'invoice.pdf';
a.click();Success response (200)
(Binary PDF stream with Content-Type: application/pdf)