Skip to content
Navira نافيرا — by SHAHMCO
Sign in
On this page

Quickstart

Three steps, in order. Steps 1 and 2 are done once per account; step 3 is the integration.

  1. 1

    Make sure the plan includes API access

    Settings → API is available on Connect and above, in both Saudi Arabia and the UAE. It is locked on the entry tier. POS device keys are available on every current plan.

  2. 2

    Onboard the account to ZATCA (Saudi Arabia)

    Until this is done, documents are still created and stored — but they come back with zatca_status of not_submitted and no QR. That is a state to handle, not an error, and assuming a QR is always present is the single most common way a first integration breaks in the field.

  3. 3

    Send your first request

    Every call carries Authorization: Bearer <key>. Below is a complete till sale — copy it, swap the key, and it runs.

curl -X POST https://navira.shahmco.com/api/v1/pos/sales \
  -H "Authorization: Bearer nvr_pos_YOURKEY" \
  -H "Content-Type: application/json" \
  -d '{
    "idempotency_key": "6f1c2a34-0000-4000-8000-000000000000",
    "currency": "SAR",
    "payment_method": "card",
    "lines": [
      {
        "description": "Flat white",
        "description_ar": "قهوة فلات وايت",
        "quantity": 2,
        "unit_price": 16,
        "tax_category": "S",
        "unit_code": "EA"
      }
    ]
  }'

Request explorer

Toggle a field and watch both the request and the compliance outcome change. The buyer’s VAT number is the field that decides whether ZATCA must clear the document before it is valid, or whether it is reported after the fact — so it is worth seeing that happen rather than reading about it.

Include in the request

What this issues

Standard tax invoice (B2B)

Cleared by ZATCA inside this request. In Saudi Arabia a standard invoice is not valid until it is cleared, so read zatca_status from the response before you send the document to your customer.

Authenticates with a nvr_live_… key. The two key types are not interchangeable.

curl -X POST https://navira.shahmco.com/api/v1/invoices \
  -H "Authorization: Bearer nvr_live_YOURKEY" \
  -H "Content-Type: application/json" \
  -d '{
    "idempotency_key": "6f1c2a34-0000-4000-8000-000000000000",
    "document_type": "commercial",
    "customer_name": "Al Fanar Trading Co.",
    "customer_name_ar": "شركة الفنار التجارية",
    "customer_trn": "300000000000003",
    "currency": "SAR",
    "issue_date": "2026-08-31",
    "payment_terms": 30,
    "lines": [
      {
        "description": "Implementation services",
        "description_ar": "خدمات التنفيذ",
        "quantity": 40,
        "unit_price": 350,
        "unit_code": "HUR",
        "tax_category": "S"
      }
    ]
  }'

Authentication

There are two key types and they are not interchangeable — they authenticate against different stores, so the wrong type returns 401, not a permission error.

nvr_live_…
Scope
One merchant
Issued
Settings → API
Used for
Reading documents, and pushing invoices from your ERP.

Requires a plan that includes API access.

nvr_pos_…
Scope
One till / device
Issued
Settings → POS devices
Used for
Pushing point-of-sale transactions.

Available on every current plan.

How keys are stored

Keys are stored only as a SHA-256 hash. The plaintext is shown once at creation and never again — if it is lost, the key is rotated, not recovered. Revocation takes effect immediately.

Errors & rate limits

Errors share one shape across every endpoint. Match on error, which is a stable machine-readable code — never on message, whose wording changes.

Error response
{
  "error": "rate_limited",
  "message": "Too many requests. Retry after the interval in Retry-After.",
  "retry_after": 12
}

Rate limit

120 requests per minute, per key. Exceeding it returns 429 with a Retry-After header and a retry_after field in the body. Back off for that many seconds rather than retrying immediately.

Idempotency

Every write carries a client-generated idempotency_key. If the connection drops and your system never learns whether the request landed, send it again with the same key: you get the original document back with deduped: true, never a duplicate. On the POS endpoint the key is scoped to the device — the same key from a different till is a different sale.

API reference

Generated from the same OpenAPI document served at /openapi.json, so the fields below cannot drift from the fields the API accepts.

GET/api/v1/invoices

List invoices

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.

Parameters2
  • limitinteger · querydefault 50max 200 · min 1
  • X-Navira-Tenantuuid · header

    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.

Responses4
  • 200OK
  • 401Missing, malformed or revoked key
  • 429Rate limited
  • 503Server not configured
curl -X GET https://navira.shahmco.com/api/v1/invoices?limit=50 \
  -H "Authorization: Bearer nvr_live_YOURKEY"
200 · Invoice
{
  "id": "0f7b1e0a-0000-4000-8000-000000000001",
  "invoice_number": "INV-2026-00184",
  "document_type": "commercial",
  "status": "sent",
  "zatca_status": "cleared",
  "customer_name": "Al Fanar Trading Co.",
  "currency": "SAR",
  "grand_total": 21275,
  "amount_paid": 0,
  "issue_date": "2026-08-31",
  "due_date": "2026-09-30",
  "created_at": "2026-08-31T09:15:04Z"
}
POST/api/v1/invoices

Push an invoice from your ERP

The enterprise ingestion endpoint. Your system stays the record of truth; Navira produces the compliant document, signs it and retains the audit artefacts. In Saudi Arabia it is then submitted to ZATCA. In the UAE the PINT AE document is built and stored but NOT transmitted: Navira is not an FTA-accredited service provider.

Submitted once, and not retried. The authority submission happens synchronously inside this request. If it fails, the invoice is durably stored with zatca_status: "not_submitted" and is NOT picked up by any retry job — the catch-up sweep covers POS sales only. Read zatca_status from the response, and poll GET /api/v1/invoices or re-submit from the app.

Standard or simplified is inferred from the buyer. With a customer_trn the invoice is a STANDARD tax invoice, which in Saudi Arabia must be CLEARED by ZATCA before it is valid. Without one it is SIMPLIFIED and is reported within 24 hours. Sending subtype: "standard" with no customer_trn is rejected here rather than by ZATCA later.

Idempotent. Send the same idempotency_key again after a dropped connection and you get the ORIGINAL invoice back with deduped: true — never a duplicate. On a nightly batch over an unreliable link this is the difference between a reconciliation and an incident.

Cost centres allocate an invoice, or individual lines, to a department, branch or campus. An unknown code is a 400: a silently unallocated line is what an audit fails on months later. There is no self-serve screen for creating them yet, so ask us to set your codes up before you send them — otherwise every code is an unknown one and rejects the whole invoice.

Any industry, without a schema change. A line is a description, a quantity, a price and a UN/CEFACT unit — HUR for consulting hours, DAY for hotel nights, MON for a school term, C62 for a countable item. There is no SKU field to work around. Anything your own system needs to reconcile by — a student id, a patient reference, a matter number — goes in metadata.

Tax treatment is stated, never guessed — on THIS endpoint. Any line that is not standard-rated must name its exemption reason code, validated against the authority's list for the account's country. This is why a school can bill zero-rated tuition and 5% uniforms on one invoice and have both come out right. /api/v1/pos/sales does not accept an exemption reason yet and falls back to a per-category default, so push zero-rated and exempt lines through here.

Accounting firms can be issued one key that acts on several client accounts, named per request in X-Navira-Tenant, rather than holding one key per client. This is provisioned by hand today — talk to us before you build against it.

Requires an nvr_live_… key.

Parameters1
  • X-Navira-Tenantuuid · header

    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.

Request body29
  • idempotency_keyuuidrequired

    One per logical invoice. Resending the same key returns the original invoice with deduped: true.

  • document_typestringdefault "commercial"
    commercialcredit_notedebit_note
  • subtypestring
    standardsimplified

    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_namestringrequiredmax 300 chars
  • customer_name_arstring · nullable
  • customer_trnstring · nullable

    Buyer VAT number. Its presence is what makes the supply B2B.

  • customer_emailemail · nullable
  • customer_phonestring · nullable
  • customer_addressstring · nullable
  • customer_streetstring · nullable
  • customer_building_numberstring · nullable
  • customer_districtstring · nullable
  • customer_citystring · nullable
  • customer_postal_codestring · nullable
  • customer_country_codestring · nullableexactly 2 chars
  • po_numberstring · nullable

    Your customer's purchase-order number.

  • buyer_referencestring · nullable

    UBL BuyerReference — a distinct field from po_number.

  • billing_referencestring · nullable

    The invoice NUMBER this note corrects. Required on a credit or debit note, and rejected on a commercial invoice.

  • issue_datedate

    Defaults to today in the account's own timezone.

  • due_datedate · nullable
  • payment_termsinteger · nullablemax 365

    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_datedate · nullable

    Start of the billing period.

  • supply_end_datedate · nullable

    End of the billing period. Cannot precede supply_date.

  • currencystringdefault "SAR"exactly 3 chars
  • payment_methodstring · nullable
  • notesstring · nullable
  • cost_centerstring · nullable

    Header-level allocation. A line may override it.

  • metadataobject · nullable

    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.

  • linesarray of B2BLinerequired1–1000 items
    Child attributes11
    • descriptionstringrequiredmax 1000 chars
    • description_arstring · nullable

      Arabic description. Required in practice for KSA.

    • quantitynumberrequired
    • unit_pricenumberrequired

      Excluding VAT.

    • discount_pctnumbermax 100
    • tax_categorystringdefault "S"
      SZEOAE

      S standard-rated · Z zero-rated · E exempt · O out of scope · AE reverse charge.

    • unit_codestring

      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_codestring · nullable

      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.

      KSA (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.

      Note 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%.

      UAE: VATEX-AE-EXEMPT · VATEX-AE-RC reverse charge · VATEX-AE-OOS.

    • tax_exemption_reasonstring · nullable

      The taxpayer's own wording. Required alongside VATEX-SA-OOS and VATEX-AE-OOS.

    • cost_centerstring · nullable

      Cost-centre CODE as your system knows it. Overrides the header's. An unknown code is rejected with 400 rather than silently unallocated.

    • metadataobject · nullable

      Per-line identifiers from your system. Stored and returned; never written into the e-invoice XML. 8 KB max.

Responses7
  • 200Invoice created, or the original returned on a retry
  • 207Invoice created but some line items failed
  • 400Invalid payload, unknown cost centre, or a note with no billing reference
  • 401Missing, malformed or revoked key
  • 429Rate limited
  • 500Server error
  • 503Not enabled on this environment
curl -X POST https://navira.shahmco.com/api/v1/invoices \
  -H "Authorization: Bearer nvr_pos_YOURKEY" \
  -H "Content-Type: application/json" \
  -d '{
    "idempotency_key": "6f1c2a34-0000-4000-8000-000000000000",
    "customer_name": "Walk-in customer",
    "currency": "SAR",
    "payment_method": "card",
    "lines": [
      {
        "description": "Flat white",
        "description_ar": "قهوة فلات وايت",
        "quantity": 2,
        "unit_price": 16,
        "tax_category": "S",
        "unit_code": "EA"
      }
    ]
  }'
200 · B2BResult
{
  "ok": true,
  "deduped": false,
  "invoice_id": "0f7b1e0a-0000-4000-8000-000000000001",
  "invoice_number": "INV-2026-00184",
  "document_type": "commercial",
  "subtype": "standard",
  "zatca_status": "cleared",
  "due_date": "2026-09-30",
  "grand_total": 21275,
  "currency": "SAR"
}
POST/api/v1/pos/sales

Push a sale

Creates and locally signs the invoice, then races the ZATCA submission against an 8-second timeout before responding. The ordering matters for a till: the QR is returned only if that submission completes in time. Otherwise the response carries qr: null and zatca_status: "not_submitted" — your till must handle a null QR rather than assume one.

A slow or failed authority call never fails the sale: the invoice is durably written before any network call is attempted. A catch-up sweep re-submits anything not yet reported or cleared; it currently runs ONCE A DAY, which is one attempt inside a simplified invoice's 24-hour reporting window.

Always issues a SIMPLIFIED tax invoice. customer_trn is recorded on the receipt but does not make it a standard invoice — post to /api/v1/invoices for a cleared B2B document.

Requires an nvr_pos_… device key. Returns 403 upgrade_required if the merchant's plan does not include POS.

Request body7
  • idempotency_keyuuidrequired

    One per logical sale. Resending the same key returns the original invoice.

  • occurred_attimestamp

    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_namestringmax 300 chars
  • customer_trnstring

    Buyer VAT number. Supply it for a standard (B2B) invoice; omit for a simplified (B2C) one.

  • currencystringdefault "SAR"exactly 3 chars
  • payment_methodstringdefault "cash"
  • linesarray of SaleLinerequired1–200 items
    Child attributes7
    • descriptionstringrequiredmax 1000 chars
    • description_arstring · nullablemax 1000 chars

      Arabic description. Recommended for KSA.

    • quantitynumberrequired
    • unit_pricenumberrequired

      Excluding VAT.

    • discount_pctnumbermax 100
    • tax_categorystringdefault "S"
      SZEOAE

      S standard-rated · Z zero-rated · E exempt · O out of scope · AE reverse charge.

    • unit_codestring

      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.

Responses8
  • 200Invoice created, or the original returned on a retry
  • 207Invoice created but some line items failed
  • 400Invalid JSON or payload
  • 401Missing, malformed or revoked device key
  • 403Plan does not include POS ingestion
  • 429Rate limited
  • 500Server error
  • 503Not configured on this environment
curl -X POST https://navira.shahmco.com/api/v1/pos/sales \
  -H "Authorization: Bearer nvr_pos_YOURKEY" \
  -H "Content-Type: application/json" \
  -d '{
    "idempotency_key": "6f1c2a34-0000-4000-8000-000000000000",
    "customer_name": "Walk-in customer",
    "currency": "SAR",
    "payment_method": "card",
    "lines": [
      {
        "description": "Flat white",
        "description_ar": "قهوة فلات وايت",
        "quantity": 2,
        "unit_price": 16,
        "tax_category": "S",
        "unit_code": "EA"
      }
    ]
  }'
200 · SaleResult
{
  "ok": true,
  "deduped": false,
  "invoice_id": "0f7b1e0a-0000-4000-8000-000000000002",
  "invoice_number": "POS-2026-01920",
  "zatca_status": "reported",
  "qr": "AQ5OYXZpcmEgU3RvcmUCDzMwMDAwMDAwMDAwMDAwMw==",
  "grand_total": 36.8,
  "currency": "SAR"
}

How systems connect

Enterprise ERP — SAP, Oracle

These run on your own infrastructure and we do not connect into them. Your IT team writes an outbound step in your existing document flow that posts JSON to Navira. You keep your ERP as the system of record; Navira is the compliance layer — it builds and signs the document, and in Saudi Arabia submits it to ZATCA. No ERP vendor licence, add-on or consultant engagement is required to do this — the invoice data is yours.

An invoice pushed this way is submitted once, inside the same request. If that submission fails, the invoice is safely stored with zatca_status not_submitted and is not retried automatically — poll GET /api/v1/invoices and re-submit from the app, or ask us to set up an alert. Automatic retry currently covers POS sales only.

Cloud accounting — Zoho, Odoo, Xero, QuickBooks

We have no prebuilt connector for these today. The route that works is the same one the enterprise ERPs use: an outbound step in your own system, or a small middleware script, that maps their invoice payload onto the JSON above and posts it to /api/v1/invoices. If your platform can call a webhook when an invoice is raised, point it at us and we will scope the field mapping with you.

Point of sale

Your till posts each sale to /api/v1/pos/sales. When the account is onboarded to ZATCA and ZATCA responds in time, the QR payload comes back in the same response, fast enough to print on the receipt. The call waits up to 8 seconds for that; if ZATCA is slow, unreachable, or the account is not yet onboarded, you get qr: null and zatca_status not_submitted. Your till must handle a null QR. The sale itself is already durably saved at that point, and a sweep re-submits it later.

That sweep currently runs once a day — one attempt inside a simplified invoice’s 24-hour reporting window. A more frequent sweep is available for high-volume accounts; ask us before you design a till that depends on it.

E-commerce — Salla

There is a Salla integration: install the app and Navira receives your orders over a webhook, with no API work on your side. It is in limited release, and it currently declines to invoice orders carrying shipping or discounts rather than filing a signed document for the wrong amount. Talk to us before you rely on it for a live storefront.

Limits & what is next

Three operations are public and documented above: reading documents, pushing an invoice from your ERP, and pushing a point-of-sale transaction. All three are deployed and live, and the schema behind them is in production. Authority submission is enabled per account as part of onboarding, so ask us to confirm which authority environment your account is pointed at before you commit to a filing deadline. We pair with your team on the first integration rather than handing you a key and wishing you luck.

Worth knowing before you design against it

Cost centres are set up by us on request

There is no self-serve screen yet, so an unrecognised code is a 400 that rejects the whole invoice — send us your codes first, or leave the field out.

metadata is write-side today

Your own identifiers, such as a student id or a matter number, are stored against the invoice and its lines, but are not yet echoed back on GET /api/v1/invoices or in CSV exports. They are never written into the signed e-invoice XML.

Exemption reasons are only accepted on /api/v1/invoices

There, any line that is not standard-rated must name its exemption reason code and it is validated against ZATCA’s list. /api/v1/pos/sales does not accept one yet, so push zero-rated and exempt lines through the invoices endpoint.

Firm-scoped keys are provisioned by hand

One key acting on several client accounts, named per request in X-Navira-Tenant. Talk to us before you build against that header.

Reads are not paginated

Beyond limit (max 200, newest first) there is no cursor or offset. Ask us if you need a full export.

No prebuilt connectors for Shopify, Magento or WooCommerce

Those need platform-specific adapters — their webhooks carry no buyer VAT number and no per-line tax category, so no generic API can absorb them. Talk to us about scope if that is your channel.

Talk to an engineer, not a form

Spec version 1.1.0 · openapi.json