{"openapi":"3.1.0","info":{"title":"Navira Compliance API","version":"1.1.0","summary":"Push invoice data from any system. Navira builds and signs the compliant document, and submits it to ZATCA in Saudi Arabia.","description":"Navira is a compliance middleware engine. Your accounting or ERP system stays where it is —\nyou push invoice data to Navira over HTTPS, and Navira produces the compliant document,\nsigns it, and retains the audit artefacts.\n\n**Which authority.** In Saudi Arabia, Navira submits the document to ZATCA and returns the\noutcome. In the UAE, Navira builds and stores the PINT AE document alongside the invoice but\ndoes NOT transmit it: Navira is not an FTA-accredited service provider.\n\n**Authentication.** Every request carries `Authorization: Bearer <key>`. There are two key types\nand they are not interchangeable:\n\n- `nvr_live_…` — tenant-scoped, generated by the merchant in Settings → API. Used for reads\n  and to push invoices from an ERP. Requires a plan that includes API access (Connect and above).\n- `nvr_pos_…` — device-scoped, issued per till. Used to push sales.\n\nKeys are stored only as a SHA-256 hash; the plaintext is shown once at creation and never again.\nA key can be revoked at any time and takes effect immediately.\n\n**Rate limits.** 120 requests per minute per key, and there is no separate per-IP tier on these\nendpoints. Exceeding the limit returns `429` with a `Retry-After` header and a `retry_after`\nfield in the body.\n\n**Idempotency.** The POS endpoint requires a client-generated `idempotency_key` (UUID) per sale.\nA retry after a dropped connection returns the ORIGINAL invoice with `deduped: true` rather than\nringing the sale up twice. This is the contract that makes the API safe on an unreliable link.\nOn POS the key is scoped to the device: the same key from another till is a different sale.\n\n**Before a key works.** The merchant's plan must include API access, and the account must be\nonboarded to ZATCA (Saudi Arabia). Until it is, documents are created and stored but return\n`zatca_status: \"not_submitted\"` and no QR — that is a state to handle, not an error.","contact":{"name":"Navira by SHAHMCO","url":"https://navira.shahmco.com/contact"}},"servers":[{"url":"https://navira.shahmco.com","description":"Production"}],"tags":[{"name":"Invoices","description":"Read documents Navira holds for your organisation."},{"name":"POS","description":"Push point-of-sale transactions for compliant issuance."}],"security":[{"BearerKey":[]}],"components":{"securitySchemes":{"BearerKey":{"type":"http","scheme":"bearer","description":"An `nvr_live_…` or `nvr_pos_…` key, depending on the endpoint."}},"schemas":{"Error":{"type":"object","required":["error"],"properties":{"error":{"type":"string","description":"Stable machine-readable code. Match on this, never on `message`.","examples":["unauthorized","rate_limited","upgrade_required","invalid_request"]},"message":{"type":"string","description":"Human-readable explanation. Wording may change."},"retry_after":{"type":"integer","description":"Seconds to wait. Present on 429 only."}}},"Invoice":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"invoice_number":{"type":"string"},"document_type":{"type":"string","examples":["commercial","credit_note","debit_note"]},"status":{"type":"string"},"zatca_status":{"type":"string","description":"Authority outcome. Terminal success values are `cleared`, `cleared_with_warnings`, `reported`, `reported_with_warnings`.","examples":["not_submitted","pending","reported","cleared","rejected","submission_failed"]},"customer_name":{"type":["string","null"]},"currency":{"type":"string","examples":["SAR","AED"]},"grand_total":{"type":"number"},"amount_paid":{"type":"number"},"issue_date":{"type":["string","null"],"format":"date"},"due_date":{"type":["string","null"],"format":"date"},"created_at":{"type":"string","format":"date-time"}}},"SaleLine":{"type":"object","required":["description","quantity","unit_price"],"properties":{"description":{"type":"string","minLength":1,"maxLength":1000},"description_ar":{"type":["string","null"],"maxLength":1000,"description":"Arabic description. Recommended for KSA."},"quantity":{"type":"number","exclusiveMinimum":0},"unit_price":{"type":"number","minimum":0,"description":"Excluding VAT."},"discount_pct":{"type":"number","minimum":0,"maximum":100},"tax_category":{"type":"string","enum":["S","Z","E","O","AE"],"default":"S","description":"S standard-rated · Z zero-rated · E exempt · O out of scope · AE reverse charge."},"unit_code":{"type":"string","description":"UN/ECE Rec 20 unit of measure. On /api/v1/pos/sales this is NOT validated and defaults to EA — send a valid UN/ECE Rec 20 code. /api/v1/invoices validates it and defaults to C62."}}},"Sale":{"type":"object","required":["idempotency_key","lines"],"properties":{"idempotency_key":{"type":"string","format":"uuid","description":"One per logical sale. Resending the same key returns the original invoice."},"occurred_at":{"type":"string","format":"date-time","description":"When the sale actually happened on the till. Send this when flushing an offline backlog — each sale is then measured against its OWN 24-hour filing deadline, not against when the sync ran."},"customer_name":{"type":"string","maxLength":300},"customer_trn":{"type":"string","description":"Buyer VAT number. Supply it for a standard (B2B) invoice; omit for a simplified (B2C) one."},"currency":{"type":"string","minLength":3,"maxLength":3,"default":"SAR"},"payment_method":{"type":"string","default":"cash"},"lines":{"type":"array","minItems":1,"maxItems":200,"items":{"$ref":"#/components/schemas/SaleLine"}}}},"B2BLine":{"type":"object","required":["description","quantity","unit_price"],"properties":{"description":{"type":"string","minLength":1,"maxLength":1000},"description_ar":{"type":["string","null"],"description":"Arabic description. Required in practice for KSA."},"quantity":{"type":"number","exclusiveMinimum":0},"unit_price":{"type":"number","minimum":0,"description":"Excluding VAT."},"discount_pct":{"type":"number","minimum":0,"maximum":100},"tax_category":{"type":"string","enum":["S","Z","E","O","AE"],"default":"S","description":"S standard-rated · Z zero-rated · E exempt · O out of scope · AE reverse charge."},"unit_code":{"type":"string","description":"UN/ECE Rec 20/21 unit of measure. HUR consulting hours · DAY hotel nights · MON a school term · ANN an annual licence · C62 a countable item · KGM · LTR · MTK. An unrecognised code is a 400 naming the likely intended one — it would otherwise be written into the e-invoice for ZATCA to reject at clearance. Defaults to C62."},"tax_exemption_reason_code":{"type":["string","null"],"description":"REQUIRED whenever tax_category is not \"S\". States why the line carries no VAT, and is validated against the authority's list for your country — a code belonging to another category is rejected, so exempt financial services cannot be filed as zero-rated.\n\nKSA (ZATCA): VATEX-SA-32/33 exports · VATEX-SA-34-1…34-5 international transport · VATEX-SA-35 medicines and medical equipment · VATEX-SA-36 qualifying metals · VATEX-SA-EDU private education to a citizen · VATEX-SA-HEA private healthcare to a citizen · VATEX-SA-29 / 29-7 / 30 exempt · VATEX-SA-OOS out of scope.\n\nNote that VATEX-SA-EDU and VATEX-SA-HEA turn on the STUDENT's or PATIENT's citizenship, not on the sector: the same school billing an expatriate charges 15%.\n\nUAE: VATEX-AE-EXEMPT · VATEX-AE-RC reverse charge · VATEX-AE-OOS."},"tax_exemption_reason":{"type":["string","null"],"description":"The taxpayer's own wording. Required alongside VATEX-SA-OOS and VATEX-AE-OOS."},"cost_center":{"type":["string","null"],"description":"Cost-centre CODE as your system knows it. Overrides the header's. An unknown code is rejected with 400 rather than silently unallocated."},"metadata":{"type":["object","null"],"additionalProperties":true,"description":"Per-line identifiers from your system. Stored and returned; never written into the e-invoice XML. 8 KB max."}}},"B2BInvoice":{"type":"object","required":["idempotency_key","customer_name","lines"],"properties":{"idempotency_key":{"type":"string","format":"uuid","description":"One per logical invoice. Resending the same key returns the original invoice with deduped: true."},"document_type":{"type":"string","enum":["commercial","credit_note","debit_note"],"default":"commercial"},"subtype":{"type":"string","enum":["standard","simplified"],"description":"Omit and it is inferred from customer_trn: with a buyer VAT number the invoice is STANDARD (cleared by the authority before it is valid in KSA); without one it is SIMPLIFIED (reported within 24 hours). Sending \"standard\" without a customer_trn is rejected."},"customer_name":{"type":"string","maxLength":300},"customer_name_ar":{"type":["string","null"]},"customer_trn":{"type":["string","null"],"description":"Buyer VAT number. Its presence is what makes the supply B2B."},"customer_email":{"type":["string","null"],"format":"email"},"customer_phone":{"type":["string","null"]},"customer_address":{"type":["string","null"]},"customer_street":{"type":["string","null"]},"customer_building_number":{"type":["string","null"]},"customer_district":{"type":["string","null"]},"customer_city":{"type":["string","null"]},"customer_postal_code":{"type":["string","null"]},"customer_country_code":{"type":["string","null"],"minLength":2,"maxLength":2},"po_number":{"type":["string","null"],"description":"Your customer's purchase-order number."},"buyer_reference":{"type":["string","null"],"description":"UBL BuyerReference — a distinct field from po_number."},"billing_reference":{"type":["string","null"],"description":"The invoice NUMBER this note corrects. Required on a credit or debit note, and rejected on a commercial invoice."},"issue_date":{"type":"string","format":"date","description":"Defaults to today in the account's own timezone."},"due_date":{"type":["string","null"],"format":"date"},"payment_terms":{"type":["integer","null"],"minimum":0,"maximum":365,"description":"Net-N days from the issue date. Used when due_date is omitted; falls back to the customer's stored terms, then the account default."},"supply_date":{"type":["string","null"],"format":"date","description":"Start of the billing period."},"supply_end_date":{"type":["string","null"],"format":"date","description":"End of the billing period. Cannot precede supply_date."},"currency":{"type":"string","minLength":3,"maxLength":3,"default":"SAR"},"payment_method":{"type":["string","null"]},"notes":{"type":["string","null"]},"cost_center":{"type":["string","null"],"description":"Header-level allocation. A line may override it."},"metadata":{"type":["object","null"],"additionalProperties":true,"description":"Identifiers from your own system — a student id, a patient reference, a matter number. Stored against the invoice and its lines. NOT yet echoed back on GET /api/v1/invoices, in the POST response, or in CSV exports — it is write-side today. Never written into the e-invoice XML: that would alter the signed document and put your customer's reference on a tax record. 8 KB max."},"lines":{"type":"array","minItems":1,"maxItems":1000,"items":{"$ref":"#/components/schemas/B2BLine"}}}},"B2BResult":{"type":"object","properties":{"ok":{"type":"boolean"},"deduped":{"type":"boolean","description":"True when this was a retry and the original invoice is being returned."},"invoice_id":{"type":"string","format":"uuid"},"invoice_number":{"type":"string"},"document_type":{"type":"string"},"subtype":{"type":"string","enum":["standard","simplified"]},"zatca_status":{"type":"string"},"due_date":{"type":["string","null"],"format":"date"},"grand_total":{"type":"number"},"currency":{"type":"string"}}},"SaleResult":{"type":"object","properties":{"ok":{"type":"boolean"},"deduped":{"type":"boolean","description":"True when this was a retry and the original invoice is being returned."},"invoice_id":{"type":"string","format":"uuid"},"invoice_number":{"type":"string"},"zatca_status":{"type":"string"},"qr":{"type":["string","null"],"description":"Base64 TLV QR payload for the printed receipt."},"grand_total":{"type":"number"},"currency":{"type":"string"}}}}},"paths":{"/api/v1/invoices":{"get":{"tags":["Invoices"],"summary":"List invoices","description":"Newest first, scoped to the tenant the key belongs to. Requires an `nvr_live_…` key. There is no cursor or offset: `limit` (max 200) is the only control, so an account with more invoices than that cannot be paged through the API — ask us if you need a full export.","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","default":50,"maximum":200,"minimum":1}},{"name":"X-Navira-Tenant","in":"header","required":false,"description":"For accounting firms only: the Navira account id of the client this request is for. The account must be linked to your firm, and firm-scoped keys are issued by Navira. Omit it and the request applies to your own account. NOTE: firm-scoped keys are provisioned by hand today — talk to us before building against this header so we can set up your firm and test the routing with you.","schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Invoice"}}}}}}},"401":{"description":"Missing, malformed or revoked key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Server not configured","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"tags":["Invoices"],"summary":"Push an invoice from your ERP","description":"The enterprise ingestion endpoint. Your system stays the record of truth; Navira produces the\ncompliant document, signs it and retains the audit artefacts. In Saudi Arabia it is then\nsubmitted to ZATCA. In the UAE the PINT AE document is built and stored but NOT transmitted:\nNavira is not an FTA-accredited service provider.\n\n**Submitted once, and not retried.** The authority submission happens synchronously inside this\nrequest. If it fails, the invoice is durably stored with `zatca_status: \"not_submitted\"` and is\nNOT picked up by any retry job — the catch-up sweep covers POS sales only. Read `zatca_status`\nfrom the response, and poll `GET /api/v1/invoices` or re-submit from the app.\n\n**Standard or simplified is inferred from the buyer.** With a `customer_trn` the invoice is a\nSTANDARD tax invoice, which in Saudi Arabia must be CLEARED by ZATCA before it is valid. Without\none it is SIMPLIFIED and is reported within 24 hours. Sending `subtype: \"standard\"` with no\n`customer_trn` is rejected here rather than by ZATCA later.\n\n**Idempotent.** Send the same `idempotency_key` again after a dropped connection and you get the\nORIGINAL invoice back with `deduped: true` — never a duplicate. On a nightly batch over an\nunreliable link this is the difference between a reconciliation and an incident.\n\n**Cost centres** allocate an invoice, or individual lines, to a department, branch or campus.\nAn unknown code is a 400: a silently unallocated line is what an audit fails on months later.\nThere is no self-serve screen for creating them yet, so ask us to set your codes up before you\nsend them — otherwise every code is an unknown one and rejects the whole invoice.\n\n**Any industry, without a schema change.** A line is a description, a quantity, a price and a\nUN/CEFACT unit — `HUR` for consulting hours, `DAY` for hotel nights, `MON` for a school term,\n`C62` for a countable item. There is no SKU field to work around. Anything your own system needs\nto reconcile by — a student id, a patient reference, a matter number — goes in `metadata`.\n\n**Tax treatment is stated, never guessed — on THIS endpoint.** Any line that is not\nstandard-rated must name its exemption reason code, validated against the authority's list for\nthe account's country. This is why a school can bill zero-rated tuition and 5% uniforms on one\ninvoice and have both come out right. /api/v1/pos/sales does not accept an exemption reason yet\nand falls back to a per-category default, so push zero-rated and exempt lines through here.\n\n**Accounting firms** can be issued one key that acts on several client accounts, named per\nrequest in `X-Navira-Tenant`, rather than holding one key per client. This is provisioned by\nhand today — talk to us before you build against it.\n\nRequires an `nvr_live_…` key.","parameters":[{"name":"X-Navira-Tenant","in":"header","required":false,"description":"For accounting firms only: the Navira account id of the client this request is for. The account must be linked to your firm, and firm-scoped keys are issued by Navira. Omit it and the request applies to your own account. NOTE: firm-scoped keys are provisioned by hand today — talk to us before building against this header so we can set up your firm and test the routing with you.","schema":{"type":"string","format":"uuid"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/B2BInvoice"}}}},"responses":{"200":{"description":"Invoice created, or the original returned on a retry","content":{"application/json":{"schema":{"$ref":"#/components/schemas/B2BResult"}}}},"207":{"description":"Invoice created but some line items failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"400":{"description":"Invalid payload, unknown cost centre, or a note with no billing reference","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, malformed or revoked key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Not enabled on this environment","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/v1/pos/sales":{"post":{"tags":["POS"],"summary":"Push a sale","description":"Creates and locally signs the invoice, then races the ZATCA submission against an 8-second\ntimeout before responding. The ordering matters for a till: **the QR is returned only if that\nsubmission completes in time.** Otherwise the response carries `qr: null` and\n`zatca_status: \"not_submitted\"` — your till must handle a null QR rather than assume one.\n\n**A slow or failed authority call never fails the sale**: the invoice is durably written before\nany network call is attempted. A catch-up sweep re-submits anything not yet reported or cleared;\nit currently runs ONCE A DAY, which is one attempt inside a simplified invoice's 24-hour\nreporting window.\n\nAlways issues a SIMPLIFIED tax invoice. `customer_trn` is recorded on the receipt but does not\nmake it a standard invoice — post to /api/v1/invoices for a cleared B2B document.\n\nRequires an `nvr_pos_…` device key. Returns 403 `upgrade_required` if the merchant's plan does not include POS.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Sale"}}}},"responses":{"200":{"description":"Invoice created, or the original returned on a retry","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SaleResult"}}}},"207":{"description":"Invoice created but some line items failed","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"400":{"description":"Invalid JSON or payload","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"Missing, malformed or revoked device key","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"Plan does not include POS ingestion","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"Rate limited","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"Server error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Not configured on this environment","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}}}