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

We sign and persist first, then call Fatoora inline. 200 = finished (reported / cleared). 202 = queued — poll GET or wait for webhooks. Details: Understanding responses.
Accepted with warnings (still HTTP 200) — do not resubmit
{
  "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.

POST/v1/invoices/simplified#

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.

Requires API Key (Bearer)Supports Idempotency-Key header

Request body

CreateSimplifiedInvoiceDto
AttributeTypePresenceDescription
egs_unit_idstringRequiredRequired. Active EGS unit ID for the issuing branch or POS device.
invoice_numberstringRequired
invoice_datestringRequiredYYYY-MM-DD
invoice_timestringRequiredHH:mm:ss
sellerSellerDtoRequired
line_itemsLineItemDto[]Required
currencystringOptionalDefault: SAR
Child object: SellerDto
AttributeTypePresenceDescription
namestringRequiredSeller legal name
name_arstringOptionalArabic name
vat_numberstringRequired15-digit VAT registration number (PartyTaxScheme CompanyID)
cr_numberstringOptionalCommercial 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.
addressAddressDtoOptionalOptional when a Saudi address is on the company profile (invoice overrides profile per field). Otherwise MISSING_SELLER_ADDRESS.
Child object: AddressDto
AttributeTypePresenceDescription
streetstringRequiredStreet name
building_numberstringOptionalZATCA expects 4 digits for SA (BR-KSA-37). Omitted or invalid values pass through to Fatoora.
districtstringOptionalDistrict / CitySubdivisionName. Recommended for SA.
citystringRequired
postal_codestringOptionalZATCA expects 5 digits for SA (BR-KSA-66). Invalid formats pass through to Fatoora.
countrystringRequiredISO code, e.g. "SA"
Child object: LineItemDto
AttributeTypePresenceDescription
descriptionstringRequiredItem description
description_arstringOptional
quantitynumberRequiredMin 0.0001
unit_pricenumberRequiredIn SAR
vat_category"S" | "Z" | "E" | "O"OptionalS = 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_codestringOptionalRecommended 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
      }
    }
POST/v1/invoices/standard#

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.

Requires API Key (Bearer)Supports Idempotency-Key header

Request body

CreateStandardInvoiceDto
AttributeTypePresenceDescription
egs_unit_idstringRequiredRequired. Active EGS unit ID for the issuing branch or POS device.
invoice_numberstringRequired
invoice_datestringRequired
invoice_timestringRequired
sellerSellerDtoRequired
buyerBuyerDtoRequiredRequired for B2B
line_itemsLineItemDto[]Required
currencystringOptional
Child object: SellerDto
AttributeTypePresenceDescription
namestringRequiredSeller legal name
name_arstringOptionalArabic name
vat_numberstringRequired15-digit VAT registration number (PartyTaxScheme CompanyID)
cr_numberstringOptionalCommercial 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.
addressAddressDtoOptionalOptional when a Saudi address is on the company profile (invoice overrides profile per field). Otherwise MISSING_SELLER_ADDRESS.
Child object: AddressDto
AttributeTypePresenceDescription
streetstringRequiredStreet name
building_numberstringOptionalZATCA expects 4 digits for SA (BR-KSA-37). Omitted or invalid values pass through to Fatoora.
districtstringOptionalDistrict / CitySubdivisionName. Recommended for SA.
citystringRequired
postal_codestringOptionalZATCA expects 5 digits for SA (BR-KSA-66). Invalid formats pass through to Fatoora.
countrystringRequiredISO code, e.g. "SA"
Child object: BuyerDto
AttributeTypePresenceDescription
namestringRequired
name_arstringOptional
vat_numberstringRequired
cr_numberstringOptionalBuyer Commercial Registration Number (PartyIdentification schemeID=CRN). VAT goes in PartyTaxScheme.
addressBuyerAddressDtoRequired
Child object: BuyerAddressDto
AttributeTypePresenceDescription
streetstringRequired
building_numberstringOptionalZATCA expects 4 digits for SA (BR-KSA-37). Omitted or invalid values pass through to Fatoora.
districtstringOptionalDistrict / CitySubdivisionName. Recommended for SA.
citystringRequired
postal_codestringOptionalZATCA expects 5 digits for SA (BR-KSA-66). Invalid formats pass through to Fatoora.
countrystringRequired
Child object: LineItemDto
AttributeTypePresenceDescription
descriptionstringRequiredItem description
description_arstringOptional
quantitynumberRequiredMin 0.0001
unit_pricenumberRequiredIn SAR
vat_category"S" | "Z" | "E" | "O"OptionalS = 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_codestringOptionalRecommended 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
      }
    }
POST/v1/invoices/credit-note#

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.

Requires API Key (Bearer)Supports Idempotency-Key header

Request body

CreateCreditNoteDto
AttributeTypePresenceDescription
egs_unit_idstringRequiredRequired. Active EGS unit ID for the issuing branch or POS device.
invoice_numberstringRequired
invoice_datestringRequired
invoice_timestringRequired
original_invoice_uuidstringRequiredUUID of the original invoice
reasonstringRequired
reason_arstringOptional
sellerSellerDtoRequired
buyerBuyerDtoOptionalRequired if original was B2B
line_itemsLineItemDto[]Required
currencystringOptional
Child object: SellerDto
AttributeTypePresenceDescription
namestringRequiredSeller legal name
name_arstringOptionalArabic name
vat_numberstringRequired15-digit VAT registration number (PartyTaxScheme CompanyID)
cr_numberstringOptionalCommercial 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.
addressAddressDtoOptionalOptional when a Saudi address is on the company profile (invoice overrides profile per field). Otherwise MISSING_SELLER_ADDRESS.
Child object: AddressDto
AttributeTypePresenceDescription
streetstringRequiredStreet name
building_numberstringOptionalZATCA expects 4 digits for SA (BR-KSA-37). Omitted or invalid values pass through to Fatoora.
districtstringOptionalDistrict / CitySubdivisionName. Recommended for SA.
citystringRequired
postal_codestringOptionalZATCA expects 5 digits for SA (BR-KSA-66). Invalid formats pass through to Fatoora.
countrystringRequiredISO code, e.g. "SA"
Child object: BuyerDto
AttributeTypePresenceDescription
namestringRequired
name_arstringOptional
vat_numberstringRequired
cr_numberstringOptionalBuyer Commercial Registration Number (PartyIdentification schemeID=CRN). VAT goes in PartyTaxScheme.
addressBuyerAddressDtoRequired
Child object: BuyerAddressDto
AttributeTypePresenceDescription
streetstringRequired
building_numberstringOptionalZATCA expects 4 digits for SA (BR-KSA-37). Omitted or invalid values pass through to Fatoora.
districtstringOptionalDistrict / CitySubdivisionName. Recommended for SA.
citystringRequired
postal_codestringOptionalZATCA expects 5 digits for SA (BR-KSA-66). Invalid formats pass through to Fatoora.
countrystringRequired
Child object: LineItemDto
AttributeTypePresenceDescription
descriptionstringRequiredItem description
description_arstringOptional
quantitynumberRequiredMin 0.0001
unit_pricenumberRequiredIn SAR
vat_category"S" | "Z" | "E" | "O"OptionalS = 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_codestringOptionalRecommended 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
      }
    }
POST/v1/invoices/debit-note#

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.

Requires API Key (Bearer)Supports Idempotency-Key header

Request body

CreateDebitNoteDto
AttributeTypePresenceDescription
egs_unit_idstringRequiredRequired. Active EGS unit ID for the issuing branch or POS device.
invoice_numberstringRequired
invoice_datestringRequired
invoice_timestringRequired
original_invoice_uuidstringRequired
reasonstringRequired
reason_arstringOptional
sellerSellerDtoRequired
buyerBuyerDtoOptional
line_itemsLineItemDto[]Required
currencystringOptional

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
      }
    }
GET/v1/invoices#

List invoices (cursor pagination)

Returns a paginated list of invoices. Filter by status, invoice_type, document_type, date range, and egs_unit_id.

Requires API Key (Bearer)

Query parameters

NameTypeRequiredDescription
cursorstringNoPagination cursor from previous response
limitnumberNoPage size
statusstringNoFilter by status
invoice_typestringNosimplified | standard
document_typestringNoinvoice | credit_note | debit_note
date_fromstringNoYYYY-MM-DD
date_tostringNoYYYY-MM-DD
egs_unit_idstringNoFilter 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
  }
}
GET/v1/invoices/:id#

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.

Requires API Key (Bearer)

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" }
  ]
}
GET/v1/invoices/:id/xml#

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).

Requires API Key (Bearer)

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."
    }
GET/v1/invoices/:id/pdf#

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.

Requires API Key (Bearer)

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)