Trial
Free
- Instant sandbox, 100 invoices an hour
- Unlimited on the developer portal with your own CSID
- The exact same API
ZATCA e-invoicing · Phase 2
Matheel signs the invoice, computes the hash and QR, files it with ZATCA and returns the answer with the stamped XML. It keeps no invoices.
Why ZATCA integration hurts
Validate your invoice against ZATCA's rules, free → · Every ZATCA error explained →
What Matheel does
Send "environment": "sandbox" without a certificate and Matheel files it on ZATCA's developer portal as a shared test taxpayer. The answer is ZATCA's own.
{ "status": "reported",
"warnings": [], "errors": [],
"qr": "AQ1TaGFy…",
"invoice_xml": "PD94bWwg…" }Send the taxpayer's details to /api/v1/onboarding. Matheel generates the CSR, gets the compliance CSID, passes the six compliance checks, gets the production CSID and hands it back.
The stamped invoice comes back in the same response and the archive stays with you. You own the counter and the hash chain, so there is no hidden state on our side that can drift from yours.
Copy the prompt into Claude, Cursor or ChatGPT and it writes the integration in your codebase. Tools that read docs themselves get llms.txt.
Integrate the Matheel API (https://matheel.app) into my app so it files Saudi ZATCA Phase 2 e-invoices.
Reference (read it first): https://matheel.app/llms.txt
API: POST https://matheel.app/api/v1/invoices
Header: Authorization: Bearer $MATHEEL_KEY (read it from an environment variable)
Body: { "environment": "sandbox" | "simulation" | "production", "seller": {...}, "csid": {...}, "invoice": {...} }
Device certificates: POST https://matheel.app/api/v1/onboarding returns the csid in one call.
Do this:
1. Ask me which language/framework I use and where invoices are created, then add one small client module.
2. Keep the invoice chain per device (EGS): store the counter (ICV) and the last invoice_hash.
Send previous_invoice_hash; move the counter forward only after a 200.
3. On 200 store invoice_xml (base64, the legal document) and qr (print it on the receipt).
4. On 422 show ZATCA's errors and keep the same counter. On 402 tell the user a device plan is needed for production.
On 429 or 503 retry with backoff using the same counter and uuid.
5. Start with "environment": "sandbox" and no seller/csid (Matheel's instant sandbox), and write one test that files an invoice.Any language
curl https://matheel.app/api/v1/invoices \
-H "Authorization: Bearer $MATHEEL_KEY" \
-H "Content-Type: application/json" \
-d '{"environment":"sandbox","invoice":{"type":"simplified","counter":1,
"uuid":"8d487816-70b8-4ade-a618-9d620b73814a","number":"INV-1",
"line_items":[{"name":"Coffee","quantity":2,"price":"50.00"}],
"subtotal":"100.00","vat_amount":"15.00","total":"115.00"}}'
const res = await fetch("https://matheel.app/api/v1/invoices", {
method: "POST",
headers: { Authorization: `Bearer ${process.env.MATHEEL_KEY}`, "Content-Type": "application/json" },
body: JSON.stringify({ environment: "sandbox", invoice: {
type: "simplified", counter: 1, uuid: crypto.randomUUID(), number: "INV-1",
line_items: [{ name: "Coffee", quantity: 2, price: "50.00" }],
subtotal: "100.00", vat_amount: "15.00", total: "115.00" } })
});
const { status, invoice_hash, qr, invoice_xml } = await res.json();
import os, uuid, requests
r = requests.post("https://matheel.app/api/v1/invoices",
headers={"Authorization": f"Bearer {os.environ['MATHEEL_KEY']}"},
json={"environment": "sandbox", "invoice": {
"type": "simplified", "counter": 1, "uuid": str(uuid.uuid4()), "number": "INV-1",
"line_items": [{"name": "Coffee", "quantity": 2, "price": "50.00"}],
"subtotal": "100.00", "vat_amount": "15.00", "total": "115.00"}})
print(r.json()["status"]) # reported
$ch = curl_init("https://matheel.app/api/v1/invoices");
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ["Authorization: Bearer " . getenv("MATHEEL_KEY"), "Content-Type: application/json"],
CURLOPT_POSTFIELDS => json_encode(["environment" => "sandbox", "invoice" => [
"type" => "simplified", "counter" => 1, "uuid" => "8d487816-70b8-4ade-a618-9d620b73814a", "number" => "INV-1",
"line_items" => [["name" => "Coffee", "quantity" => 2, "price" => "50.00"]],
"subtotal" => "100.00", "vat_amount" => "15.00", "total" => "115.00"]])]);
$result = json_decode(curl_exec($ch), true);
Pricing
Best fit: Team · 149 a month · 49.67 per device, unlimited invoices
Free
39 / month
149 / month
449 / month
Monthly, cancel any time. Prices in Saudi riyals, paid by card or Apple Pay through Polar. A device is an EGS unit with its own CSID. If you go over your plan's devices, your invoices keep filing and we let you know to upgrade; we never stop your invoicing.
Questions
Yes. Send "environment": "sandbox" without seller or csid and Matheel files the invoice on ZATCA's developer portal as its shared test taxpayer. You get ZATCA's real answer.
One request to /api/v1/onboarding: Matheel generates the CSR, gets the compliance CSID, passes the six checks and gets the production CSID. Simulation and production take the OTP from the taxpayer's Fatoora portal. Step by step in the ZATCA Phase 2 guide.
Standard (B2B) tax invoices must be cleared by ZATCA before they reach the buyer. Simplified (B2C) invoices are reported within 24 hours. Matheel picks the right one from the invoice type.
No. The request is processed and answered, and nothing from it is kept: not the invoice, not the key. The legal archive is the taxpayer's, and this keeps it in your hands.
You get ZATCA's errors and warnings verbatim with a 422, and a rejected invoice is not counted against your plan. See common ZATCA errors and fixes.