Report your first sandbox invoice¶
Pick Python, C#, or TypeScript and run one program that creates a simplified invoice for SAR 115, signs it, validates the signed XML, and reports it to ZATCA's Integration Sandbox. It saves the response and checks whether reporting was accepted. Each language follows the same steps and uses the same input file.
You need a sandbox certificate and its matching private key. Reporting also needs the sandbox token and secret issued for that device. Without credentials, you can run the local steps with the public test certificate below; that run sends nothing.
Where this fits in the ZATCA journey¶
An EGS is your electronic invoice generation system. Onboarding identifies each EGS unit and gives it credentials. Once onboarded, it can submit invoices and notes.
flowchart TD
A[Select environment and EGS] --> B[Private key, CSR and OTP]
B --> C[Compliance credentials]
C --> D[Check invoice and note samples]
D --> E{Checks pass?}
E -->|No| F[Correct and restart onboarding]
F --> B
E -->|Yes| G[Production-type credentials]
G --> H[Create invoice and record history]
H --> I{Subtype}
I -->|Simplified| J[Sign and validate]
J --> K[Issue to customer and report]
I -->|Standard| L[Prepare XML and validate]
L --> M[Request clearance before issue]
K --> N{API outcome}
M --> N
N -->|Accepted| O[Store XML and response]
O --> P[Standard only: deliver cleared XML]
N -->|Rejected| Q[Follow correction procedure]
N -->|Unknown| R[Reconcile before retry]
Keep credentials under lifecycle management too: renew them before expiry and revoke them when the EGS unit is retired or its credentials must be withdrawn. A connection failure can leave the API outcome unknown.
The runnable examples cover the simplified branch: creation through reporting. In live operation, simplified documents are reported within 24 hours. Standard documents follow clearance before customer delivery; retain the returned cleared XML. These are separate submission paths. ZATCA describes both in sections 4.3.1 and 4.3.2 of its Detailed Technical Guidelines.
Local validation catches problems before an API call. ZATCA still makes the submission decision, and a sandbox result is only a test result.
1. Get the sandbox inputs¶
If you already have sandbox production-type credentials, continue to the files below. Otherwise, use the ZATCA Developer Portal to complete this sequence for your test EGS unit:
- Generate a private key and CSR using the non-production environment and your test seller identity. Keep the private key. The CSR guide has runnable commands; declare the document types your unit supports.
- Send the CSR with the sandbox's OTP through Compliance CSID (
POST /compliance). Retain the returned compliance token, secret, certificate, and request ID. - Sign and submit the required samples to Compliance Invoice
(
POST /compliance/invoices) using the compliance credentials. The checks cover invoice and credit/debit note types declared in the CSR. One successful invoice check does not complete all onboarding checks. - Use the compliance credentials and returned request ID to request
Production CSID (
POST /production/csids). Save that response's token and secret, and the issued certificate corresponding to your private key.
The library exposes these operations as post_csr_for_ccsid,
check_invoice_compliance, and post_ccsid_for_pcsid in Python, with corresponding
methods in the other bindings. ZATCA documents the
onboarding sequence in section 3.3 of the
Detailed Technical Guidelines.
“Production CSID” is the credential's role. A production-type CSID issued by the Integration Sandbox belongs to the sandbox. It cannot authenticate against the live production environment. See ZATCA's Developer Portal Manual.
Prepare these inputs:
| Input | What to use |
|---|---|
certificate.pem |
PEM certificate issued for the sandbox device. |
private-key.pem |
The private key used to create that device's CSR. |
ZATCA_TOKEN |
binarySecurityToken from the sandbox production-CSID response, unchanged. |
ZATCA_SECRET |
secret from that same response. |
input.json |
Seller details matching your test device, plus invoice identity and trusted predecessor context. |
The token is an API credential; the signing examples expect a PEM certificate file. Export the issued certificate in PEM format. A newly generated key will not match a certificate issued for an earlier CSR.
2. Download one complete example¶
Download one project and extract it into an empty working folder. Each archive contains the complete source, project configuration, and input.json. Open a terminal in that folder.
Edit the seller in input.json to match your sandbox onboarding identity. The
sample contains demonstration data. invoice_counter is a decimal string to
avoid JavaScript integer precision loss; use the next counter for your EGS unit.
The predecessor hash must come from your trusted invoice history. The sample
value is for the demonstration's initial invoice, not a value to reuse for every
invoice. Each run generates a new UUID and the current UTC issue time.
Use CPython 3.13 for this walkthrough. Version 0.5.0 publishes wheels for CPython 3.9–3.13; Python 3.14 needs a source build and is outside this setup. Individual files: main.py and requirements.txt.
python3.13 -m venv .venv
. .venv/bin/activate
pip install -r requirements.txt
On Windows PowerShell, create the environment with py -3.13 -m venv .venv
and activate it with .venv\Scripts\Activate.ps1.
Complete Python program
"""One simplified invoice: create, sign, validate, report, inspect the result."""
import argparse
from datetime import datetime, timezone
import json
import os
from pathlib import Path
import sys
from uuid import uuid4
from fatoora import InvoiceBuilder, InvoiceSubType, InvoiceTypeKind, VatCategory
from fatoora.api import CsidProduction, ZatcaClient
from fatoora.config import Config, Environment
from fatoora.sign import Signer
from fatoora.errors import FfiError
from fatoora.validation import validate_zatca_invoice_from_str
ENVIRONMENT = Environment.NON_PRODUCTION # Integration Sandbox only.
def required(name):
value = os.environ.get(name, "").strip()
if not value:
raise ValueError(f"Set {name} first")
return value
def save_json(path, value):
path.write_text(json.dumps(value, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")
def messages(items):
result = []
for item in items:
with item:
result.append({"code": item.code(), "message": item.message()})
return result
def handle_response(response, output):
with response.validation_results() as results:
summary = {
"http_status": response.http_status(),
"outcome": response.outcome().name,
"reporting_status": response.reporting_status(),
"validation_status": results.status(),
"info": messages(results.info_messages()),
"warnings": messages(results.warning_messages()),
"errors": messages(results.error_messages()),
}
save_json(output / "response.json", summary)
print(json.dumps(summary, indent=2, ensure_ascii=False))
response.ensure_accepted() # A successful HTTP status alone is insufficient.
print("Sandbox reporting accepted. Keep any warnings in response.json.")
def run(input_path, submit):
# Read credentials before creating an invoice; never write them into artifacts.
certificate = Path(required("ZATCA_CERT_PATH")).read_text(encoding="utf-8")
key = Path(required("ZATCA_KEY_PATH")).read_text(encoding="utf-8")
token, secret = (required("ZATCA_TOKEN"), required("ZATCA_SECRET")) if submit else (None, None)
data = json.loads(Path(input_path).read_text(encoding="utf-8"))
seller = data["seller"]
invoice_uuid = str(uuid4())
output = Path("runs") / invoice_uuid
output.mkdir(parents=True, exist_ok=False)
print(f"Artifacts: {output}")
state = {"uuid": invoice_uuid, "invoice_id": data["invoice_id"], "stage": "create", "submitted": False}
save_json(output / "state.json", state)
try:
with InvoiceBuilder.new(InvoiceTypeKind.TAX, InvoiceSubType.SIMPLIFIED) as builder:
builder.set_id(data["invoice_id"])
builder.set_uuid(invoice_uuid)
builder.set_issue_datetime(datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"))
builder.set_currency("SAR")
builder.set_invoice_counter(int(data["invoice_counter"]))
builder.set_previous_invoice_hash(data["previous_invoice_hash"])
builder.set_payment_means_code("10")
builder.set_vat_category(VatCategory.STANDARD)
builder.set_seller(
name=seller["name"], country_code="SA", city=seller["city"],
street=seller["street"], building_number=seller["building_number"],
postal_code=seller["postal_code"], district=seller["district"],
additional_number=seller["additional_number"], vat_id=seller["vat_id"],
other_id=seller["crn"], other_id_scheme="CRN",
)
builder.add_line_item("دفتر", "2", "PCE", "50", "15", VatCategory.STANDARD)
with builder.build() as invoice, Signer.from_pem(certificate, key) as signer:
(output / "invoice.xml").write_text(invoice.to_xml(), encoding="utf-8")
print("Created: SAR 100 net + SAR 15 VAT = SAR 115 payable")
state["stage"] = "sign"
with invoice.sign(signer) as signed, Config(ENVIRONMENT) as config:
signed_xml = signed.xml()
(output / "signed.xml").write_text(signed_xml, encoding="utf-8")
state["invoice_hash"] = signed.invoice_hash()
state["stage"] = "validate"
report = validate_zatca_invoice_from_str(
config, signed_xml, previous_invoice_hash=data["previous_invoice_hash"],
)
save_json(output / "validation.json", report)
if not report["is_valid"]:
raise ValueError("Local validation failed; inspect validation.json. Nothing was submitted.")
print("Local validation passed; inspect validation.json for warnings")
if not submit:
state["stage"] = "local_only"
print("Local run complete. No reporting request sent. Use --submit with sandbox credentials.")
return
state["stage"] = "report"
state["submitted"] = True # Attempted; outcome may be unknown if the connection fails.
save_json(output / "state.json", state)
with CsidProduction.new(ENVIRONMENT, token, secret, None) as credentials:
with ZatcaClient(config) as client:
with client.report_simplified_invoice(signed, credentials, False, "en") as response:
handle_response(response, output)
state["stage"] = "accepted"
except Exception as error:
state["error_type"] = type(error).__name__
if isinstance(error, FfiError):
save_json(output / "error.json", {"code": error.code, "details": error.details})
raise
finally:
save_json(output / "state.json", state)
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--input", default="input.json")
parser.add_argument("--submit", action="store_true", help="Report to the Integration Sandbox")
args = parser.parse_args()
try:
run(args.input, args.submit)
except Exception as error:
print(f"Stopped: {error}", file=sys.stderr)
print("No automatic retry. Inspect saved artifacts before another run.", file=sys.stderr)
sys.exit(1)
Requires the .NET 8 SDK and a supported native platform. Individual files: Program.cs and Journey.csproj.
dotnet restore
The project enables nullable references and imports the Fatoora namespace.
Complete C# program
using System.Text.Json;
using System.Globalization;
using Fatoora;
public static class Program
{
private const byte Sandbox = 0; // Integration Sandbox only.
private static readonly JsonSerializerOptions JsonOptions = new() { WriteIndented = true };
private static string Required(string name) =>
!string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable(name))
? Environment.GetEnvironmentVariable(name)!.Trim()
: throw new InvalidOperationException($"Set {name} first");
private static void Save(string path, object value) =>
File.WriteAllText(path, JsonSerializer.Serialize(value, JsonOptions) + "\n");
private static string? ReadText(Text? value)
{
using (value) return value?.Value();
}
private static object[] Messages(nuint count, Func<nuint, ValidationMessage> get)
{
var result = new List<object>();
for (nuint i = 0; i < count; i++)
{
using var message = get(i);
result.Add(new { code = ReadText(message.Code()), message = ReadText(message.Message()) });
}
return result.ToArray();
}
public static void HandleResponse(ValidationResponse response, string output)
{
using var results = response.ValidationResults();
var summary = new {
http_status = response.HttpStatus(), outcome = response.Outcome().ToString(),
reporting_status = ReadText(response.ReportingStatus()),
validation_status = ReadText(results.Status()),
info = Messages(results.InfoLen(), results.InfoMessage),
warnings = Messages(results.WarningLen(), results.WarningMessage),
errors = Messages(results.ErrorLen(), results.ErrorMessage),
};
Save(Path.Combine(output, "response.json"), summary);
Console.WriteLine(JsonSerializer.Serialize(summary, JsonOptions));
response.EnsureAccepted(); // A successful HTTP status alone is insufficient.
Console.WriteLine("Sandbox reporting accepted. Keep any warnings in response.json.");
}
public static void Run(string inputPath, bool submit)
{
string certificate = File.ReadAllText(Required("ZATCA_CERT_PATH"));
string key = File.ReadAllText(Required("ZATCA_KEY_PATH"));
string token = submit ? Required("ZATCA_TOKEN") : "";
string secret = submit ? Required("ZATCA_SECRET") : "";
using var input = JsonDocument.Parse(File.ReadAllText(inputPath));
var data = input.RootElement;
var seller = data.GetProperty("seller");
string Field(JsonElement parent, string name) => parent.GetProperty(name).GetString()
?? throw new InvalidOperationException($"Missing input field: {name}");
string uuid = Guid.NewGuid().ToString();
string output = Path.Combine("runs", uuid);
if (Directory.Exists(output)) throw new IOException("Output directory already exists");
Directory.CreateDirectory(output);
Console.WriteLine($"Artifacts: {output}");
var state = new Dictionary<string, object> {
["uuid"] = uuid, ["invoice_id"] = Field(data, "invoice_id"),
["stage"] = "create", ["submitted"] = false,
};
Save(Path.Combine(output, "state.json"), state);
try
{
using var builder = InvoiceBuilder.New(0, 1, null, null, null, null);
builder.SetId(Field(data, "invoice_id"));
builder.SetUuid(uuid);
builder.SetIssueDatetime(DateTime.UtcNow.ToString("yyyy-MM-dd'T'HH:mm:ss'Z'", CultureInfo.InvariantCulture));
builder.SetCurrency("SAR");
builder.SetInvoiceCounter(ulong.Parse(Field(data, "invoice_counter")));
builder.SetPreviousInvoiceHash(Field(data, "previous_invoice_hash"));
builder.SetPaymentMeansCode("10");
builder.SetVatCategory(1);
using var address = Address.New("SA", Field(seller, "city"), Field(seller, "street"),
Field(seller, "building_number"), Field(seller, "postal_code"), null,
Field(seller, "additional_number"), Field(seller, "district"));
builder.SetSeller(Field(seller, "name"), address, Field(seller, "vat_id"), Field(seller, "crn"), "CRN");
builder.AddLineItem("دفتر", "2", "PCE", "50", "15", 1);
using var invoice = builder.Build();
File.WriteAllText(Path.Combine(output, "invoice.xml"), invoice.Xml());
Console.WriteLine("Created: SAR 100 net + SAR 15 VAT = SAR 115 payable");
state["stage"] = "sign";
using var signer = Signer.FromPem(certificate, key);
using var signed = signer.Sign(invoice);
string signedXml = signed.Xml();
File.WriteAllText(Path.Combine(output, "signed.xml"), signedXml);
state["invoice_hash"] = signed.InvoiceHash();
state["stage"] = "validate";
using var config = Config.New(Sandbox);
string reportJson = Xml.ValidateZatca(config, signedXml, JsonSerializer.Serialize(new {
previous_invoice_hash = Field(data, "previous_invoice_hash"),
}));
File.WriteAllText(Path.Combine(output, "validation.json"), reportJson);
using var report = JsonDocument.Parse(reportJson);
if (!report.RootElement.GetProperty("is_valid").GetBoolean())
throw new InvalidOperationException("Local validation failed; inspect validation.json. Nothing was submitted.");
Console.WriteLine("Local validation passed; inspect validation.json for warnings");
if (!submit)
{
state["stage"] = "local_only";
Console.WriteLine("Local run complete. No reporting request sent. Use --submit with sandbox credentials.");
return;
}
state["stage"] = "report";
state["submitted"] = true; // Attempted; a failed connection can leave the outcome unknown.
Save(Path.Combine(output, "state.json"), state);
using var credentials = CsidProduction.Create(Sandbox, null, token, secret);
using var client = ZatcaClient.Create(config);
using var response = client.ReportSimplifiedInvoice(signed, credentials, false, "en");
HandleResponse(response, output);
state["stage"] = "accepted";
}
catch (Exception error)
{
state["error_type"] = error.GetType().Name;
if (error is BindingErrorException binding)
{
using var details = JsonDocument.Parse(binding.Inner.DetailsJson());
Save(Path.Combine(output, "error.json"), new { code = binding.Inner.Code(), details = details.RootElement });
}
throw;
}
finally { Save(Path.Combine(output, "state.json"), state); }
}
public static int Main(string[] args)
{
try
{
string input = "input.json";
bool submit = false;
for (int i = 0; i < args.Length; i++)
{
if (args[i] == "--submit") submit = true;
else if (args[i] == "--input" && i + 1 < args.Length) input = args[++i];
else throw new ArgumentException("Usage: dotnet run -- [--input input.json] [--submit]");
}
Run(input, submit);
return 0;
}
catch (Exception error)
{
Console.Error.WriteLine($"Stopped: {error.Message}");
Console.Error.WriteLine("No automatic retry. Inspect saved artifacts before another run.");
return 1;
}
}
}
Download the TypeScript project
Requires Node.js 22+ and npm. This is a server-side Node program. Individual files: main.ts, package.json, and tsconfig.json.
npm install
npm run build
Keep the installed package's WASM file with its generated modules. Node loads it through the package; no browser asset setup is needed for this program.
Complete TypeScript program
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
import { randomUUID } from 'node:crypto';
import { pathToFileURL } from 'node:url';
import { resolve } from 'node:path';
import {
Address, BindingError, Config, CsidProduction, InvoiceBuilder, Signer, Xml, ZatcaClient,
type ValidationResponse, type ValidationMessage,
} from 'fatoority';
const ENVIRONMENT = 0; // Integration Sandbox only.
interface Input {
invoice_id: string;
invoice_counter: string;
previous_invoice_hash: string;
seller: { name: string; vat_id: string; crn: string; city: string; street: string;
building_number: string; postal_code: string; additional_number: string; district: string };
}
function required(name: string): string {
const value = process.env[name]?.trim();
if (!value) throw new Error(`Set ${name} first`);
return value;
}
function saveJson(path: string, value: unknown): void {
writeFileSync(path, JSON.stringify(value, null, 2) + '\n');
}
function messages(count: number, get: (index: number) => ValidationMessage) {
return Array.from({ length: count }, (_, index) => {
const item = get(index);
return { code: item.code()?.value() ?? null, message: item.message()?.value() ?? null };
});
}
export function handleResponse(response: ValidationResponse, output: string): void {
const results = response.validationResults();
const summary = {
http_status: response.httpStatus(), outcome: response.outcome().value,
reporting_status: response.reportingStatus()?.value() ?? null,
validation_status: results.status()?.value() ?? null,
info: messages(results.infoLen(), i => results.infoMessage(i)),
warnings: messages(results.warningLen(), i => results.warningMessage(i)),
errors: messages(results.errorLen(), i => results.errorMessage(i)),
};
saveJson(`${output}/response.json`, summary);
console.log(JSON.stringify(summary, null, 2));
response.ensureAccepted(); // A successful HTTP status alone is insufficient.
console.log('Sandbox reporting accepted. Keep any warnings in response.json.');
}
export async function run(inputPath: string, submit: boolean, transport?: typeof fetch): Promise<void> {
const certificate = readFileSync(required('ZATCA_CERT_PATH'), 'utf8');
const key = readFileSync(required('ZATCA_KEY_PATH'), 'utf8');
const token = submit ? required('ZATCA_TOKEN') : '';
const secret = submit ? required('ZATCA_SECRET') : '';
const data: Input = JSON.parse(readFileSync(inputPath, 'utf8'));
const seller = data.seller;
const uuid = randomUUID();
const output = `runs/${uuid}`;
mkdirSync('runs', { recursive: true });
mkdirSync(output); // A new directory preserves earlier attempts.
console.log(`Artifacts: ${output}`);
const state: Record<string, unknown> = {
uuid, invoice_id: data.invoice_id, stage: 'create', submitted: false,
};
saveJson(`${output}/state.json`, state);
try {
const builder = InvoiceBuilder.new_(0, 1, null, null, null, null); // Tax, simplified.
builder.setId(data.invoice_id);
builder.setUuid(uuid);
builder.setIssueDatetime(new Date().toISOString().replace(/\.\d{3}Z$/, 'Z'));
builder.setCurrency('SAR');
builder.setInvoiceCounter(BigInt(data.invoice_counter));
builder.setPreviousInvoiceHash(data.previous_invoice_hash);
builder.setPaymentMeansCode('10');
builder.setVatCategory(1); // Standard VAT category.
const address = Address.new_('SA', seller.city, seller.street, seller.building_number,
seller.postal_code, null, seller.additional_number, seller.district);
builder.setSeller(seller.name, address, seller.vat_id, seller.crn, 'CRN');
builder.addLineItem('دفتر', '2', 'PCE', '50', '15', 1);
const invoice = builder.build();
writeFileSync(`${output}/invoice.xml`, invoice.xml());
console.log('Created: SAR 100 net + SAR 15 VAT = SAR 115 payable');
state.stage = 'sign';
const signed = Signer.fromPem(certificate, key).sign(invoice);
const signedXml = signed.xml();
writeFileSync(`${output}/signed.xml`, signedXml);
state.invoice_hash = signed.invoiceHash();
state.stage = 'validate';
const config = Config.new_(ENVIRONMENT);
const report = JSON.parse(Xml.validateZatca(config, signedXml, JSON.stringify({
previous_invoice_hash: data.previous_invoice_hash,
})));
saveJson(`${output}/validation.json`, report);
if (!report.is_valid) throw new Error('Local validation failed; inspect validation.json. Nothing was submitted.');
console.log('Local validation passed; inspect validation.json for warnings');
if (!submit) {
state.stage = 'local_only';
console.log('Local run complete. No reporting request sent. Use --submit with sandbox credentials.');
return;
}
state.stage = 'report';
state.submitted = true; // Attempted; a failed connection can leave the outcome unknown.
saveJson(`${output}/state.json`, state);
const client = new ZatcaClient(config, transport ? { fetch: transport } : {});
const credentials = CsidProduction.create(ENVIRONMENT, null, token, secret);
const response = await client.reportSimplifiedInvoice(signed, credentials, false, 'en');
handleResponse(response, output);
state.stage = 'accepted';
} catch (error) {
state.error_type = error instanceof Error ? error.name : 'UnknownError';
if (error instanceof Error && error.cause instanceof BindingError) {
saveJson(`${output}/error.json`, { code: error.cause.code(), details: JSON.parse(error.cause.detailsJson()) });
}
throw error;
} finally {
saveJson(`${output}/state.json`, state);
}
}
if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) {
const args = process.argv.slice(2);
let input = 'input.json';
let submit = false;
let valid = true;
for (let i = 0; i < args.length; i++) {
if (args[i] === '--submit') submit = true;
else if (args[i] === '--input' && args[i + 1]) input = args[++i];
else valid = false;
}
if (!valid) {
console.error('Usage: npm start -- [--input input.json] [--submit]');
process.exitCode = 1;
} else {
run(input, submit).catch(error => {
console.error(`Stopped: ${error instanceof Error ? error.message : String(error)}`);
console.error('No automatic retry. Inspect saved artifacts before another run.');
process.exitCode = 1;
});
}
}
3. Run creation, signing and validation locally¶
Set the certificate and private-key paths. Use absolute paths if you keep them outside the working folder.
export ZATCA_CERT_PATH="$PWD/certificate.pem"
export ZATCA_KEY_PATH="$PWD/private-key.pem"
$env:ZATCA_CERT_PATH = Join-Path $PWD "certificate.pem"
$env:ZATCA_KEY_PATH = Join-Path $PWD "private-key.pem"
For a run without sandbox credentials, download the public
test certificate and
matching test key, keeping those filenames.
Use the unmodified demonstration input.json with them. These files are for local
testing; replace them with your issued sandbox pair before reporting.
python main.py
dotnet run
npm start
Expected output, with a different UUID on each run:
Artifacts: runs/<uuid>
Created: SAR 100 net + SAR 15 VAT = SAR 115 payable
Local validation passed; inspect validation.json for warnings
Local run complete. No reporting request sent. Use --submit with sandbox credentials.
Open runs/<uuid>/signed.xml and validation.json. The validation report checks
the exact signed XML, including the applicable signature and QR checks, using the
predecessor supplied independently in input.json. If it fails, the program exits
with a nonzero status before constructing a reporting request.
4. Report with your sandbox credentials¶
Use your issued sandbox certificate/key pair and matching seller data. Load
ZATCA_TOKEN and ZATCA_SECRET into the process environment through your shell or
secret manager. They must come from the production-type sandbox response,
not the compliance-CSID response. Do not put credentials in input.json.
export ZATCA_TOKEN='<binarySecurityToken from sandbox production CSID>'
export ZATCA_SECRET='<secret from that same response>'
$env:ZATCA_TOKEN = '<binarySecurityToken from sandbox production CSID>'
$env:ZATCA_SECRET = '<secret from that same response>'
Run one command for the complete workflow:
python main.py --submit
dotnet run -- --submit
npm start -- --submit
This creates a new invoice and sends one reporting request. It does not submit the
previous local run's artifact. Before repeated submissions, allocate a new invoice
ID and counter and supply the correct predecessor in input.json. The examples
do not maintain an invoice ledger or automatically advance those fields.
5. Read the response and keep the artifacts¶
The program saves response.json before calling the library's acceptance check.
It prints gateway messages alongside the HTTP status and reporting outcome. For an accepted
response, the last line is:
Sandbox reporting accepted. Keep any warnings in response.json.
| Result | What the example does | What to do next |
|---|---|---|
| Accepted | Exits successfully and sets state.json stage to accepted. |
Store the invoice and response together. |
| Accepted with warnings | Succeeds and retains the warnings. | Review the warning codes; acceptance does not erase them. |
| Rejected or unknown outcome in a decoded response | Saves response.json, then exits nonzero. |
Read the findings and resolve the outcome before creating another invoice. |
| HTTP error, such as 400 or 401 | Exits nonzero; binding diagnostics go to error.json. |
Inspect status/body details; correct the reported issue or credentials. |
| Connection failure or unreadable response | Exits nonzero; state.json records the attempted stage. |
Treat acceptance as unresolved. Reconcile the attempt before retrying. |
| Local validation failure | Saves validation.json and sends nothing. |
Fix the input indicated by the local findings. |
HTTP 200 alone is insufficient. Each example calls ensure_accepted or
EnsureAccepted/ensureAccepted, which checks the operation's reporting status
and validation results. The API decoder raises an exception for non-2xx responses;
such errors may have error.json instead of response.json.
Every run writes to a new runs/<uuid>/ directory:
invoice.xml: unsigned XML created from the input.signed.xml: exact signed XML used for validation and reporting.validation.json: local rule findings and stage coverage.response.json: decoded reporting result, when one was returned.error.json: structured binding diagnostics, when available.state.json: invoice identity/hash and the last workflow stage.
In state.json, submitted: true means a reporting call was attempted. It does
not mean accepted. Preserve these files after a failure. The program has no retry
loop, and running it again creates a different invoice. Protect invoice artifacts
and error bodies as business data; the examples do not write credentials into them.
What changes for production¶
| Area | Sandbox walkthrough | Production integration |
|---|---|---|
| Environment | NonProduction / 0, the developer-portal endpoint. |
Complete the required testing and onboarding for the live environment; configure Production / 2, the core endpoint. Simulation / 1 is a separate test environment. |
| Credentials | Test device's certificate/key pair and API token/secret. | Credentials issued for the live EGS unit. Changing an environment number does not convert credentials. |
| Invoice data | Fixed two-item SAR 115 sale and editable demonstration identity. | Real seller/customer data, items, tax treatment, and applicable invoice subtype. |
| History | Manually supplied ID, counter, and previous hash. | Durable per-unit sequencing and invoice history, safe under concurrent creation. History follows generated invoices; do not advance it only when reporting succeeds. |
| Submission | One blocking/awaited sandbox call. | Durable submission records and response reconciliation. A retry policy must distinguish rejection from an unknown outcome and preserve invoice identity for retries. |
| Operations | Local files and process environment. | Managed key storage, credential renewal/revocation, access controls, backups, and monitoring of pending submissions and warnings. |
Keep environment selection consistent in the client, credentials, and CSR creation. The example constants deliberately select the sandbox. Before adapting them for production, implement the history and submission behavior above and use the current ZATCA developer materials for your integration requirements.
For standard invoices, supply the required buyer data and use the clearance
operation. The library accepts a SignedInvoice for that call; ZATCA clearance
produces the document to deliver. Inspect clearance_status.
On successful clearance, save cleared_invoice_xml() / ClearedInvoiceXml() /
clearedInvoiceXml() and use the returned document for customer delivery. The
API reference explains those return types and the
nullable cleared-document field.