Invoices
Submit and manage ZATCA-compliant invoices: simplified (B2C), standard (B2B), credit notes, and debit notes. All create endpoints support the Idempotency-Key header to safely retry requests.
EGS unit: You can send egs_unit_id in every request (one key, multiple units), or omit it and use an API key that has a default EGS unit set in the dashboard. The unit must match your API key environment (sandbox key → sandbox unit, production key → production unit). See Flow & integration.
Submit simplified (B2C) invoice
Creates a simplified tax invoice (B2C). Reported to ZATCA asynchronously. Returns invoice_id, uuid, status, amounts, QR code, pdf_url, and report_deadline.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| egs_unit_id | string | No | Active EGS unit ID. Omit if your API key has a default EGS unit set. |
| invoice_number | string | Yes | — |
| invoice_date | string | Yes | YYYY-MM-DD |
| invoice_time | string | Yes | HH:mm:ss |
| seller | SellerDto | Yes | — |
| line_items | LineItemDto[] | Yes | — |
| currency | string | No | Default: SAR |
| Field | Type | Required | Description |
|---|---|---|---|
| name | string | Yes | Seller legal name |
| name_ar | string | No | Arabic name |
| vat_number | string | Yes | VAT registration number |
| address | AddressDto | Yes | Seller address |
| Field | Type | Required | Description |
|---|---|---|---|
| street | string | Yes | — |
| city | string | Yes | — |
| postal_code | string | No | — |
| country | string | Yes | — |
| Field | Type | Required | Description |
|---|---|---|---|
| description | string | Yes | Item description |
| description_ar | string | No | — |
| quantity | number | Yes | Min 0.0001 |
| unit_price | number | Yes | In SAR |
| vat_category | "S" | "Z" | "E" | "O" | No | VAT category code |
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',
address: { street: 'King Fahd Rd', city: 'Riyadh', 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"
}Error responses
- 400
{ "error": { "code": "EGS_UNIT_REQUIRED", "message": "Provide egs_unit_id in the request body or set a default EGS unit for this API key in the dashboard.", "retryable": false } } - 400
{ "error": { "code": "EGS_UNIT_NOT_FOUND", "message": "EGS unit not found", "retryable": false } } - 400
{ "error": { "code": "EGS_UNIT_ENV_MISMATCH", "message": "This API key is for production. Use an EGS unit in production or switch key.", "retryable": false } } - 400
{ "error": { "code": "EGS_NOT_READY", "message": "EGS unit onboarding not complete", "retryable": false } }
Submit standard (B2B) invoice
Creates a standard tax invoice (B2B). Cleared synchronously with ZATCA. Returns signed_xml_url and pdf_url so you can download the signed XML (ZATCA compliance) and a human-readable PDF.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| egs_unit_id | string | No | Omit if API key has a default EGS unit |
| invoice_number | string | Yes | — |
| invoice_date | string | Yes | — |
| invoice_time | string | Yes | — |
| seller | SellerDto | Yes | — |
| buyer | BuyerDto | Yes | Required for B2B |
| line_items | LineItemDto[] | Yes | — |
| currency | string | No | — |
| Field | Type | Required | Description |
|---|---|---|---|
| name | string | Yes | Seller legal name |
| name_ar | string | No | Arabic name |
| vat_number | string | Yes | VAT registration number |
| address | AddressDto | Yes | Seller address |
| Field | Type | Required | Description |
|---|---|---|---|
| street | string | Yes | — |
| city | string | Yes | — |
| postal_code | string | No | — |
| country | string | Yes | — |
| Field | Type | Required | Description |
|---|---|---|---|
| name | string | Yes | — |
| name_ar | string | No | — |
| vat_number | string | Yes | — |
| address | BuyerAddressDto | Yes | — |
| Field | Type | Required | Description |
|---|---|---|---|
| street | string | Yes | — |
| city | string | Yes | — |
| postal_code | string | No | — |
| country | string | Yes | — |
| Field | Type | Required | Description |
|---|---|---|---|
| description | string | Yes | Item description |
| description_ar | string | No | — |
| quantity | number | Yes | Min 0.0001 |
| unit_price | number | Yes | In SAR |
| vat_category | "S" | "Z" | "E" | "O" | No | VAT category code |
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',
address: { street: 'King Fahd Rd', city: 'Riyadh', country: 'SA' },
},
buyer: {
name: 'Client Co',
vat_number: '300098765400003',
address: { street: 'Olaya St', city: 'Riyadh', 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"
}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.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| egs_unit_id | string | No | Omit if API key has a default EGS unit |
| invoice_number | string | Yes | — |
| invoice_date | string | Yes | — |
| invoice_time | string | Yes | — |
| original_invoice_uuid | string | Yes | UUID of the original invoice |
| reason | string | Yes | — |
| reason_ar | string | No | — |
| seller | SellerDto | Yes | — |
| buyer | BuyerDto | No | Required if original was B2B |
| line_items | LineItemDto[] | Yes | — |
| currency | string | No | — |
| Field | Type | Required | Description |
|---|---|---|---|
| name | string | Yes | Seller legal name |
| name_ar | string | No | Arabic name |
| vat_number | string | Yes | VAT registration number |
| address | AddressDto | Yes | Seller address |
| Field | Type | Required | Description |
|---|---|---|---|
| street | string | Yes | — |
| city | string | Yes | — |
| postal_code | string | No | — |
| country | string | Yes | — |
| Field | Type | Required | Description |
|---|---|---|---|
| name | string | Yes | — |
| name_ar | string | No | — |
| vat_number | string | Yes | — |
| address | BuyerAddressDto | Yes | — |
| Field | Type | Required | Description |
|---|---|---|---|
| street | string | Yes | — |
| city | string | Yes | — |
| postal_code | string | No | — |
| country | string | Yes | — |
| Field | Type | Required | Description |
|---|---|---|---|
| description | string | Yes | Item description |
| description_ar | string | No | — |
| quantity | number | Yes | Min 0.0001 |
| unit_price | number | Yes | In SAR |
| vat_category | "S" | "Z" | "E" | "O" | No | VAT category code |
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: '...', address: {...} },
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
}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).
Request body
| Field | Type | Required | Description |
|---|---|---|---|
| egs_unit_id | string | No | Omit if API key has a default EGS unit |
| invoice_number | string | Yes | — |
| invoice_date | string | Yes | — |
| invoice_time | string | Yes | — |
| original_invoice_uuid | string | Yes | — |
| reason | string | Yes | — |
| reason_ar | string | No | — |
| seller | SellerDto | Yes | — |
| buyer | BuyerDto | No | — |
| line_items | LineItemDto[] | Yes | — |
| currency | string | No | — |
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: '...', address: {...} },
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
}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"
}
],
"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 (pending → submitted → reported → cleared).
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",
"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",
"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"
}
]
}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)