Test keys
A mzk_test_ key reads like a live one. Its writes to invoices, quotes, credit notes and e-invoices are validated and priced and answer "livemode": false; nothing is saved, numbered or sent.
A versioned REST API for the books: issue invoices and ZATCA e-invoices from your own system, record payments, pull reports, and hear about every change by webhook. The full contract is an OpenAPI document you can read and call.
https://aqiderp.com/api/v1Create a key in the app under Sales → Invoicing API → API keys. It is shown once; we keep only its hash. Start with a Test key: its writes are checked and priced but nothing is saved or sent to ZATCA.
curl -s https://aqiderp.com/api/v1/whoami \
-H "Authorization: Bearer $AQID_API_KEY"curl -s https://aqiderp.com/api/v1/entities \
-H "Authorization: Bearer $AQID_API_KEY"curl -s -X POST https://aqiderp.com/api/v1/entities/$ENTITY_ID/invoices \
-H "Authorization: Bearer $AQID_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 8f14e45f-ceea-467a-9f9c-1b0c0f5a1e11" \
-d '{"contactId":"'$CONTACT_ID'","issueDate":"2026-10-16",
"lines":[{"description":"Consulting","qty":"2","unitPrice":"500.00","taxCode":"SA-STD"}]}'curl -s -X POST https://aqiderp.com/api/v1/invoices/$INVOICE_ID/approve \
-H "Authorization: Bearer $AQID_API_KEY"Approval answers with the invoice's gapless number, its journal and the QR payload. A Saudi invoice is then cleared or reported to ZATCA, and the result arrives by webhook.
A mzk_test_ key reads like a live one. Its writes to invoices, quotes, credit notes and e-invoices are validated and priced and answer "livemode": false; nothing is saved, numbered or sent.
Send an Idempotency-Key header on writes. A repeat returns the first response with Idempotency-Replayed: true, so a retry after a timeout never makes a second invoice.
Lists are cursor based: pass limit, then the nextCursor from the page you have. Records added during a walk are never skipped or repeated.
RFC 7807 application/problem+json, with a machine-readable code (BAD_VAT, ZATCA_VALIDATION, UPGRADE_REQUIRED…). A ZATCA refusal lists ZATCA's own findings.
A token bucket per key: 120 requests refilling over 60 seconds by default. Responses carry X-RateLimit-Limit and X-RateLimit-Remaining; a 429 adds Retry-After.
Amounts are decimal strings and tax codes name the rate (SA-STD for Saudi 15%). Foreign-currency invoices take a currency and an exchange rate; a Saudi invoice goes to ZATCA with its VAT in SAR.
| Invoices | GET/POST /v1/entities/{entityId}/invoices · POST /v1/invoices/{id}/approve |
|---|---|
| ZATCA e-invoices | POST /v1/entities/{entityId}/einvoices · POST …/einvoices/validate · GET /v1/einvoices/{id}/xml |
| Contacts | GET/POST /v1/contacts · PATCH /v1/contacts/{id} |
| Payments | POST /v1/entities/{entityId}/payments · POST /v1/payments/{id}/allocations |
| Supplier bills | GET/POST /v1/entities/{entityId}/bills · POST /v1/bills/{id}/approve |
| Manual journals | GET/POST /v1/entities/{entityId}/journals |
| Bank accounts and statements | GET/POST /v1/entities/{entityId}/bank-accounts · POST /v1/bank-accounts/{id}/statements |
| Reports | GET /v1/entities/{entityId}/reports/{profit-and-loss | balance-sheet | cash-flow | vat-return} |
| Employees and payslips | GET/POST /v1/entities/{entityId}/employees · GET /v1/payslips/{id}/pdf |
| Warehouses and stock | GET/POST /v1/entities/{entityId}/warehouses · GET …/stock-levels |
Also quotes, credit and debit notes, purchase orders, expenses, bank transfers, items, cost centres, projects, branches and tax rates. The interactive reference lists every route and field.
A billing system, POS or marketplace can send its sales to the API and get back ZATCA-compliant invoices: validated first (POST …/einvoices/validate shows ZATCA's findings without issuing), then numbered, signed and cleared (B2B) or reported (B2C) in one call, with the signed XML to download.
Register an endpoint on the Invoicing API page and choose events. Delivery is at least once, retried with backoff; deduplicate on the AqidERP-Event-Id header. The endpoint must be publicly reachable.
invoice.approved · invoice.paid · invoice.voided · credit_note.approved · bill.approved · payment.received · payment.sent · quote.accepted · quote.declined · einvoice.cleared · einvoice.reported · einvoice.rejected · contact.created · contact.updated · bank_transfer.posted
Each delivery is signed: AqidERP-Signature carries a timestamp and an HMAC-SHA256 of "<timestamp>.<raw body>" with your endpoint's secret. Verify it in constant time and reject old timestamps:
import { createHmac, timingSafeEqual } from "node:crypto";
// header: the AqidERP-Signature header, "t=<unix>,v1=<hex>"
export function verify(secret, rawBody, header, toleranceSeconds = 300) {
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=", 2)));
const t = Number(parts.t);
if (!Number.isFinite(t)) return false;
if (Math.abs(Math.floor(Date.now() / 1000) - t) > toleranceSeconds) return false;
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
const a = Buffer.from(expected, "utf8");
const b = Buffer.from(parts.v1 ?? "", "utf8");
return a.length === b.length && timingSafeEqual(a, b);
}An app a company did not write, such as a POS or a marketplace connector, gets a token instead of a key: the company registers it under Settings → Developer, and the authorization-code flow with PKCE (required for every client) returns an access token with only the scopes asked for, such as invoices:write contacts:read.
GET https://aqiderp.com/oauth/authorize?response_type=code&client_id=mzc_…
&redirect_uri=https://your-app.example/callback&scope=invoices:write%20contacts:read
&state=…&code_challenge=…&code_challenge_method=S256
POST https://aqiderp.com/api/oauth/token (grant_type=authorization_code, code_verifier=…)